按时区(BST/GMT、CET/CEST 等)查找下一个夏令时日期

Posted

技术标签:

【中文标题】按时区(BST/GMT、CET/CEST 等)查找下一个夏令时日期【英文标题】:Find next daylight savings dates by time-zone (BST/GMT, CET/CEST, etc.) 【发布时间】:2020-02-17 17:32:21 【问题描述】:

根据英国夏令时 (BST) / 夏令时 (DST) (https://www.gov.uk/when-do-the-clocks-change) 的规则:

在 3 月的最后一个星期日凌晨 1 点前进 1 小时(英国夏令时开始), 10 月最后一个星期日凌晨 2 点返回 1 小时(格林威治标准时间 GMT 开始 = 世界协调时间 UTC)。

在 2019 年,这种民间当地时间的变化发生在 3 月 31 日和 10 月 27 日,但日子每年都会略有变化。

类似的 DST 规则适用于中欧时间 CET(冬季)> CEST(夏季)检查 3 月/10 月的最后一个星期日 (https://www.timeanddate.com/time/change/denmark/copenhagen)。这些 BST/GMT 和 CET/CEST 规则的组合影响例如北海周边的所有国家。无论 BST/GMT,还是 CET/CEST,下面的 UTC 时间戳都应该是一样的。

我已经根据time.UTC 编写了以下代码,提供了 BST/GMT 的日期,但我想知道是否有更简单/更通用的方法来使用可能适用于 CET/ 的任意 time.Location CEST 和(理想情况下)任何 DST 规则。

检查文档(这里是https://golang.org/pkg/time/#FixedZone)似乎time.FixedZone 应该处理不变的时区,但是那里的offset 是什么?如何使以下函数不受时区影响? 有没有办法避免每月星期日出现for 循环? 有没有办法避免基于 BST/GMT 或 CET/CEST 和月份的硬编码 map,以找出下一个时钟变化?

代码:

func beginOfMonth(year, month int, loc *time.Location) time.Time 
    return time.Date(year, time.Month(month), 1, 0, 0, 0, 0, loc)


// https://www.gov.uk/when-do-the-clocks-change
func lastUTCSunday(year, month int) time.Time 
    beginOfMonth := beginOfMonth(year, month, time.UTC)
    // we can find max 5 sundays in a month
    sundays := make([]time.Time, 5)
    for d := 1; d <= 31; d++ 
        currDay := beginOfMonth.Add(time.Duration(24*d) * time.Hour)
        if currDay.Weekday().String() == "Sunday" 
            sundays = append(sundays, currDay)
        
        if currDay.Month() != beginOfMonth.Month() 
            break
        
    
    // check if last date is same month
    if sundays[len(sundays)-1].Month() == beginOfMonth.Month() 
        // the 5th sunday
        return sundays[len(sundays)-1]
    
    // if not like before, then we only have 4 Sundays
    return sundays[len(sundays)-2]


// https://www.gov.uk/when-do-the-clocks-change
func MarchClockSwitchTime(year int) time.Time 
    lastSunday := lastUTCSunday(year, int(time.March)) // month: 3
    return time.Date(
        year, lastSunday.Month(), lastSunday.Day(),
        1, 0, 0, 0, // 1:00 AM
        lastSunday.Location(),
    )


// https://www.gov.uk/when-do-the-clocks-change
func OctoberClockSwitchTime(year int) time.Time 
    lastSunday := lastUTCSunday(year, int(time.October)) // month: 10
    return time.Date(
        year, lastSunday.Month(), lastSunday.Day(),
        2, 0, 0, 0, // 2:00 AM
        lastSunday.Location(),
    )


我还使用 GoConvey 编写了一些测试,这些测试应该验证这些基于星期日的奇怪夏令时 (DST) 规则,但它们仅适用于 2019 年和 2020 年。找到一种方法使此代码更通用是件好事。

func TestLastSunday(t *testing.T) 
    Convey("Should find the last UTC Sunday of each month\n\n", t, func() 
        for year := 2019; year <= 2020; year++ 
            for month := 1; month <= 12; month++ 
                lastUtcSunday := lastUTCSunday(year, month)

                So(lastUtcSunday.Month(), ShouldEqual, time.Month(month))
                So(lastUtcSunday.Weekday().String(), ShouldEqual, "Sunday")
                So(lastUtcSunday.Year(), ShouldEqual, year)
                So(lastUtcSunday.Day(), ShouldBeGreaterThanOrEqualTo, 28-7)
            
        
    )


// https://www.gov.uk/when-do-the-clocks-change
func TestClockChange(t *testing.T) 
    Convey("Should find the last UTC Sunday for the March switch\n\n", t, func() 
        switch2019 := MarchClockSwitchTime(2019)
        So(switch2019.Month(), ShouldEqual, time.March)
        So(switch2019.Weekday().String(), ShouldEqual, "Sunday")
        So(switch2019.Day(), ShouldEqual, 31)
        So(switch2019.Location().String(), ShouldEqual, "UTC")
        So(switch2019.Location().String(), ShouldEqual, time.UTC.String())

        switch2020 := MarchClockSwitchTime(2020)
        So(switch2020.Month(), ShouldEqual, time.March)
        So(switch2020.Weekday().String(), ShouldEqual, "Sunday")
        So(switch2020.Day(), ShouldEqual, 29)
        So(switch2020.Location().String(), ShouldEqual, "UTC")
        So(switch2020.Location().String(), ShouldEqual, time.UTC.String())
    )
    Convey("Should find the last UTC Sunday for the October switch\n\n", t, func() 
        switch2019 := OctoberClockSwitchTime(2019)
        So(switch2019.Month(), ShouldEqual, time.October)
        So(switch2019.Weekday().String(), ShouldEqual, "Sunday")
        So(switch2019.Day(), ShouldEqual, 27)
        So(switch2019.Location().String(), ShouldEqual, "UTC")
        So(switch2019.Location().String(), ShouldEqual, time.UTC.String())

        switch2020 := OctoberClockSwitchTime(2020)
        So(switch2020.Month(), ShouldEqual, time.October)
        So(switch2020.Weekday().String(), ShouldEqual, "Sunday")
        So(switch2020.Day(), ShouldEqual, 25)
        So(switch2020.Location().String(), ShouldEqual, "UTC")
        So(switch2020.Location().String(), ShouldEqual, time.UTC.String())
    )

【问题讨论】:

不幸的是,所有这些详细信息都保存在 time 包中未导出的字段中:golang.org/src/time/zoneinfo.go 【参考方案1】:

Go 已经通过time.LoadLocation 获得完整的IANA TZDB 支持。在英国使用Europe/London,在丹麦使用Europe/Copenhagen,等等。请参阅the list here。

您不应该自己重新实现时区逻辑。正如汤姆斯科特在The Problem with Time & Timezones 中恰当地打趣的那样 - “这就是疯狂。”

【讨论】:

我遇到的问题不在于加载(或切换)时区。这甚至与重新实施时区无关。当夏令时(DST,Europe/London 的 BST 部分,Europe/Copenhagen 的 CEST 部分)将激活/停用时,它很难以编程方式找到未来的一天。 Go 标准库通过您和我发布的链接提供了一些功能,但它似乎不够丰富以协助 DST,所以我想知道我是否遗漏了文档中的某些内容,或者我真的必须实现该功能我自己(就像我一样)。 我明白了。我想我误解了你的问题。您真的很喜欢未导出的Location.tx 字段中的zoneTrans 对象数组。 (请参阅您的问题中引用的链接 Adrian)。不知道你会怎么做。似乎缺少功能。【参考方案2】:

根据您的情况,调用zdump 并解析响应可能更实际。

例如,列出太平洋/奥克兰时区的夏令时转换:

zdump "Pacific/Auckland" -c 2020,2022 -V

结果显示 NZDT(夏令时)或 NZST(标准时间),以及 'isdst' 标志:

Pacific/Auckland  Sat Apr  4 13:59:59 2020 UT = Sun Apr  5 02:59:59 2020 NZDT isdst=1 gmtoff=46800
Pacific/Auckland  Sat Apr  4 14:00:00 2020 UT = Sun Apr  5 02:00:00 2020 NZST isdst=0 gmtoff=43200
Pacific/Auckland  Sat Sep 26 13:59:59 2020 UT = Sun Sep 27 01:59:59 2020 NZST isdst=0 gmtoff=43200
Pacific/Auckland  Sat Sep 26 14:00:00 2020 UT = Sun Sep 27 03:00:00 2020 NZDT isdst=1 gmtoff=46800
Pacific/Auckland  Sat Apr  3 13:59:59 2021 UT = Sun Apr  4 02:59:59 2021 NZDT isdst=1 gmtoff=46800

【讨论】:

这个zdump 命令似乎很有用(例如zdump "Europe/London" -c 2020,2021 -Vzdump "Europe/Copenhagen" -c 2020,2021 -V)。它似乎只适用于 Linux,是否有适用于 Windows 的等效版本?

以上是关于按时区(BST/GMT、CET/CEST 等)查找下一个夏令时日期的主要内容,如果未能解决你的问题,请参考以下文章

从 CET / CEST 到 UTC 的 TimeSeries 转换

如何更改 R 中的默认时区?

返回给定日期的 BST/GMT

如何按城市代码查找时区?

修改php上传文件尺寸响应时间时区时间等设置

如何查找当前时区 EST 或 EDT