TPPZ
TPPZ

Reputation: 4901

Find next daylight savings dates by time-zone (BST/GMT, CET/CEST, etc.)

According to the rules of British Summer Time (BST) / daylight saving time (DST) (https://www.gov.uk/when-do-the-clocks-change) the clocks:

In 2019 this civil local time change happens on March 31st and October 27th, but the days slightly change every year.

A similar DST rule applies to Central European Time CET (Winter) > CEST (Summer) checking the last Sunday of March/October (https://www.timeanddate.com/time/change/denmark/copenhagen). The combination of these BST/GMT and CET/CEST rules affect e.g. all the countries around the North Sea. Regardless BST/GMT, or CET/CEST, the UTC timestamp underneath should be the same.

I've written the following code based on time.UTC providing the dates for BST/GMT, but I am wondering if there is a simpler / more general way to use an arbitrary time.Location that could be applicable to CET/CEST and (ideally) any DST rule.

The code:

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(),
    )
}

I've also written some tests using GoConvey that should validate these weird Daylight saving (DST) rules based Sundays, but they work just for 2019, 2020. It would be good to find a way to make this code more general.

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())
    })
}

Upvotes: 2

Views: 1733

Answers (2)

Mark
Mark

Reputation: 7091

Depending on your situation, it may be practical to call out to zdump instead, and parse the response.

For example, to list daylight saving transitions for Pacific/Auckland timezone:

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

Results show NZDT (daylight time) or NZST (standard time), and 'isdst' flag:

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

Upvotes: 1

Matt Johnson-Pint
Matt Johnson-Pint

Reputation: 241798

Go already has full IANA TZDB support via time.LoadLocation. Use Europe/London for the UK, Europe/Copenhagen for CET/CEST in Denmark, etc. See the list here.

You should not re-implement time zone logic yourself. As Tom Scott aptly quips in The Problem with Time & Timezones - "That way lies madness."

Upvotes: 1

Related Questions