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 a0e656c commit 50c2baf
Showing 1 changed file with 149 additions and 118 deletions.
267 changes: 149 additions & 118 deletions faker/providers/date_time/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,35 +26,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 @@ -1839,6 +1826,132 @@ def _rand_seconds(self, start_datetime: int, end_datetime: int) -> float:

regex = re.compile(timedelta_pattern)

@classmethod
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 ("today", "now"):
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 == "now":
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 ("today", "now"):
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 @@ -1852,7 +1965,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 @@ -1905,7 +2018,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 @@ -1971,87 +2084,6 @@ def time_object(self, end_datetime: Optional[DateParseType] = None) -> dttime:
"""
return self.date_time(end_datetime=end_datetime).time()

@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 == "now":
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 ("today", "now"):
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 @@ -2068,7 +2100,10 @@ def date_time_between(
:example: datetime('1999-02-02 11:42:52')
:return: datetime
"""
now = get_now_date_time(start_date, end_date, tzinfo)
if end_date is None:
end_date = "now"

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 @@ -2090,7 +2125,10 @@ def date_between(self, start_date: DateParseType = "-30y", end_date: DateParseTy
:example: Date('1999-02-02')
:return: Date
"""
today = get_now_date(start_date, end_date)

if end_date is None:
end_date = "now"
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 @@ -2164,7 +2202,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 @@ -2425,7 +2463,9 @@ def time_series(
``distrib`` is a callable that accepts ``<datetime>`` and returns ``<value>``
"""
now = get_now_date_time(start_date, end_date, tzinfo)
if end_date is None:
end_date = "now"
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 @@ -2530,12 +2570,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 50c2baf

Please sign in to comment.