如何创建适用于所有日期的时区感知时间对象?

How to create a Timezone aware Time object that works with all dates?

我已经为这个问题苦苦挣扎了一段时间。我觉得我很了解时区,而且我认为我了解如何正确使用 pytz,所以我经常使用它,而且我通常没有任何问题。也许我正在尝试使用错误的工具来实现我想要的。

对于我当前的应用程序,我需要处理抽象 time 对象。也就是说,我关心发生在 16:00 的事情。我不在乎它发生在 12 月 23 日 16:00。 pytz 似乎更喜欢使用 datetime 对象:这是有道理的。由于夏令时和历史原因等原因,它无法计算出偏移量,因为它会根据一天的不同而不同。

我试图让用户协调世界各地的日常活动。如果日本的用户说活动每天从 21:00 到 23:00,我希望 US/Central 的用户每天都能看到 6:00 到 8:00。我以为我有这个工作......直到两周前。看,DST 在美国大部分地区刚刚结束,所以现在 6:00-8:00 实际上以前是 7:00-9:00.

这打破了我通常需要以 UTC 格式存储时间,然后转换它们仅供查看的想法。它创建的时区实际上非常重要。如果我们颠倒这一点,美国时区遵守夏令时的用户设置了事件时间,那么日本的时间需要更改,即使他们不遵守夏令时!如果我将该时间存储为 UTC,在日本没有任何变化,但这不是我想要的功能。

所以我想用 tzinfo 存储为 time 对象。但是你不能创建一个没有日期的精确 pytz tzinfo 的时间对象,但日期并不重要。如果我使用当前日期计算出 tzinfo,那么一旦时区发生变化,它实际上将不再准确。

我想我的问题是:存储“东部时间下午 4 点”的最佳方式是什么,以便将来可以在世界任何地方检索 ?包括东方!我希望它在 DST 期间和 DST 之外是下午 4 点。我不能将它存储为 UTC,因为 12:00 UTC 与 12:00 UTC 在整年中的时间相同,但我不希望这样。我想我想要的是 "abstract" 或 "temporary" pytz.timezone 在给出日期(查看日期)之前没有实际偏移量。那是一回事吗?我在这个网站上阅读了无数问题,包括 python 和 pytz 文档,但找不到类似的问题,也找不到任何有类似问题的人。一切似乎都在谈论特定的 datetimes 或仅在 datetimes 内工作,但这似乎与我的问题无关。

我的应用程序非常庞大,因此很难抽出具体的部分,但我可以尝试展示我尝试过的内容以及为什么无法正常工作。

event_time = datetime.time(hour=12, tzinfo=pytz.timezone("US/Eastern")) 将是我理想的解决方案。但是使用 pytz 来创建 tzinfo 并不是一个好主意(由于历史原因,这会给我一个类似 -5:04 的偏移量)——有没有办法指定要使用的 US/Eastern 版本?

datetime.now(pytz.timezone("US/Eastern")).replace(hour=12, minute=0, second=0, microsecond=0).timetz() 给了我一些看起来像我想要的东西,但它只有在 US/Eastern 不变的情况下才能正常工作。如果我将此应用到我在 DST 更改之前移回的日期,它会给我 13:00,这不是我想要的。

看起来 pytz 通过 'localize' 方法提供了这个,如`this answer.

中所建议的

来自pytz documentation

If you insist on working with local times, this library provides a facility for constructing them unambiguously:

eastern = timezone('US/Eastern')

loc_dt = datetime(2002, 10, 27, 1, 30, 00)
est_dt = eastern.localize(loc_dt, is_dst=True)
edt_dt = eastern.localize(loc_dt, is_dst=False)
print(est_dt.strftime(fmt) + ' / ' + edt_dt.strftime(fmt))
2002-10-27 01:30:00 EDT-0400 / 2002-10-27 01:30:00 EST-0500`

通过存储本地时间,和本地时区,你可以在使用的时候转换,转换成其他地区的时区。

我构建了一个自动处理 DST 转换的 tzinfo class。我从日期时间文档中的 USTimeZone class 示例中得到了这个想法。

这里的技巧是 pytz 时区数据库具有夏令时生效的所有历史日期。这也是为什么当您创建一个没有日期的日期时间对象时,它会错误地转换 DST;它基于数据库中的第一个条目。

from datetime import datetime, tzinfo, timedelta
import pytz

ZERO = timedelta(0)

