import unittest from zoneinfo import ZoneInfo from datetime import datetime, timezone, tzinfo, timedelta utc = timezone.utc tz_name = "America/Los_Angeles" dt_utc = datetime(2020, 11, 1, 8, tzinfo=utc) def run_it(tz): print(f"\n\n{type(tz) = }") dt_local = dt_utc.astimezone(tz) print(f"{dt_local == dt_utc = }") print(f"{dt_local <= dt_utc = }") print(f"{dt_local >= dt_utc = }") print(f"{dt_local - dt_utc = }") class tzinfo2(tzinfo): def fromutc(self, dt): "datetime in UTC -> datetime in local time." if not isinstance(dt, datetime): raise TypeError("fromutc() requires a datetime argument") if dt.tzinfo is not self: raise ValueError("dt.tzinfo is not self") # Returned value satisfies # dt + ldt.utcoffset() = ldt off0 = dt.replace(fold=0).utcoffset() off1 = dt.replace(fold=1).utcoffset() if off0 is None or off1 is None or dt.dst() is None: raise ValueError if off0 == off1: ldt = dt + off0 off1 = ldt.utcoffset() if off0 == off1: return ldt # Now, we discovered both possible offsets, so # we can just try four possible solutions: for off in [off0, off1]: ldt = dt + off if ldt.utcoffset() == off: return ldt ldt = ldt.replace(fold=1) if ldt.utcoffset() == off: return ldt raise ValueError("No suitable local time found") def first_sunday_on_or_after(dt): days_to_go = 6 - dt.weekday() if days_to_go: dt += timedelta(days_to_go) return dt ZERO = timedelta(0) MINUTE = timedelta(minutes=1) HOUR = timedelta(hours=1) DAY = timedelta(days=1) # In the US, DST starts at 2am (standard time) on the first Sunday in April. DSTSTART = datetime(1, 4, 1, 2) # and ends at 2am (DST time; 1am standard time) on the last Sunday of Oct, # which is the first Sunday on or after Oct 25. Because we view 1:MM as # being standard time on that day, there is no spelling in local time of # the last hour of DST (that's 1:MM DST, but 1:MM is taken as standard time). DSTEND = datetime(1, 10, 25, 1) class USTimeZone2(tzinfo2): def __init__(self, hours, reprname, stdname, dstname): self.stdoffset = timedelta(hours=hours) self.reprname = reprname self.stdname = stdname self.dstname = dstname def __repr__(self): return self.reprname def tzname(self, dt): if self.dst(dt): return self.dstname else: return self.stdname def utcoffset(self, dt): return self.stdoffset + self.dst(dt) def dst(self, dt): if dt is None or dt.tzinfo is None: # An exception instead may be sensible here, in one or more of # the cases. return ZERO assert dt.tzinfo is self # Find first Sunday in April. start = first_sunday_on_or_after(DSTSTART.replace(year=dt.year)) assert start.weekday() == 6 and start.month == 4 and start.day <= 7 # Find last Sunday in October. end = first_sunday_on_or_after(DSTEND.replace(year=dt.year)) assert end.weekday() == 6 and end.month == 10 and end.day >= 25 # Can't compare naive to aware objects, so strip the timezone from # dt first. dt = dt.replace(tzinfo=None) if start + HOUR <= dt < end: # DST is in effect. return HOUR elif end <= dt < end + HOUR: # Fold (an ambiguous hour): use dt.fold to disambiguate. return ZERO if dt.fold else HOUR elif start <= dt < start + HOUR: # Gap (a non-existent hour): reverse the fold rule. return HOUR if dt.fold else ZERO else: # DST is off. return ZERO Eastern2 = USTimeZone2(-5, "Eastern2", "EST", "EDT") class TestLocalTimeDisambiguation(unittest.TestCase): def test_mixed_compare_fold(self): t_fold = datetime(2002, 10, 27, 1, 45, tzinfo=Eastern2) t_fold_utc = t_fold.astimezone(timezone.utc) self.assertNotEqual(t_fold, t_fold_utc) self.assertNotEqual(t_fold_utc, t_fold) def test_mixed_compare_gap(self): t_gap = datetime(2002, 4, 7, 2, 45, tzinfo=Eastern2) t_gap_utc = t_gap.astimezone(timezone.utc) self.assertNotEqual(t_gap, t_gap_utc) self.assertNotEqual(t_gap_utc, t_gap) run_it(Eastern2) run_it(ZoneInfo(tz_name)) if __name__ == "__main__": unittest.main()