From c969edacab30289aafd2aec043ff8b67146629cd Mon Sep 17 00:00:00 2001 From: Xyedo Date: Mon, 6 May 2024 14:53:13 +0700 Subject: [PATCH 1/2] fix: daylight savings --- rrule.go | 72 ++++++++++++++++++++++++--------------------------- rrule_test.go | 65 ++++++++++++++++++++++++++++++++++++++++++++++ util.go | 6 +++++ 3 files changed, 105 insertions(+), 38 deletions(-) diff --git a/rrule.go b/rrule.go index 4a49f3e..ffccd16 100644 --- a/rrule.go +++ b/rrule.go @@ -132,7 +132,7 @@ type RRule struct { byminute []int bysecond []int byeaster []int - timeset []time.Time + timeDurr []time.Duration len int } @@ -238,18 +238,18 @@ func buildRRule(arg ROption) RRule { } // Reset the timeset value - r.timeset = nil + r.timeDurr = nil if r.freq < HOURLY { - r.timeset = make([]time.Time, 0, len(r.byhour)*len(r.byminute)*len(r.bysecond)) + r.timeDurr = make([]time.Duration, 0, len(r.byhour)*len(r.byminute)*len(r.bysecond)) for _, hour := range r.byhour { for _, minute := range r.byminute { for _, second := range r.bysecond { - r.timeset = append(r.timeset, time.Date(1, 1, 1, hour, minute, second, 0, r.dtstart.Location())) + r.timeDurr = append(r.timeDurr, time.Duration(hour)*time.Hour+time.Duration(minute)*time.Minute+time.Duration(second)*time.Second) } } } - sort.Sort(timeSlice(r.timeset)) + sort.Sort(durrSlice(r.timeDurr)) } r.Options = arg @@ -523,33 +523,33 @@ func (info *iterInfo) calcDaySet(freq Frequency, year int, month time.Month, day } } -func (info *iterInfo) fillTimeSet(set *[]time.Time, freq Frequency, hour, minute, second int) { +func (info *iterInfo) fillTimeDurr(set *[]time.Duration, freq Frequency, hour, minute, second int) { switch freq { case HOURLY: prepareTimeSet(set, len(info.rrule.byminute)*len(info.rrule.bysecond)) for _, minute := range info.rrule.byminute { for _, second := range info.rrule.bysecond { - *set = append(*set, time.Date(1, 1, 1, hour, minute, second, 0, info.rrule.dtstart.Location())) + *set = append(*set, time.Duration(hour)*time.Hour+time.Duration(minute)*time.Minute+time.Duration(second)*time.Second) } } - sort.Sort(timeSlice(*set)) + sort.Sort(durrSlice(*set)) case MINUTELY: prepareTimeSet(set, len(info.rrule.bysecond)) for _, second := range info.rrule.bysecond { - *set = append(*set, time.Date(1, 1, 1, hour, minute, second, 0, info.rrule.dtstart.Location())) + *set = append(*set, time.Duration(hour)*time.Hour+time.Duration(minute)*time.Minute+time.Duration(second)*time.Second) } - sort.Sort(timeSlice(*set)) + sort.Sort(durrSlice(*set)) case SECONDLY: prepareTimeSet(set, 1) - *set = append(*set, time.Date(1, 1, 1, hour, minute, second, 0, info.rrule.dtstart.Location())) + *set = append(*set, time.Duration(hour)*time.Hour+time.Duration(minute)*time.Minute+time.Duration(second)*time.Second) default: prepareTimeSet(set, 0) } } -func prepareTimeSet(set *[]time.Time, length int) { +func prepareTimeSet(set *[]time.Duration, length int) { if len(*set) < length { - *set = make([]time.Time, 0, length) + *set = make([]time.Duration, 0, length) return } @@ -566,7 +566,7 @@ type rIterator struct { second int weekday int ii iterInfo - timeset []time.Time + timedurr []time.Duration total int count int remain reusingRemainSlice @@ -612,14 +612,14 @@ func (iterator *rIterator) generate() { } // Output results - if len(r.bysetpos) != 0 && len(iterator.timeset) != 0 { + if len(r.bysetpos) != 0 && len(iterator.timedurr) != 0 { var poslist []time.Time for _, pos := range r.bysetpos { var daypos, timepos int if pos < 0 { - daypos, timepos = divmod(pos, len(iterator.timeset)) + daypos, timepos = divmod(pos, len(iterator.timedurr)) } else { - daypos, timepos = divmod(pos-1, len(iterator.timeset)) + daypos, timepos = divmod(pos-1, len(iterator.timedurr)) } var temp []int for _, day := range dayset { @@ -631,12 +631,10 @@ func (iterator *rIterator) generate() { if err != nil { continue } - timeTemp := iterator.timeset[timepos] - dateYear, dateMonth, dateDay := iterator.ii.firstyday.AddDate(0, 0, i).Date() - tempHour, tempMinute, tempSecond := timeTemp.Clock() - res := time.Date(dateYear, dateMonth, dateDay, - tempHour, tempMinute, tempSecond, - timeTemp.Nanosecond(), timeTemp.Location()) + timeTemp := iterator.timedurr[timepos] + res := iterator.ii.firstyday.AddDate(0, 0, i). + Add(timeTemp) + if !timeContains(poslist, res) { poslist = append(poslist, res) } @@ -666,19 +664,17 @@ func (iterator *rIterator) generate() { continue } i := day.Int - dateYear, dateMonth, dateDay := iterator.ii.firstyday.AddDate(0, 0, i).Date() - for _, timeTemp := range iterator.timeset { - tempHour, tempMinute, tempSecond := timeTemp.Clock() - res := time.Date(dateYear, dateMonth, dateDay, - tempHour, tempMinute, tempSecond, - timeTemp.Nanosecond(), timeTemp.Location()) - if !r.until.IsZero() && res.After(r.until) { + res := iterator.ii.firstyday.AddDate(0, 0, i) + for _, timeTemp := range iterator.timedurr { + + date := res.Add(timeTemp) + if !r.until.IsZero() && date.After(r.until) { r.len = iterator.total iterator.finished = true return - } else if !res.Before(r.dtstart) { + } else if !date.Before(r.dtstart) { iterator.total++ - iterator.remain.Append(res) + iterator.remain.Append(date) if iterator.count != 0 { iterator.count-- if iterator.count == 0 { @@ -746,7 +742,7 @@ func (iterator *rIterator) generate() { break } } - iterator.ii.fillTimeSet(&iterator.timeset, r.freq, iterator.hour, iterator.minute, iterator.second) + iterator.ii.fillTimeDurr(&iterator.timedurr, r.freq, iterator.hour, iterator.minute, iterator.second) } else if r.freq == MINUTELY { if filtered { // Jump to one iteration before next day @@ -770,7 +766,7 @@ func (iterator *rIterator) generate() { break } } - iterator.ii.fillTimeSet(&iterator.timeset, r.freq, iterator.hour, iterator.minute, iterator.second) + iterator.ii.fillTimeDurr(&iterator.timedurr, r.freq, iterator.hour, iterator.minute, iterator.second) } else if r.freq == SECONDLY { if filtered { // Jump to one iteration before next day @@ -800,7 +796,7 @@ func (iterator *rIterator) generate() { break } } - iterator.ii.fillTimeSet(&iterator.timeset, r.freq, iterator.hour, iterator.minute, iterator.second) + iterator.ii.fillTimeDurr(&iterator.timedurr, r.freq, iterator.hour, iterator.minute, iterator.second) } if fixday && iterator.day > 28 { daysinmonth := daysIn(iterator.month, iterator.year) @@ -888,14 +884,14 @@ func (r *RRule) Iterator() Next { iterator.ii.rebuild(iterator.year, iterator.month) if r.freq < HOURLY { - iterator.timeset = r.timeset + iterator.timedurr = r.timeDurr } else { if r.freq >= HOURLY && len(r.byhour) != 0 && !contains(r.byhour, iterator.hour) || r.freq >= MINUTELY && len(r.byminute) != 0 && !contains(r.byminute, iterator.minute) || r.freq >= SECONDLY && len(r.bysecond) != 0 && !contains(r.bysecond, iterator.second) { - iterator.timeset = nil + iterator.timedurr = nil } else { - iterator.ii.fillTimeSet(&iterator.timeset, r.freq, iterator.hour, iterator.minute, iterator.second) + iterator.ii.fillTimeDurr(&iterator.timedurr, r.freq, iterator.hour, iterator.minute, iterator.second) } } iterator.count = r.count diff --git a/rrule_test.go b/rrule_test.go index a49f09f..cd2cd1c 100644 --- a/rrule_test.go +++ b/rrule_test.go @@ -3883,6 +3883,71 @@ func TestWeekdayGetters(t *testing.T) { } } +func TestDST_HourlyDSTStart(t *testing.T) { + sydLoc, _ := time.LoadLocation("Australia/Sydney") + r, _ := NewRRule(ROption{Freq: HOURLY, Interval: 1, Count: 3, + Dtstart: time.Date(2022, 10, 2, 1, 0, 0, 0, sydLoc), + }) + got := r.All() + want := []string{ + "2022-10-02 01:00:00 +1000 AEST", + "2022-10-02 03:00:00 +1100 AEDT", + "2022-10-02 04:00:00 +1100 AEDT", + } + for i, g := range got { + if g.String() != want[i] { + t.Errorf("got: %v, want: %v", g, want[i]) + } + } + var utcTimes []time.Time + for _, dt := range got { + utcTimes = append(utcTimes, dt.UTC()) + } + want = []string{ + "2022-10-01 15:00:00 +0000 UTC", + "2022-10-01 16:00:00 +0000 UTC", + "2022-10-01 17:00:00 +0000 UTC", + } + + for i, g := range utcTimes { + if g.String() != want[i] { + t.Errorf("got: %v, want: %v", g, want[i]) + } + } +} + +func TestDST_HourlyDSTEnd(t *testing.T) { + sydLoc, _ := time.LoadLocation("Australia/Sydney") + r, _ := NewRRule(ROption{Freq: HOURLY, Interval: 1, Count: 3, + Dtstart: time.Date(2023, 4, 2, 1, 0, 0, 0, sydLoc), + }) + got := r.All() + want := []string{ + "2023-04-02 01:00:00 +1100 AEDT", + "2023-04-02 02:00:00 +1100 AEDT", + "2023-04-02 02:00:00 +1000 AEST", + } + for i, g := range got { + if g.String() != want[i] { + t.Errorf("got: %v, want: %v", g, want[i]) + } + } + + var utcTimes []time.Time + for _, dt := range got { + utcTimes = append(utcTimes, dt.UTC()) + } + want = []string{ + "2023-04-01 14:00:00 +0000 UTC", + "2023-04-01 15:00:00 +0000 UTC", + "2023-04-01 16:00:00 +0000 UTC", + } + for i, g := range utcTimes { + if g.String() != want[i] { + t.Errorf("got: %v, want: %v", g, want[i]) + } + } +} func TestRuleChangeDTStartTimezoneRespected(t *testing.T) { /* https://golang.org/pkg/time/#LoadLocation diff --git a/util.go b/util.go index f187dfe..0620d45 100644 --- a/util.go +++ b/util.go @@ -23,6 +23,12 @@ func (s timeSlice) Len() int { return len(s) } func (s timeSlice) Swap(i, j int) { s[i], s[j] = s[j], s[i] } func (s timeSlice) Less(i, j int) bool { return s[i].Before(s[j]) } +type durrSlice []time.Duration + +func (s durrSlice) Len() int { return len(s) } +func (s durrSlice) Swap(i, j int) { s[i], s[j] = s[j], s[i] } +func (s durrSlice) Less(i, j int) bool { return s[i] < s[j] } + // Python: MO-SU: 0 - 6 // Golang: SU-SAT 0 - 6 func toPyWeekday(from time.Weekday) int { From 77476e592c3847fc6fb88c53973c4ca54f407d21 Mon Sep 17 00:00:00 2001 From: Xyedo Date: Mon, 20 May 2024 17:40:00 +0700 Subject: [PATCH 2/2] Revert "fix: daylight savings" This reverts commit c969edacab30289aafd2aec043ff8b67146629cd. chore: revert back to original solution and add fixes --- rrule.go | 78 +++++++++++++++++++++++------------------ rrule_test.go | 96 +++++++++++++++++++++++++++++++-------------------- util.go | 6 ---- 3 files changed, 103 insertions(+), 77 deletions(-) diff --git a/rrule.go b/rrule.go index ffccd16..44a71a9 100644 --- a/rrule.go +++ b/rrule.go @@ -132,7 +132,7 @@ type RRule struct { byminute []int bysecond []int byeaster []int - timeDurr []time.Duration + timeset []time.Time len int } @@ -238,18 +238,18 @@ func buildRRule(arg ROption) RRule { } // Reset the timeset value - r.timeDurr = nil + r.timeset = nil if r.freq < HOURLY { - r.timeDurr = make([]time.Duration, 0, len(r.byhour)*len(r.byminute)*len(r.bysecond)) + r.timeset = make([]time.Time, 0, len(r.byhour)*len(r.byminute)*len(r.bysecond)) for _, hour := range r.byhour { for _, minute := range r.byminute { for _, second := range r.bysecond { - r.timeDurr = append(r.timeDurr, time.Duration(hour)*time.Hour+time.Duration(minute)*time.Minute+time.Duration(second)*time.Second) + r.timeset = append(r.timeset, time.Date(1, 1, 1, hour, minute, second, 0, r.dtstart.Location())) } } } - sort.Sort(durrSlice(r.timeDurr)) + sort.Sort(timeSlice(r.timeset)) } r.Options = arg @@ -523,33 +523,33 @@ func (info *iterInfo) calcDaySet(freq Frequency, year int, month time.Month, day } } -func (info *iterInfo) fillTimeDurr(set *[]time.Duration, freq Frequency, hour, minute, second int) { +func (info *iterInfo) fillTimeSet(set *[]time.Time, freq Frequency, hour, minute, second int) { switch freq { case HOURLY: prepareTimeSet(set, len(info.rrule.byminute)*len(info.rrule.bysecond)) for _, minute := range info.rrule.byminute { for _, second := range info.rrule.bysecond { - *set = append(*set, time.Duration(hour)*time.Hour+time.Duration(minute)*time.Minute+time.Duration(second)*time.Second) + *set = append(*set, time.Date(1, 1, 1, hour, minute, second, 0, info.rrule.dtstart.Location())) } } - sort.Sort(durrSlice(*set)) + sort.Sort(timeSlice(*set)) case MINUTELY: prepareTimeSet(set, len(info.rrule.bysecond)) for _, second := range info.rrule.bysecond { - *set = append(*set, time.Duration(hour)*time.Hour+time.Duration(minute)*time.Minute+time.Duration(second)*time.Second) + *set = append(*set, time.Date(1, 1, 1, hour, minute, second, 0, info.rrule.dtstart.Location())) } - sort.Sort(durrSlice(*set)) + sort.Sort(timeSlice(*set)) case SECONDLY: prepareTimeSet(set, 1) - *set = append(*set, time.Duration(hour)*time.Hour+time.Duration(minute)*time.Minute+time.Duration(second)*time.Second) + *set = append(*set, time.Date(1, 1, 1, hour, minute, second, 0, info.rrule.dtstart.Location())) default: prepareTimeSet(set, 0) } } -func prepareTimeSet(set *[]time.Duration, length int) { +func prepareTimeSet(set *[]time.Time, length int) { if len(*set) < length { - *set = make([]time.Duration, 0, length) + *set = make([]time.Time, 0, length) return } @@ -566,7 +566,7 @@ type rIterator struct { second int weekday int ii iterInfo - timedurr []time.Duration + timeset []time.Time total int count int remain reusingRemainSlice @@ -612,14 +612,14 @@ func (iterator *rIterator) generate() { } // Output results - if len(r.bysetpos) != 0 && len(iterator.timedurr) != 0 { + if len(r.bysetpos) != 0 && len(iterator.timeset) != 0 { var poslist []time.Time for _, pos := range r.bysetpos { var daypos, timepos int if pos < 0 { - daypos, timepos = divmod(pos, len(iterator.timedurr)) + daypos, timepos = divmod(pos, len(iterator.timeset)) } else { - daypos, timepos = divmod(pos-1, len(iterator.timedurr)) + daypos, timepos = divmod(pos-1, len(iterator.timeset)) } var temp []int for _, day := range dayset { @@ -631,10 +631,12 @@ func (iterator *rIterator) generate() { if err != nil { continue } - timeTemp := iterator.timedurr[timepos] - res := iterator.ii.firstyday.AddDate(0, 0, i). - Add(timeTemp) - + timeTemp := iterator.timeset[timepos] + dateYear, dateMonth, dateDay := iterator.ii.firstyday.AddDate(0, 0, i).Date() + tempHour, tempMinute, tempSecond := timeTemp.Clock() + res := time.Date(dateYear, dateMonth, dateDay, + tempHour, tempMinute, tempSecond, + timeTemp.Nanosecond(), timeTemp.Location()) if !timeContains(poslist, res) { poslist = append(poslist, res) } @@ -664,17 +666,27 @@ func (iterator *rIterator) generate() { continue } i := day.Int - res := iterator.ii.firstyday.AddDate(0, 0, i) - for _, timeTemp := range iterator.timedurr { + date := iterator.ii.firstyday.AddDate(0, 0, i) + dateYear, dateMonth, dateDay := date.Date() + for _, timeTemp := range iterator.timeset { + tempHour, tempMinute, tempSecond := timeTemp.Clock() + + var res time.Time + if r.freq < HOURLY { + res = time.Date(dateYear, dateMonth, dateDay, + tempHour, tempMinute, tempSecond, + timeTemp.Nanosecond(), timeTemp.Location()) + } else { + res = date.Add(time.Duration(tempHour)*time.Hour + time.Duration(tempMinute)*time.Minute + time.Duration(tempSecond)*time.Second) + } - date := res.Add(timeTemp) - if !r.until.IsZero() && date.After(r.until) { + if !r.until.IsZero() && res.After(r.until) { r.len = iterator.total iterator.finished = true return - } else if !date.Before(r.dtstart) { + } else if !res.Before(r.dtstart) { iterator.total++ - iterator.remain.Append(date) + iterator.remain.Append(res) if iterator.count != 0 { iterator.count-- if iterator.count == 0 { @@ -742,7 +754,7 @@ func (iterator *rIterator) generate() { break } } - iterator.ii.fillTimeDurr(&iterator.timedurr, r.freq, iterator.hour, iterator.minute, iterator.second) + iterator.ii.fillTimeSet(&iterator.timeset, r.freq, iterator.hour, iterator.minute, iterator.second) } else if r.freq == MINUTELY { if filtered { // Jump to one iteration before next day @@ -766,7 +778,7 @@ func (iterator *rIterator) generate() { break } } - iterator.ii.fillTimeDurr(&iterator.timedurr, r.freq, iterator.hour, iterator.minute, iterator.second) + iterator.ii.fillTimeSet(&iterator.timeset, r.freq, iterator.hour, iterator.minute, iterator.second) } else if r.freq == SECONDLY { if filtered { // Jump to one iteration before next day @@ -796,7 +808,7 @@ func (iterator *rIterator) generate() { break } } - iterator.ii.fillTimeDurr(&iterator.timedurr, r.freq, iterator.hour, iterator.minute, iterator.second) + iterator.ii.fillTimeSet(&iterator.timeset, r.freq, iterator.hour, iterator.minute, iterator.second) } if fixday && iterator.day > 28 { daysinmonth := daysIn(iterator.month, iterator.year) @@ -884,14 +896,14 @@ func (r *RRule) Iterator() Next { iterator.ii.rebuild(iterator.year, iterator.month) if r.freq < HOURLY { - iterator.timedurr = r.timeDurr + iterator.timeset = r.timeset } else { if r.freq >= HOURLY && len(r.byhour) != 0 && !contains(r.byhour, iterator.hour) || r.freq >= MINUTELY && len(r.byminute) != 0 && !contains(r.byminute, iterator.minute) || r.freq >= SECONDLY && len(r.bysecond) != 0 && !contains(r.bysecond, iterator.second) { - iterator.timedurr = nil + iterator.timeset = nil } else { - iterator.ii.fillTimeDurr(&iterator.timedurr, r.freq, iterator.hour, iterator.minute, iterator.second) + iterator.ii.fillTimeSet(&iterator.timeset, r.freq, iterator.hour, iterator.minute, iterator.second) } } iterator.count = r.count diff --git a/rrule_test.go b/rrule_test.go index cd2cd1c..f807b6c 100644 --- a/rrule_test.go +++ b/rrule_test.go @@ -1,5 +1,6 @@ // 2017-2022, Teambition. All rights reserved. + package rrule import ( @@ -3883,6 +3884,49 @@ func TestWeekdayGetters(t *testing.T) { } } +func TestRuleChangeDTStartTimezoneRespected(t *testing.T) { + /* + https://golang.org/pkg/time/#LoadLocation + + "The time zone database needed by LoadLocation may not be present on all systems, especially non-Unix systems. + LoadLocation looks in the directory or uncompressed zip file named by the ZONEINFO environment variable, + if any, then looks in known installation locations on Unix systems, and finally looks in + $GOROOT/lib/time/zoneinfo.zip." + */ + loc, err := time.LoadLocation("CET") + if err != nil { + t.Fatal("expected", nil, "got", err) + } + + rule, err := NewRRule( + ROption{ + Freq: DAILY, + Count: 10, + Wkst: MO, + Dtstart: time.Date(2019, 3, 6, 1, 1, 1, 0, loc), + }, + ) + if err != nil { + t.Fatal("expected", nil, "got", err) + } + rule.DTStart(time.Date(2019, 3, 6, 0, 0, 0, 0, time.UTC)) + + events := rule.All() + if len(events) != 10 { + t.Fatal("expected", 10, "got", len(events)) + } + + for _, e := range events { + if e.Location().String() != "UTC" { + t.Fatal("expected", "UTC", "got", e.Location().String()) + } + h, m, s := e.Clock() + if (h + m + s) != 0 { + t.Fatal("expected", "0", "got", h, m, s) + } + } +} + func TestDST_HourlyDSTStart(t *testing.T) { sydLoc, _ := time.LoadLocation("Australia/Sydney") r, _ := NewRRule(ROption{Freq: HOURLY, Interval: 1, Count: 3, @@ -3948,49 +3992,25 @@ func TestDST_HourlyDSTEnd(t *testing.T) { } } } -func TestRuleChangeDTStartTimezoneRespected(t *testing.T) { - /* - https://golang.org/pkg/time/#LoadLocation - "The time zone database needed by LoadLocation may not be present on all systems, especially non-Unix systems. - LoadLocation looks in the directory or uncompressed zip file named by the ZONEINFO environment variable, - if any, then looks in known installation locations on Unix systems, and finally looks in - $GOROOT/lib/time/zoneinfo.zip." - */ - loc, err := time.LoadLocation("CET") - if err != nil { - t.Fatal("expected", nil, "got", err) - } - rule, err := NewRRule( - ROption{ - Freq: DAILY, - Count: 10, - Wkst: MO, - Dtstart: time.Date(2019, 3, 6, 1, 1, 1, 0, loc), - }, - ) - if err != nil { - t.Fatal("expected", nil, "got", err) - } - rule.DTStart(time.Date(2019, 3, 6, 0, 0, 0, 0, time.UTC)) - - events := rule.All() - if len(events) != 10 { - t.Fatal("expected", 10, "got", len(events)) +func TestDailyDST(t *testing.T) { + sydney, _ := time.LoadLocation("Australia/Sydney") + r, _ := NewRRule(ROption{ + Freq: DAILY, + Count: 3, + Dtstart: time.Date(2023, 4, 1, 9, 0, 0, 0, sydney), + }) + want := []time.Time{ + time.Date(2023, 4, 1, 9, 0, 0, 0, sydney), + time.Date(2023, 4, 2, 9, 0, 0, 0, sydney), + time.Date(2023, 4, 3, 9, 0, 0, 0, sydney), } - - for _, e := range events { - if e.Location().String() != "UTC" { - t.Fatal("expected", "UTC", "got", e.Location().String()) - } - h, m, s := e.Clock() - if (h + m + s) != 0 { - t.Fatal("expected", "0", "got", h, m, s) - } + value := r.All() + if !timesEqual(value, want) { + t.Errorf("get %v, want %v", value, want) } } - func BenchmarkIterator(b *testing.B) { type testCase struct { Name string diff --git a/util.go b/util.go index 0620d45..f187dfe 100644 --- a/util.go +++ b/util.go @@ -23,12 +23,6 @@ func (s timeSlice) Len() int { return len(s) } func (s timeSlice) Swap(i, j int) { s[i], s[j] = s[j], s[i] } func (s timeSlice) Less(i, j int) bool { return s[i].Before(s[j]) } -type durrSlice []time.Duration - -func (s durrSlice) Len() int { return len(s) } -func (s durrSlice) Swap(i, j int) { s[i], s[j] = s[j], s[i] } -func (s durrSlice) Less(i, j int) bool { return s[i] < s[j] } - // Python: MO-SU: 0 - 6 // Golang: SU-SAT 0 - 6 func toPyWeekday(from time.Weekday) int {