Source code for dateparser.freshness_date_parser

import contextlib
from datetime import datetime, time, timezone, tzinfo
from typing import TYPE_CHECKING, Any

import regex as re
from dateutil.relativedelta import relativedelta

from dateparser.utils import (
    _get_localzone,
    apply_timezone,
    localize_timezone,
    set_correct_day_from_settings,
    set_correct_month_from_settings,
    strip_braces,
)

from .parser import time_parser
from .timezone_parser import pop_tz_offset_from_string

if TYPE_CHECKING:
    from .conf import Settings
    from .date import DateData

_UNITS = r"century|decade|fortnight|year|month|week|day|hour|minute|second"
# A number that continues a date or a time, e.g. the year in "03/06/2020 year",
# is not a quantity, and a string with one is not a relative date.
PATTERN = re.compile(
    rf"(?<![\d/.:])([+-]?\s*\d++[.,]?\d*+)\s*({_UNITS})\b", re.I | re.S | re.U
)
_GLUED_QUANTITY = re.compile(rf"\d\s*(?:{_UNITS})\b", re.I)
# "the 1st of last month" translates to " 1 1 month ago".
_DAY_OF_MONTH = re.compile(r"^\s*(\d{1,2})\s+(?=(?:in\s+)?\d+\s+month\b)")


