Skip to content

Commit

Permalink
MAke relative date calculation more robust
Browse files Browse the repository at this point in the history
  • Loading branch information
fcurella committed Jan 17, 2025
1 parent 6609891 commit 248f51f
Showing 1 changed file with 143 additions and 130 deletions.
273 changes: 143 additions & 130 deletions faker/providers/date_time/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
from datetime import time as dttime
from datetime import timedelta
from datetime import tzinfo as TzInfo
from functools import lru_cache
from typing import Any, Callable, Dict, Iterator, Optional, Tuple, Union

from dateutil import relativedelta
Expand All @@ -26,35 +27,22 @@ def datetime_to_timestamp(dt: Union[dtdate, datetime]) -> int:
return timegm(dt.timetuple())


def convert_timestamp_to_datetime(timestamp: Union[int, float], tzinfo: TzInfo) -> datetime:
import datetime as dt

if timestamp >= 0:
return dt.datetime.fromtimestamp(timestamp, tzinfo)
else:
return dt.datetime(1970, 1, 1, tzinfo=tzinfo) + dt.timedelta(seconds=int(timestamp))


def timestamp_to_datetime(timestamp: Union[int, float], tzinfo: Optional[TzInfo]) -> datetime:
if tzinfo is None:
pick = convert_timestamp_to_datetime(timestamp, tzlocal())
return pick.astimezone(tzutc()).replace(tzinfo=None)
return convert_timestamp_to_datetime(timestamp, tzinfo)


def get_now_date_time(
start_date: Optional[DateParseType], end_date: Optional[DateParseType], tzinfo: Optional[TzInfo]
) -> datetime:
if isinstance(start_date, datetime) and not isinstance(end_date, datetime):
now = start_date
elif isinstance(end_date, datetime) and not isinstance(start_date, datetime):
now = end_date
else:
now = datetime.now(tzinfo)
return now


def get_now_date(start_date: Optional[DateParseType], end_date: Optional[DateParseType]) -> dtdate:
if isinstance(start_date, dtdate) and not isinstance(end_date, dtdate):
now = start_date
elif isinstance(end_date, dtdate) and not isinstance(start_date, dtdate):
now = end_date
else:
now = dtdate.today()
return now


