timer: add scheduler/timer with day precision

timer.go has a timer struct for scheduling things to happen once every n
days before or after a given time. Pass a string list of day deltas to
parse, a unit to parse these as (only 24/-24 hours really make sense),
then call Check() on the returned struct with your "since" time, and the
time a timer was last fired. If one goes off, store the time so you can
pass it in subsequent calls. To be used in the user daemon for "remind
every N days" functionality. Was initially gonna allow more precision
than days, but ran into problems, most likely from me overcomplicating
it and not wanting to store too much data. some tests also in
timer_test.go.
This commit is contained in:
Harvey Tindall
2025-08-04 16:02:04 +01:00
parent db1e812190
commit 5fe0e0ab9f
2 changed files with 230 additions and 0 deletions
+138
View File
@@ -0,0 +1,138 @@
package main
import (
"testing"
"time"
)
type fakeClock struct {
now time.Time
}
func (f fakeClock) Now() time.Time { return f.now }
func (f fakeClock) Since(t time.Time) time.Duration { return f.now.Sub(t) }
// Tests the timer with negative time deltas, i.e. reminders before an event.
func TestTimerNegative(t *testing.T) {
as := NewDayTimerSet([]string{
"1", "2", "3", "7",
}, -24*time.Hour)
since := time.Date(2025, 8, 9, 1, 0, 0, 0, time.UTC)
nowTimes := []time.Time{
time.Date(2025, 8, 1, 23, 59, 0, 0, time.UTC),
time.Date(2025, 8, 2, 1, 0, 0, 0, time.UTC),
time.Date(2025, 8, 3, 7, 0, 0, 0, time.UTC),
time.Date(2025, 8, 6, 7, 0, 0, 0, time.UTC),
time.Date(2025, 8, 6, 12, 0, 0, 0, time.UTC),
time.Date(2025, 8, 7, 5, 0, 0, 0, time.UTC),
time.Date(2025, 8, 8, 0, 30, 0, 0, time.UTC),
time.Date(2025, 8, 8, 4, 30, 0, 0, time.UTC),
time.Date(2025, 8, 8, 16, 30, 0, 0, time.UTC),
}
returnValues := []time.Duration{
0, 7, 0, 3, 0, 2, 1, 0, 0,
}
for i := range returnValues {
returnValues[i] *= -24 * time.Hour
}
lastFired := time.Time{}
for i, nt := range nowTimes {
target := returnValues[i]
as.clock = fakeClock{now: nt}
ret := as.Check(since, lastFired)
if ret != target {
t.Fatalf("incorrect return value (%v != %v): i=%d, now=%+v, since=%+v, lastFired=%+v", ret, target, i, nt, since, lastFired)
}
if ret != 0 {
lastFired = nt
}
}
}
func TestTimerSmallInterval(t *testing.T) {
as := NewDayTimerSet([]string{
"1", "1.1", "2", "3", "7",
}, -24*time.Hour)
since := time.Date(2025, 8, 9, 1, 0, 0, 0, time.UTC)
nowTimes := []time.Time{
time.Date(2025, 8, 1, 23, 59, 0, 0, time.UTC),
time.Date(2025, 8, 2, 1, 0, 0, 0, time.UTC),
time.Date(2025, 8, 3, 7, 0, 0, 0, time.UTC),
time.Date(2025, 8, 6, 7, 0, 0, 0, time.UTC),
time.Date(2025, 8, 6, 7, 30, 0, 0, time.UTC),
time.Date(2025, 8, 6, 12, 0, 0, 0, time.UTC),
time.Date(2025, 8, 7, 5, 0, 0, 0, time.UTC),
time.Date(2025, 8, 8, 0, 30, 0, 0, time.UTC),
time.Date(2025, 8, 8, 4, 30, 0, 0, time.UTC),
time.Date(2025, 8, 8, 16, 30, 0, 0, time.UTC),
}
returnValues := []time.Duration{
0, 7, 0, 3, 0, 0, 2, 1, 0, 0,
}
for i := range returnValues {
returnValues[i] *= -24 * time.Hour
}
lastFired := time.Time{}
for i, nt := range nowTimes {
target := returnValues[i]
as.clock = fakeClock{now: nt}
ret := as.Check(since, lastFired)
if ret != target {
t.Fatalf("incorrect return value (%v != %v): i=%d, now=%+v, since=%+v, lastFired=%+v", ret, target, i, nt, since, lastFired)
}
if ret != 0 {
lastFired = nt
}
}
}
func TestTimerBruteForce(t *testing.T) {
as := NewDayTimerSet([]string{
"1", "2", "3", "7",
}, -24*time.Hour)
since := time.Date(2025, 8, 9, 1, 0, 0, 0, time.UTC)
returnedValues := map[time.Duration]time.Time{}
lastFired := time.Time{}
for dd := range 12 {
for hh := range 24 {
for mm := range 60 {
nt := time.Date(2025, 8, dd, hh, mm, 0, 0, time.UTC)
as.clock = fakeClock{now: nt}
ret := as.Check(since, lastFired)
if dupe, ok := returnedValues[ret]; ok {
t.Fatalf("duplicate return value (%v): now=%+v, dupe=%+v, since=%+v, lastFired=%+v", ret, nt, dupe, since, lastFired)
}
if ret != 0 {
returnedValues[ret] = nt
lastFired = nt
}
}
}
}
if len(returnedValues) != len(as.deltas) {
t.Fatalf("not all timers fired (%d/%d)", len(returnedValues), len(as.deltas))
}
}