[docs] class FreshnessDateDataParser: """Parses date string like "1 year, 2 months ago" and "3 hours, 50 minutes ago".""" def _are_all_words_units(self, date_string: str) -> bool: skip = [_UNITS, r"ago|in|\d+", r":|[ap]m"] if _GLUED_QUANTITY.search(PATTERN.sub("", date_string)): return False date_string = re.sub(r"\s+", " ", date_string.strip()) words = [x for x in re.split(r"\W", date_string) if x] words = [x for x in words if not re.match("|".join(skip), x)] return not words def _parse_time(self, date_string: str, settings: "Settings") -> time | None: """Attempts to parse time part of date strings like '1 day ago, 2 PM'""" date_string = PATTERN.sub("", date_string) date_string = re.sub(r"\b(?:ago|in)\b", "", date_string) with contextlib.suppress(Exception): return time_parser(date_string) return None
[docs] def get_local_tz(self) -> tzinfo: return _get_localzone()
[docs] def parse( self, date_string: str, settings: "Settings" ) -> tuple[datetime | None, str | None]: date, period, _ = self._parse_with_parts(date_string, settings) return date, period
def _parse_with_parts( # noqa: PLR0912, PLR0915 self, date_string: str, settings: "Settings" ) -> tuple[datetime | None, str | None, tuple[str, ...]]: date_string = strip_braces(date_string) date_string, ptz = pop_tz_offset_from_string(date_string, settings=settings) day = None if match := _DAY_OF_MONTH.match(date_string): day = int(match[1]) date_string = date_string[match.end() :] _time = self._parse_time(date_string, settings) _settings_tz = settings.TIMEZONE.lower() def apply_time(dateobj: datetime, timeobj: time | None) -> datetime: if not isinstance(timeobj, time): return dateobj return dateobj.replace( hour=timeobj.hour, minute=timeobj.minute, second=timeobj.second, microsecond=timeobj.microsecond, ) if settings.RELATIVE_BASE: now = settings.RELATIVE_BASE if "local" not in _settings_tz: now = localize_timezone(now, settings.TIMEZONE) if ptz: now = now.astimezone(ptz) if now.tzinfo else ptz.localize(now) if not now.tzinfo: now = now.replace(tzinfo=self.get_local_tz()) elif ptz: localized_now = datetime.now(ptz) if "local" in _settings_tz: now = localized_now else: now = apply_timezone(localized_now, settings.TIMEZONE) elif "local" not in _settings_tz: utc_dt = datetime.now(tz=timezone.utc) now = apply_timezone(utc_dt, settings.TIMEZONE) else: now = datetime.now(self.get_local_tz()) date, period, parts = self._parse_date( date_string, now, settings.PREFER_DATES_FROM, settings.RETURN_TIME_AS_PERIOD, ) if date and day is not None: if period != "month": return None, None, () try: date = date.replace(day=day) except ValueError: return None, None, () period = "day" parts += ("day",) if date: if period == "year": date = set_correct_month_from_settings(date, settings, date.month) if period in ("year", "month"): date = set_correct_day_from_settings(date, settings, date.day) old_date = date date = apply_time(date, _time) if settings.RETURN_TIME_AS_PERIOD and old_date != date: period = "time" if isinstance(_time, time) and "time" not in parts: parts += ("time",) if settings.TO_TIMEZONE: date = apply_timezone(date, settings.TO_TIMEZONE) if not settings.RETURN_AS_TIMEZONE_AWARE or ( settings.RETURN_AS_TIMEZONE_AWARE and "default" == settings.RETURN_AS_TIMEZONE_AWARE and not ptz ): date = date.replace(tzinfo=None) return date, period, parts def _parse_date( # noqa: PLR0912 self, date_string: str, now: datetime, prefer_dates_from: str, return_time_as_period: bool, ) -> tuple[datetime, str, tuple[str, ...]] | tuple[None, None, tuple[()]]: if not self._are_all_words_units(date_string): return None, None, () result = self.get_kwargs(date_string) if isinstance(result, tuple): kwargs, explicit_signs = result else: kwargs = result explicit_signs = {} if not kwargs: return None, None, () period = "day" if return_time_as_period and any( unit in kwargs for unit in ("hours", "minutes", "seconds") ): period = "time" elif "days" not in kwargs: for k in ["weeks", "fortnights", "months", "years", "decades", "centuries"]: if k in kwargs: period = { "fortnights": "week", "decades": "year", "centuries": "year", }.get(k, k[:-1]) break going_forward = re.search(r"\bin\b", date_string) or ( re.search(r"\bfuture\b", prefer_dates_from) and not re.search(r"\bago\b", date_string) ) adjusted_kwargs: dict[str, Any] = {} for key, value in kwargs.items(): if explicit_signs.get(key, False) or going_forward: adjusted_kwargs[key] = value else: adjusted_kwargs[key] = -value # Fold ``decades`` into ``years`` after each component's sign has been # resolved so that an unsigned component combined with an explicitly # signed one still follows the default ago/future context (fixes # #1304). for unit, factor, base in ( ("centuries", 100, "years"), ("decades", 10, "years"), ("fortnights", 2, "weeks"), ): if unit in adjusted_kwargs: adjusted_kwargs[base] = adjusted_kwargs.get( base, 0 ) + factor * adjusted_kwargs.pop(unit) td = relativedelta(**adjusted_kwargs) date = now + td # The smallest unit in the string determines which parts of the # resulting date are meaningful. parts: tuple[str, ...] if kwargs.keys() & {"seconds", "minutes", "hours"}: parts = ("year", "month", "day", "time") elif kwargs.keys() & {"days", "weeks", "fortnights"}: parts = ("year", "month", "day") elif "months" in kwargs: parts = ("year", "month") else: parts = ("year",) return date, period, parts
[docs] def get_kwargs( self, date_string: str ) -> tuple[dict[str, float], dict[str, bool]] | dict[str, float]: m = PATTERN.findall(date_string) if not m: return {} kwargs: dict[str, float] = {} explicit_signs: dict[str, bool] = {} for num, unit in m: has_explicit_sign = num.startswith(("+", "-")) key = "centuries" if unit.lower() == "century" else unit.lower() + "s" explicit_signs[key] = has_explicit_sign kwargs[key] = float(num.replace(",", ".").replace(" ", "")) return kwargs, explicit_signs
[docs] def get_date_data(self, date_string: str, settings: "Settings") -> "DateData": from dateparser.date import DateData # noqa: PLC0415 date, period, parts = self._parse_with_parts(date_string, settings) return DateData(date_obj=date, period=period, parts=parts)
freshness_date_parser = FreshnessDateDataParser()