Reputation: 4901
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.
time.FixedZone
should deal with invariant time-zones, but what's the offset
in there? How can I make the following functions invariant from the time-zone?for
loop on the Sunday(s) of the month?map
based on BST/GMT or CET/CEST and the month, to find out the next clock change?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
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
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