X-Git-Url: https://wannabe.guru.org/gitweb/?a=blobdiff_plain;f=datetime_utils.py;h=6f504f6c304b850e830cffab08d0ed8be67fed39;hb=a4bf4d05230474ad14243d67ac7f8c938f670e58;hp=db5b2b5e19ba8e74fdf192dc122e518e90ec6bf9;hpb=83c1e0d04fe2e78963c8b508e8b7d0ae03bfcb16;p=python_utils.git diff --git a/datetime_utils.py b/datetime_utils.py index db5b2b5..6f504f6 100644 --- a/datetime_utils.py +++ b/datetime_utils.py @@ -16,18 +16,87 @@ import constants logger = logging.getLogger(__name__) -def replace_timezone(dt: datetime.datetime, - tz: datetime.tzinfo) -> datetime.datetime: +def is_timezone_aware(dt: datetime.datetime) -> bool: + """See: https://docs.python.org/3/library/datetime.html + #determining-if-an-object-is-aware-or-naive + + >>> is_timezone_aware(datetime.datetime.now()) + False + + >>> is_timezone_aware(now_pacific()) + True + + """ + return dt.tzinfo is not None and dt.tzinfo.utcoffset(dt) is not None + + +def is_timezone_naive(dt: datetime.datetime) -> bool: + return not is_timezone_aware(dt) + + +def replace_timezone(dt: datetime.datetime, tz: datetime.tzinfo) -> datetime.datetime: """ - Replaces the timezone on a datetime object. + Replaces the timezone on a datetime object directly (leaving + the year, month, day, hour, minute, second, micro, etc... alone). + Note: this changes the instant to which this dt refers. >>> from pytz import UTC >>> d = now_pacific() >>> d.tzinfo.tzname(d)[0] # Note: could be PST or PDT 'P' + >>> h = d.hour >>> o = replace_timezone(d, UTC) >>> o.tzinfo.tzname(o) 'UTC' + >>> o.hour == h + True + + """ + return datetime.datetime( + dt.year, + dt.month, + dt.day, + dt.hour, + dt.minute, + dt.second, + dt.microsecond, + tzinfo=tz, + ) + + +def replace_time_timezone(t: datetime.time, tz: datetime.tzinfo) -> datetime.time: + """ + Replaces the timezone on a datetime.time directly without performing + any translation. + + >>> t = datetime.time(8, 15, 12, 0, pytz.UTC) + >>> t.tzname() + 'UTC' + + >>> t = replace_time_timezone(t, pytz.timezone('US/Pacific')) + >>> t.tzname() + 'US/Pacific' + + """ + return t.replace(tzinfo=tz) + + +def translate_timezone(dt: datetime.datetime, tz: datetime.tzinfo) -> datetime.datetime: + """ + Translates dt into a different timezone by adjusting the year, month, + day, hour, minute, second, micro, etc... appropriately. The returned + dt is the same instant in another timezone. + + >>> from pytz import UTC + >>> d = now_pacific() + >>> d.tzinfo.tzname(d)[0] # Note: could be PST or PDT + 'P' + >>> h = d.hour + >>> o = translate_timezone(d, UTC) + >>> o.tzinfo.tzname(o) + 'UTC' + >>> o.hour == h + False """ return dt.replace(tzinfo=None).astimezone(tz=tz) @@ -42,9 +111,9 @@ def now() -> datetime.datetime: def now_pacific() -> datetime.datetime: """ - What time is it? Result in US/Pacifit time (PST/PDT) + What time is it? Result in US/Pacific time (PST/PDT) """ - return replace_timezone(now(), pytz.timezone("US/Pacific")) + return datetime.datetime.now(pytz.timezone("US/Pacific")) def date_to_datetime(date: datetime.date) -> datetime.datetime: @@ -56,16 +125,48 @@ def date_to_datetime(date: datetime.date) -> datetime.datetime: datetime.datetime(2021, 12, 25, 0, 0) """ - return datetime.datetime( - date.year, - date.month, - date.day, - 0, 0, 0, 0 - ) + return datetime.datetime(date.year, date.month, date.day, 0, 0, 0, 0) + + +def time_to_datetime_today(time: datetime.time) -> datetime.datetime: + """ + Given a time, returns that time as a datetime with a date component + set based on the current date. If the time passed is timezone aware, + the resulting datetime will also be (and will use the same tzinfo). + If the time is timezone naive, the datetime returned will be too. + + >>> t = datetime.time(13, 14, 0) + >>> d = now_pacific().date() + >>> dt = time_to_datetime_today(t) + >>> dt.date() == d + True + + >>> dt.time() == t + True + + >>> dt.tzinfo == t.tzinfo + True + + >>> dt.tzinfo == None + True + + >>> t = datetime.time(8, 15, 12, 0, pytz.UTC) + >>> t.tzinfo == None + False + + >>> dt = time_to_datetime_today(t) + >>> dt.tzinfo == None + False + + """ + now = now_pacific() + tz = time.tzinfo + return datetime.datetime.combine(now, time, tz) -def date_and_time_to_datetime(date: datetime.date, - time: datetime.time) -> datetime.datetime: +def date_and_time_to_datetime( + date: datetime.date, time: datetime.time +) -> datetime.datetime: """ Given a date and time, merge them and return a datetime. @@ -88,7 +189,7 @@ def date_and_time_to_datetime(date: datetime.date, def datetime_to_date_and_time( - dt: datetime.datetime + dt: datetime.datetime, ) -> Tuple[datetime.date, datetime.time]: """Return the component date and time objects of a datetime. @@ -130,6 +231,7 @@ def datetime_to_time(dt: datetime.datetime) -> datetime.time: class TimeUnit(enum.Enum): """An enum to represent units with which we can compute deltas.""" + MONDAYS = 0 TUESDAYS = 1 WEDNESDAYS = 2 @@ -160,9 +262,7 @@ class TimeUnit(enum.Enum): def n_timeunits_from_base( - count: int, - unit: TimeUnit, - base: datetime.datetime + count: int, unit: TimeUnit, base: datetime.datetime ) -> datetime.datetime: """Return a datetime that is N units before/after a base datetime. e.g. 3 Wednesdays from base datetime, 2 weeks from base date, 10 @@ -204,6 +304,17 @@ def n_timeunits_from_base( >>> n_timeunits_from_base(50, TimeUnit.SECONDS, base) datetime.datetime(2021, 9, 10, 11, 25, 41, tzinfo=datetime.timezone(datetime.timedelta(days=-1, seconds=61200))) + Next month corner case -- it will try to make Feb 31, 2022 then count + backwards. + >>> base = string_to_datetime("2022/01/31 11:24:51AM-0700")[0] + >>> n_timeunits_from_base(1, TimeUnit.MONTHS, base) + datetime.datetime(2022, 2, 28, 11, 24, 51, tzinfo=datetime.timezone(datetime.timedelta(days=-1, seconds=61200))) + + Last month with the same corner case + >>> base = string_to_datetime("2022/03/31 11:24:51AM-0700")[0] + >>> n_timeunits_from_base(-1, TimeUnit.MONTHS, base) + datetime.datetime(2022, 2, 28, 11, 24, 51, tzinfo=datetime.timezone(datetime.timedelta(days=-1, seconds=61200))) + """ assert TimeUnit.is_valid(unit) if count == 0: @@ -243,10 +354,8 @@ def n_timeunits_from_base( if base.year != old_year: skips = holidays.US(years=base.year).keys() if ( - base.weekday() < 5 and - datetime.date(base.year, - base.month, - base.day) not in skips + base.weekday() < 5 + and datetime.date(base.year, base.month, base.day) not in skips ): count -= 1 return base @@ -266,16 +375,23 @@ def n_timeunits_from_base( new_month %= 12 year_term += 1 new_year = base.year + year_term - return datetime.datetime( - new_year, - new_month, - base.day, - base.hour, - base.minute, - base.second, - base.microsecond, - base.tzinfo, - ) + day = base.day + while True: + try: + ret = datetime.datetime( + new_year, + new_month, + day, + base.hour, + base.minute, + base.second, + base.microsecond, + base.tzinfo, + ) + break + except ValueError: + day -= 1 + return ret # N years from base elif unit == TimeUnit.YEARS: @@ -291,13 +407,17 @@ def n_timeunits_from_base( base.tzinfo, ) - if unit not in set([TimeUnit.MONDAYS, - TimeUnit.TUESDAYS, - TimeUnit.WEDNESDAYS, - TimeUnit.THURSDAYS, - TimeUnit.FRIDAYS, - TimeUnit.SATURDAYS, - TimeUnit.SUNDAYS]): + if unit not in set( + [ + TimeUnit.MONDAYS, + TimeUnit.TUESDAYS, + TimeUnit.WEDNESDAYS, + TimeUnit.THURSDAYS, + TimeUnit.FRIDAYS, + TimeUnit.SATURDAYS, + TimeUnit.SUNDAYS, + ] + ): raise ValueError(unit) # N weekdays from base (e.g. 4 wednesdays from today) @@ -315,14 +435,14 @@ def n_timeunits_from_base( def get_format_string( - *, - date_time_separator=" ", - include_timezone=True, - include_dayname=False, - use_month_abbrevs=False, - include_seconds=True, - include_fractional=False, - twelve_hour=True, + *, + date_time_separator=" ", + include_timezone=True, + include_dayname=False, + use_month_abbrevs=False, + include_seconds=True, + include_fractional=False, + twelve_hour=True, ) -> str: """ Helper to return a format string without looking up the documentation @@ -397,20 +517,21 @@ def datetime_to_string( include_dayname=include_dayname, include_seconds=include_seconds, include_fractional=include_fractional, - twelve_hour=twelve_hour) + twelve_hour=twelve_hour, + ) return dt.strftime(fstring).strip() def string_to_datetime( - txt: str, - *, - date_time_separator=" ", - include_timezone=True, - include_dayname=False, - use_month_abbrevs=False, - include_seconds=True, - include_fractional=False, - twelve_hour=True, + txt: str, + *, + date_time_separator=" ", + include_timezone=True, + include_dayname=False, + use_month_abbrevs=False, + include_seconds=True, + include_fractional=False, + twelve_hour=True, ) -> Tuple[datetime.datetime, str]: """A nice way to convert a string into a datetime. Returns both the datetime and the format string used to parse it. Also consider @@ -429,11 +550,9 @@ def string_to_datetime( include_dayname=include_dayname, include_seconds=include_seconds, include_fractional=include_fractional, - twelve_hour=twelve_hour) - return ( - datetime.datetime.strptime(txt, fstring), - fstring + twelve_hour=twelve_hour, ) + return (datetime.datetime.strptime(txt, fstring), fstring) def timestamp() -> str: @@ -600,7 +719,7 @@ def parse_duration(duration: str) -> int: return seconds -def describe_duration(seconds: int, *, include_seconds = False) -> str: +def describe_duration(seconds: int, *, include_seconds=False) -> str: """ Describe a duration represented as a count of seconds nicely. @@ -652,6 +771,18 @@ def describe_duration(seconds: int, *, include_seconds = False) -> str: return descr +def describe_timedelta(delta: datetime.timedelta) -> str: + """ + Describe a duration represented by a timedelta object. + + >>> d = datetime.timedelta(1, 600) + >>> describe_timedelta(d) + '1 day, and 10 minutes' + + """ + return describe_duration(int(delta.total_seconds())) # Note: drops milliseconds + + def describe_duration_briefly(seconds: int, *, include_seconds=False) -> str: """ Describe a duration briefly. @@ -685,6 +816,21 @@ def describe_duration_briefly(seconds: int, *, include_seconds=False) -> str: return descr.strip() +def describe_timedelta_briefly(delta: datetime.timedelta) -> str: + """ + Describe a duration represented by a timedelta object. + + >>> d = datetime.timedelta(1, 600) + >>> describe_timedelta_briefly(d) + '1d 10m' + + """ + return describe_duration_briefly( + int(delta.total_seconds()) + ) # Note: drops milliseconds + + if __name__ == '__main__': import doctest + doctest.testmod()