diff --git a/.gitignore b/.gitignore index 836a306..3fb7ae3 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1 @@ -test.db* -plannersync -plan +*.db* diff --git a/Makefile b/Makefile index ea7ba3d..3a302ca 100644 --- a/Makefile +++ b/Makefile @@ -7,5 +7,15 @@ sync-run: sync-debug: cd sync/service && dlv debug . -- -dbname localhost -dbport 5432 -dbname planner -dbuser test -dbpassword test -port 8092 -key testKey +sync-build: + go build -o dist/plannersync ./sync/service/ + +sync-deploy: + ssh server sudo /usr/bin/systemctl stop plannersync.service + scp dist/plannersync server:/usr/local/bin/plannersync + ssh server sudo /usr/bin/systemctl start plannersync.service + database: docker run -e POSTGRES_USER=test -e POSTGRES_PASSWORD=test -e POSTGRES_DB=planner -p 5432:5432 postgres:16 + + diff --git a/dist/plannersync b/dist/plannersync new file mode 100755 index 0000000..5a456c3 Binary files /dev/null and b/dist/plannersync differ diff --git a/item/date.go b/item/date.go new file mode 100644 index 0000000..6454d61 --- /dev/null +++ b/item/date.go @@ -0,0 +1,279 @@ +package item + +import ( + "encoding/json" + "fmt" + "sort" + "strings" + "time" +) + +const ( + DateFormat = "2006-01-02 (Monday)" +) + +func Today() Date { + year, month, day := time.Now().Date() + return NewDate(year, int(month), day) +} + +type Weekdays []time.Weekday + +func (wds Weekdays) Len() int { return len(wds) } +func (wds Weekdays) Swap(i, j int) { wds[j], wds[i] = wds[i], wds[j] } +func (wds Weekdays) Less(i, j int) bool { + if wds[i] == time.Sunday { + return false + } + if wds[j] == time.Sunday { + return true + } + + return int(wds[i]) < int(wds[j]) +} + +func (wds Weekdays) Unique() Weekdays { + mwds := map[time.Weekday]bool{} + for _, wd := range wds { + mwds[wd] = true + } + newWds := Weekdays{} + for wd := range mwds { + newWds = append(newWds, wd) + } + sort.Sort(newWds) + + return newWds +} + +type Date struct { + t time.Time +} + +func (d *Date) MarshalJSON() ([]byte, error) { + return json.Marshal(d.String()) +} + +func (d *Date) UnmarshalJSON(data []byte) error { + dateString := "" + if err := json.Unmarshal(data, &dateString); err != nil { + return err + } + nd := NewDateFromString(dateString) + d.t = nd.Time() + + return nil +} + +func NewDate(year, month, day int) Date { + + if year == 0 || month == 0 || month > 12 || day == 0 { + return Date{} + } + + var m time.Month + switch month { + case 1: + m = time.January + case 2: + m = time.February + case 3: + m = time.March + case 4: + m = time.April + case 5: + m = time.May + case 6: + m = time.June + case 7: + m = time.July + case 8: + m = time.August + case 9: + m = time.September + case 10: + m = time.October + case 11: + m = time.November + case 12: + m = time.December + } + + t := time.Date(year, m, day, 0, 0, 0, 0, time.UTC) + + return Date{ + t: t, + } +} + +func NewDateFromString(date string) Date { + date = strings.ToLower(strings.TrimSpace(date)) + + switch date { + case "": + fallthrough + case "no-date": + fallthrough + case "no date": + return Date{} + case "today": + return Today() + case "tod": + return Today() + case "tomorrow": + return Today().AddDays(1) + case "tom": + return Today().AddDays(1) + } + + t, err := time.Parse("2006-01-02", fmt.Sprintf("%.10s", date)) + if err == nil { + return Date{t: t} + } + + newWeekday, ok := ParseWeekday(date) + if !ok { + return Date{} + } + daysToAdd := findDaysToWeekday(Today().Weekday(), newWeekday) + + return Today().Add(daysToAdd) +} + +func findDaysToWeekday(current, wanted time.Weekday) int { + daysToAdd := int(wanted) - int(current) + if daysToAdd <= 0 { + daysToAdd += 7 + } + + return daysToAdd +} + +func (d Date) DaysBetween(d2 Date) int { + tDate := d2 + end := d + if !end.After(tDate) { + end = d2 + tDate = d + } + + days := 0 + for { + if tDate.Add(days).Equal(end) { + return days + } + days++ + } +} + +func (d Date) String() string { + if d.t.IsZero() { + return "no date" + } + + return strings.ToLower(d.t.Format(DateFormat)) +} + +func (d Date) Human() string { + switch { + case d.IsZero(): + return "-" + case d.Equal(Today()): + return "today" + case d.Equal(Today().Add(1)): + return "tomorrow" + case d.After(Today()) && Today().Add(8).After(d): + return strings.ToLower(d.t.Format("Monday")) + default: + return strings.ToLower(d.t.Format(DateFormat)) + } +} + +func (d Date) IsZero() bool { + return d.t.IsZero() +} + +func (d Date) Time() time.Time { + return d.t +} + +func (d Date) Weekday() time.Weekday { + return d.t.Weekday() +} + +func (d Date) Day() int { + return d.t.Day() +} + +func (d Date) Add(days int) Date { + year, month, day := d.t.Date() + return NewDate(year, int(month), day+days) +} + +func (d Date) AddMonths(addMonths int) Date { + year, mmonth, day := d.t.Date() + month := int(mmonth) + for m := 1; m <= addMonths; m++ { + month += 1 + if month == 12 { + year += 1 + month = 1 + } + } + + return NewDate(year, month, day) +} + +func (d Date) Equal(ud Date) bool { + return d.t.Equal(ud.Time()) +} + +// After reports whether d is after ud +func (d Date) After(ud Date) bool { + return d.t.After(ud.Time()) +} + +func (d Date) AddDays(amount int) Date { + year, month, date := d.t.Date() + + return NewDate(year, int(month), date+amount) +} + +func ParseWeekday(wd string) (time.Weekday, bool) { + switch lowerAndTrim(wd) { + case "monday": + return time.Monday, true + case "mon": + return time.Monday, true + case "tuesday": + return time.Tuesday, true + case "tue": + return time.Tuesday, true + case "wednesday": + return time.Wednesday, true + case "wed": + return time.Wednesday, true + case "thursday": + return time.Thursday, true + case "thu": + return time.Thursday, true + case "friday": + return time.Friday, true + case "fri": + return time.Friday, true + case "saturday": + return time.Saturday, true + case "sat": + return time.Saturday, true + case "sunday": + return time.Sunday, true + case "sun": + return time.Sunday, true + default: + return time.Monday, false + } +} + +func lowerAndTrim(str string) string { + return strings.TrimSpace(strings.ToLower(str)) +} diff --git a/item/date_test.go b/item/date_test.go new file mode 100644 index 0000000..864efe3 --- /dev/null +++ b/item/date_test.go @@ -0,0 +1,310 @@ +package item_test + +// func TestWeekdaysSort(t *testing.T) { +// for _, tc := range []struct { +// name string +// input task.Weekdays +// exp task.Weekdays +// }{ +// { +// name: "empty", +// }, +// { +// name: "one", +// input: task.Weekdays{time.Tuesday}, +// exp: task.Weekdays{time.Tuesday}, +// }, +// { +// name: "multiple", +// input: task.Weekdays{time.Wednesday, time.Tuesday, time.Monday}, +// exp: task.Weekdays{time.Monday, time.Tuesday, time.Wednesday}, +// }, +// { +// name: "sunday is last", +// input: task.Weekdays{time.Saturday, time.Sunday, time.Monday}, +// exp: task.Weekdays{time.Monday, time.Saturday, time.Sunday}, +// }, +// } { +// t.Run(tc.name, func(t *testing.T) { +// sort.Sort(tc.input) +// test.Equals(t, tc.exp, tc.input) +// }) +// } +// } + +// func TestWeekdaysUnique(t *testing.T) { +// for _, tc := range []struct { +// name string +// input task.Weekdays +// exp task.Weekdays +// }{ +// { +// name: "empty", +// input: task.Weekdays{}, +// exp: task.Weekdays{}, +// }, +// { +// name: "single", +// input: task.Weekdays{time.Monday}, +// exp: task.Weekdays{time.Monday}, +// }, +// { +// name: "no doubles", +// input: task.Weekdays{time.Monday, time.Tuesday, time.Wednesday}, +// exp: task.Weekdays{time.Monday, time.Tuesday, time.Wednesday}, +// }, +// { +// name: "doubles", +// input: task.Weekdays{time.Monday, time.Monday, time.Wednesday, time.Monday}, +// exp: task.Weekdays{time.Monday, time.Wednesday}, +// }, +// } { +// t.Run(tc.name, func(t *testing.T) { +// test.Equals(t, tc.exp, tc.input.Unique()) +// }) +// } +// } + +// func TestNewDateFromString(t *testing.T) { +// t.Run("no date", func(t *testing.T) { +// for _, tc := range []struct { +// name string +// input string +// exp task.Date +// }{ +// { +// name: "empty", +// exp: task.Date{}, +// }, +// { +// name: "no date", +// input: "no date", +// exp: task.Date{}, +// }, +// } { +// t.Run(tc.name, func(t *testing.T) { +// test.Equals(t, tc.exp, task.NewDateFromString(tc.input)) +// }) +// } +// }) + +// t.Run("digits", func(t *testing.T) { +// for _, tc := range []struct { +// name string +// input string +// exp task.Date +// }{ +// { +// name: "normal", +// input: "2021-01-30 (saturday)", +// exp: task.NewDate(2021, 1, 30), +// }, +// { +// name: "short", +// input: "2021-01-30", +// exp: task.NewDate(2021, 1, 30), +// }, +// } { +// t.Run(tc.name, func(t *testing.T) { +// test.Equals(t, tc.exp, task.NewDateFromString(tc.input)) + +// }) +// } +// }) + +// t.Run("day name", func(t *testing.T) { +// monday := task.Today().Add(1) +// for { +// if monday.Weekday() == time.Monday { +// break +// } +// monday = monday.Add(1) +// } +// for _, tc := range []struct { +// name string +// input string +// exp task.Date +// }{ +// { +// name: "dayname lowercase", +// input: "monday", +// }, +// { +// name: "dayname capitalized", +// input: "Monday", +// }, +// { +// name: "dayname short", +// input: "mon", +// }, +// } { +// t.Run(tc.name, func(t *testing.T) { +// test.Equals(t, monday, task.NewDateFromString(tc.input)) +// }) +// } +// }) + +// t.Run("relative days", func(t *testing.T) { +// for _, tc := range []struct { +// name string +// exp task.Date +// }{ +// { +// name: "today", +// exp: task.Today(), +// }, +// { +// name: "tod", +// exp: task.Today(), +// }, +// { +// name: "tomorrow", +// exp: task.Today().Add(1), +// }, +// { +// name: "tom", +// exp: task.Today().Add(1), +// }, +// } { +// t.Run(tc.name, func(t *testing.T) { +// test.Equals(t, tc.exp, task.NewDateFromString(tc.name)) +// }) +// } +// }) +// } + +// func TestDateDaysBetween(t *testing.T) { +// for _, tc := range []struct { +// name string +// d1 task.Date +// d2 task.Date +// exp int +// }{ +// { +// name: "same", +// d1: task.NewDate(2021, 6, 23), +// d2: task.NewDate(2021, 6, 23), +// }, +// { +// name: "one", +// d1: task.NewDate(2021, 6, 23), +// d2: task.NewDate(2021, 6, 24), +// exp: 1, +// }, +// { +// name: "many", +// d1: task.NewDate(2021, 6, 23), +// d2: task.NewDate(2024, 3, 7), +// exp: 988, +// }, +// { +// name: "edge", +// d1: task.NewDate(2020, 12, 30), +// d2: task.NewDate(2021, 1, 3), +// exp: 4, +// }, +// { +// name: "reverse", +// d1: task.NewDate(2021, 6, 23), +// d2: task.NewDate(2021, 5, 23), +// exp: 31, +// }, +// } { +// t.Run(tc.name, func(t *testing.T) { +// test.Equals(t, tc.exp, tc.d1.DaysBetween(tc.d2)) +// }) +// } +// } + +// func TestDateString(t *testing.T) { +// for _, tc := range []struct { +// name string +// date task.Date +// exp string +// }{ +// { +// name: "zero", +// date: task.NewDate(0, 0, 0), +// exp: "no date", +// }, +// { +// name: "normal", +// date: task.NewDate(2021, 5, 30), +// exp: "2021-05-30 (sunday)", +// }, +// { +// name: "normalize", +// date: task.NewDate(2021, 5, 32), +// exp: "2021-06-01 (tuesday)", +// }, +// } { +// t.Run(tc.name, func(t *testing.T) { +// test.Equals(t, tc.exp, tc.date.String()) +// }) +// } +// } + +// func TestDateHuman(t *testing.T) { +// for _, tc := range []struct { +// name string +// date task.Date +// exp string +// }{ +// { +// name: "zero", +// date: task.NewDate(0, 0, 0), +// exp: "-", +// }, +// { +// name: "default", +// date: task.NewDate(2020, 1, 1), +// exp: "2020-01-01 (wednesday)", +// }, +// { +// name: "today", +// date: task.Today(), +// exp: "today", +// }, +// { +// name: "tomorrow", +// date: task.Today().Add(1), +// exp: "tomorrow", +// }, +// } { +// t.Run(tc.name, func(t *testing.T) { +// test.Equals(t, tc.exp, tc.date.Human()) +// }) +// } +// } + +// func TestDateIsZero(t *testing.T) { +// test.Equals(t, true, task.Date{}.IsZero()) +// test.Equals(t, false, task.NewDate(2021, 6, 24).IsZero()) +// } + +// func TestDateAfter(t *testing.T) { +// day := task.NewDate(2021, 1, 31) +// for _, tc := range []struct { +// name string +// tDay task.Date +// exp bool +// }{ +// { +// name: "after", +// tDay: task.NewDate(2021, 1, 30), +// exp: true, +// }, +// { +// name: "on", +// tDay: day, +// }, +// { +// name: "before", +// tDay: task.NewDate(2021, 2, 1), +// }, +// } { +// t.Run(tc.name, func(t *testing.T) { +// test.Equals(t, tc.exp, day.After(tc.tDay)) +// }) +// } +// } diff --git a/item/recur.go b/item/recur.go index a671cf1..9808183 100644 --- a/item/recur.go +++ b/item/recur.go @@ -1,61 +1,312 @@ package item import ( - "slices" + "fmt" + "strconv" + "strings" "time" ) -type RecurPeriod string - -const ( - PeriodDay RecurPeriod = "day" - PeriodMonth RecurPeriod = "month" -) - -var ValidPeriods = []RecurPeriod{PeriodDay, PeriodMonth} - -type Recur struct { - Start time.Time `json:"start"` - Period RecurPeriod `json:"period"` - Count int `json:"count"` +type Recurrer interface { + RecursOn(date Date) bool + String() string } -func (r *Recur) On(date time.Time) bool { - switch r.Period { - case PeriodDay: - return r.onDays(date) - case PeriodMonth: - return r.onMonths(date) - default: - return false +func NewRecurrer(recurStr string) Recurrer { + terms := strings.Split(recurStr, ",") + if len(terms) < 2 { + return nil } + + start := NewDateFromString(terms[0]) + if start.IsZero() { + return nil + } + + terms = terms[1:] + for i, t := range terms { + terms[i] = strings.TrimSpace(t) + } + + for _, parseFunc := range []func(Date, []string) (Recurrer, bool){ + ParseDaily, ParseEveryNDays, ParseWeekly, ParseBiweekly, + ParseEveryNWeeks, ParseEveryNMonths, + } { + if recur, ok := parseFunc(start, terms); ok { + return recur + } + } + + return nil } -func (r *Recur) onDays(date time.Time) bool { - if r.Start.After(date) { +type Daily struct { + Start Date +} + +// yyyy-mm-dd, daily +func ParseDaily(start Date, terms []string) (Recurrer, bool) { + if len(terms) < 1 { + return nil, false + } + + if terms[0] != "daily" { + return nil, false + } + + return Daily{ + Start: start, + }, true +} + +func (d Daily) RecursOn(date Date) bool { + return date.Equal(d.Start) || date.After(d.Start) +} + +func (d Daily) String() string { + return fmt.Sprintf("%s, daily", d.Start.String()) +} + +type EveryNDays struct { + Start Date + N int +} + +// yyyy-mm-dd, every 3 days +func ParseEveryNDays(start Date, terms []string) (Recurrer, bool) { + if len(terms) != 1 { + return EveryNDays{}, false + } + + terms = strings.Split(terms[0], " ") + if len(terms) != 3 || terms[0] != "every" || terms[2] != "days" { + return EveryNDays{}, false + } + + n, err := strconv.Atoi(terms[1]) + if err != nil { + return EveryNDays{}, false + } + + return EveryNDays{ + Start: start, + N: n, + }, true +} + +func (nd EveryNDays) RecursOn(date Date) bool { + if nd.Start.After(date) { return false } - testDate := r.Start + testDate := nd.Start for { - if testDate.Equal(date) { + switch { + case testDate.Equal(date): + return true + case testDate.After(date): + return false + default: + testDate = testDate.Add(nd.N) + } + } +} + +func (nd EveryNDays) String() string { + return fmt.Sprintf("%s, every %d days", nd.Start.String(), nd.N) +} + +type Weekly struct { + Start Date + Weekdays Weekdays +} + +// yyyy-mm-dd, weekly, wednesday & saturday & sunday +func ParseWeekly(start Date, terms []string) (Recurrer, bool) { + if len(terms) < 2 { + return nil, false + } + + if terms[0] != "weekly" { + return nil, false + } + + wds := Weekdays{} + for _, wdStr := range strings.Split(terms[1], "&") { + wd, ok := ParseWeekday(wdStr) + if !ok { + continue + } + wds = append(wds, wd) + } + if len(wds) == 0 { + return nil, false + } + + return Weekly{ + Start: start, + Weekdays: wds.Unique(), + }, true +} + +func (w Weekly) RecursOn(date Date) bool { + if w.Start.After(date) { + return false + } + + for _, wd := range w.Weekdays { + if wd == date.Weekday() { return true } - if testDate.After(date) { - return false - } - - dur := time.Duration(r.Count) * 24 * time.Hour - testDate = testDate.Add(dur) } + + return false } -func (r *Recur) onMonths(date time.Time) bool { - if r.Start.After(date) { +func (w Weekly) String() string { + weekdayStrs := []string{} + for _, wd := range w.Weekdays { + weekdayStrs = append(weekdayStrs, wd.String()) + } + weekdayStr := strings.Join(weekdayStrs, " & ") + + return fmt.Sprintf("%s, weekly, %s", w.Start.String(), strings.ToLower(weekdayStr)) +} + +type Biweekly struct { + Start Date + Weekday time.Weekday +} + +// yyyy-mm-dd, biweekly, wednesday +func ParseBiweekly(start Date, terms []string) (Recurrer, bool) { + if len(terms) < 2 { + return nil, false + } + + if terms[0] != "biweekly" { + return nil, false + } + + wd, ok := ParseWeekday(terms[1]) + if !ok { + return nil, false + } + + return Biweekly{ + Start: start, + Weekday: wd, + }, true +} + +func (b Biweekly) RecursOn(date Date) bool { + if b.Start.After(date) { return false } - tDate := r.Start + if b.Weekday != date.Weekday() { + return false + } + + // find first + tDate := b.Start + for { + if tDate.Weekday() == b.Weekday { + break + } + tDate = tDate.AddDays(1) + } + + // add weeks + for { + switch { + case tDate.Equal(date): + return true + case tDate.After(date): + return false + } + tDate = tDate.AddDays(14) + } +} + +func (b Biweekly) String() string { + return fmt.Sprintf("%s, biweekly, %s", b.Start.String(), strings.ToLower(b.Weekday.String())) +} + +type EveryNWeeks struct { + Start Date + N int +} + +// yyyy-mm-dd, every 3 weeks +func ParseEveryNWeeks(start Date, terms []string) (Recurrer, bool) { + if len(terms) != 1 { + return nil, false + } + + terms = strings.Split(terms[0], " ") + if len(terms) != 3 || terms[0] != "every" || terms[2] != "weeks" { + return nil, false + } + n, err := strconv.Atoi(terms[1]) + if err != nil || n < 1 { + return nil, false + } + + return EveryNWeeks{ + Start: start, + N: n, + }, true +} + +func (enw EveryNWeeks) RecursOn(date Date) bool { + if enw.Start.After(date) { + return false + } + if enw.Start.Equal(date) { + return true + } + + intervalDays := enw.N * 7 + return enw.Start.DaysBetween(date)%intervalDays == 0 +} + +func (enw EveryNWeeks) String() string { + return fmt.Sprintf("%s, every %d weeks", enw.Start.String(), enw.N) +} + +type EveryNMonths struct { + Start Date + N int +} + +// yyyy-mm-dd, every 3 months +func ParseEveryNMonths(start Date, terms []string) (Recurrer, bool) { + if len(terms) != 1 { + return nil, false + } + + terms = strings.Split(terms[0], " ") + if len(terms) != 3 || terms[0] != "every" || terms[2] != "months" { + return nil, false + } + n, err := strconv.Atoi(terms[1]) + if err != nil { + return nil, false + } + + return EveryNMonths{ + Start: start, + N: n, + }, true +} + +func (enm EveryNMonths) RecursOn(date Date) bool { + if enm.Start.After(date) { + return false + } + + tDate := enm.Start for { if tDate.Equal(date) { return true @@ -63,23 +314,11 @@ func (r *Recur) onMonths(date time.Time) bool { if tDate.After(date) { return false } - - y, m, d := tDate.Date() - tDate = time.Date(y, m+time.Month(r.Count), d, 0, 0, 0, 0, time.UTC) + tDate = tDate.AddMonths(enm.N) } + } -func (r *Recur) NextAfter(old time.Time) time.Time { - day, _ := time.ParseDuration("24h") - test := old.Add(day) - for { - if r.On(test) || test.After(time.Date(2500, 1, 1, 0, 0, 0, 0, time.UTC)) { - return test - } - test.Add(day) - } -} - -func (r *Recur) Valid() bool { - return r.Start.IsZero() || !slices.Contains(ValidPeriods, r.Period) +func (enm EveryNMonths) String() string { + return fmt.Sprintf("%s, every %d months", enm.Start.String(), enm.N) } diff --git a/item/recur_test.go b/item/recur_test.go index 3ccca11..152862c 100644 --- a/item/recur_test.go +++ b/item/recur_test.go @@ -1,105 +1,417 @@ package item_test -import ( - "testing" - "time" +// func TestDaily(t *testing.T) { +// daily := task.Daily{ +// Start: task.NewDate(2021, 1, 31), // a sunday +// } +// dailyStr := "2021-01-31 (sunday), daily" - "go-mod.ewintr.nl/planner/item" -) +// t.Run("parse", func(t *testing.T) { +// test.Equals(t, daily, task.NewRecurrer(dailyStr)) +// }) -func TestRecur(t *testing.T) { - t.Parallel() +// t.Run("string", func(t *testing.T) { +// test.Equals(t, dailyStr, daily.String()) +// }) - t.Run("days", func(t *testing.T) { - r := item.Recur{ - Start: time.Date(2024, 12, 1, 0, 0, 0, 0, time.UTC), - Period: item.PeriodDay, - Count: 5, - } - day := 24 * time.Hour +// t.Run("recurs_on", func(t *testing.T) { +// for _, tc := range []struct { +// name string +// date task.Date +// exp bool +// }{ +// { +// name: "before", +// date: task.NewDate(2021, 1, 30), +// }, +// { +// name: "on", +// date: daily.Start, +// exp: true, +// }, +// { +// name: "after", +// date: task.NewDate(2021, 2, 1), +// exp: true, +// }, +// } { +// t.Run(tc.name, func(t *testing.T) { +// test.Equals(t, tc.exp, daily.RecursOn(tc.date)) +// }) +// } +// }) +// } - for _, tc := range []struct { - name string - date time.Time - exp bool - }{ - { - name: "before", - date: time.Date(202, 1, 1, 0, 0, 0, 0, time.UTC), - }, - { - name: "start", - date: r.Start, - exp: true, - }, - { - name: "after true", - date: r.Start.Add(15 * day), - exp: true, - }, - { - name: "after false", - date: r.Start.Add(16 * day), - }, - } { - t.Run(tc.name, func(t *testing.T) { - if act := r.On(tc.date); tc.exp != act { - t.Errorf("exp %v, got %v", tc.exp, act) - } - }) - } - }) +// func TestEveryNDays(t *testing.T) { +// every := task.EveryNDays{ +// Start: task.NewDate(2022, 6, 8), +// N: 5, +// } +// everyStr := "2022-06-08 (wednesday), every 5 days" - t.Run("months", func(t *testing.T) { - r := item.Recur{ - Start: time.Date(2021, 2, 3, 0, 0, 0, 0, time.UTC), - Period: item.PeriodMonth, - Count: 3, - } +// t.Run("parse", func(t *testing.T) { +// test.Equals(t, every, task.NewRecurrer(everyStr)) +// }) - for _, tc := range []struct { - name string - date time.Time - exp bool - }{ - { - name: "before start", - date: time.Date(2021, 1, 27, 0, 0, 0, 0, time.UTC), - }, - { - name: "on start", - date: time.Date(2021, 2, 3, 0, 0, 0, 0, time.UTC), - exp: true, - }, - { - name: "8 weeks after", - date: time.Date(2021, 3, 31, 0, 0, 0, 0, time.UTC), - }, - { - name: "one month", - date: time.Date(2021, 3, 3, 0, 0, 0, 0, time.UTC), - }, - { - name: "3 months", - date: time.Date(2021, 5, 3, 0, 0, 0, 0, time.UTC), - exp: true, - }, - { - name: "4 months", - date: time.Date(2021, 6, 3, 0, 0, 0, 0, time.UTC), - }, - { - name: "6 months", - date: time.Date(2021, 8, 3, 0, 0, 0, 0, time.UTC), - exp: true, - }, - } { - t.Run(tc.name, func(t *testing.T) { - if act := r.On(tc.date); tc.exp != act { - t.Errorf("exp %v, got %v", tc.exp, act) - } - }) - } - }) +// t.Run("string", func(t *testing.T) { +// test.Equals(t, everyStr, every.String()) +// }) -} +// t.Run("recurs on", func(t *testing.T) { +// for _, tc := range []struct { +// name string +// date task.Date +// exp bool +// }{ +// { +// name: "before", +// date: task.NewDate(2022, 1, 1), +// }, +// { +// name: "start", +// date: every.Start, +// exp: true, +// }, +// { +// name: "after true", +// date: every.Start.Add(15), +// exp: true, +// }, +// { +// name: "after false", +// date: every.Start.Add(16), +// }, +// } { +// t.Run(tc.name, func(t *testing.T) { +// test.Equals(t, tc.exp, every.RecursOn(tc.date)) +// }) +// } +// }) +// } + +// func TestParseWeekly(t *testing.T) { +// start := task.NewDate(2021, 2, 7) +// for _, tc := range []struct { +// name string +// input []string +// expOk bool +// expWeekly task.Weekly +// }{ +// { +// name: "empty", +// }, +// { +// name: "wrong type", +// input: []string{"daily"}, +// }, +// { +// name: "wrong count", +// input: []string{"weeekly"}, +// }, +// { +// name: "unknown day", +// input: []string{"weekly", "festivus"}, +// }, +// { +// name: "one day", +// input: []string{"weekly", "monday"}, +// expOk: true, +// expWeekly: task.Weekly{ +// Start: start, +// Weekdays: task.Weekdays{ +// time.Monday, +// }, +// }, +// }, +// { +// name: "multiple days", +// input: []string{"weekly", "monday & thursday & saturday"}, +// expOk: true, +// expWeekly: task.Weekly{ +// Start: start, +// Weekdays: task.Weekdays{ +// time.Monday, +// time.Thursday, +// time.Saturday, +// }, +// }, +// }, +// { +// name: "wrong order", +// input: []string{"weekly", "sunday & thursday & wednesday"}, +// expOk: true, +// expWeekly: task.Weekly{ +// Start: start, +// Weekdays: task.Weekdays{ +// time.Wednesday, +// time.Thursday, +// time.Sunday, +// }, +// }, +// }, +// { +// name: "doubles", +// input: []string{"weekly", "sunday & sunday & monday"}, +// expOk: true, +// expWeekly: task.Weekly{ +// Start: start, +// Weekdays: task.Weekdays{ +// time.Monday, +// time.Sunday, +// }, +// }, +// }, +// { +// name: "one unknown", +// input: []string{"weekly", "sunday & someday"}, +// expOk: true, +// expWeekly: task.Weekly{ +// Start: start, +// Weekdays: task.Weekdays{ +// time.Sunday, +// }, +// }, +// }, +// } { +// t.Run(tc.name, func(t *testing.T) { +// weekly, ok := task.ParseWeekly(start, tc.input) +// test.Equals(t, tc.expOk, ok) +// if tc.expOk { +// test.Equals(t, tc.expWeekly, weekly) +// } +// }) +// } +// } + +// func TestWeekly(t *testing.T) { +// weekly := task.Weekly{ +// Start: task.NewDate(2021, 1, 31), // a sunday +// Weekdays: task.Weekdays{ +// time.Monday, +// time.Wednesday, +// time.Thursday, +// }, +// } +// weeklyStr := "2021-01-31 (sunday), weekly, monday & wednesday & thursday" + +// t.Run("parse", func(t *testing.T) { +// test.Equals(t, weekly, task.NewRecurrer(weeklyStr)) +// }) + +// t.Run("string", func(t *testing.T) { +// test.Equals(t, weeklyStr, weekly.String()) +// }) + +// t.Run("recurs_on", func(t *testing.T) { +// for _, tc := range []struct { +// name string +// date task.Date +// exp bool +// }{ +// { +// name: "before start", +// date: task.NewDate(2021, 1, 27), // a wednesday +// }, +// { +// name: "right weekday", +// date: task.NewDate(2021, 2, 1), // a monday +// exp: true, +// }, +// { +// name: "another right day", +// date: task.NewDate(2021, 2, 3), // a wednesday +// exp: true, +// }, +// { +// name: "wrong weekday", +// date: task.NewDate(2021, 2, 5), // a friday +// }, +// } { +// t.Run(tc.name, func(t *testing.T) { +// test.Equals(t, tc.exp, weekly.RecursOn(tc.date)) +// }) +// } +// }) +// } + +// func TestBiweekly(t *testing.T) { +// biweekly := task.Biweekly{ +// Start: task.NewDate(2021, 1, 31), // a sunday +// Weekday: time.Wednesday, +// } +// biweeklyStr := "2021-01-31 (sunday), biweekly, wednesday" + +// t.Run("parse", func(t *testing.T) { +// test.Equals(t, biweekly, task.NewRecurrer(biweeklyStr)) +// }) + +// t.Run("string", func(t *testing.T) { +// test.Equals(t, biweeklyStr, biweekly.String()) +// }) + +// t.Run("recurs_on", func(t *testing.T) { +// for _, tc := range []struct { +// name string +// date task.Date +// exp bool +// }{ +// { +// name: "before start", +// date: task.NewDate(2021, 1, 27), // a wednesday +// }, +// { +// name: "wrong weekday", +// date: task.NewDate(2021, 2, 1), // a monday +// }, +// { +// name: "odd week count", +// date: task.NewDate(2021, 2, 10), // a wednesday +// }, +// { +// name: "right", +// date: task.NewDate(2021, 2, 17), // a wednesday +// exp: true, +// }, +// } { +// t.Run(tc.name, func(t *testing.T) { +// test.Equals(t, tc.exp, biweekly.RecursOn(tc.date)) +// }) +// } +// }) +// } + +// func TestEveryNWeeks(t *testing.T) { +// everyNWeeks := task.EveryNWeeks{ +// Start: task.NewDate(2021, 2, 3), +// N: 3, +// } +// everyNWeeksStr := "2021-02-03 (wednesday), every 3 weeks" + +// t.Run("parse", func(t *testing.T) { +// test.Equals(t, everyNWeeks, task.NewRecurrer(everyNWeeksStr)) +// }) + +// t.Run("string", func(t *testing.T) { +// test.Equals(t, everyNWeeksStr, everyNWeeks.String()) +// }) + +// t.Run("recurs on", func(t *testing.T) { +// for _, tc := range []struct { +// name string +// date task.Date +// exp bool +// }{ +// { +// name: "before start", +// date: task.NewDate(2021, 1, 27), +// }, +// { +// name: "on start", +// date: task.NewDate(2021, 2, 3), +// exp: true, +// }, +// { +// name: "wrong day", +// date: task.NewDate(2021, 2, 4), +// }, +// { +// name: "one week after", +// date: task.NewDate(2021, 2, 10), +// }, +// { +// name: "first interval", +// date: task.NewDate(2021, 2, 24), +// exp: true, +// }, +// { +// name: "second interval", +// date: task.NewDate(2021, 3, 17), +// exp: true, +// }, +// { +// name: "second interval plus one week", +// date: task.NewDate(2021, 3, 24), +// }, +// } { +// t.Run(tc.name, func(t *testing.T) { +// test.Equals(t, tc.exp, everyNWeeks.RecursOn(tc.date)) +// }) +// } +// }) +// } + +// func TestEveryNMonths(t *testing.T) { +// everyNMonths := task.EveryNMonths{ +// Start: task.NewDate(2021, 2, 3), +// N: 3, +// } +// everyNMonthsStr := "2021-02-03 (wednesday), every 3 months" + +// t.Run("parse", func(t *testing.T) { +// test.Equals(t, everyNMonths, task.NewRecurrer(everyNMonthsStr)) +// }) + +// t.Run("string", func(t *testing.T) { +// test.Equals(t, everyNMonthsStr, everyNMonths.String()) +// }) + +// t.Run("recurs on", func(t *testing.T) { +// for _, tc := range []struct { +// name string +// date task.Date +// exp bool +// }{ +// { +// name: "before start", +// date: task.NewDate(2021, 1, 27), +// }, +// { +// name: "on start", +// date: task.NewDate(2021, 2, 3), +// exp: true, +// }, +// { +// name: "8 weeks after", +// date: task.NewDate(2021, 3, 31), +// }, +// { +// name: "one month", +// date: task.NewDate(2021, 3, 3), +// }, +// { +// name: "3 months", +// date: task.NewDate(2021, 5, 3), +// exp: true, +// }, +// { +// name: "4 months", +// date: task.NewDate(2021, 6, 3), +// }, +// { +// name: "6 months", +// date: task.NewDate(2021, 8, 3), +// exp: true, +// }, +// } { +// t.Run(tc.name, func(t *testing.T) { +// test.Equals(t, tc.exp, everyNMonths.RecursOn(tc.date)) +// }) +// } +// }) + +// t.Run("recurs every year", func(t *testing.T) { +// recur := task.EveryNMonths{ +// Start: task.NewDate(2021, 3, 1), +// N: 12, +// } +// test.Equals(t, false, recur.RecursOn(task.NewDate(2021, 3, 9))) +// }) + +// t.Run("bug", func(t *testing.T) { +// recur := task.EveryNMonths{ +// Start: task.NewDate(2021, 3, 1), +// N: 1, +// } +// test.Equals(t, false, recur.RecursOn(task.NewDate(2021, 11, 3))) +// }) +// } diff --git a/plan/test-conf.yaml b/plan/test-conf.yaml new file mode 100644 index 0000000..0b2f181 --- /dev/null +++ b/plan/test-conf.yaml @@ -0,0 +1,3 @@ +db_path: ./plan.db +sync_url: http://localhost:8092 +api_key: testKey diff --git a/sync/service/postgres.go b/sync/service/postgres.go index 2243261..0a3660b 100644 --- a/sync/service/postgres.go +++ b/sync/service/postgres.go @@ -66,6 +66,7 @@ func (p *Postgres) Update(i item.Item, ts time.Time) error { if err != nil { return fmt.Errorf("%w: %v", ErrPostgresFailure, err) } + i.RecurNext = i.Recurrer.Start } else { recurrerJSON = nil } diff --git a/sync/service/recur.go b/sync/service/recur.go index 04d70fc..c0e3ee3 100644 --- a/sync/service/recur.go +++ b/sync/service/recur.go @@ -1,6 +1,7 @@ package main import ( + "fmt" "log/slog" "time" @@ -35,21 +36,26 @@ func (r *Recur) Run(interval time.Duration) { } func (r *Recur) Recur() error { + r.logger.Info("start looking for recurring items") items, err := r.repoRecur.RecursBefore(time.Now()) if err != nil { return err } + r.logger.Info("found recurring items", "count", len(items)) for _, i := range items { + r.logger.Info("processing recurring item", "item", fmt.Sprintf("%+v", i)) // spawn instance ne, err := item.NewEvent(i) if err != nil { return err } + r.logger.Info("processing recurring event", "event", fmt.Sprintf("%+v", ne)) y, m, d := i.RecurNext.Date() ne.ID = uuid.New().String() ne.Recurrer = nil ne.RecurNext = time.Time{} ne.Start = time.Date(y, m, d, ne.Start.Hour(), ne.Start.Minute(), 0, 0, time.UTC) + r.logger.Info("created instance of recurring event", "event", fmt.Sprintf("%+v", ne)) ni, err := ne.Item() if err != nil { @@ -58,11 +64,14 @@ func (r *Recur) Recur() error { if err := r.repoSync.Update(ni, time.Now()); err != nil { return err } + r.logger.Info("storen instance of recurring event", "recEventID", ne.ID, "instanceID", ni.ID) // set next - if err := r.repoRecur.RecursNext(i.ID, i.Recurrer.NextAfter(i.RecurNext), time.Now()); err != nil { + next := i.Recurrer.NextAfter(i.RecurNext) + if err := r.repoRecur.RecursNext(i.ID, next, time.Now()); err != nil { return err } + r.logger.Info("updated recur date", "recEventID", ne.ID, "next", next) } r.logger.Info("processed recurring items", "count", len(items)) diff --git a/sync/service/service.go b/sync/service/service.go index b671b61..4238b1c 100644 --- a/sync/service/service.go +++ b/sync/service/service.go @@ -39,7 +39,7 @@ func main() { "dbUser": *dbUser, }) recurrer := NewRecur(repo, repo, logger) - go recurrer.Run(12 * time.Hour) + go recurrer.Run(10 * time.Second) srv := NewServer(repo, *apiKey, logger) go http.ListenAndServe(fmt.Sprintf(":%s", *apiPort), srv)