def change_year(current_date: dtdate, year_diff: int) -> dtdate:
"""
Unless the current_date is February 29th, it is fine to just subtract years.
Expand Down Expand Up @@ -1841,6 +1829,133 @@ def _rand_seconds(self, start_datetime: int, end_datetime: int) -> float:
now_word = "now"
regex = re.compile(timedelta_pattern)

@classmethod
@lru_cache(maxsize=10)
def _is_absolute(cls, obj: Optional[DateParseType]) -> bool:
if obj is None:
return False
if isinstance(obj, (datetime, dtdate, int)):
return True
elif isinstance(obj, timedelta):
return False
elif isinstance(obj, str):
if obj in (cls.today_word, cls.now_word):
return False
return cls.regex.fullmatch(obj) is None
return False

@classmethod
def _get_reference_date_time(
cls, start_date: Optional[DateParseType], end_date: Optional[DateParseType], tzinfo: Optional[TzInfo]
) -> datetime:
"""
Return Which datetime is absolute, or now if both are relative.
If both are absolute, return the most recent one.
If both are None, return now.
"""
min_ = datetime_to_timestamp(datetime.min)
now = datetime.now(tzinfo)
if start_date is None and end_date is None:
return now

start_int = cls._parse_date_time(start_date, now) if start_date is not None else min_
end_int = cls._parse_date_time(end_date, now) if end_date is not None else min_
if not cls._is_absolute(start_date) and not cls._is_absolute(end_date):
return now
if cls._is_absolute(start_date) and cls._is_absolute(end_date):
reference = max([start_int, end_int])
elif cls._is_absolute(start_date) and not cls._is_absolute(end_date):
reference = start_int
elif cls._is_absolute(end_date) and not cls._is_absolute(start_date):
reference = end_int
return timestamp_to_datetime(reference, tzinfo)

@classmethod
def _get_reference_date(cls, start_date: Optional[DateParseType], end_date: Optional[DateParseType]) -> dtdate:
reference = cls._get_reference_date_time(start_date, end_date, None)
return reference.date()

@classmethod
def _parse_start_datetime(cls, now: datetime, value: Optional[DateParseType]) -> int:
if value is None:
return 0

return cls._parse_date_time(value, now)

@classmethod
def _parse_end_datetime(cls, now: datetime, value: Optional[DateParseType]) -> int:
if value is None:
return datetime_to_timestamp(now)

return cls._parse_date_time(value, now)

@classmethod
def _parse_date_string(cls, value: str) -> Dict[str, float]:
parts = cls.regex.match(value)
if not parts:
raise ParseError(f"Can't parse date string `{value}`")
parts = parts.groupdict()
time_params: Dict[str, float] = {}
for name_, param_ in parts.items():
if param_:
time_params[name_] = int(param_)

if "years" in time_params:
if "days" not in time_params:
time_params["days"] = 0
time_params["days"] += 365.24 * time_params.pop("years")
if "months" in time_params:
if "days" not in time_params:
time_params["days"] = 0
time_params["days"] += 30.42 * time_params.pop("months")

if not time_params:
raise ParseError(f"Can't parse date string `{value}`")
return time_params

@classmethod
def _parse_timedelta(cls, value: Union[timedelta, str, float]) -> Union[float, int]:
if isinstance(value, timedelta):
return value.total_seconds()
if isinstance(value, str):
time_params = cls._parse_date_string(value)
return timedelta(**time_params).total_seconds() # type: ignore
if isinstance(value, (int, float)):
return value
raise ParseError(f"Invalid format for timedelta {value!r}")

@classmethod
def _parse_date_time(cls, value: DateParseType, now: datetime, tzinfo: Optional[TzInfo] = None) -> int:
if isinstance(value, (datetime, dtdate)):
return datetime_to_timestamp(value)
if isinstance(value, timedelta):
return datetime_to_timestamp(now + value)
if isinstance(value, str):
if value == cls.now_word:
return datetime_to_timestamp(datetime.now(tzinfo))
time_params = cls._parse_date_string(value)
return datetime_to_timestamp(now + timedelta(**time_params)) # type: ignore
if isinstance(value, int):
return value
raise ParseError(f"Invalid format for date {value!r}")

@classmethod
def _parse_date(cls, value: DateParseType, today: dtdate) -> dtdate:
if isinstance(value, datetime):
return value.date()
elif isinstance(value, dtdate):
return value
if isinstance(value, timedelta):
return today + value
if isinstance(value, str):
if value in (cls.today_word, cls.now_word):
return today
time_params = cls._parse_date_string(value)
return today + timedelta(**time_params) # type: ignore
if isinstance(value, int):
return today + timedelta(value)
raise ParseError(f"Invalid format for date {value!r}")

def unix_time(
self,
end_datetime: Optional[DateParseType] = None,
Expand All @@ -1854,7 +1969,7 @@ def unix_time(
:example: 1061306726.6
"""
now = get_now_date_time(start_datetime, end_datetime, tzinfo=None)
now = self._get_reference_date_time(start_datetime, end_datetime, tzinfo=None)
start_datetime = self._parse_start_datetime(now, start_datetime)
end_datetime = self._parse_end_datetime(now, end_datetime)
return float(self._rand_seconds(start_datetime, end_datetime))
Expand Down Expand Up @@ -1907,7 +2022,7 @@ def date_time_ad(
# simply change that class method to use this magic number as a
# default value when None is provided.

now = get_now_date_time(start_datetime, end_datetime, tzinfo)
now = self._get_reference_date_time(start_datetime, end_datetime, tzinfo)
start_time = -62135596800 if start_datetime is None else self._parse_start_datetime(now, start_datetime)
end_datetime = self._parse_end_datetime(now, end_datetime)

Expand Down Expand Up @@ -1973,99 +2088,6 @@ def time_object(self, end_datetime: Optional[DateParseType] = None) -> dttime:
"""
return self.date_time(end_datetime=end_datetime).time()

@classmethod
def _is_absolute(cls, obj: DateParseType) -> bool:
if isinstance(obj, (datetime, dtdate, int)):
return True
elif isinstance(obj, timedelta):
return False
elif isinstance(obj, str):
if obj in (cls.today_word, cls.now_word):
return False
return cls.regex.fullmatch(obj) is None
return False

@classmethod
def _parse_start_datetime(cls, now: datetime, value: Optional[DateParseType]) -> int:
if value is None:
return 0

return cls._parse_date_time(value, now)

@classmethod
def _parse_end_datetime(cls, now: datetime, value: Optional[DateParseType]) -> int:
if value is None:
return datetime_to_timestamp(now)

return cls._parse_date_time(value, now)

@classmethod
def _parse_date_string(cls, value: str) -> Dict[str, float]:
parts = cls.regex.match(value)
if not parts:
raise ParseError(f"Can't parse date string `{value}`")
parts = parts.groupdict()
time_params: Dict[str, float] = {}
for name_, param_ in parts.items():
if param_:
time_params[name_] = int(param_)

if "years" in time_params:
if "days" not in time_params:
time_params["days"] = 0
time_params["days"] += 365.24 * time_params.pop("years")
if "months" in time_params:
if "days" not in time_params:
time_params["days"] = 0
time_params["days"] += 30.42 * time_params.pop("months")

if not time_params:
raise ParseError(f"Can't parse date string `{value}`")
return time_params

@classmethod
def _parse_timedelta(cls, value: Union[timedelta, str, float]) -> Union[float, int]:
if isinstance(value, timedelta):
return value.total_seconds()
if isinstance(value, str):
time_params = cls._parse_date_string(value)
return timedelta(**time_params).total_seconds() # type: ignore
if isinstance(value, (int, float)):
return value
raise ParseError(f"Invalid format for timedelta {value!r}")

@classmethod
def _parse_date_time(cls, value: DateParseType, now: datetime, tzinfo: Optional[TzInfo] = None) -> int:
if isinstance(value, (datetime, dtdate)):
return datetime_to_timestamp(value)
if isinstance(value, timedelta):
return datetime_to_timestamp(now + value)
if isinstance(value, str):
if value == cls.now_word:
return datetime_to_timestamp(datetime.now(tzinfo))
time_params = cls._parse_date_string(value)
return datetime_to_timestamp(now + timedelta(**time_params)) # type: ignore
if isinstance(value, int):
return value
raise ParseError(f"Invalid format for date {value!r}")

@classmethod
def _parse_date(cls, value: DateParseType, today: dtdate) -> dtdate:
if isinstance(value, datetime):
return value.date()
elif isinstance(value, dtdate):
return value
if isinstance(value, timedelta):
return today + value
if isinstance(value, str):
if value in (cls.today_word, cls.now_word):
return today
time_params = cls._parse_date_string(value)
return today + timedelta(**time_params) # type: ignore
if isinstance(value, int):
return today + timedelta(value)
raise ParseError(f"Invalid format for date {value!r}")

def date_time_between(
self,
start_date: DateParseType = "-30y",
Expand All @@ -2085,7 +2107,7 @@ def date_time_between(
if end_date is None:
end_date = self.now_word

now = get_now_date_time(start_date, end_date, tzinfo)
now = self._get_reference_date_time(start_date, end_date, tzinfo)
start_date = self._parse_date_time(start_date, now, tzinfo=tzinfo)
end_date = self._parse_date_time(end_date, now, tzinfo=tzinfo)
if end_date - start_date <= 1:
Expand All @@ -2109,7 +2131,7 @@ def date_between(self, start_date: DateParseType = "-30y", end_date: Optional[Da
"""
if end_date is None:
end_date = self.today_word
today = get_now_date(start_date, end_date)
today = self._get_reference_date(start_date, end_date)
start_date = self._parse_date(start_date, today)
end_date = self._parse_date(end_date, today)
return self.date_between_dates(date_start=start_date, date_end=end_date)
Expand Down Expand Up @@ -2183,7 +2205,7 @@ def date_time_between_dates(
:example: datetime('1999-02-02 11:42:52')
:return: datetime
"""
today = get_now_date(datetime_start, datetime_end)
today = self._get_reference_date(datetime_start, datetime_end)
now = datetime.combine(today, datetime.min.time(), tzinfo)
datetime_start_ = (
datetime_to_timestamp(datetime.now(tzinfo))
Expand Down Expand Up @@ -2446,7 +2468,7 @@ def time_series(
"""
if end_date is None:
end_date = self.now_word
now = get_now_date_time(start_date, end_date, tzinfo)
now = self._get_reference_date_time(start_date, end_date, tzinfo)
start_date_ = self._parse_date_time(start_date, now, tzinfo=tzinfo)
end_date_ = self._parse_date_time(end_date, now, tzinfo=tzinfo)

Expand Down Expand Up @@ -2551,12 +2573,3 @@ def date_of_birth(
dob = self.date_time_ad(tzinfo=tzinfo, start_datetime=start_date, end_datetime=end_date).date()

return dob if dob != start_date else dob + timedelta(days=1)


def convert_timestamp_to_datetime(timestamp: Union[int, float], tzinfo: TzInfo) -> datetime:
import datetime as dt

if timestamp >= 0:
return dt.datetime.fromtimestamp(timestamp, tzinfo)
else:
return dt.datetime(1970, 1, 1, tzinfo=tzinfo) + dt.timedelta(seconds=int(timestamp))

0 comments on commit 248f51f

Please sign in to comment.