From 15bbcaa68c455275a30fb2863179ca2bb8a5249f Mon Sep 17 00:00:00 2001 From: sunshineplan Date: Fri, 12 Jan 2024 16:36:30 +0800 Subject: [PATCH] Create clock package --- clock/clock.go | 148 +++++++++++++++++++++++++++++++++ clock/clock_test.go | 97 ++++++++++++++++++++++ scheduler/clock.go | 167 ++++++++++++++++++++++---------------- scheduler/complex_test.go | 2 +- scheduler/schedule.go | 4 +- 5 files changed, 345 insertions(+), 73 deletions(-) create mode 100644 clock/clock.go create mode 100644 clock/clock_test.go diff --git a/clock/clock.go b/clock/clock.go new file mode 100644 index 0000000..cad836a --- /dev/null +++ b/clock/clock.go @@ -0,0 +1,148 @@ +package clock + +import ( + "cmp" + "encoding" + "fmt" + "time" +) + +const ( + secondsPerMinute = 60 + secondsPerHour = 60 * secondsPerMinute + secondsPerDay = 24 * secondsPerHour +) + +var ( + _ encoding.TextMarshaler = Clock{} + _ encoding.TextUnmarshaler = new(Clock) +) + +type Clock struct { + wall uint64 +} + +func abs[Int int | uint64](wall Int) uint64 { + for wall < 0 { + wall += secondsPerDay + } + return uint64(wall % secondsPerDay) +} + +func New(hour, min, sec int) Clock { + return Clock{abs(hour*secondsPerHour + min*secondsPerMinute + sec)} +} + +var clockLayout = []string{ + "15:04", + time.TimeOnly, + time.Kitchen, + "03:04PM", + "03:04:05PM", +} + +func Parse(v string) (Clock, error) { + for _, layout := range clockLayout { + if t, err := time.Parse(layout, v); err == nil { + return ParseTime(t), nil + } + } + return Clock{}, fmt.Errorf("cannot parse %q as clock", v) +} + +func ParseTime(t time.Time) Clock { + return New(t.Clock()) +} + +func Now() Clock { + return ParseTime(time.Now()) +} + +func (c Clock) Time() time.Time { + return time.Unix(int64(c.wall), 0).UTC() +} + +func (c Clock) Clock() (hour, min, sec int) { + sec = int(c.wall) + hour = sec / secondsPerHour + sec -= hour * secondsPerHour + min = sec / secondsPerMinute + sec -= min * secondsPerMinute + return +} + +func (c Clock) Hour() int { + return int(c.wall%secondsPerDay) / secondsPerHour +} + +func (c Clock) Minute() int { + return int(c.wall%secondsPerHour) / secondsPerMinute +} + +func (c Clock) Second() int { + return int(c.wall % secondsPerMinute) +} + +func (c Clock) String() string { + return fmt.Sprintf("%d:%02d:%02d", c.Hour(), c.Minute(), c.Second()) +} + +func (c Clock) MarshalText() (text []byte, err error) { + text = []byte(c.String()) + return +} + +func (c *Clock) UnmarshalText(text []byte) error { + clock, err := Parse(string(text)) + if err != nil { + return err + } + *c = clock + return nil +} + +func (c Clock) After(u Clock) bool { + return c.wall > u.wall +} + +func (c Clock) Before(u Clock) bool { + return c.wall < u.wall +} + +func (c Clock) Equal(u Clock) bool { + return c.wall == u.wall +} + +func (c Clock) Compare(u Clock) int { + return cmp.Compare(c.wall, u.wall) +} + +func (c Clock) Add(d time.Duration) Clock { + return Clock{abs(c.wall + uint64(d.Seconds()))} +} + +func (c Clock) Sub(u Clock) time.Duration { + return time.Duration(int64(c.wall)-int64(u.wall)) * time.Second +} + +func (c Clock) Since(u Clock) time.Duration { + return u.Until(c) +} + +func (c Clock) Until(u Clock) time.Duration { + d := int64(u.wall) - int64(c.wall) + if d == 0 { + return 0 + } else if d < 0 { + d += secondsPerDay + } + return time.Duration(d) * time.Second +} + +func Since(c Clock) time.Duration { + return c.Until(Now()) +} + +func Until(c Clock) time.Duration { + return Now().Until(c) +} diff --git a/clock/clock_test.go b/clock/clock_test.go new file mode 100644 index 0000000..f6f3b66 --- /dev/null +++ b/clock/clock_test.go @@ -0,0 +1,97 @@ +package clock + +import ( + "testing" + "time" +) + +func TestClock(t *testing.T) { + for i, tc := range []struct { + hour, min, sec int + c Clock + str string + }{ + {0, 0, 0, Clock{}, "0:00:00"}, + {1, 2, 3, New(1, 2, 3), "1:02:03"}, + {24, 0, 0, Clock{}, "0:00:00"}, + {0, 60, 0, New(1, 0, 0), "1:00:00"}, + {0, 0, 60, New(0, 1, 0), "0:01:00"}, + {0, 0, -1, New(23, 59, 59), "23:59:59"}, + } { + if got := New(tc.hour, tc.min, tc.sec); got != tc.c { + t.Errorf("#%d: New(%d, %d, %d): got %v; want %v", i, tc.hour, tc.min, tc.sec, got, tc.c) + } else if got.String() != tc.str { + t.Errorf("#%d: New(%d, %d, %d): got %q; want %q", i, tc.hour, tc.min, tc.sec, got.String(), tc.str) + } + } +} + +func TestParse(t *testing.T) { + for i, testcase := range []struct { + s string + expected Clock + str string + }{ + {"7:01", New(7, 1, 0), "7:01:00"}, + {"7:01:02", New(7, 1, 2), "7:01:02"}, + {"7:02PM", New(19, 2, 0), "19:02:00"}, + {"07:03", New(7, 3, 0), "7:03:00"}, + {"07:04:02", New(7, 4, 2), "7:04:02"}, + {"19:04:30", New(19, 4, 30), "19:04:30"}, + } { + if res, err := Parse(testcase.s); err != nil { + t.Error(err) + } else if res != testcase.expected { + t.Errorf("%s expected %v; got %v", testcase.s, testcase.expected, res) + } else if res.String() != testcase.str { + t.Errorf("#%d: got %q; want %q", i, res.String(), testcase.str) + } + } + for _, testcase := range []string{ + "", + "abc", + "24:00", + } { + if _, err := Parse(testcase); err == nil { + t.Errorf("%s expected error; got nil", testcase) + } + } +} + +func TestSub(t *testing.T) { + for i, tc := range []struct { + c Clock + u Clock + d time.Duration + }{ + {Clock{}, Clock{}, 0}, + {New(0, 0, 1), Clock{}, time.Second}, + {Clock{}, New(0, 0, 1), -time.Second}, + {New(6, 5, 4), Clock{}, 6*time.Hour + 5*time.Minute + 4*time.Second}, + {New(1, 0, 0), New(0, 30, 0), 30 * time.Minute}, + {New(12, 0, 0), New(12, 30, 0), -30 * time.Minute}, + } { + if got := tc.c.Sub(tc.u); got != tc.d { + t.Errorf("#%d: Sub(%v, %v): got %v; want %v", i, tc.c, tc.u, got, tc.d) + } + } +} + +func TestUntil(t *testing.T) { + for i, tc := range []struct { + c Clock + u Clock + d time.Duration + }{ + {Clock{}, Clock{}, 0}, + {Clock{}, New(0, 0, 1), time.Second}, + {New(0, 0, 1), Clock{}, 23*time.Hour + 59*time.Minute + 59*time.Second}, + {Clock{}, New(6, 5, 4), 6*time.Hour + 5*time.Minute + 4*time.Second}, + {New(0, 30, 0), New(1, 0, 0), 30 * time.Minute}, + {New(12, 30, 0), New(12, 0, 0), 23*time.Hour + 30*time.Minute}, + } { + if got := tc.c.Until(tc.u); got != tc.d { + t.Errorf("#%d: Sub(%v, %v): got %v; want %v", i, tc.c, tc.u, got, tc.d) + } + } +} diff --git a/scheduler/clock.go b/scheduler/clock.go index 0d2788e..74e29f1 100644 --- a/scheduler/clock.go +++ b/scheduler/clock.go @@ -3,6 +3,8 @@ package scheduler import ( "fmt" "time" + + "github.com/sunshineplan/utils/clock" ) var ( @@ -10,13 +12,22 @@ var ( _ Schedule = clockSched{} ) -var clockLayout = []string{ - "15:04", - "15:04:05", +type Clock struct { + clock.Clock + hour, min, sec bool } -type Clock struct { - hour, min, sec int +func atClock(hour, min, sec int) clock.Clock { + if hour == -1 { + hour = 0 + } + if min == -1 { + min = 0 + } + if sec == -1 { + sec = 0 + } + return clock.New(hour, min, sec) } func AtClock(hour, min, sec int) *Clock { @@ -25,7 +36,18 @@ func AtClock(hour, min, sec int) *Clock { sec > 59 || sec < -1 { panic(fmt.Sprintf("invalid clock: hour(%d) min(%d) sec(%d)", hour, min, sec)) } - return &Clock{hour, min, sec} + var c Clock + if hour > -1 { + c.hour = true + } + if min > -1 { + c.min = true + } + if sec > -1 { + c.sec = true + } + c.Clock = atClock(hour, min, sec) + return &c } func FullClock() *Clock { return AtClock(-1, -1, -1) } @@ -43,11 +65,11 @@ func AtSecond(sec int) *Clock { } func ClockFromString(str string) *Clock { - t, err := parseTime(str, clockLayout) + c, err := clock.Parse(str) if err != nil { panic(err) } - return AtClock(t.Clock()) + return &Clock{c, true, true, true} } func HourSchedule(hour ...int) Schedule { @@ -78,7 +100,12 @@ func (c *Clock) Hour(hour int) *Clock { if hour > 23 || hour < -1 { panic(fmt.Sprint("invalid hour ", hour)) } - c.hour = hour + if hour > -1 { + c.hour = true + } else { + c.hour = false + } + c.Clock = atClock(hour, c.Clock.Minute(), c.Clock.Second()) return c } @@ -86,7 +113,12 @@ func (c *Clock) Minute(min int) *Clock { if min > 59 || min < -1 { panic(fmt.Sprint("invalid minute ", min)) } - c.min = min + if min > -1 { + c.min = true + } else { + c.min = false + } + c.Clock = atClock(c.Clock.Hour(), min, c.Clock.Second()) return c } @@ -94,102 +126,95 @@ func (c *Clock) Second(sec int) *Clock { if sec > 59 || sec < -1 { panic(fmt.Sprint("invalid second ", sec)) } - c.sec = sec + if sec > -1 { + c.sec = true + } else { + c.sec = false + } + c.Clock = atClock(c.Clock.Hour(), c.Clock.Minute(), sec) return c } func (c Clock) IsMatched(t time.Time) bool { hour, min, sec := t.Clock() - return (c.hour == -1 || c.hour == hour) && - (c.min == -1 || c.min == min) && - (c.sec == -1 || c.sec == sec) + return (!c.hour || c.Clock.Hour() == hour) && + (!c.min || c.Clock.Minute() == min) && + (!c.sec || c.Clock.Second() == sec) } func (c Clock) First(t time.Time) time.Duration { - now, clock := AtClock(t.Clock()), Clock{} - if c.sec == -1 { - clock.sec = now.sec + u, nc := AtClock(t.Clock()), new(Clock) + if c.sec { + nc.Second(c.Clock.Second()) } else { - clock.sec = c.sec + nc.Second(u.Clock.Second()) } - if c.min == -1 { - clock.min = now.min + if c.min { + nc.Minute(c.Clock.Minute()) } else { - clock.min = c.min + nc.Minute(u.Clock.Minute()) } - if c.hour == -1 { - clock.hour = now.hour + if c.hour { + nc.Hour(c.Clock.Hour()) } else { - clock.hour = c.hour + nc.Hour(u.Clock.Hour()) } - if clock.Time().Equal(now.Time()) { + if nc.Equal(u.Clock) { return 0 } - if c.sec == -1 { - clock.sec = 0 + if !c.sec { + nc.Second(0) } - if clock.Time().Before(now.Time()) { - if c.min == -1 { - if clock.hour == now.hour { - return clock.Time().Add(time.Minute).Sub(now.Time()) + if nc.Before(u.Clock) { + if !c.min { + if nc.Clock.Hour() == u.Clock.Hour() { + return nc.Add(time.Minute).Since(u.Clock) } - clock.min = 0 + nc.Minute(0) } - if c.hour == -1 { - return clock.Time().Add(time.Hour).Sub(now.Time()) + if !c.hour { + return nc.Add(time.Hour).Since(u.Clock) } - return clock.Time().Add(24 * time.Hour).Sub(now.Time()) - } else if c.min == -1 && clock.hour != now.hour { - clock.min = 0 + return nc.Add(24 * time.Hour).Since(u.Clock) + } else if !c.min && nc.Clock.Hour() != u.Clock.Hour() { + nc.Minute(0) } - return clock.Time().Sub(now.Time()) + return nc.Since(u.Clock) } func (c Clock) TickerDuration() time.Duration { - switch c.sec { - case -1: + if !c.sec { return time.Second - default: - switch c.min { - case -1: - return time.Minute - default: - switch c.hour { - case -1: - return time.Hour - default: - return 24 * time.Hour - } - } + } else if !c.min { + return time.Minute + } else if !c.hour { + return time.Hour + } else { + return 24 * time.Hour } } func (c Clock) String() string { var hour, min, sec string - if c.hour == -1 { + if !c.hour { hour = "--" } else { - hour = fmt.Sprint(c.hour) + hour = fmt.Sprint(c.Clock.Hour()) } - if c.min == -1 { + if !c.min { min = "--" } else { - min = fmt.Sprintf("%02d", c.min) + min = fmt.Sprintf("%02d", c.Clock.Minute()) } - if c.sec == -1 { + if !c.sec { sec = "--" } else { - sec = fmt.Sprintf("%02d", c.sec) + sec = fmt.Sprintf("%02d", c.Clock.Second()) } return fmt.Sprintf("%s:%s:%s", hour, min, sec) } -func (c Clock) Time() time.Time { - t, _ := time.Parse("15:04:05", c.String()) - return t -} - type clockSched struct { start, end *Clock d time.Duration @@ -203,27 +228,27 @@ func ClockSchedule(start, end *Clock, d time.Duration) Schedule { } func (s clockSched) IsMatched(t time.Time) bool { - start, end, t := s.start.Time(), s.end.Time(), AtClock(t.Clock()).Time() - return (start.Equal(t) || start.Before(t) && end.After(t) || end.Equal(t)) && t.Sub(start)%s.d == 0 + start, end, tc := s.start, s.end, AtClock(t.Clock()).Clock + return (start.Equal(tc) || start.Before(tc) && end.After(tc) || end.Equal(tc)) && tc.Since(start.Clock)%s.d == 0 } func (s clockSched) First(t time.Time) time.Duration { if s.IsMatched(t) { return 0 } - start, end, t := s.start.Time(), s.end.Time(), AtClock(t.Clock()).Time() - for clock := AtClock(t.Clock()).Time(); clock.Compare(start) != -1 && clock.Compare(end) != 1; clock = clock.Add(time.Second) { - if s.IsMatched(clock) { - return clock.Sub(t) + start, end, tc := s.start.Clock, s.end.Clock, AtClock(t.Clock()).Clock + for c := AtClock(t.Clock()); c.Compare(start) != -1 && c.Compare(end) != 1; c.Clock = c.Add(time.Second) { + if s.IsMatched(c.Time()) { + return c.Since(tc) } } return s.start.First(t) } func (s clockSched) TickerDuration() time.Duration { - if s.start.sec != 0 { + if s.start.Clock.Second() != 0 { return time.Second - } else if s.start.min != 0 && s.d%time.Minute == 0 { + } else if s.start.Clock.Minute() != 0 && s.d%time.Minute == 0 { return time.Minute } return s.d diff --git a/scheduler/complex_test.go b/scheduler/complex_test.go index 4aefaf9..ab124a5 100644 --- a/scheduler/complex_test.go +++ b/scheduler/complex_test.go @@ -44,7 +44,7 @@ func TestMultiScheduleTickerDuration(t *testing.T) { } func TestConditionSchedule(t *testing.T) { - s := ConditionSchedule(Weekends, MultiSchedule(AtClock(9, 30, 0), AtHour(15))) + s := ConditionSchedule(Weekdays, MultiSchedule(AtClock(9, 30, 0), AtHour(15))) if d := s.TickerDuration(); d != time.Second { t.Fatalf("expected 1s: got %s", d) } diff --git a/scheduler/schedule.go b/scheduler/schedule.go index 7cf142e..8b81cbe 100644 --- a/scheduler/schedule.go +++ b/scheduler/schedule.go @@ -3,6 +3,8 @@ package scheduler import ( "fmt" "time" + + "github.com/sunshineplan/utils/clock" ) type Schedule interface { @@ -28,7 +30,7 @@ var datetimeLayout = []string{ func ScheduleFromString(str ...string) Schedule { var s multiSched for _, str := range str { - if _, err := parseTime(str, clockLayout); err == nil { + if _, err := clock.Parse(str); err == nil { s = append(s, ClockFromString(str)) continue }