package again import ( "fmt" "time" webhooks "git.rootprojects.org/root/go-again/webhooks" ) type Webhook webhooks.Webhook type Schedule struct { ID string `json:"id" db:"id"` AccessID string `json:"-" db:"access_id"` Date string `json:"date" db:"date"` Time string `json:"time" db:"time"` TZ string `json:"tz" db:"tz"` NextRunAt time.Time `json:"next_run_at" db:"next_run_at"` Webhooks []Webhook `json:"webhooks" db"webhooks"` } type Schedules []*Schedule func (s Schedules) Len() int { return len(s) } func (s Schedules) Less(i, j int) bool { return s[i].NextRunAt.Sub(s[j].NextRunAt) < 0 } func (s Schedules) Swap(i, j int) { s[j], s[i] = s[i], s[j] } // https://yourbasic.org/golang/time-change-convert-location-timezone/ // https://sebest.github.io/post/create-a-small-docker-image-for-a-golang-binary/ // https://github.com/FKSE/docker-golang-base // tar cfz zoneinfo.tar.gz /usr/share/zoneinfo // git clone https://github.com/eggert/tz // grep '^Rule' -r tz/ | cut -f8-10 | egrep -iv 'Rule|SAVE' // egrep '\s0:30' -r tz/ func Run() { // blacklist "", "Local" // UTC to TZ should always be correct // TZ to UTC may not be correct now := time.Now() fmt.Println("Now", now.Format(time.RFC3339)) loc, err := time.LoadLocation("America/Phoenix") if nil != err { panic(err) } fmt.Println("Loc", now.In(loc)) /* boundary checks */ for _, st := range [][]int{ []int{2019, 11, 10, 01, 02, 56, 0}, []int{2019, 11, 10, 01, 02, 60, 0}, []int{2019, 11, 10, 01, 60, 59, 0}, []int{2019, 11, 10, 24, 59, 59, 0}, []int{2019, 11, 10, 25, 59, 59, 0}, []int{2019, 11, 10, 23, 59, 59, 0}, []int{2019, 11, 31, 23, 0, 0, 0}, } { _, err := Exists(st, "America/Denver") if nil != err { fmt.Println(err) } } /* funky times */ tz := "America/Denver" fmt.Println("funky") for _, st := range [][]int{ []int{2019, 3, 10, 01, 59, 00, 0}, []int{2019, 3, 10, 02, 00, 00, 0}, []int{2019, 3, 10, 02, 01, 00, 0}, []int{2019, 3, 10, 02, 59, 00, 0}, []int{2019, 3, 10, 03, 00, 00, 0}, []int{2019, 3, 10, 03, 01, 00, 0}, []int{2019, 11, 03, 0, 59, 00, 0}, []int{2019, 11, 03, 01, 59, 00, 0}, []int{2019, 11, 03, 02, 00, 00, 0}, []int{2019, 11, 03, 02, 01, 00, 0}, []int{2019, 11, 03, 02, 59, 00, 0}, []int{2019, 11, 03, 03, 00, 00, 0}, []int{2019, 11, 03, 03, 01, 00, 0}, []int{2019, 11, 03, 04, 01, 00, 0}, []int{2019, 11, 03, 05, 01, 00, 0}, } { err := IsAmbiguous(st, tz) if nil != err { fmt.Println(err) } } } type ErrNoExist struct { e string t []int z string } func (err ErrNoExist) Error() string { return fmt.Sprintf("E_INVALID_TIME: '%#v' is not a valid timestamp at '%s': %s", err.t, err.z, err.e) } // Check if the time is a real time in the given timezone. // // For example: 2:30am doesn't happen on 2019 March 10th according // to America/Denver local time, due to the end of Daylight Savings Time, // but it does happen in America/Phoenix. // // Also rejects times that are parsable and return a valid date object, // but are not canonical, such as 24:60:75 November 31st, 2020. // (which would be represented as `2020-12-02 02:01:15 +0000 UTC`) // // Example of parsable, but non-canonical time: // // var loc *time.Location // loc, _ = time.LoadLocation("America/Denver") // t := time.Date(2019, time.March, 10, 1, 30, 0, 0, loc) // fmt.Println(t, "==", t.UTC()) // // 2019-03-10 01:30:00 -0700 MST == 2019-03-10 08:30:00 +0000 UTC // t = t.Add(time.Duration(1) * time.Hour) // fmt.Println(t, "==", t.UTC()) // // 2019-03-10 03:30:00 -0600 MDT == 2019-03-10 09:30:00 +0000 UTC // // Example of a canonical, but non-parsable time (the 2016 leap second): // // fmt.Println(time.Date(2016, time.December, 31, 23, 59, 60, 0, time.UTC)) // "2020-12-02 02:00:00 +0000 UTC" // should be "2016-12-31 23:59:60 +0000 UTC" // func Exists(st []int, tzstr string) (*time.Time, error) { tz, err := time.LoadLocation(tzstr) if nil != err { return nil, err } m := time.Month(st[1]) t1 := time.Date(st[0], m, st[2], st[3], st[4], st[5], 0, tz) if st[5] != t1.Second() { return nil, ErrNoExist{ t: st, z: tzstr, e: "invalid second, probably just bad math on your part", } } if st[4] != t1.Minute() { return nil, ErrNoExist{ t: st, z: tzstr, e: "invalid minute, probably just bad math on your part, but perhaps a half-hour daylight savings or summer time", } } if st[3] != t1.Hour() { return nil, ErrNoExist{ t: st, z: tzstr, e: "invalid hour, possibly a Daylight Savings or Summer Time error, or perhaps bad math on your part", } } if st[2] != t1.Day() { return nil, ErrNoExist{ t: st, z: tzstr, e: "invalid day of month, most likely bad math on your part. Remember: 31 28ΒΌ 31 30 31 30 31 31 30 31 30 31", } } if st[1] != int(t1.Month()) { return nil, ErrNoExist{ t: st, z: tzstr, e: "invalid month, most likely bad math on your part. Remember: Decemberween isn't until next year", } } if st[0] != t1.Year() { return nil, ErrNoExist{ t: st, z: tzstr, e: "invalid year, must have reached the end of time...", } } return &t1, nil } // Check if the time happens more than once in a given timezone. // // For example: 1:30am happens only once on 2019 Nov 3rd according to // America/Phoenix time but due to the start of Daylight Savings Time, // it happens twice in America/Denver. // // Example of duplicate, non-canonical time: // // var loc *time.Location // loc, _ = time.LoadLocation("America/Denver") // t = time.Date(2019, time.November, 3, 1, 30, 0, 0, loc) // fmt.Println(t, "==", t.UTC()) // // 2019-11-03 01:30:00 -0600 MDT == 2019-11-03 07:30:00 +0000 UTC // t = t.Add(time.Duration(1) * time.Hour) // fmt.Println(t, "==", t.UTC()) // // 2019-11-03 01:30:00 -0700 MST == 2019-11-03 08:30:00 +0000 UTC // t = t.Add(time.Duration(1) * time.Hour) // fmt.Println(t, "==", t.UTC()) // // 2019-11-03 02:30:00 -0700 MST == 2019-11-03 09:30:00 +0000 UTC // func IsAmbiguous(st []int, tzstr string) error { // Does the time exist twice? // (if I change the time in UTC, do I still get the same time) // Note: Some timezones change by half or quarter hour // However, it seems that DST always changes by one or two whole hours // Oh, and then there's Luthuania... // Rule LH 2008 max - Oct Sun>=1 2:00 0:30 - // // https://en.wikipedia.org/wiki/Daylight_saving_time_by_country // https://en.wikipedia.org/wiki/Winter_time_(clock_lag) // https://en.wikipedia.org/wiki/Summer_time_in_Europe // https://en.wikipedia.org/wiki/Daylight_saving_time_in_the_Americas // If I change the time iadd or subtract time in UTC, do I see the same difference in TZ? tz, err := time.LoadLocation(tzstr) if nil != err { return err } m := time.Month(st[1]) t1 := time.Date(st[0], m, st[2], st[3], st[4], st[5], 0, tz) u1 := t1.UTC() // Australia/Lord_Howe has a 30-minute DST // 60-minute DST is common // Antarctica/Troll has a 120-minute DST for _, n := range []int{30, 60, 120} { t2 := time.Date(st[0], m, st[2], st[3], st[4]+n, st[5], 0, tz) u2 := t2.UTC() if u1.Equal(u2) { return fmt.Errorf("Ambiguous: %s, %s, %+d\n", t1, t2, n) } } return nil } /* ////// ////// 9:01 twice ////// var d = new Date("3/10/2019, 01:59:00"); console.log("tzUTC:" + tzUTC(d, 'America/Denver')); // tzUTC:Sun, 10 Mar 2019 08:59:00 GMT var d = new Date("3/10/2019, 02:01:00"); console.log("tzUTC:" + tzUTC(d, 'America/Denver')); // tzUTC:Sun, 10 Mar 2019 09:01:00 GMT var d = new Date("3/10/2019, 02:59:00"); console.log("tzUTC:" + tzUTC(d, 'America/Denver')); // tzUTC:Sun, 10 Mar 2019 09:59:00 GMT var d = new Date("3/10/2019, 03:01:00"); console.log("tzUTC:" + tzUTC(d, 'America/Denver')); // tzUTC:Sun, 10 Mar 2019 09:01:00 GMT ////// ////// 8:01 never ////// var d = new Date("11/03/2019, 01:59:00"); console.log("tzUTC:" + tzUTC(d, 'America/Denver')); // tzUTC:Sun, 03 Nov 2019 07:59:00 GMT var d = new Date("11/03/2019, 02:01:00"); console.log("tzUTC:" + tzUTC(d, 'America/Denver')); // tzUTC:Sun, 03 Nov 2019 09:01:00 GMT var d = new Date("11/03/2019, 02:59:00"); console.log("tzUTC:" + tzUTC(d, 'America/Denver')); // tzUTC:Sun, 03 Nov 2019 09:59:00 GMT var d = new Date("11/03/2019, 03:01:00"); console.log("tzUTC:" + tzUTC(d, 'America/Denver')); tzUTC:Sun, 03 Nov 2019 10:01:00 GMT */