def format_timedelta(td):
    if td < timedelta(0):
        return '-' + format_timedelta(-td)
    else:
        # Change this to format positive timedeltas the way you want
        return str(td)



class WorldTimeZone(tzinfo):
    """
    A self adjusting according to DST rules in the PYTZ database tzinfo class
    See pytz.all_timezones for a list of all zone names and offsets.
    """

    def __init__(self, zone):
        """
        :param zone:  (str) Proper tzdatabase timze zone name. 
        """
        # initialize the pytz timezone with current time
        # this is done to avoid confusing tznames found in the start of the tz database
        # _utcoffset should always be STD rather than DST.
        self.pytzinfo = self.__getSTD(zone)
        self._utcoffset = self.pytzinfo._utcoffset


    @staticmethod
    def __getSTD(tname):
        """
        This returns a pytz timezone object normalized to standard time for the zone requested.
        If the zone does not follow DST or a future transition time cannot be found, it normalizes to NOW instead.

        :param tname:  Proper timezone name found in the tzdatabase. example: "US/Central"
        """

        # This defaults to the STD time for the zone rather than current time which could be DST
        tzone = pytz.timezone(tname)
        NOW = datetime.now(tz=pytz.UTC)
        std_date = NOW
        hasdst = False
        try:
            #transitions are in UTC.  They need to be converted to localtime once we find the correct STD transition.
            for utcdate, info in zip(tzone._utc_transition_times, tzone._transition_info):
                utcdate = utcdate.replace(tzinfo=pytz.UTC)
                utcoffset, dstoffset, tzname = info
                if dstoffset == ZERO:
                    std_date = utcdate
                if utcdate > NOW:
                    hasdst = True
                    break
        except AttributeError:
            std_date = NOW
        if not hasdst:
            std_date = NOW
        std_date = tzone.normalize(std_date)
        return std_date.tzinfo

    # This needs to be dynamic because pytzinfo updates everytime .dst() is called; which is a lot.
    @property
    def _dst(self):
        return self.pytzinfo._dst

    def __repr__(self):
        # return self.pytzinfo.__repr__()
        if self._dst:
            dst = 'DST'
        else:
            dst = 'STD'
        if self._utcoffset > timedelta(seconds=0):
            msg = '<WorldTimeZone %r %s+%s %s>'
        else:
            msg = '<WorldTimeZone %r %s%s %s>'
        return msg % (self.pytzinfo.zone, self.pytzinfo._tzname,
                      format_timedelta(self._utcoffset + self._dst), dst)

    def __str__(self):
        return "%s %s" % (self.pytzinfo._tzname, self.pytzinfo)

    def tzname(self, dt):
        # print "   TZNAME called"
        return "%s %s" % (self.pytzinfo._tzname, self.pytzinfo)

    def utcoffset(self, dt):
        # print "   UTCOFFSET CALLED"
        return self._utcoffset + self.dst(dt)

    def dst(self, dt):
        # print "   DST CALLED"
        if dt is None or dt.tzinfo is None:
            # An exception may be sensible here, in one or both cases.
            # It depends on how you want to treat them.  The default
            # fromutc() implementation (called by the default astimezone()
            # implementation) passes a datetime with dt.tzinfo is self.
            return ZERO

        assert dt.tzinfo is self  # WE ASSUME THE TZINFO ON THE DATE PASSED IN IS OUR TZINFO OBJECT.
        tmpdt = self.pytzinfo.normalize(dt)
        self.pytzinfo = tmpdt.tzinfo
        return tmpdt.tzinfo._dst

示例代码

EST = WorldTimeZone('US/Eastern')
PST = WorldTimeZone('US/Pacific')
dt_dst = datetime(2018, 11, 1, 1, 30, 00)
dt_std = datetime(2018, 11, 6, 1, 30, 00)
est_dst = dt_dst.replace(tzinfo=EST)
est_std = dt_std.replace(tzinfo=EST)
pst_dst = est_dst.astimezone(PST)
pst_std = est_std.astimezone(PST)

print(f"{dt_dst} >> {est_dst} >> {pst_dst}")
print(f"{dt_std} >> {est_std} >> {pst_std}")

产出

2018-11-01 01:30:00 >> 2018-11-01 01:30:00-04:00 >> 2018-10-31 22:30:00-07:00
2018-11-06 01:30:00 >> 2018-11-06 01:30:00-05:00 >> 2018-11-05 22:30:00-08:00