new types for date, time and recurrer
This commit is contained in:
parent
caa1a45efb
commit
34cdfc73e2
|
@ -1,3 +1 @@
|
||||||
test.db*
|
*.db*
|
||||||
plannersync
|
|
||||||
plan
|
|
||||||
|
|
13
Makefile
13
Makefile
|
@ -4,5 +4,18 @@ plan-deploy:
|
||||||
sync-run:
|
sync-run:
|
||||||
cd sync/service && go run . -dbname localhost -dbport 5432 -dbname planner -dbuser test -dbpassword test -port 8092 -key testKey
|
cd sync/service && go run . -dbname localhost -dbport 5432 -dbname planner -dbuser test -dbpassword test -port 8092 -key testKey
|
||||||
|
|
||||||
|
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:
|
database:
|
||||||
docker run -e POSTGRES_USER=test -e POSTGRES_PASSWORD=test -e POSTGRES_DB=planner -p 5432:5432 postgres:16
|
docker run -e POSTGRES_USER=test -e POSTGRES_PASSWORD=test -e POSTGRES_DB=planner -p 5432:5432 postgres:16
|
||||||
|
|
||||||
|
|
||||||
|
|
Binary file not shown.
|
@ -0,0 +1,278 @@
|
||||||
|
package item
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
DateFormat = "2006-01-02"
|
||||||
|
)
|
||||||
|
|
||||||
|
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))
|
||||||
|
}
|
|
@ -0,0 +1,327 @@
|
||||||
|
package item_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"sort"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/google/go-cmp/cmp"
|
||||||
|
"go-mod.ewintr.nl/planner/item"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestWeekdaysSort(t *testing.T) {
|
||||||
|
for _, tc := range []struct {
|
||||||
|
name string
|
||||||
|
input item.Weekdays
|
||||||
|
exp item.Weekdays
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "empty",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "one",
|
||||||
|
input: item.Weekdays{time.Tuesday},
|
||||||
|
exp: item.Weekdays{time.Tuesday},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "multiple",
|
||||||
|
input: item.Weekdays{time.Wednesday, time.Tuesday, time.Monday},
|
||||||
|
exp: item.Weekdays{time.Monday, time.Tuesday, time.Wednesday},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "sunday is last",
|
||||||
|
input: item.Weekdays{time.Saturday, time.Sunday, time.Monday},
|
||||||
|
exp: item.Weekdays{time.Monday, time.Saturday, time.Sunday},
|
||||||
|
},
|
||||||
|
} {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
sort.Sort(tc.input)
|
||||||
|
if diff := cmp.Diff(tc.exp, tc.input); diff != "" {
|
||||||
|
t.Errorf("(-exp, +got)%s\n", diff)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWeekdaysUnique(t *testing.T) {
|
||||||
|
for _, tc := range []struct {
|
||||||
|
name string
|
||||||
|
input item.Weekdays
|
||||||
|
exp item.Weekdays
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "empty",
|
||||||
|
input: item.Weekdays{},
|
||||||
|
exp: item.Weekdays{},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "single",
|
||||||
|
input: item.Weekdays{time.Monday},
|
||||||
|
exp: item.Weekdays{time.Monday},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "no doubles",
|
||||||
|
input: item.Weekdays{time.Monday, time.Tuesday, time.Wednesday},
|
||||||
|
exp: item.Weekdays{time.Monday, time.Tuesday, time.Wednesday},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "doubles",
|
||||||
|
input: item.Weekdays{time.Monday, time.Monday, time.Wednesday, time.Monday},
|
||||||
|
exp: item.Weekdays{time.Monday, time.Wednesday},
|
||||||
|
},
|
||||||
|
} {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
if diff := cmp.Diff(tc.exp, tc.input.Unique()); diff != "" {
|
||||||
|
t.Errorf("(-exp, +got)%s\n", diff)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNewDateFromString(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
t.Run("simple", func(t *testing.T) {
|
||||||
|
for _, tc := range []struct {
|
||||||
|
name string
|
||||||
|
input string
|
||||||
|
exp item.Date
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "empty",
|
||||||
|
exp: item.Date{},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "no date",
|
||||||
|
input: "no date",
|
||||||
|
exp: item.Date{},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "short",
|
||||||
|
input: "2021-01-30",
|
||||||
|
exp: item.NewDate(2021, 1, 30),
|
||||||
|
},
|
||||||
|
} {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
if diff := cmp.Diff(tc.exp, item.NewDateFromString(tc.input)); diff != "" {
|
||||||
|
t.Errorf("(-exp, +got)%s\n", diff)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("day name", func(t *testing.T) {
|
||||||
|
monday := item.Today().Add(1)
|
||||||
|
for {
|
||||||
|
if monday.Weekday() == time.Monday {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
monday = monday.Add(1)
|
||||||
|
}
|
||||||
|
for _, tc := range []struct {
|
||||||
|
name string
|
||||||
|
input string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "dayname lowercase",
|
||||||
|
input: "monday",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "dayname capitalized",
|
||||||
|
input: "Monday",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "dayname short",
|
||||||
|
input: "mon",
|
||||||
|
},
|
||||||
|
} {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
if diff := cmp.Diff(monday, item.NewDateFromString(tc.input)); diff != "" {
|
||||||
|
t.Errorf("(-exp, +got)%s\n", diff)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("relative days", func(t *testing.T) {
|
||||||
|
for _, tc := range []struct {
|
||||||
|
name string
|
||||||
|
exp item.Date
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "today",
|
||||||
|
exp: item.Today(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "tod",
|
||||||
|
exp: item.Today(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "tomorrow",
|
||||||
|
exp: item.Today().Add(1),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "tom",
|
||||||
|
exp: item.Today().Add(1),
|
||||||
|
},
|
||||||
|
} {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
if diff := cmp.Diff(tc.exp, item.NewDateFromString(tc.name)); diff != "" {
|
||||||
|
t.Errorf("(-exp, +got)%s\n", diff)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDateDaysBetween(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
for _, tc := range []struct {
|
||||||
|
name string
|
||||||
|
d1 item.Date
|
||||||
|
d2 item.Date
|
||||||
|
exp int
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "same",
|
||||||
|
d1: item.NewDate(2021, 6, 23),
|
||||||
|
d2: item.NewDate(2021, 6, 23),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "one",
|
||||||
|
d1: item.NewDate(2021, 6, 23),
|
||||||
|
d2: item.NewDate(2021, 6, 24),
|
||||||
|
exp: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "many",
|
||||||
|
d1: item.NewDate(2021, 6, 23),
|
||||||
|
d2: item.NewDate(2024, 3, 7),
|
||||||
|
exp: 988,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "edge",
|
||||||
|
d1: item.NewDate(2020, 12, 30),
|
||||||
|
d2: item.NewDate(2021, 1, 3),
|
||||||
|
exp: 4,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "reverse",
|
||||||
|
d1: item.NewDate(2021, 6, 23),
|
||||||
|
d2: item.NewDate(2021, 5, 23),
|
||||||
|
exp: 31,
|
||||||
|
},
|
||||||
|
} {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
if tc.exp != tc.d1.DaysBetween(tc.d2) {
|
||||||
|
t.Errorf("exp %v, got %v", tc.exp, tc.d1.DaysBetween(tc.d2))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDateString(t *testing.T) {
|
||||||
|
for _, tc := range []struct {
|
||||||
|
name string
|
||||||
|
date item.Date
|
||||||
|
exp string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "zero",
|
||||||
|
date: item.NewDate(0, 0, 0),
|
||||||
|
exp: "no date",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "normal",
|
||||||
|
date: item.NewDate(2021, 5, 30),
|
||||||
|
exp: "2021-05-30",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "normalize",
|
||||||
|
date: item.NewDate(2021, 5, 32),
|
||||||
|
exp: "2021-06-01",
|
||||||
|
},
|
||||||
|
} {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
if tc.exp != tc.date.String() {
|
||||||
|
t.Errorf("exp %v, got %v", 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) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
if !(item.Date{}.IsZero()) {
|
||||||
|
t.Errorf("exp true, got false")
|
||||||
|
}
|
||||||
|
if item.NewDate(2021, 6, 24).IsZero() {
|
||||||
|
t.Errorf("exp false, got true")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDateAfter(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
day := item.NewDate(2021, 1, 31)
|
||||||
|
for _, tc := range []struct {
|
||||||
|
name string
|
||||||
|
tDay item.Date
|
||||||
|
exp bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "after",
|
||||||
|
tDay: item.NewDate(2021, 1, 30),
|
||||||
|
exp: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "on",
|
||||||
|
tDay: day,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "before",
|
||||||
|
tDay: item.NewDate(2021, 2, 1),
|
||||||
|
},
|
||||||
|
} {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
if diff := cmp.Diff(tc.exp, day.After(tc.tDay)); diff != "" {
|
||||||
|
t.Errorf("(-exp, +got)%s\n", diff)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
|
@ -4,22 +4,22 @@ import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/google/go-cmp/cmp"
|
||||||
)
|
)
|
||||||
|
|
||||||
type EventBody struct {
|
type EventBody struct {
|
||||||
Title string `json:"title"`
|
Title string `json:"title"`
|
||||||
Start time.Time `json:"start"`
|
Time Time `json:"time"`
|
||||||
Duration time.Duration `json:"duration"`
|
Duration time.Duration `json:"duration"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e EventBody) MarshalJSON() ([]byte, error) {
|
func (e EventBody) MarshalJSON() ([]byte, error) {
|
||||||
type Alias EventBody
|
type Alias EventBody
|
||||||
return json.Marshal(&struct {
|
return json.Marshal(&struct {
|
||||||
Start string `json:"start"`
|
|
||||||
Duration string `json:"duration"`
|
Duration string `json:"duration"`
|
||||||
*Alias
|
*Alias
|
||||||
}{
|
}{
|
||||||
Start: e.Start.UTC().Format(time.RFC3339),
|
|
||||||
Duration: e.Duration.String(),
|
Duration: e.Duration.String(),
|
||||||
Alias: (*Alias)(&e),
|
Alias: (*Alias)(&e),
|
||||||
})
|
})
|
||||||
|
@ -28,7 +28,6 @@ func (e EventBody) MarshalJSON() ([]byte, error) {
|
||||||
func (e *EventBody) UnmarshalJSON(data []byte) error {
|
func (e *EventBody) UnmarshalJSON(data []byte) error {
|
||||||
type Alias EventBody
|
type Alias EventBody
|
||||||
aux := &struct {
|
aux := &struct {
|
||||||
Start string `json:"start"`
|
|
||||||
Duration string `json:"duration"`
|
Duration string `json:"duration"`
|
||||||
*Alias
|
*Alias
|
||||||
}{
|
}{
|
||||||
|
@ -39,10 +38,6 @@ func (e *EventBody) UnmarshalJSON(data []byte) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
var err error
|
var err error
|
||||||
if e.Start, err = time.Parse(time.RFC3339, aux.Start); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if e.Duration, err = time.ParseDuration(aux.Duration); err != nil {
|
if e.Duration, err = time.ParseDuration(aux.Duration); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -52,8 +47,9 @@ func (e *EventBody) UnmarshalJSON(data []byte) error {
|
||||||
|
|
||||||
type Event struct {
|
type Event struct {
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
Recurrer *Recur `json:"recurrer"`
|
Date Date `json:"date"`
|
||||||
RecurNext time.Time `json:"recurNext"`
|
Recurrer Recurrer `json:"recurrer"`
|
||||||
|
RecurNext Date `json:"recurNext"`
|
||||||
EventBody
|
EventBody
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -68,6 +64,7 @@ func NewEvent(i Item) (Event, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
e.ID = i.ID
|
e.ID = i.ID
|
||||||
|
e.Date = i.Date
|
||||||
e.Recurrer = i.Recurrer
|
e.Recurrer = i.Recurrer
|
||||||
e.RecurNext = i.RecurNext
|
e.RecurNext = i.RecurNext
|
||||||
|
|
||||||
|
@ -75,18 +72,15 @@ func NewEvent(i Item) (Event, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e Event) Item() (Item, error) {
|
func (e Event) Item() (Item, error) {
|
||||||
body, err := json.Marshal(EventBody{
|
body, err := json.Marshal(e.EventBody)
|
||||||
Title: e.Title,
|
|
||||||
Start: e.Start,
|
|
||||||
Duration: e.Duration,
|
|
||||||
})
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return Item{}, fmt.Errorf("could not marshal event to json")
|
return Item{}, fmt.Errorf("could not marshal event body to json")
|
||||||
}
|
}
|
||||||
|
|
||||||
return Item{
|
return Item{
|
||||||
ID: e.ID,
|
ID: e.ID,
|
||||||
Kind: KindEvent,
|
Kind: KindEvent,
|
||||||
|
Date: e.Date,
|
||||||
Recurrer: e.Recurrer,
|
Recurrer: e.Recurrer,
|
||||||
RecurNext: e.RecurNext,
|
RecurNext: e.RecurNext,
|
||||||
Body: string(body),
|
Body: string(body),
|
||||||
|
@ -97,15 +91,26 @@ func (e Event) Valid() bool {
|
||||||
if e.Title == "" {
|
if e.Title == "" {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
if e.Start.IsZero() || e.Start.Year() < 2024 {
|
if e.Date.IsZero() {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
if e.Duration.Seconds() < 1 {
|
if e.Duration.Seconds() < 1 {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
if e.Recurrer != nil && !e.Recurrer.Valid() {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func EventDiff(a, b Event) string {
|
||||||
|
aJSON, _ := json.Marshal(a)
|
||||||
|
bJSON, _ := json.Marshal(b)
|
||||||
|
|
||||||
|
return cmp.Diff(string(aJSON), string(bJSON))
|
||||||
|
}
|
||||||
|
|
||||||
|
func EventDiffs(a, b []Event) string {
|
||||||
|
aJSON, _ := json.Marshal(a)
|
||||||
|
bJSON, _ := json.Marshal(b)
|
||||||
|
|
||||||
|
return cmp.Diff(string(aJSON), string(bJSON))
|
||||||
|
}
|
||||||
|
|
|
@ -25,10 +25,11 @@ func TestNewEvent(t *testing.T) {
|
||||||
name: "wrong kind",
|
name: "wrong kind",
|
||||||
it: item.Item{
|
it: item.Item{
|
||||||
ID: "a",
|
ID: "a",
|
||||||
|
Date: item.NewDate(2024, 9, 20),
|
||||||
Kind: item.KindTask,
|
Kind: item.KindTask,
|
||||||
Body: `{
|
Body: `{
|
||||||
"title":"title",
|
"title":"title",
|
||||||
"start":"2024-09-20T08:00:00Z",
|
"time":"08:00",
|
||||||
"duration":"1h"
|
"duration":"1h"
|
||||||
}`,
|
}`,
|
||||||
},
|
},
|
||||||
|
@ -48,27 +49,21 @@ func TestNewEvent(t *testing.T) {
|
||||||
it: item.Item{
|
it: item.Item{
|
||||||
ID: "a",
|
ID: "a",
|
||||||
Kind: item.KindEvent,
|
Kind: item.KindEvent,
|
||||||
Recurrer: &item.Recur{
|
Date: item.NewDate(2024, 9, 20),
|
||||||
Start: time.Date(2024, 12, 8, 9, 0, 0, 0, time.UTC),
|
Recurrer: item.NewRecurrer("2024-12-08, daily"),
|
||||||
Period: item.PeriodDay,
|
|
||||||
Count: 1,
|
|
||||||
},
|
|
||||||
Body: `{
|
Body: `{
|
||||||
"title":"title",
|
"title":"title",
|
||||||
"start":"2024-09-20T08:00:00Z",
|
"time":"08:00",
|
||||||
"duration":"1h"
|
"duration":"1h"
|
||||||
}`,
|
}`,
|
||||||
},
|
},
|
||||||
expEvent: item.Event{
|
expEvent: item.Event{
|
||||||
ID: "a",
|
ID: "a",
|
||||||
Recurrer: &item.Recur{
|
Date: item.NewDate(2024, 9, 20),
|
||||||
Start: time.Date(2024, 12, 8, 9, 0, 0, 0, time.UTC),
|
Recurrer: item.NewRecurrer("2024-12-08, daily"),
|
||||||
Period: item.PeriodDay,
|
|
||||||
Count: 1,
|
|
||||||
},
|
|
||||||
EventBody: item.EventBody{
|
EventBody: item.EventBody{
|
||||||
Title: "title",
|
Title: "title",
|
||||||
Start: time.Date(2024, 9, 20, 8, 0, 0, 0, time.UTC),
|
Time: item.NewTime(8, 0),
|
||||||
Duration: oneHour,
|
Duration: oneHour,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -82,8 +77,8 @@ func TestNewEvent(t *testing.T) {
|
||||||
if tc.expErr {
|
if tc.expErr {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if diff := cmp.Diff(tc.expEvent, actEvent); diff != "" {
|
if diff := item.EventDiff(tc.expEvent, actEvent); diff != "" {
|
||||||
t.Errorf("(exp +, got -)\n%s", diff)
|
t.Errorf("(+exp, -got)\n%s", diff)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -107,16 +102,17 @@ func TestEventItem(t *testing.T) {
|
||||||
expItem: item.Item{
|
expItem: item.Item{
|
||||||
Kind: item.KindEvent,
|
Kind: item.KindEvent,
|
||||||
Updated: time.Time{},
|
Updated: time.Time{},
|
||||||
Body: `{"start":"0001-01-01T00:00:00Z","duration":"0s","title":""}`,
|
Body: `{"duration":"0s","title":"","time":"00:00"}`,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "normal",
|
name: "normal",
|
||||||
event: item.Event{
|
event: item.Event{
|
||||||
ID: "a",
|
ID: "a",
|
||||||
|
Date: item.NewDate(2024, 9, 23),
|
||||||
EventBody: item.EventBody{
|
EventBody: item.EventBody{
|
||||||
Title: "title",
|
Title: "title",
|
||||||
Start: time.Date(2024, 9, 23, 8, 0, 0, 0, time.UTC),
|
Time: item.NewTime(8, 0),
|
||||||
Duration: oneHour,
|
Duration: oneHour,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -124,7 +120,8 @@ func TestEventItem(t *testing.T) {
|
||||||
ID: "a",
|
ID: "a",
|
||||||
Kind: item.KindEvent,
|
Kind: item.KindEvent,
|
||||||
Updated: time.Time{},
|
Updated: time.Time{},
|
||||||
Body: `{"start":"2024-09-23T08:00:00Z","duration":"1h0m0s","title":"title"}`,
|
Date: item.NewDate(2024, 9, 23),
|
||||||
|
Body: `{"duration":"1h0m0s","title":"title","time":"08:00"}`,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
} {
|
} {
|
||||||
|
@ -163,8 +160,9 @@ func TestEventValidate(t *testing.T) {
|
||||||
name: "missing title",
|
name: "missing title",
|
||||||
event: item.Event{
|
event: item.Event{
|
||||||
ID: "a",
|
ID: "a",
|
||||||
|
Date: item.NewDate(2024, 9, 20),
|
||||||
EventBody: item.EventBody{
|
EventBody: item.EventBody{
|
||||||
Start: time.Date(2024, 9, 20, 8, 0, 0, 0, time.UTC),
|
Time: item.NewTime(8, 0),
|
||||||
Duration: oneHour,
|
Duration: oneHour,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -175,7 +173,7 @@ func TestEventValidate(t *testing.T) {
|
||||||
ID: "a",
|
ID: "a",
|
||||||
EventBody: item.EventBody{
|
EventBody: item.EventBody{
|
||||||
Title: "title",
|
Title: "title",
|
||||||
Start: time.Date(0, 0, 0, 8, 0, 0, 0, time.UTC),
|
Time: item.NewTime(8, 0),
|
||||||
Duration: oneHour,
|
Duration: oneHour,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -184,9 +182,10 @@ func TestEventValidate(t *testing.T) {
|
||||||
name: "no duration",
|
name: "no duration",
|
||||||
event: item.Event{
|
event: item.Event{
|
||||||
ID: "a",
|
ID: "a",
|
||||||
|
Date: item.NewDate(2024, 9, 20),
|
||||||
EventBody: item.EventBody{
|
EventBody: item.EventBody{
|
||||||
Title: "title",
|
Title: "title",
|
||||||
Start: time.Date(2024, 9, 20, 8, 0, 0, 0, time.UTC),
|
Time: item.NewTime(8, 0),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -194,9 +193,10 @@ func TestEventValidate(t *testing.T) {
|
||||||
name: "valid",
|
name: "valid",
|
||||||
event: item.Event{
|
event: item.Event{
|
||||||
ID: "a",
|
ID: "a",
|
||||||
|
Date: item.NewDate(2024, 9, 20),
|
||||||
EventBody: item.EventBody{
|
EventBody: item.EventBody{
|
||||||
Title: "title",
|
Title: "title",
|
||||||
Start: time.Date(2024, 9, 20, 8, 0, 0, 0, time.UTC),
|
Time: item.NewTime(8, 0),
|
||||||
Duration: oneHour,
|
Duration: oneHour,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
37
item/item.go
37
item/item.go
|
@ -1,6 +1,7 @@
|
||||||
package item
|
package item
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
|
@ -22,11 +23,43 @@ type Item struct {
|
||||||
Kind Kind `json:"kind"`
|
Kind Kind `json:"kind"`
|
||||||
Updated time.Time `json:"updated"`
|
Updated time.Time `json:"updated"`
|
||||||
Deleted bool `json:"deleted"`
|
Deleted bool `json:"deleted"`
|
||||||
Recurrer *Recur `json:"recurrer"`
|
Date Date `json:"date"`
|
||||||
RecurNext time.Time `json:"recurNext"`
|
Recurrer Recurrer `json:"recurrer"`
|
||||||
|
RecurNext Date `json:"recurNext"`
|
||||||
Body string `json:"body"`
|
Body string `json:"body"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (i Item) MarshalJSON() ([]byte, error) {
|
||||||
|
var recurStr string
|
||||||
|
if i.Recurrer != nil {
|
||||||
|
recurStr = i.Recurrer.String()
|
||||||
|
}
|
||||||
|
type Alias Item
|
||||||
|
return json.Marshal(&struct {
|
||||||
|
Recurrer string `json:"recurrer"`
|
||||||
|
*Alias
|
||||||
|
}{
|
||||||
|
Recurrer: recurStr,
|
||||||
|
Alias: (*Alias)(&i),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *Item) UnmarshalJSON(data []byte) error {
|
||||||
|
type Alias Item
|
||||||
|
aux := &struct {
|
||||||
|
Recurrer string `json:"recurrer"`
|
||||||
|
*Alias
|
||||||
|
}{
|
||||||
|
Alias: (*Alias)(i),
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal(data, &aux); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
i.Recurrer = NewRecurrer(aux.Recurrer)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func NewItem(k Kind, body string) Item {
|
func NewItem(k Kind, body string) Item {
|
||||||
return Item{
|
return Item{
|
||||||
ID: uuid.New().String(),
|
ID: uuid.New().String(),
|
||||||
|
|
305
item/recur.go
305
item/recur.go
|
@ -1,61 +1,270 @@
|
||||||
package item
|
package item
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"slices"
|
"fmt"
|
||||||
"time"
|
"strconv"
|
||||||
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
type RecurPeriod string
|
type Recurrer interface {
|
||||||
|
RecursOn(date Date) bool
|
||||||
const (
|
First() Date
|
||||||
PeriodDay RecurPeriod = "day"
|
String() string
|
||||||
PeriodMonth RecurPeriod = "month"
|
|
||||||
)
|
|
||||||
|
|
||||||
var ValidPeriods = []RecurPeriod{PeriodDay, PeriodMonth}
|
|
||||||
|
|
||||||
type Recur struct {
|
|
||||||
Start time.Time `json:"start"`
|
|
||||||
Period RecurPeriod `json:"period"`
|
|
||||||
Count int `json:"count"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *Recur) On(date time.Time) bool {
|
func NewRecurrer(recurStr string) Recurrer {
|
||||||
switch r.Period {
|
terms := strings.Split(recurStr, ",")
|
||||||
case PeriodDay:
|
if len(terms) < 2 {
|
||||||
return r.onDays(date)
|
return nil
|
||||||
case PeriodMonth:
|
|
||||||
return r.onMonths(date)
|
|
||||||
default:
|
|
||||||
return false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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,
|
||||||
|
ParseEveryNWeeks, ParseEveryNMonths,
|
||||||
|
} {
|
||||||
|
if recur, ok := parseFunc(start, terms); ok {
|
||||||
|
return recur
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *Recur) onDays(date time.Time) bool {
|
func FirstRecurAfter(r Recurrer, d Date) Date {
|
||||||
if r.Start.After(date) {
|
lim := NewDate(2050, 1, 1)
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
testDate := r.Start
|
|
||||||
for {
|
for {
|
||||||
if testDate.Equal(date) {
|
d = d.Add(1)
|
||||||
|
if r.RecursOn(d) || d.Equal(lim) {
|
||||||
|
return d
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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) First() Date { return FirstRecurAfter(d, d.Start.Add(-1)) }
|
||||||
|
|
||||||
|
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 := nd.Start
|
||||||
|
for {
|
||||||
|
switch {
|
||||||
|
case testDate.Equal(date):
|
||||||
|
return true
|
||||||
|
case testDate.After(date):
|
||||||
|
return false
|
||||||
|
default:
|
||||||
|
testDate = testDate.Add(nd.N)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (nd EveryNDays) First() Date { return FirstRecurAfter(nd, nd.Start.Add(-1)) }
|
||||||
|
|
||||||
|
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
|
return true
|
||||||
}
|
}
|
||||||
if testDate.After(date) {
|
|
||||||
return false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
dur := time.Duration(r.Count) * 24 * time.Hour
|
return false
|
||||||
testDate = testDate.Add(dur)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *Recur) onMonths(date time.Time) bool {
|
func (w Weekly) First() Date { return FirstRecurAfter(w, w.Start.Add(-1)) }
|
||||||
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 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) First() Date { return FirstRecurAfter(enw, enw.Start.Add(-1)) }
|
||||||
|
|
||||||
|
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
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
tDate := r.Start
|
tDate := enm.Start
|
||||||
for {
|
for {
|
||||||
if tDate.Equal(date) {
|
if tDate.Equal(date) {
|
||||||
return true
|
return true
|
||||||
|
@ -63,23 +272,13 @@ func (r *Recur) onMonths(date time.Time) bool {
|
||||||
if tDate.After(date) {
|
if tDate.After(date) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
tDate = tDate.AddMonths(enm.N)
|
||||||
y, m, d := tDate.Date()
|
|
||||||
tDate = time.Date(y, m+time.Month(r.Count), d, 0, 0, 0, 0, time.UTC)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *Recur) NextAfter(old time.Time) time.Time {
|
func (enm EveryNMonths) First() Date { return FirstRecurAfter(enm, enm.Start.Add(-1)) }
|
||||||
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 {
|
func (enm EveryNMonths) String() string {
|
||||||
return r.Start.IsZero() || !slices.Contains(ValidPeriods, r.Period)
|
return fmt.Sprintf("%s, every %d months", enm.Start.String(), enm.N)
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,102 +4,425 @@ import (
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/google/go-cmp/cmp"
|
||||||
"go-mod.ewintr.nl/planner/item"
|
"go-mod.ewintr.nl/planner/item"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestRecur(t *testing.T) {
|
func TestDaily(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
t.Run("days", func(t *testing.T) {
|
daily := item.Daily{
|
||||||
r := item.Recur{
|
Start: item.NewDate(2021, 1, 31), // a sunday
|
||||||
Start: time.Date(2024, 12, 1, 0, 0, 0, 0, time.UTC),
|
|
||||||
Period: item.PeriodDay,
|
|
||||||
Count: 5,
|
|
||||||
}
|
}
|
||||||
day := 24 * time.Hour
|
dailyStr := "2021-01-31, daily"
|
||||||
|
|
||||||
|
t.Run("parse", func(t *testing.T) {
|
||||||
|
if diff := cmp.Diff(daily, item.NewRecurrer(dailyStr)); diff != "" {
|
||||||
|
t.Errorf("(-exp +got):\n%s", diff)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("string", func(t *testing.T) {
|
||||||
|
if dailyStr != daily.String() {
|
||||||
|
t.Errorf("exp %v, got %v", dailyStr, daily.String())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("recurs_on", func(t *testing.T) {
|
||||||
for _, tc := range []struct {
|
for _, tc := range []struct {
|
||||||
name string
|
name string
|
||||||
date time.Time
|
date item.Date
|
||||||
exp bool
|
exp bool
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "before",
|
name: "before",
|
||||||
date: time.Date(202, 1, 1, 0, 0, 0, 0, time.UTC),
|
date: item.NewDate(2021, 1, 30),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "on",
|
||||||
|
date: daily.Start,
|
||||||
|
exp: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "after",
|
||||||
|
date: item.NewDate(2021, 2, 1),
|
||||||
|
exp: true,
|
||||||
|
},
|
||||||
|
} {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
if tc.exp != daily.RecursOn(tc.date) {
|
||||||
|
t.Errorf("exp %v, got %v", tc.exp, daily.RecursOn(tc.date))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEveryNDays(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
every := item.EveryNDays{
|
||||||
|
Start: item.NewDate(2022, 6, 8),
|
||||||
|
N: 5,
|
||||||
|
}
|
||||||
|
everyStr := "2022-06-08, every 5 days"
|
||||||
|
|
||||||
|
t.Run("parse", func(t *testing.T) {
|
||||||
|
if diff := cmp.Diff(every, item.NewRecurrer(everyStr)); diff != "" {
|
||||||
|
t.Errorf("(-exp +got):\n%s", diff)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("string", func(t *testing.T) {
|
||||||
|
if everyStr != every.String() {
|
||||||
|
t.Errorf("exp %v, got %v", everyStr, every.String())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("recurs on", func(t *testing.T) {
|
||||||
|
for _, tc := range []struct {
|
||||||
|
name string
|
||||||
|
date item.Date
|
||||||
|
exp bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "before",
|
||||||
|
date: item.NewDate(2022, 1, 1),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "start",
|
name: "start",
|
||||||
date: r.Start,
|
date: every.Start,
|
||||||
exp: true,
|
exp: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "after true",
|
name: "after true",
|
||||||
date: r.Start.Add(15 * day),
|
date: every.Start.Add(15),
|
||||||
exp: true,
|
exp: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "after false",
|
name: "after false",
|
||||||
date: r.Start.Add(16 * day),
|
date: every.Start.Add(16),
|
||||||
},
|
},
|
||||||
} {
|
} {
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
if act := r.On(tc.date); tc.exp != act {
|
if tc.exp != every.RecursOn(tc.date) {
|
||||||
t.Errorf("exp %v, got %v", tc.exp, act)
|
t.Errorf("exp %v, got %v", tc.exp, tc.date)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
}
|
||||||
|
|
||||||
t.Run("months", func(t *testing.T) {
|
func TestParseWeekly(t *testing.T) {
|
||||||
r := item.Recur{
|
t.Parallel()
|
||||||
Start: time.Date(2021, 2, 3, 0, 0, 0, 0, time.UTC),
|
|
||||||
Period: item.PeriodMonth,
|
|
||||||
Count: 3,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
start := item.NewDate(2021, 2, 7)
|
||||||
for _, tc := range []struct {
|
for _, tc := range []struct {
|
||||||
name string
|
name string
|
||||||
date time.Time
|
input []string
|
||||||
|
expOK bool
|
||||||
|
expWeekly item.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: item.Weekly{
|
||||||
|
Start: start,
|
||||||
|
Weekdays: item.Weekdays{
|
||||||
|
time.Monday,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "multiple days",
|
||||||
|
input: []string{"weekly", "monday & thursday & saturday"},
|
||||||
|
expOK: true,
|
||||||
|
expWeekly: item.Weekly{
|
||||||
|
Start: start,
|
||||||
|
Weekdays: item.Weekdays{
|
||||||
|
time.Monday,
|
||||||
|
time.Thursday,
|
||||||
|
time.Saturday,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "wrong order",
|
||||||
|
input: []string{"weekly", "sunday & thursday & wednesday"},
|
||||||
|
expOK: true,
|
||||||
|
expWeekly: item.Weekly{
|
||||||
|
Start: start,
|
||||||
|
Weekdays: item.Weekdays{
|
||||||
|
time.Wednesday,
|
||||||
|
time.Thursday,
|
||||||
|
time.Sunday,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "doubles",
|
||||||
|
input: []string{"weekly", "sunday & sunday & monday"},
|
||||||
|
expOK: true,
|
||||||
|
expWeekly: item.Weekly{
|
||||||
|
Start: start,
|
||||||
|
Weekdays: item.Weekdays{
|
||||||
|
time.Monday,
|
||||||
|
time.Sunday,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "one unknown",
|
||||||
|
input: []string{"weekly", "sunday & someday"},
|
||||||
|
expOK: true,
|
||||||
|
expWeekly: item.Weekly{
|
||||||
|
Start: start,
|
||||||
|
Weekdays: item.Weekdays{
|
||||||
|
time.Sunday,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
actWeekly, actOK := item.ParseWeekly(start, tc.input)
|
||||||
|
if tc.expOK != actOK {
|
||||||
|
t.Errorf("exp %v, got %v", tc.expOK, actOK)
|
||||||
|
}
|
||||||
|
if !tc.expOK {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if diff := cmp.Diff(tc.expWeekly, actWeekly); diff != "" {
|
||||||
|
t.Errorf("(-exp, +got)%s\n", diff)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWeekly(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
weekly := item.Weekly{
|
||||||
|
Start: item.NewDate(2021, 1, 31), // a sunday
|
||||||
|
Weekdays: item.Weekdays{
|
||||||
|
time.Monday,
|
||||||
|
time.Wednesday,
|
||||||
|
time.Thursday,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
weeklyStr := "2021-01-31, weekly, monday & wednesday & thursday"
|
||||||
|
|
||||||
|
t.Run("parse", func(t *testing.T) {
|
||||||
|
if diff := cmp.Diff(weekly, item.NewRecurrer(weeklyStr)); diff != "" {
|
||||||
|
t.Errorf("(-exp, +got)%s\n", diff)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("string", func(t *testing.T) {
|
||||||
|
if weeklyStr != weekly.String() {
|
||||||
|
t.Errorf("exp %v, got %v", weeklyStr, weekly.String())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("recurs_on", func(t *testing.T) {
|
||||||
|
for _, tc := range []struct {
|
||||||
|
name string
|
||||||
|
date item.Date
|
||||||
exp bool
|
exp bool
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "before start",
|
name: "before start",
|
||||||
date: time.Date(2021, 1, 27, 0, 0, 0, 0, time.UTC),
|
date: item.NewDate(2021, 1, 27), // a wednesday
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "right weekday",
|
||||||
|
date: item.NewDate(2021, 2, 1), // a monday
|
||||||
|
exp: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "another right day",
|
||||||
|
date: item.NewDate(2021, 2, 3), // a wednesday
|
||||||
|
exp: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "wrong weekday",
|
||||||
|
date: item.NewDate(2021, 2, 5), // a friday
|
||||||
|
},
|
||||||
|
} {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
if tc.exp != weekly.RecursOn(tc.date) {
|
||||||
|
t.Errorf("exp %v, got %v", tc.exp, weekly.RecursOn(tc.date))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEveryNWeeks(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
everyNWeeks := item.EveryNWeeks{
|
||||||
|
Start: item.NewDate(2021, 2, 3),
|
||||||
|
N: 3,
|
||||||
|
}
|
||||||
|
everyNWeeksStr := "2021-02-03, every 3 weeks"
|
||||||
|
|
||||||
|
t.Run("parse", func(t *testing.T) {
|
||||||
|
if everyNWeeks != item.NewRecurrer(everyNWeeksStr) {
|
||||||
|
t.Errorf("exp %v, got %v", everyNWeeks, item.NewRecurrer(everyNWeeksStr))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("string", func(t *testing.T) {
|
||||||
|
if everyNWeeksStr != everyNWeeks.String() {
|
||||||
|
t.Errorf("exp %v, got %v", everyNWeeksStr, everyNWeeks.String())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("recurs on", func(t *testing.T) {
|
||||||
|
for _, tc := range []struct {
|
||||||
|
name string
|
||||||
|
date item.Date
|
||||||
|
exp bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "before start",
|
||||||
|
date: item.NewDate(2021, 1, 27),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "on start",
|
name: "on start",
|
||||||
date: time.Date(2021, 2, 3, 0, 0, 0, 0, time.UTC),
|
date: item.NewDate(2021, 2, 3),
|
||||||
|
exp: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "wrong day",
|
||||||
|
date: item.NewDate(2021, 2, 4),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "one week after",
|
||||||
|
date: item.NewDate(2021, 2, 10),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "first interval",
|
||||||
|
date: item.NewDate(2021, 2, 24),
|
||||||
|
exp: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "second interval",
|
||||||
|
date: item.NewDate(2021, 3, 17),
|
||||||
|
exp: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "second interval plus one week",
|
||||||
|
date: item.NewDate(2021, 3, 24),
|
||||||
|
},
|
||||||
|
} {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
if tc.exp != everyNWeeks.RecursOn(tc.date) {
|
||||||
|
t.Errorf("exp %v, got %v", tc.exp, everyNWeeks.RecursOn(tc.date))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEveryNMonths(t *testing.T) {
|
||||||
|
everyNMonths := item.EveryNMonths{
|
||||||
|
Start: item.NewDate(2021, 2, 3),
|
||||||
|
N: 3,
|
||||||
|
}
|
||||||
|
everyNMonthsStr := "2021-02-03, every 3 months"
|
||||||
|
|
||||||
|
t.Run("parse", func(t *testing.T) {
|
||||||
|
if diff := cmp.Diff(everyNMonths, item.NewRecurrer(everyNMonthsStr)); diff != "" {
|
||||||
|
t.Errorf("(-exp, +got)%s\n", diff)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("string", func(t *testing.T) {
|
||||||
|
if everyNMonthsStr != everyNMonths.String() {
|
||||||
|
t.Errorf("exp %v, got %v", everyNMonthsStr, everyNMonths.String())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("recurs on", func(t *testing.T) {
|
||||||
|
for _, tc := range []struct {
|
||||||
|
name string
|
||||||
|
date item.Date
|
||||||
|
exp bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "before start",
|
||||||
|
date: item.NewDate(2021, 1, 27),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "on start",
|
||||||
|
date: item.NewDate(2021, 2, 3),
|
||||||
exp: true,
|
exp: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "8 weeks after",
|
name: "8 weeks after",
|
||||||
date: time.Date(2021, 3, 31, 0, 0, 0, 0, time.UTC),
|
date: item.NewDate(2021, 3, 31),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "one month",
|
name: "one month",
|
||||||
date: time.Date(2021, 3, 3, 0, 0, 0, 0, time.UTC),
|
date: item.NewDate(2021, 3, 3),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "3 months",
|
name: "3 months",
|
||||||
date: time.Date(2021, 5, 3, 0, 0, 0, 0, time.UTC),
|
date: item.NewDate(2021, 5, 3),
|
||||||
exp: true,
|
exp: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "4 months",
|
name: "4 months",
|
||||||
date: time.Date(2021, 6, 3, 0, 0, 0, 0, time.UTC),
|
date: item.NewDate(2021, 6, 3),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "6 months",
|
name: "6 months",
|
||||||
date: time.Date(2021, 8, 3, 0, 0, 0, 0, time.UTC),
|
date: item.NewDate(2021, 8, 3),
|
||||||
exp: true,
|
exp: true,
|
||||||
},
|
},
|
||||||
} {
|
} {
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
if act := r.On(tc.date); tc.exp != act {
|
if tc.exp != everyNMonths.RecursOn(tc.date) {
|
||||||
t.Errorf("exp %v, got %v", tc.exp, act)
|
t.Errorf("exp %v, got %v", tc.exp, everyNMonths.RecursOn(tc.date))
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
t.Run("recurs every year", func(t *testing.T) {
|
||||||
|
recur := item.EveryNMonths{
|
||||||
|
Start: item.NewDate(2021, 3, 1),
|
||||||
|
N: 12,
|
||||||
|
}
|
||||||
|
if recur.RecursOn(item.NewDate(2021, 3, 9)) {
|
||||||
|
t.Errorf("exp false, got true")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("bug", func(t *testing.T) {
|
||||||
|
recur := item.EveryNMonths{
|
||||||
|
Start: item.NewDate(2021, 3, 1),
|
||||||
|
N: 1,
|
||||||
|
}
|
||||||
|
if recur.RecursOn(item.NewDate(2021, 11, 3)) {
|
||||||
|
t.Errorf("exp false, got true")
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,70 @@
|
||||||
|
package item
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
TimeFormat = "15:04"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Time struct {
|
||||||
|
t time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t Time) MarshalJSON() ([]byte, error) {
|
||||||
|
return json.Marshal(t.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Time) UnmarshalJSON(data []byte) error {
|
||||||
|
timeString := ""
|
||||||
|
if err := json.Unmarshal(data, &timeString); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
nt := NewTimeFromString(timeString)
|
||||||
|
t.t = nt.Time()
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewTime(hour, minute int) Time {
|
||||||
|
return Time{
|
||||||
|
t: time.Date(0, 0, 0, hour, minute, 0, 0, time.UTC),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewTimeFromString(timeStr string) Time {
|
||||||
|
tm, err := time.Parse(TimeFormat, timeStr)
|
||||||
|
if err != nil {
|
||||||
|
return Time{t: time.Time{}}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Time{t: tm}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Time) String() string {
|
||||||
|
return t.t.Format(TimeFormat)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Time) Time() time.Time {
|
||||||
|
return t.t
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Time) IsZero() bool {
|
||||||
|
return t.t.IsZero()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Time) Hour() int {
|
||||||
|
return t.t.Hour()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Time) Minute() int {
|
||||||
|
return t.t.Minute()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Time) Add(d time.Duration) Time {
|
||||||
|
return Time{
|
||||||
|
t: t.t.Add(d),
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,67 @@
|
||||||
|
package item_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"go-mod.ewintr.nl/planner/item"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestTime(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
h, m := 11, 18
|
||||||
|
tm := item.NewTime(h, m)
|
||||||
|
expStr := "11:18"
|
||||||
|
if expStr != tm.String() {
|
||||||
|
t.Errorf("exp %v, got %v", expStr, tm.String())
|
||||||
|
}
|
||||||
|
actJSON, err := json.Marshal(tm)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("exp nil, got %v", err)
|
||||||
|
}
|
||||||
|
expJSON := fmt.Sprintf("%q", expStr)
|
||||||
|
if expJSON != string(actJSON) {
|
||||||
|
t.Errorf("exp %v, got %v", expJSON, string(actJSON))
|
||||||
|
}
|
||||||
|
var actTM item.Time
|
||||||
|
if err := json.Unmarshal(actJSON, &actTM); err != nil {
|
||||||
|
t.Errorf("exp nil, got %v", err)
|
||||||
|
}
|
||||||
|
if expStr != actTM.String() {
|
||||||
|
t.Errorf("ecp %v, got %v", expStr, actTM.String())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTimeFromString(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
for _, tc := range []struct {
|
||||||
|
name string
|
||||||
|
str string
|
||||||
|
exp string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "empty",
|
||||||
|
exp: "00:00",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid",
|
||||||
|
str: "invalid",
|
||||||
|
exp: "00:00",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "valid",
|
||||||
|
str: "11:42",
|
||||||
|
exp: "11:42",
|
||||||
|
},
|
||||||
|
} {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
act := item.NewTimeFromString(tc.str)
|
||||||
|
if tc.exp != act.String() {
|
||||||
|
t.Errorf("exp %v, got %v", tc.exp, act.String())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
|
@ -3,7 +3,6 @@ package command
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
"go-mod.ewintr.nl/planner/item"
|
"go-mod.ewintr.nl/planner/item"
|
||||||
|
@ -27,8 +26,7 @@ func NewAdd(localRepo storage.LocalID, eventRepo storage.Event, syncRepo storage
|
||||||
FlagOn: &FlagDate{},
|
FlagOn: &FlagDate{},
|
||||||
FlagAt: &FlagTime{},
|
FlagAt: &FlagTime{},
|
||||||
FlagFor: &FlagDuration{},
|
FlagFor: &FlagDuration{},
|
||||||
FlagRecStart: &FlagDate{},
|
FlagRec: &FlagRecurrer{},
|
||||||
FlagRecPeriod: &FlagPeriod{},
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
@ -70,39 +68,25 @@ func (add *Add) Execute(main []string, flags map[string]string) error {
|
||||||
return fmt.Errorf("could not set duration to 24 hours")
|
return fmt.Errorf("could not set duration to 24 hours")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if as.IsSet(FlagRecStart) != as.IsSet(FlagRecPeriod) {
|
|
||||||
return fmt.Errorf("rec-start required rec-period and vice versa")
|
|
||||||
}
|
|
||||||
|
|
||||||
return add.do()
|
return add.do()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (add *Add) do() error {
|
func (add *Add) do() error {
|
||||||
as := add.argSet
|
as := add.argSet
|
||||||
start := as.GetTime(FlagOn)
|
rec := as.GetRecurrer(FlagRec)
|
||||||
if as.IsSet(FlagAt) {
|
|
||||||
at := as.GetTime(FlagAt)
|
|
||||||
h := time.Duration(at.Hour()) * time.Hour
|
|
||||||
m := time.Duration(at.Minute()) * time.Minute
|
|
||||||
start = start.Add(h).Add(m)
|
|
||||||
}
|
|
||||||
|
|
||||||
e := item.Event{
|
e := item.Event{
|
||||||
ID: uuid.New().String(),
|
ID: uuid.New().String(),
|
||||||
|
Date: as.GetDate(FlagOn),
|
||||||
|
Recurrer: rec,
|
||||||
EventBody: item.EventBody{
|
EventBody: item.EventBody{
|
||||||
Title: as.Main,
|
Title: as.Main,
|
||||||
Start: start,
|
Time: as.GetTime(FlagAt),
|
||||||
|
Duration: as.GetDuration(FlagFor),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
if rec != nil {
|
||||||
if as.IsSet(FlagFor) {
|
e.RecurNext = rec.First()
|
||||||
e.Duration = as.GetDuration(FlagFor)
|
|
||||||
}
|
|
||||||
if as.IsSet(FlagRecStart) {
|
|
||||||
e.Recurrer = &item.Recur{
|
|
||||||
Start: as.GetTime(FlagRecStart),
|
|
||||||
Period: as.GetRecurPeriod(FlagRecPeriod),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := add.eventRepo.Store(e); err != nil {
|
if err := add.eventRepo.Store(e); err != nil {
|
||||||
|
|
|
@ -4,7 +4,6 @@ import (
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/google/go-cmp/cmp"
|
|
||||||
"go-mod.ewintr.nl/planner/item"
|
"go-mod.ewintr.nl/planner/item"
|
||||||
"go-mod.ewintr.nl/planner/plan/command"
|
"go-mod.ewintr.nl/planner/plan/command"
|
||||||
"go-mod.ewintr.nl/planner/plan/storage/memory"
|
"go-mod.ewintr.nl/planner/plan/storage/memory"
|
||||||
|
@ -13,13 +12,11 @@ import (
|
||||||
func TestAdd(t *testing.T) {
|
func TestAdd(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
aDateStr := "2024-11-02"
|
aDate := item.NewDate(2024, 11, 2)
|
||||||
aDate := time.Date(2024, 11, 2, 0, 0, 0, 0, time.UTC)
|
aTime := item.NewTime(12, 0)
|
||||||
aTimeStr := "12:00"
|
|
||||||
aDay := time.Duration(24) * time.Hour
|
aDay := time.Duration(24) * time.Hour
|
||||||
anHourStr := "1h"
|
anHourStr := "1h"
|
||||||
anHour := time.Hour
|
anHour := time.Hour
|
||||||
aDateAndTime := time.Date(2024, 11, 2, 12, 0, 0, 0, time.UTC)
|
|
||||||
|
|
||||||
for _, tc := range []struct {
|
for _, tc := range []struct {
|
||||||
name string
|
name string
|
||||||
|
@ -36,7 +33,7 @@ func TestAdd(t *testing.T) {
|
||||||
name: "title missing",
|
name: "title missing",
|
||||||
main: []string{"add"},
|
main: []string{"add"},
|
||||||
flags: map[string]string{
|
flags: map[string]string{
|
||||||
command.FlagOn: aDateStr,
|
command.FlagOn: aDate.String(),
|
||||||
},
|
},
|
||||||
expErr: true,
|
expErr: true,
|
||||||
},
|
},
|
||||||
|
@ -49,46 +46,31 @@ func TestAdd(t *testing.T) {
|
||||||
name: "only date",
|
name: "only date",
|
||||||
main: []string{"add", "title"},
|
main: []string{"add", "title"},
|
||||||
flags: map[string]string{
|
flags: map[string]string{
|
||||||
command.FlagOn: aDateStr,
|
command.FlagOn: aDate.String(),
|
||||||
},
|
},
|
||||||
expEvent: item.Event{
|
expEvent: item.Event{
|
||||||
ID: "title",
|
ID: "title",
|
||||||
|
Date: aDate,
|
||||||
EventBody: item.EventBody{
|
EventBody: item.EventBody{
|
||||||
Title: "title",
|
Title: "title",
|
||||||
Start: aDate,
|
|
||||||
Duration: aDay,
|
Duration: aDay,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
|
||||||
name: "date and time",
|
|
||||||
main: []string{"add", "title"},
|
|
||||||
flags: map[string]string{
|
|
||||||
command.FlagOn: aDateStr,
|
|
||||||
command.FlagAt: aTimeStr,
|
|
||||||
},
|
|
||||||
expEvent: item.Event{
|
|
||||||
ID: "title",
|
|
||||||
EventBody: item.EventBody{
|
|
||||||
Title: "title",
|
|
||||||
Start: aDateAndTime,
|
|
||||||
Duration: anHour,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
name: "date, time and duration",
|
name: "date, time and duration",
|
||||||
main: []string{"add", "title"},
|
main: []string{"add", "title"},
|
||||||
flags: map[string]string{
|
flags: map[string]string{
|
||||||
command.FlagOn: aDateStr,
|
command.FlagOn: aDate.String(),
|
||||||
command.FlagAt: aTimeStr,
|
command.FlagAt: aTime.String(),
|
||||||
command.FlagFor: anHourStr,
|
command.FlagFor: anHourStr,
|
||||||
},
|
},
|
||||||
expEvent: item.Event{
|
expEvent: item.Event{
|
||||||
ID: "title",
|
ID: "title",
|
||||||
|
Date: aDate,
|
||||||
EventBody: item.EventBody{
|
EventBody: item.EventBody{
|
||||||
Title: "title",
|
Title: "title",
|
||||||
Start: aDateAndTime,
|
Time: aTime,
|
||||||
Duration: anHour,
|
Duration: anHour,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -97,51 +79,11 @@ func TestAdd(t *testing.T) {
|
||||||
name: "date and duration",
|
name: "date and duration",
|
||||||
main: []string{"add", "title"},
|
main: []string{"add", "title"},
|
||||||
flags: map[string]string{
|
flags: map[string]string{
|
||||||
command.FlagOn: aDateStr,
|
command.FlagOn: aDate.String(),
|
||||||
command.FlagFor: anHourStr,
|
command.FlagFor: anHourStr,
|
||||||
},
|
},
|
||||||
expErr: true,
|
expErr: true,
|
||||||
},
|
},
|
||||||
{
|
|
||||||
name: "rec-start without rec-period",
|
|
||||||
main: []string{"add", "title"},
|
|
||||||
flags: map[string]string{
|
|
||||||
command.FlagOn: aDateStr,
|
|
||||||
command.FlagRecStart: "2024-12-08",
|
|
||||||
},
|
|
||||||
expErr: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "rec-period without rec-start",
|
|
||||||
main: []string{"add", "title"},
|
|
||||||
flags: map[string]string{
|
|
||||||
command.FlagOn: aDateStr,
|
|
||||||
command.FlagRecPeriod: "day",
|
|
||||||
},
|
|
||||||
expErr: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "rec-start with rec-period",
|
|
||||||
main: []string{"add", "title"},
|
|
||||||
flags: map[string]string{
|
|
||||||
command.FlagOn: aDateStr,
|
|
||||||
command.FlagRecStart: "2024-12-08",
|
|
||||||
command.FlagRecPeriod: "day",
|
|
||||||
},
|
|
||||||
expEvent: item.Event{
|
|
||||||
ID: "title",
|
|
||||||
Recurrer: &item.Recur{
|
|
||||||
Start: time.Date(2024, 12, 8, 0, 0, 0, 0, time.UTC),
|
|
||||||
Period: item.PeriodDay,
|
|
||||||
},
|
|
||||||
RecurNext: time.Time{},
|
|
||||||
EventBody: item.EventBody{
|
|
||||||
Title: "title",
|
|
||||||
Start: aDate,
|
|
||||||
Duration: aDay,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
} {
|
} {
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
eventRepo := memory.NewEvent()
|
eventRepo := memory.NewEvent()
|
||||||
|
@ -179,7 +121,7 @@ func TestAdd(t *testing.T) {
|
||||||
t.Errorf("exp string not te be empty")
|
t.Errorf("exp string not te be empty")
|
||||||
}
|
}
|
||||||
tc.expEvent.ID = actEvents[0].ID
|
tc.expEvent.ID = actEvents[0].ID
|
||||||
if diff := cmp.Diff(tc.expEvent, actEvents[0]); diff != "" {
|
if diff := item.EventDiff(tc.expEvent, actEvents[0]); diff != "" {
|
||||||
t.Errorf("(exp -, got +)\n%s", diff)
|
t.Errorf("(exp -, got +)\n%s", diff)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -40,14 +40,26 @@ func (as *ArgSet) GetString(name string) string {
|
||||||
return val
|
return val
|
||||||
}
|
}
|
||||||
|
|
||||||
func (as *ArgSet) GetTime(name string) time.Time {
|
func (as *ArgSet) GetDate(name string) item.Date {
|
||||||
flag, ok := as.Flags[name]
|
flag, ok := as.Flags[name]
|
||||||
if !ok {
|
if !ok {
|
||||||
return time.Time{}
|
return item.Date{}
|
||||||
}
|
}
|
||||||
val, ok := flag.Get().(time.Time)
|
val, ok := flag.Get().(item.Date)
|
||||||
if !ok {
|
if !ok {
|
||||||
return time.Time{}
|
return item.Date{}
|
||||||
|
}
|
||||||
|
return val
|
||||||
|
}
|
||||||
|
|
||||||
|
func (as *ArgSet) GetTime(name string) item.Time {
|
||||||
|
flag, ok := as.Flags[name]
|
||||||
|
if !ok {
|
||||||
|
return item.Time{}
|
||||||
|
}
|
||||||
|
val, ok := flag.Get().(item.Time)
|
||||||
|
if !ok {
|
||||||
|
return item.Time{}
|
||||||
}
|
}
|
||||||
return val
|
return val
|
||||||
}
|
}
|
||||||
|
@ -64,14 +76,26 @@ func (as *ArgSet) GetDuration(name string) time.Duration {
|
||||||
return val
|
return val
|
||||||
}
|
}
|
||||||
|
|
||||||
func (as *ArgSet) GetRecurPeriod(name string) item.RecurPeriod {
|
func (as *ArgSet) GetRecurrer(name string) item.Recurrer {
|
||||||
flag, ok := as.Flags[name]
|
flag, ok := as.Flags[name]
|
||||||
if !ok {
|
if !ok {
|
||||||
return item.RecurPeriod("")
|
return nil
|
||||||
}
|
}
|
||||||
val, ok := flag.Get().(item.RecurPeriod)
|
val, ok := flag.Get().(item.Recurrer)
|
||||||
if !ok {
|
if !ok {
|
||||||
return item.RecurPeriod("")
|
return nil
|
||||||
|
}
|
||||||
|
return val
|
||||||
|
}
|
||||||
|
|
||||||
|
func (as *ArgSet) GetInt(name string) int {
|
||||||
|
flag, ok := as.Flags[name]
|
||||||
|
if !ok {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
val, ok := flag.Get().(int)
|
||||||
|
if !ok {
|
||||||
|
return 0
|
||||||
}
|
}
|
||||||
return val
|
return val
|
||||||
}
|
}
|
||||||
|
|
|
@ -56,11 +56,11 @@ func TestArgSet(t *testing.T) {
|
||||||
{
|
{
|
||||||
name: "recur period flag success",
|
name: "recur period flag success",
|
||||||
flags: map[string]command.Flag{
|
flags: map[string]command.Flag{
|
||||||
"period": &command.FlagPeriod{Name: "period"},
|
"recur": &command.FlagRecurrer{Name: "recur"},
|
||||||
},
|
},
|
||||||
flagName: "period",
|
flagName: "recur",
|
||||||
setValue: "month",
|
setValue: "2024-12-23, daily",
|
||||||
exp: item.PeriodMonth,
|
exp: item.NewRecurrer("2024-12-23, daily"),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "unknown flag error",
|
name: "unknown flag error",
|
||||||
|
@ -95,26 +95,9 @@ func TestArgSet(t *testing.T) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Verify IsSet() returns true after setting
|
|
||||||
if !as.IsSet(tt.flagName) {
|
if !as.IsSet(tt.flagName) {
|
||||||
t.Errorf("ArgSet.IsSet() = false, want true for flag %s", tt.flagName)
|
t.Errorf("ArgSet.IsSet() = false, want true for flag %s", tt.flagName)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Verify the value was set correctly based on flag type
|
|
||||||
switch v := tt.exp.(type) {
|
|
||||||
case string:
|
|
||||||
if got := as.GetString(tt.flagName); got != v {
|
|
||||||
t.Errorf("ArgSet.GetString() = %v, want %v", got, v)
|
|
||||||
}
|
|
||||||
case time.Time:
|
|
||||||
if got := as.GetTime(tt.flagName); !got.Equal(v) {
|
|
||||||
t.Errorf("ArgSet.GetTime() = %v, want %v", got, v)
|
|
||||||
}
|
|
||||||
case time.Duration:
|
|
||||||
if got := as.GetDuration(tt.flagName); got != v {
|
|
||||||
t.Errorf("ArgSet.GetDuration() = %v, want %v", got, v)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,8 +11,7 @@ const (
|
||||||
FlagOn = "on"
|
FlagOn = "on"
|
||||||
FlagAt = "at"
|
FlagAt = "at"
|
||||||
FlagFor = "for"
|
FlagFor = "for"
|
||||||
FlagRecStart = "rec-start"
|
FlagRec = "rec"
|
||||||
FlagRecPeriod = "rec-period"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type Command interface {
|
type Command interface {
|
||||||
|
|
|
@ -1,109 +1,68 @@
|
||||||
package command_test
|
package command_test
|
||||||
|
|
||||||
// func TestArgSet(t *testing.T) {
|
import (
|
||||||
// t.Parallel()
|
"testing"
|
||||||
|
|
||||||
// as := command.ArgSet{
|
"github.com/google/go-cmp/cmp"
|
||||||
// Main: "main",
|
"go-mod.ewintr.nl/planner/plan/command"
|
||||||
// Flags: map[string]string{
|
)
|
||||||
// "name 1": "value 1",
|
|
||||||
// "name 2": "value 2",
|
|
||||||
// "name 3": "value 3",
|
|
||||||
// },
|
|
||||||
// }
|
|
||||||
|
|
||||||
// t.Run("hasflag", func(t *testing.T) {
|
func TestParseArgs(t *testing.T) {
|
||||||
// t.Run("true", func(t *testing.T) {
|
t.Parallel()
|
||||||
// if has := as.HasFlag("name 1"); !has {
|
|
||||||
// t.Errorf("exp true, got %v", has)
|
|
||||||
// }
|
|
||||||
// })
|
|
||||||
// t.Run("false", func(t *testing.T) {
|
|
||||||
// if has := as.HasFlag("unknown"); has {
|
|
||||||
// t.Errorf("exp false, got %v", has)
|
|
||||||
// }
|
|
||||||
// })
|
|
||||||
// })
|
|
||||||
|
|
||||||
// t.Run("flag", func(t *testing.T) {
|
for _, tc := range []struct {
|
||||||
// t.Run("known", func(t *testing.T) {
|
name string
|
||||||
// if val := as.Flag("name 1"); val != "value 1" {
|
args []string
|
||||||
// t.Errorf("exp value 1, got %v", val)
|
expMain []string
|
||||||
// }
|
expFlags map[string]string
|
||||||
// })
|
expErr bool
|
||||||
// t.Run("unknown", func(t *testing.T) {
|
}{
|
||||||
// if val := as.Flag("unknown"); val != "" {
|
{
|
||||||
// t.Errorf(`exp "", got %v`, val)
|
name: "empty",
|
||||||
// }
|
expMain: []string{},
|
||||||
// })
|
expFlags: map[string]string{},
|
||||||
// })
|
},
|
||||||
|
{
|
||||||
// t.Run("setflag", func(t *testing.T) {
|
name: "just main",
|
||||||
// exp := "new value"
|
args: []string{"one", "two three", "four"},
|
||||||
// as.SetFlag("new name", exp)
|
expMain: []string{"one", "two three", "four"},
|
||||||
// if act := as.Flag("new name"); exp != act {
|
expFlags: map[string]string{},
|
||||||
// t.Errorf("exp %v, got %v", exp, act)
|
},
|
||||||
// }
|
{
|
||||||
// })
|
name: "with flags",
|
||||||
// }
|
args: []string{"-flag1", "value1", "one", "two", "-flag2", "value2", "-flag3", "value3"},
|
||||||
|
expMain: []string{"one", "two"},
|
||||||
// func TestParseArgs(t *testing.T) {
|
expFlags: map[string]string{
|
||||||
// t.Parallel()
|
"flag1": "value1",
|
||||||
|
"flag2": "value2",
|
||||||
// for _, tc := range []struct {
|
"flag3": "value3",
|
||||||
// name string
|
},
|
||||||
// args []string
|
},
|
||||||
// expAS *command.ArgSet
|
{
|
||||||
// expErr bool
|
name: "flag without value",
|
||||||
// }{
|
args: []string{"one", "two", "-flag1"},
|
||||||
// {
|
expErr: true,
|
||||||
// name: "empty",
|
},
|
||||||
// expAS: &command.ArgSet{
|
{
|
||||||
// Flags: map[string]string{},
|
name: "split main",
|
||||||
// },
|
args: []string{"one", "-flag1", "value1", "two"},
|
||||||
// },
|
expErr: true,
|
||||||
// {
|
},
|
||||||
// name: "just main",
|
} {
|
||||||
// args: []string{"one", "two three", "four"},
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
// expAS: &command.ArgSet{
|
actMain, actFlags, actErr := command.ParseFlags(tc.args)
|
||||||
// Main: "one two three four",
|
if tc.expErr != (actErr != nil) {
|
||||||
// Flags: map[string]string{},
|
t.Errorf("exp %v, got %v", tc.expErr, actErr)
|
||||||
// },
|
}
|
||||||
// },
|
if tc.expErr {
|
||||||
// {
|
return
|
||||||
// name: "with flags",
|
}
|
||||||
// args: []string{"-flag1", "value1", "one", "two", "-flag2", "value2", "-flag3", "value3"},
|
if diff := cmp.Diff(tc.expMain, actMain); diff != "" {
|
||||||
// expAS: &command.ArgSet{
|
t.Errorf("(exp +, got -)\n%s", diff)
|
||||||
// Main: "one two",
|
}
|
||||||
// Flags: map[string]string{
|
if diff := cmp.Diff(tc.expFlags, actFlags); diff != "" {
|
||||||
// "flag1": "value1",
|
t.Errorf("(exp +, got -)\n%s", diff)
|
||||||
// "flag2": "value2",
|
}
|
||||||
// "flag3": "value3",
|
})
|
||||||
// },
|
}
|
||||||
// },
|
}
|
||||||
// },
|
|
||||||
// {
|
|
||||||
// name: "flag without value",
|
|
||||||
// args: []string{"one", "two", "-flag1"},
|
|
||||||
// expErr: true,
|
|
||||||
// },
|
|
||||||
// {
|
|
||||||
// name: "split main",
|
|
||||||
// args: []string{"one", "-flag1", "value1", "two"},
|
|
||||||
// expErr: true,
|
|
||||||
// },
|
|
||||||
// } {
|
|
||||||
// t.Run(tc.name, func(t *testing.T) {
|
|
||||||
// actAS, actErr := command.ParseArgs(tc.args)
|
|
||||||
// if tc.expErr != (actErr != nil) {
|
|
||||||
// t.Errorf("exp %v, got %v", tc.expErr, actErr)
|
|
||||||
// }
|
|
||||||
// if tc.expErr {
|
|
||||||
// return
|
|
||||||
// }
|
|
||||||
// if diff := cmp.Diff(tc.expAS, actAS); diff != "" {
|
|
||||||
// t.Errorf("(exp +, got -)\n%s", diff)
|
|
||||||
// }
|
|
||||||
// })
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
|
@ -3,7 +3,6 @@ package command_test
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
|
||||||
|
|
||||||
"go-mod.ewintr.nl/planner/item"
|
"go-mod.ewintr.nl/planner/item"
|
||||||
"go-mod.ewintr.nl/planner/plan/command"
|
"go-mod.ewintr.nl/planner/plan/command"
|
||||||
|
@ -16,9 +15,9 @@ func TestDelete(t *testing.T) {
|
||||||
|
|
||||||
e := item.Event{
|
e := item.Event{
|
||||||
ID: "id",
|
ID: "id",
|
||||||
|
Date: item.NewDate(2024, 10, 7),
|
||||||
EventBody: item.EventBody{
|
EventBody: item.EventBody{
|
||||||
Title: "name",
|
Title: "name",
|
||||||
Start: time.Date(2024, 10, 7, 9, 30, 0, 0, time.UTC),
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -3,7 +3,7 @@ package command
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"slices"
|
"strconv"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"go-mod.ewintr.nl/planner/item"
|
"go-mod.ewintr.nl/planner/item"
|
||||||
|
@ -45,35 +45,35 @@ func (fs *FlagString) Get() any {
|
||||||
|
|
||||||
type FlagDate struct {
|
type FlagDate struct {
|
||||||
Name string
|
Name string
|
||||||
Value time.Time
|
Value item.Date
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ft *FlagDate) Set(val string) error {
|
func (fd *FlagDate) Set(val string) error {
|
||||||
d, err := time.Parse(DateFormat, val)
|
d := item.NewDateFromString(val)
|
||||||
if err != nil {
|
if d.IsZero() {
|
||||||
return fmt.Errorf("could not parse date: %v", d)
|
return fmt.Errorf("could not parse date: %v", d)
|
||||||
}
|
}
|
||||||
ft.Value = d
|
fd.Value = d
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ft *FlagDate) IsSet() bool {
|
func (fd *FlagDate) IsSet() bool {
|
||||||
return !ft.Value.IsZero()
|
return !fd.Value.IsZero()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (fs *FlagDate) Get() any {
|
func (fd *FlagDate) Get() any {
|
||||||
return fs.Value
|
return fd.Value
|
||||||
}
|
}
|
||||||
|
|
||||||
type FlagTime struct {
|
type FlagTime struct {
|
||||||
Name string
|
Name string
|
||||||
Value time.Time
|
Value item.Time
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ft *FlagTime) Set(val string) error {
|
func (ft *FlagTime) Set(val string) error {
|
||||||
d, err := time.Parse(TimeFormat, val)
|
d := item.NewTimeFromString(val)
|
||||||
if err != nil {
|
if d.IsZero() {
|
||||||
return fmt.Errorf("could not parse date: %v", d)
|
return fmt.Errorf("could not parse date: %v", d)
|
||||||
}
|
}
|
||||||
ft.Value = d
|
ft.Value = d
|
||||||
|
@ -111,23 +111,46 @@ func (fs *FlagDuration) Get() any {
|
||||||
return fs.Value
|
return fs.Value
|
||||||
}
|
}
|
||||||
|
|
||||||
type FlagPeriod struct {
|
type FlagRecurrer struct {
|
||||||
Name string
|
Name string
|
||||||
Value item.RecurPeriod
|
Value item.Recurrer
|
||||||
}
|
}
|
||||||
|
|
||||||
func (fp *FlagPeriod) Set(val string) error {
|
func (fr *FlagRecurrer) Set(val string) error {
|
||||||
if !slices.Contains(item.ValidPeriods, item.RecurPeriod(val)) {
|
fr.Value = item.NewRecurrer(val)
|
||||||
return fmt.Errorf("not a valid period: %v", val)
|
if fr.Value == nil {
|
||||||
|
return fmt.Errorf("not a valid recurrer: %v", val)
|
||||||
}
|
}
|
||||||
fp.Value = item.RecurPeriod(val)
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (fp *FlagPeriod) IsSet() bool {
|
func (fr *FlagRecurrer) IsSet() bool {
|
||||||
return fp.Value != ""
|
return fr.Value != nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (fp *FlagPeriod) Get() any {
|
func (fr *FlagRecurrer) Get() any {
|
||||||
return fp.Value
|
return fr.Value
|
||||||
|
}
|
||||||
|
|
||||||
|
type FlagInt struct {
|
||||||
|
Name string
|
||||||
|
Value int
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fi *FlagInt) Set(val string) error {
|
||||||
|
i, err := strconv.Atoi(val)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("not a valid integer: %v", val)
|
||||||
|
}
|
||||||
|
|
||||||
|
fi.Value = i
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fi *FlagInt) IsSet() bool {
|
||||||
|
return fi.Value != 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fi *FlagInt) Get() any {
|
||||||
|
return fi.Value
|
||||||
}
|
}
|
||||||
|
|
|
@ -37,14 +37,13 @@ func TestFlagString(t *testing.T) {
|
||||||
func TestFlagDate(t *testing.T) {
|
func TestFlagDate(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
valid := time.Date(2024, 11, 20, 0, 0, 0, 0, time.UTC)
|
valid := item.NewDate(2024, 11, 20)
|
||||||
validStr := "2024-11-20"
|
|
||||||
f := command.FlagDate{}
|
f := command.FlagDate{}
|
||||||
if f.IsSet() {
|
if f.IsSet() {
|
||||||
t.Errorf("exp false, got true")
|
t.Errorf("exp false, got true")
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := f.Set(validStr); err != nil {
|
if err := f.Set(valid.String()); err != nil {
|
||||||
t.Errorf("exp nil, got %v", err)
|
t.Errorf("exp nil, got %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -52,26 +51,25 @@ func TestFlagDate(t *testing.T) {
|
||||||
t.Errorf("exp true, got false")
|
t.Errorf("exp true, got false")
|
||||||
}
|
}
|
||||||
|
|
||||||
act, ok := f.Get().(time.Time)
|
act, ok := f.Get().(item.Date)
|
||||||
if !ok {
|
if !ok {
|
||||||
t.Errorf("exp true, got false")
|
t.Errorf("exp true, got false")
|
||||||
}
|
}
|
||||||
if act != valid {
|
if act.String() != valid.String() {
|
||||||
t.Errorf("exp %v, got %v", valid, act)
|
t.Errorf("exp %v, got %v", valid.String(), act.String())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestFlagTime(t *testing.T) {
|
func TestFlagTime(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
valid := time.Date(0, 1, 1, 12, 30, 0, 0, time.UTC)
|
valid := item.NewTime(12, 30)
|
||||||
validStr := "12:30"
|
|
||||||
f := command.FlagTime{}
|
f := command.FlagTime{}
|
||||||
if f.IsSet() {
|
if f.IsSet() {
|
||||||
t.Errorf("exp false, got true")
|
t.Errorf("exp false, got true")
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := f.Set(validStr); err != nil {
|
if err := f.Set(valid.String()); err != nil {
|
||||||
t.Errorf("exp nil, got %v", err)
|
t.Errorf("exp nil, got %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -79,12 +77,12 @@ func TestFlagTime(t *testing.T) {
|
||||||
t.Errorf("exp true, got false")
|
t.Errorf("exp true, got false")
|
||||||
}
|
}
|
||||||
|
|
||||||
act, ok := f.Get().(time.Time)
|
act, ok := f.Get().(item.Time)
|
||||||
if !ok {
|
if !ok {
|
||||||
t.Errorf("exp true, got false")
|
t.Errorf("exp true, got false")
|
||||||
}
|
}
|
||||||
if act != valid {
|
if act.String() != valid.String() {
|
||||||
t.Errorf("exp %v, got %v", valid, act)
|
t.Errorf("exp %v, got %v", valid.String(), act.String())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -115,12 +113,12 @@ func TestFlagDurationTime(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestFlagPeriod(t *testing.T) {
|
func TestFlagRecurrer(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
valid := item.PeriodMonth
|
validStr := "2024-12-23, daily"
|
||||||
validStr := "month"
|
valid := item.NewRecurrer(validStr)
|
||||||
f := command.FlagPeriod{}
|
f := command.FlagRecurrer{}
|
||||||
if f.IsSet() {
|
if f.IsSet() {
|
||||||
t.Errorf("exp false, got true")
|
t.Errorf("exp false, got true")
|
||||||
}
|
}
|
||||||
|
@ -133,7 +131,7 @@ func TestFlagPeriod(t *testing.T) {
|
||||||
t.Errorf("exp true, got false")
|
t.Errorf("exp true, got false")
|
||||||
}
|
}
|
||||||
|
|
||||||
act, ok := f.Get().(item.RecurPeriod)
|
act, ok := f.Get().(item.Recurrer)
|
||||||
if !ok {
|
if !ok {
|
||||||
t.Errorf("exp true, got false")
|
t.Errorf("exp true, got false")
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,7 +2,6 @@ package command
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"time"
|
|
||||||
|
|
||||||
"go-mod.ewintr.nl/planner/plan/storage"
|
"go-mod.ewintr.nl/planner/plan/storage"
|
||||||
)
|
)
|
||||||
|
@ -41,7 +40,7 @@ func (list *List) do() error {
|
||||||
if !ok {
|
if !ok {
|
||||||
return fmt.Errorf("could not find local id for %s", e.ID)
|
return fmt.Errorf("could not find local id for %s", e.ID)
|
||||||
}
|
}
|
||||||
fmt.Printf("%s\t%d\t%s\t%s\t%s\n", e.ID, lid, e.Title, e.Start.Format(time.DateTime), e.Duration.String())
|
fmt.Printf("%s\t%d\t%s\t%s\t%s\n", e.ID, lid, e.Title, e.Date.String(), e.Duration.String())
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
|
|
@ -2,7 +2,6 @@ package command_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
|
||||||
|
|
||||||
"go-mod.ewintr.nl/planner/item"
|
"go-mod.ewintr.nl/planner/item"
|
||||||
"go-mod.ewintr.nl/planner/plan/command"
|
"go-mod.ewintr.nl/planner/plan/command"
|
||||||
|
@ -16,9 +15,9 @@ func TestList(t *testing.T) {
|
||||||
localRepo := memory.NewLocalID()
|
localRepo := memory.NewLocalID()
|
||||||
e := item.Event{
|
e := item.Event{
|
||||||
ID: "id",
|
ID: "id",
|
||||||
|
Date: item.NewDate(2024, 10, 7),
|
||||||
EventBody: item.EventBody{
|
EventBody: item.EventBody{
|
||||||
Title: "name",
|
Title: "name",
|
||||||
Start: time.Date(2024, 10, 7, 9, 30, 0, 0, time.UTC),
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
if err := eventRepo.Store(e); err != nil {
|
if err := eventRepo.Store(e); err != nil {
|
||||||
|
|
|
@ -138,9 +138,9 @@ func TestSyncReceive(t *testing.T) {
|
||||||
}},
|
}},
|
||||||
expEvent: []item.Event{{
|
expEvent: []item.Event{{
|
||||||
ID: "a",
|
ID: "a",
|
||||||
|
Date: item.NewDate(2024, 10, 23),
|
||||||
EventBody: item.EventBody{
|
EventBody: item.EventBody{
|
||||||
Title: "title",
|
Title: "title",
|
||||||
Start: time.Date(2024, 10, 23, 8, 0, 0, 0, time.UTC),
|
|
||||||
Duration: oneHour,
|
Duration: oneHour,
|
||||||
},
|
},
|
||||||
}},
|
}},
|
||||||
|
@ -152,9 +152,9 @@ func TestSyncReceive(t *testing.T) {
|
||||||
name: "update existing",
|
name: "update existing",
|
||||||
present: []item.Event{{
|
present: []item.Event{{
|
||||||
ID: "a",
|
ID: "a",
|
||||||
|
Date: item.NewDate(2024, 10, 23),
|
||||||
EventBody: item.EventBody{
|
EventBody: item.EventBody{
|
||||||
Title: "title",
|
Title: "title",
|
||||||
Start: time.Date(2024, 10, 23, 8, 0, 0, 0, time.UTC),
|
|
||||||
Duration: oneHour,
|
Duration: oneHour,
|
||||||
},
|
},
|
||||||
}},
|
}},
|
||||||
|
@ -169,9 +169,9 @@ func TestSyncReceive(t *testing.T) {
|
||||||
}},
|
}},
|
||||||
expEvent: []item.Event{{
|
expEvent: []item.Event{{
|
||||||
ID: "a",
|
ID: "a",
|
||||||
|
Date: item.NewDate(2024, 10, 23),
|
||||||
EventBody: item.EventBody{
|
EventBody: item.EventBody{
|
||||||
Title: "new title",
|
Title: "new title",
|
||||||
Start: time.Date(2024, 10, 23, 8, 0, 0, 0, time.UTC),
|
|
||||||
Duration: oneHour,
|
Duration: oneHour,
|
||||||
},
|
},
|
||||||
}},
|
}},
|
||||||
|
@ -210,7 +210,7 @@ func TestSyncReceive(t *testing.T) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("exp nil, got %v", err)
|
t.Errorf("exp nil, got %v", err)
|
||||||
}
|
}
|
||||||
if diff := cmp.Diff(tc.expEvent, actEvents); diff != "" {
|
if diff := item.EventDiffs(tc.expEvent, actEvents); diff != "" {
|
||||||
t.Errorf("(exp +, got -)\n%s", diff)
|
t.Errorf("(exp +, got -)\n%s", diff)
|
||||||
}
|
}
|
||||||
actLocalIDs, err := localIDRepo.FindAll()
|
actLocalIDs, err := localIDRepo.FindAll()
|
||||||
|
|
|
@ -4,9 +4,7 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
|
||||||
|
|
||||||
"go-mod.ewintr.nl/planner/item"
|
|
||||||
"go-mod.ewintr.nl/planner/plan/storage"
|
"go-mod.ewintr.nl/planner/plan/storage"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -29,8 +27,7 @@ func NewUpdate(localIDRepo storage.LocalID, eventRepo storage.Event, syncRepo st
|
||||||
FlagOn: &FlagDate{},
|
FlagOn: &FlagDate{},
|
||||||
FlagAt: &FlagTime{},
|
FlagAt: &FlagTime{},
|
||||||
FlagFor: &FlagDuration{},
|
FlagFor: &FlagDuration{},
|
||||||
FlagRecStart: &FlagDate{},
|
FlagRec: &FlagRecurrer{},
|
||||||
FlagRecPeriod: &FlagPeriod{},
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
@ -87,35 +84,17 @@ func (update *Update) do() error {
|
||||||
if as.Main != "" {
|
if as.Main != "" {
|
||||||
e.Title = as.Main
|
e.Title = as.Main
|
||||||
}
|
}
|
||||||
if as.IsSet(FlagOn) || as.IsSet(FlagAt) {
|
|
||||||
on := time.Date(e.Start.Year(), e.Start.Month(), e.Start.Day(), 0, 0, 0, 0, time.UTC)
|
|
||||||
atH := time.Duration(e.Start.Hour()) * time.Hour
|
|
||||||
atM := time.Duration(e.Start.Minute()) * time.Minute
|
|
||||||
|
|
||||||
if as.IsSet(FlagOn) {
|
if as.IsSet(FlagOn) {
|
||||||
on = as.GetTime(FlagOn)
|
e.Date = as.GetDate(FlagOn)
|
||||||
}
|
}
|
||||||
if as.IsSet(FlagAt) {
|
if as.IsSet(FlagAt) {
|
||||||
at := as.GetTime(FlagAt)
|
e.Time = as.GetTime(FlagAt)
|
||||||
atH = time.Duration(at.Hour()) * time.Hour
|
|
||||||
atM = time.Duration(at.Minute()) * time.Minute
|
|
||||||
}
|
}
|
||||||
e.Start = on.Add(atH).Add(atM)
|
|
||||||
}
|
|
||||||
|
|
||||||
if as.IsSet(FlagFor) {
|
if as.IsSet(FlagFor) {
|
||||||
e.Duration = as.GetDuration(FlagFor)
|
e.Duration = as.GetDuration(FlagFor)
|
||||||
}
|
}
|
||||||
if as.IsSet(FlagRecStart) || as.IsSet(FlagRecPeriod) {
|
if as.IsSet(FlagRec) {
|
||||||
if e.Recurrer == nil {
|
e.Recurrer = as.GetRecurrer(FlagRec)
|
||||||
e.Recurrer = &item.Recur{}
|
|
||||||
}
|
|
||||||
if as.IsSet(FlagRecStart) {
|
|
||||||
e.Recurrer.Start = as.GetTime(FlagRecStart)
|
|
||||||
}
|
|
||||||
if as.IsSet(FlagRecPeriod) {
|
|
||||||
e.Recurrer.Period = as.GetRecurPeriod(FlagRecPeriod)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if !e.Valid() {
|
if !e.Valid() {
|
||||||
|
|
|
@ -5,7 +5,6 @@ import (
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/google/go-cmp/cmp"
|
|
||||||
"go-mod.ewintr.nl/planner/item"
|
"go-mod.ewintr.nl/planner/item"
|
||||||
"go-mod.ewintr.nl/planner/plan/command"
|
"go-mod.ewintr.nl/planner/plan/command"
|
||||||
"go-mod.ewintr.nl/planner/plan/storage/memory"
|
"go-mod.ewintr.nl/planner/plan/storage/memory"
|
||||||
|
@ -21,7 +20,8 @@ func TestUpdateExecute(t *testing.T) {
|
||||||
t.Errorf("exp nil, got %v", err)
|
t.Errorf("exp nil, got %v", err)
|
||||||
}
|
}
|
||||||
title := "title"
|
title := "title"
|
||||||
start := time.Date(2024, 10, 6, 10, 0, 0, 0, time.UTC)
|
aDate := item.NewDate(2024, 10, 6)
|
||||||
|
aTime := item.NewTime(10, 0)
|
||||||
twoHour, err := time.ParseDuration("2h")
|
twoHour, err := time.ParseDuration("2h")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("exp nil, got %v", err)
|
t.Errorf("exp nil, got %v", err)
|
||||||
|
@ -50,9 +50,10 @@ func TestUpdateExecute(t *testing.T) {
|
||||||
main: []string{"update", fmt.Sprintf("%d", lid), "updated"},
|
main: []string{"update", fmt.Sprintf("%d", lid), "updated"},
|
||||||
expEvent: item.Event{
|
expEvent: item.Event{
|
||||||
ID: eid,
|
ID: eid,
|
||||||
|
Date: item.NewDate(2024, 10, 6),
|
||||||
EventBody: item.EventBody{
|
EventBody: item.EventBody{
|
||||||
Title: "updated",
|
Title: "updated",
|
||||||
Start: start,
|
Time: aTime,
|
||||||
Duration: oneHour,
|
Duration: oneHour,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -75,9 +76,10 @@ func TestUpdateExecute(t *testing.T) {
|
||||||
},
|
},
|
||||||
expEvent: item.Event{
|
expEvent: item.Event{
|
||||||
ID: eid,
|
ID: eid,
|
||||||
|
Date: item.NewDate(2024, 10, 2),
|
||||||
EventBody: item.EventBody{
|
EventBody: item.EventBody{
|
||||||
Title: title,
|
Title: title,
|
||||||
Start: time.Date(2024, 10, 2, 10, 0, 0, 0, time.UTC),
|
Time: aTime,
|
||||||
Duration: oneHour,
|
Duration: oneHour,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -100,9 +102,10 @@ func TestUpdateExecute(t *testing.T) {
|
||||||
},
|
},
|
||||||
expEvent: item.Event{
|
expEvent: item.Event{
|
||||||
ID: eid,
|
ID: eid,
|
||||||
|
Date: item.NewDate(2024, 10, 6),
|
||||||
EventBody: item.EventBody{
|
EventBody: item.EventBody{
|
||||||
Title: title,
|
Title: title,
|
||||||
Start: time.Date(2024, 10, 6, 11, 0, 0, 0, time.UTC),
|
Time: item.NewTime(11, 0),
|
||||||
Duration: oneHour,
|
Duration: oneHour,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -117,9 +120,10 @@ func TestUpdateExecute(t *testing.T) {
|
||||||
},
|
},
|
||||||
expEvent: item.Event{
|
expEvent: item.Event{
|
||||||
ID: eid,
|
ID: eid,
|
||||||
|
Date: item.NewDate(2024, 10, 2),
|
||||||
EventBody: item.EventBody{
|
EventBody: item.EventBody{
|
||||||
Title: title,
|
Title: title,
|
||||||
Start: time.Date(2024, 10, 2, 11, 0, 0, 0, time.UTC),
|
Time: item.NewTime(11, 0),
|
||||||
Duration: oneHour,
|
Duration: oneHour,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -142,61 +146,35 @@ func TestUpdateExecute(t *testing.T) {
|
||||||
},
|
},
|
||||||
expEvent: item.Event{
|
expEvent: item.Event{
|
||||||
ID: eid,
|
ID: eid,
|
||||||
|
Date: item.NewDate(2024, 10, 6),
|
||||||
EventBody: item.EventBody{
|
EventBody: item.EventBody{
|
||||||
Title: title,
|
Title: title,
|
||||||
Start: time.Date(2024, 10, 6, 10, 0, 0, 0, time.UTC),
|
Time: aTime,
|
||||||
Duration: twoHour,
|
Duration: twoHour,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "invalid rec start",
|
name: "invalid rec",
|
||||||
main: []string{"update", fmt.Sprintf("%d", lid)},
|
main: []string{"update", fmt.Sprintf("%d", lid)},
|
||||||
flags: map[string]string{
|
flags: map[string]string{
|
||||||
"rec-start": "invalud",
|
"rec": "invalud",
|
||||||
},
|
},
|
||||||
expErr: true,
|
expErr: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "valid rec start",
|
name: "valid rec",
|
||||||
main: []string{"update", fmt.Sprintf("%d", lid)},
|
main: []string{"update", fmt.Sprintf("%d", lid)},
|
||||||
flags: map[string]string{
|
flags: map[string]string{
|
||||||
"rec-start": "2024-12-08",
|
"rec": "2024-12-08, daily",
|
||||||
},
|
},
|
||||||
expEvent: item.Event{
|
expEvent: item.Event{
|
||||||
ID: eid,
|
ID: eid,
|
||||||
Recurrer: &item.Recur{
|
Date: aDate,
|
||||||
Start: time.Date(2024, 12, 8, 0, 0, 0, 0, time.UTC),
|
Recurrer: item.NewRecurrer("2024-12-08, daily"),
|
||||||
},
|
|
||||||
EventBody: item.EventBody{
|
EventBody: item.EventBody{
|
||||||
Title: title,
|
Title: title,
|
||||||
Start: start,
|
Time: aTime,
|
||||||
Duration: oneHour,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "invalid rec period",
|
|
||||||
main: []string{"update", fmt.Sprintf("%d", lid)},
|
|
||||||
flags: map[string]string{
|
|
||||||
"rec-period": "invalid",
|
|
||||||
},
|
|
||||||
expErr: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "valid rec period",
|
|
||||||
main: []string{"update", fmt.Sprintf("%d", lid)},
|
|
||||||
flags: map[string]string{
|
|
||||||
"rec-period": "month",
|
|
||||||
},
|
|
||||||
expEvent: item.Event{
|
|
||||||
ID: eid,
|
|
||||||
Recurrer: &item.Recur{
|
|
||||||
Period: item.PeriodMonth,
|
|
||||||
},
|
|
||||||
EventBody: item.EventBody{
|
|
||||||
Title: title,
|
|
||||||
Start: start,
|
|
||||||
Duration: oneHour,
|
Duration: oneHour,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -208,9 +186,10 @@ func TestUpdateExecute(t *testing.T) {
|
||||||
syncRepo := memory.NewSync()
|
syncRepo := memory.NewSync()
|
||||||
if err := eventRepo.Store(item.Event{
|
if err := eventRepo.Store(item.Event{
|
||||||
ID: eid,
|
ID: eid,
|
||||||
|
Date: aDate,
|
||||||
EventBody: item.EventBody{
|
EventBody: item.EventBody{
|
||||||
Title: title,
|
Title: title,
|
||||||
Start: start,
|
Time: aTime,
|
||||||
Duration: oneHour,
|
Duration: oneHour,
|
||||||
},
|
},
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
|
@ -233,7 +212,7 @@ func TestUpdateExecute(t *testing.T) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("exp nil, got %v", err)
|
t.Errorf("exp nil, got %v", err)
|
||||||
}
|
}
|
||||||
if diff := cmp.Diff(tc.expEvent, actEvent); diff != "" {
|
if diff := item.EventDiff(tc.expEvent, actEvent); diff != "" {
|
||||||
t.Errorf("(exp -, got +)\n%s", diff)
|
t.Errorf("(exp -, got +)\n%s", diff)
|
||||||
}
|
}
|
||||||
updated, err := syncRepo.FindAll()
|
updated, err := syncRepo.FindAll()
|
||||||
|
|
|
@ -12,12 +12,16 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
confPath, err := os.UserConfigDir()
|
confPath := os.Getenv("PLAN_CONFIG_PATH")
|
||||||
|
if confPath == "" {
|
||||||
|
userConfigDir, err := os.UserConfigDir()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Printf("could not get config path: %s\n", err)
|
fmt.Printf("could not get config path: %s\n", err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
conf, err := LoadConfig(filepath.Join(confPath, "planner", "plan", "config.yaml"))
|
confPath = filepath.Join(userConfigDir, "planner", "plan", "config.yaml")
|
||||||
|
}
|
||||||
|
conf, err := LoadConfig(confPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Printf("could not open config file: %s\n", err)
|
fmt.Printf("could not open config file: %s\n", err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
|
|
|
@ -3,7 +3,6 @@ package memory
|
||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/google/go-cmp/cmp"
|
|
||||||
"go-mod.ewintr.nl/planner/item"
|
"go-mod.ewintr.nl/planner/item"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -50,7 +49,7 @@ func TestEvent(t *testing.T) {
|
||||||
if actErr != nil {
|
if actErr != nil {
|
||||||
t.Errorf("exp nil, got %v", actErr)
|
t.Errorf("exp nil, got %v", actErr)
|
||||||
}
|
}
|
||||||
if diff := cmp.Diff([]item.Event{e1, e2}, actEvents); diff != "" {
|
if diff := item.EventDiffs([]item.Event{e1, e2}, actEvents); diff != "" {
|
||||||
t.Errorf("(exp -, got +)\n%s", diff)
|
t.Errorf("(exp -, got +)\n%s", diff)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,18 +14,25 @@ type SqliteEvent struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *SqliteEvent) Store(event item.Event) error {
|
func (s *SqliteEvent) Store(event item.Event) error {
|
||||||
|
var recurStr string
|
||||||
|
if event.Recurrer != nil {
|
||||||
|
recurStr = event.Recurrer.String()
|
||||||
|
}
|
||||||
if _, err := s.db.Exec(`
|
if _, err := s.db.Exec(`
|
||||||
INSERT INTO events
|
INSERT INTO events
|
||||||
(id, title, start, duration)
|
(id, title, date, time, duration, recurrer)
|
||||||
VALUES
|
VALUES
|
||||||
(?, ?, ?, ?)
|
(?, ?, ?, ?, ?, ?)
|
||||||
ON CONFLICT(id) DO UPDATE
|
ON CONFLICT(id) DO UPDATE
|
||||||
SET
|
SET
|
||||||
title=?,
|
title=?,
|
||||||
start=?,
|
date=?,
|
||||||
duration=?`,
|
time=?,
|
||||||
event.ID, event.Title, event.Start.Format(timestampFormat), event.Duration.String(),
|
duration=?,
|
||||||
event.Title, event.Start.Format(timestampFormat), event.Duration.String()); err != nil {
|
recurrer=?
|
||||||
|
`,
|
||||||
|
event.ID, event.Title, event.Date.String(), event.Time.String(), event.Duration.String(), recurStr,
|
||||||
|
event.Title, event.Date.String(), event.Time.String(), event.Duration.String(), recurStr); err != nil {
|
||||||
return fmt.Errorf("%w: %v", ErrSqliteFailure, err)
|
return fmt.Errorf("%w: %v", ErrSqliteFailure, err)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
|
@ -33,29 +40,32 @@ duration=?`,
|
||||||
|
|
||||||
func (s *SqliteEvent) Find(id string) (item.Event, error) {
|
func (s *SqliteEvent) Find(id string) (item.Event, error) {
|
||||||
var event item.Event
|
var event item.Event
|
||||||
var durStr string
|
var dateStr, timeStr, recurStr, durStr string
|
||||||
err := s.db.QueryRow(`
|
err := s.db.QueryRow(`
|
||||||
SELECT id, title, start, duration
|
SELECT id, title, date, time, duration, recurrer
|
||||||
FROM events
|
FROM events
|
||||||
WHERE id = ?`, id).Scan(&event.ID, &event.Title, &event.Start, &durStr)
|
WHERE id = ?`, id).Scan(&event.ID, &event.Title, &dateStr, &timeStr, &durStr, &recurStr)
|
||||||
switch {
|
switch {
|
||||||
case err == sql.ErrNoRows:
|
case err == sql.ErrNoRows:
|
||||||
return item.Event{}, fmt.Errorf("event not found: %w", err)
|
return item.Event{}, fmt.Errorf("event not found: %w", err)
|
||||||
case err != nil:
|
case err != nil:
|
||||||
return item.Event{}, fmt.Errorf("%w: %v", ErrSqliteFailure, err)
|
return item.Event{}, fmt.Errorf("%w: %v", ErrSqliteFailure, err)
|
||||||
}
|
}
|
||||||
|
event.Date = item.NewDateFromString(dateStr)
|
||||||
|
event.Time = item.NewTimeFromString(timeStr)
|
||||||
dur, err := time.ParseDuration(durStr)
|
dur, err := time.ParseDuration(durStr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return item.Event{}, fmt.Errorf("%w: %v", ErrSqliteFailure, err)
|
return item.Event{}, fmt.Errorf("could not unmarshal recurrer: %v", err)
|
||||||
}
|
}
|
||||||
event.Duration = dur
|
event.Duration = dur
|
||||||
|
event.Recurrer = item.NewRecurrer(recurStr)
|
||||||
|
|
||||||
return event, nil
|
return event, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *SqliteEvent) FindAll() ([]item.Event, error) {
|
func (s *SqliteEvent) FindAll() ([]item.Event, error) {
|
||||||
rows, err := s.db.Query(`
|
rows, err := s.db.Query(`
|
||||||
SELECT id, title, start, duration
|
SELECT id, title, date, time, duration, recurrer
|
||||||
FROM events`)
|
FROM events`)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("%w: %v", ErrSqliteFailure, err)
|
return nil, fmt.Errorf("%w: %v", ErrSqliteFailure, err)
|
||||||
|
@ -64,15 +74,19 @@ FROM events`)
|
||||||
defer rows.Close()
|
defer rows.Close()
|
||||||
for rows.Next() {
|
for rows.Next() {
|
||||||
var event item.Event
|
var event item.Event
|
||||||
var durStr string
|
var dateStr, timeStr, recurStr, durStr string
|
||||||
if err := rows.Scan(&event.ID, &event.Title, &event.Start, &durStr); err != nil {
|
if err := rows.Scan(&event.ID, &event.Title, &dateStr, &timeStr, &durStr, &recurStr); err != nil {
|
||||||
return nil, fmt.Errorf("%w: %v", ErrSqliteFailure, err)
|
return nil, fmt.Errorf("%w: %v", ErrSqliteFailure, err)
|
||||||
}
|
}
|
||||||
dur, err := time.ParseDuration(durStr)
|
dur, err := time.ParseDuration(durStr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("%w: %v", ErrSqliteFailure, err)
|
return nil, fmt.Errorf("%w: %v", ErrSqliteFailure, err)
|
||||||
}
|
}
|
||||||
|
event.Date = item.NewDateFromString(dateStr)
|
||||||
|
event.Time = item.NewTimeFromString(timeStr)
|
||||||
event.Duration = dur
|
event.Duration = dur
|
||||||
|
event.Recurrer = item.NewRecurrer(recurStr)
|
||||||
|
|
||||||
result = append(result, event)
|
result = append(result, event)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -25,6 +25,23 @@ var migrations = []string{
|
||||||
deleted BOOLEAN NOT NULL,
|
deleted BOOLEAN NOT NULL,
|
||||||
body TEXT NOT NULL
|
body TEXT NOT NULL
|
||||||
)`,
|
)`,
|
||||||
|
`ALTER TABLE events ADD COLUMN recur_period TEXT`,
|
||||||
|
`ALTER TABLE events ADD COLUMN recur_count INTEGER`,
|
||||||
|
`ALTER TABLE events ADD COLUMN recur_start TIMESTAMP`,
|
||||||
|
`ALTER TABLE events ADD COLUMN recur_next TIMESTAMP`,
|
||||||
|
`ALTER TABLE events DROP COLUMN recur_period`,
|
||||||
|
`ALTER TABLE events DROP COLUMN recur_count`,
|
||||||
|
`ALTER TABLE events DROP COLUMN recur_start`,
|
||||||
|
`ALTER TABLE events DROP COLUMN recur_next`,
|
||||||
|
`ALTER TABLE events ADD COLUMN recur TEXT`,
|
||||||
|
`ALTER TABLE items ADD COLUMN recurrer TEXT`,
|
||||||
|
`ALTER TABLE events DROP COLUMN recur`,
|
||||||
|
`ALTER TABLE events ADD COLUMN recurrer TEXT NOT NULL DEFAULT ''`,
|
||||||
|
`ALTER TABLE events ADD COLUMN recur_next TEXT NOT NULL DEFAULT ''`,
|
||||||
|
`ALTER TABLE events DROP COLUMN start`,
|
||||||
|
`ALTER TABLE events ADD COLUMN date TEXT NOT NULL DEFAULT ''`,
|
||||||
|
`ALTER TABLE events ADD COLUMN time TEXT NOT NULL DEFAULT ''`,
|
||||||
|
`ALTER TABLE items ADD COLUMN recur_next TEXT NOT NULL DEFAULT ''`,
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
|
|
@ -17,7 +17,7 @@ func NewSqliteSync(db *sql.DB) *SqliteSync {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *SqliteSync) FindAll() ([]item.Item, error) {
|
func (s *SqliteSync) FindAll() ([]item.Item, error) {
|
||||||
rows, err := s.db.Query("SELECT id, kind, updated, deleted, body FROM items")
|
rows, err := s.db.Query("SELECT id, kind, updated, deleted, recurrer, recur_next, body FROM items")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("%w: failed to query items: %v", ErrSqliteFailure, err)
|
return nil, fmt.Errorf("%w: failed to query items: %v", ErrSqliteFailure, err)
|
||||||
}
|
}
|
||||||
|
@ -26,37 +26,46 @@ func (s *SqliteSync) FindAll() ([]item.Item, error) {
|
||||||
var items []item.Item
|
var items []item.Item
|
||||||
for rows.Next() {
|
for rows.Next() {
|
||||||
var i item.Item
|
var i item.Item
|
||||||
var updatedStr string
|
var updatedStr, recurStr, recurNextStr string
|
||||||
err := rows.Scan(&i.ID, &i.Kind, &updatedStr, &i.Deleted, &i.Body)
|
err := rows.Scan(&i.ID, &i.Kind, &updatedStr, &i.Deleted, &recurStr, &recurNextStr, &i.Body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("%w: failed to scan item: %v", ErrSqliteFailure, err)
|
return nil, fmt.Errorf("%w: failed to scan item: %v", ErrSqliteFailure, err)
|
||||||
}
|
}
|
||||||
i.Updated, err = time.Parse(time.RFC3339, updatedStr)
|
i.Updated, err = time.Parse(time.RFC3339, updatedStr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("%w: failed to parse updated time: %v", ErrSqliteFailure, err)
|
return nil, fmt.Errorf("failed to parse updated time: %v", err)
|
||||||
}
|
}
|
||||||
|
i.Recurrer = item.NewRecurrer(recurStr)
|
||||||
|
i.RecurNext = item.NewDateFromString(recurNextStr)
|
||||||
|
|
||||||
items = append(items, i)
|
items = append(items, i)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err = rows.Err(); err != nil {
|
if err = rows.Err(); err != nil {
|
||||||
return nil, fmt.Errorf("%w: error iterating over rows: %v", ErrSqliteFailure, err)
|
return nil, fmt.Errorf("error iterating over rows: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return items, nil
|
return items, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *SqliteSync) Store(i item.Item) error {
|
func (s *SqliteSync) Store(i item.Item) error {
|
||||||
// Ensure we have a valid time
|
|
||||||
if i.Updated.IsZero() {
|
if i.Updated.IsZero() {
|
||||||
i.Updated = time.Now()
|
i.Updated = time.Now()
|
||||||
}
|
}
|
||||||
|
var recurStr string
|
||||||
|
if i.Recurrer != nil {
|
||||||
|
recurStr = i.Recurrer.String()
|
||||||
|
}
|
||||||
|
|
||||||
_, err := s.db.Exec(
|
_, err := s.db.Exec(
|
||||||
"INSERT OR REPLACE INTO items (id, kind, updated, deleted, body) VALUES (?, ?, ?, ?, ?)",
|
`INSERT OR REPLACE INTO items (id, kind, updated, deleted, recurrer, recur_next, body)
|
||||||
|
VALUES (?, ?, ?, ?, ?, ?, ?)`,
|
||||||
i.ID,
|
i.ID,
|
||||||
i.Kind,
|
i.Kind,
|
||||||
i.Updated.UTC().Format(time.RFC3339),
|
i.Updated.UTC().Format(time.RFC3339),
|
||||||
i.Deleted,
|
i.Deleted,
|
||||||
|
recurStr,
|
||||||
|
i.RecurNext.String(),
|
||||||
sql.NullString{String: i.Body, Valid: i.Body != ""}, // This allows empty string but not NULL
|
sql.NullString{String: i.Body, Valid: i.Body != ""}, // This allows empty string but not NULL
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
@ -0,0 +1,3 @@
|
||||||
|
db_path: ./plan.db
|
||||||
|
sync_url: http://localhost:8092
|
||||||
|
api_key: testKey
|
|
@ -45,7 +45,8 @@ func (c *HTTP) Update(items []item.Item) error {
|
||||||
return fmt.Errorf("could not make request: %v", err)
|
return fmt.Errorf("could not make request: %v", err)
|
||||||
}
|
}
|
||||||
if res.StatusCode != http.StatusNoContent {
|
if res.StatusCode != http.StatusNoContent {
|
||||||
return fmt.Errorf("server returned status %d", res.StatusCode)
|
body, _ := io.ReadAll(res.Body)
|
||||||
|
return fmt.Errorf("server returned status %d, body: %s", res.StatusCode, body)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
"slices"
|
"slices"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
@ -47,33 +46,15 @@ func (m *Memory) Updated(kinds []item.Kind, timestamp time.Time) ([]item.Item, e
|
||||||
return result, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Memory) RecursBefore(date time.Time) ([]item.Item, error) {
|
func (m *Memory) ShouldRecur(date item.Date) ([]item.Item, error) {
|
||||||
res := make([]item.Item, 0)
|
res := make([]item.Item, 0)
|
||||||
for _, i := range m.items {
|
for _, i := range m.items {
|
||||||
if i.Recurrer == nil {
|
if i.Recurrer == nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if i.RecurNext.Before(date) {
|
if date.Equal(i.RecurNext) || date.After(i.RecurNext) {
|
||||||
res = append(res, i)
|
res = append(res, i)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return res, nil
|
return res, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Memory) RecursNext(id string, date time.Time, ts time.Time) error {
|
|
||||||
i, ok := m.items[id]
|
|
||||||
if !ok {
|
|
||||||
return ErrNotFound
|
|
||||||
}
|
|
||||||
if i.Recurrer == nil {
|
|
||||||
return ErrNotARecurrer
|
|
||||||
}
|
|
||||||
if !i.Recurrer.On(date) {
|
|
||||||
return fmt.Errorf("item does not recur on %v", date)
|
|
||||||
}
|
|
||||||
i.RecurNext = date
|
|
||||||
i.Updated = ts
|
|
||||||
m.items[id] = i
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
|
@ -113,19 +113,14 @@ func TestMemoryRecur(t *testing.T) {
|
||||||
mem := NewMemory()
|
mem := NewMemory()
|
||||||
now := time.Now()
|
now := time.Now()
|
||||||
earlier := now.Add(-5 * time.Minute)
|
earlier := now.Add(-5 * time.Minute)
|
||||||
today := time.Date(2024, 12, 1, 0, 0, 0, 0, time.UTC)
|
today := item.NewDate(2024, 12, 1)
|
||||||
yesterday := time.Date(2024, 11, 30, 0, 0, 0, 0, time.UTC)
|
yesterday := item.NewDate(2024, 11, 30)
|
||||||
tomorrow := time.Date(2024, 12, 2, 0, 0, 0, 0, time.UTC)
|
|
||||||
|
|
||||||
t.Log("start")
|
t.Log("start")
|
||||||
i1 := item.Item{
|
i1 := item.Item{
|
||||||
ID: "a",
|
ID: "a",
|
||||||
Updated: earlier,
|
Updated: earlier,
|
||||||
Recurrer: &item.Recur{
|
Recurrer: item.NewRecurrer("2024-11-30, daily"),
|
||||||
Start: yesterday,
|
|
||||||
Period: item.PeriodDay,
|
|
||||||
Count: 1,
|
|
||||||
},
|
|
||||||
RecurNext: yesterday,
|
RecurNext: yesterday,
|
||||||
}
|
}
|
||||||
i2 := item.Item{
|
i2 := item.Item{
|
||||||
|
@ -140,7 +135,7 @@ func TestMemoryRecur(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
t.Log("get recurrers")
|
t.Log("get recurrers")
|
||||||
rs, err := mem.RecursBefore(today)
|
rs, err := mem.ShouldRecur(today)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("exp nil, gt %v", err)
|
t.Errorf("exp nil, gt %v", err)
|
||||||
}
|
}
|
||||||
|
@ -148,20 +143,4 @@ func TestMemoryRecur(t *testing.T) {
|
||||||
t.Errorf("(exp +, got -)\n%s", diff)
|
t.Errorf("(exp +, got -)\n%s", diff)
|
||||||
}
|
}
|
||||||
|
|
||||||
t.Log("set next")
|
|
||||||
if err := mem.RecursNext(i1.ID, tomorrow, time.Now()); err != nil {
|
|
||||||
t.Errorf("exp nil, got %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
t.Log("check result")
|
|
||||||
us, err := mem.Updated([]item.Kind{}, now)
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("exp nil, got %v", err)
|
|
||||||
}
|
|
||||||
if len(us) != 1 {
|
|
||||||
t.Errorf("exp 1, got %v", len(us))
|
|
||||||
}
|
|
||||||
if us[0].ID != i1.ID {
|
|
||||||
t.Errorf("exp %v, got %v", i1.ID, us[0].ID)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,6 +20,13 @@ var migrations = []string{
|
||||||
`CREATE INDEX idx_items_updated ON items(updated)`,
|
`CREATE INDEX idx_items_updated ON items(updated)`,
|
||||||
`CREATE INDEX idx_items_kind ON items(kind)`,
|
`CREATE INDEX idx_items_kind ON items(kind)`,
|
||||||
`ALTER TABLE items ADD COLUMN recurrer JSONB, ADD COLUMN recur_next TIMESTAMP`,
|
`ALTER TABLE items ADD COLUMN recurrer JSONB, ADD COLUMN recur_next TIMESTAMP`,
|
||||||
|
`ALTER TABLE items ALTER COLUMN recurrer TYPE TEXT USING recurrer::TEXT,
|
||||||
|
ALTER COLUMN recurrer SET NOT NULL,
|
||||||
|
ALTER COLUMN recurrer SET DEFAULT ''`,
|
||||||
|
`ALTER TABLE items ALTER COLUMN recur_next TYPE TEXT USING TO_CHAR(recur_next, 'YYYY-MM-DD'),
|
||||||
|
ALTER COLUMN recur_next SET NOT NULL,
|
||||||
|
ALTER COLUMN recur_next SET DEFAULT ''`,
|
||||||
|
`ALTER TABLE items ADD COLUMN date TEXT NOT NULL DEFAULT ''`,
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
@ -35,13 +42,10 @@ type Postgres struct {
|
||||||
|
|
||||||
func NewPostgres(host, port, dbname, user, password string) (*Postgres, error) {
|
func NewPostgres(host, port, dbname, user, password string) (*Postgres, error) {
|
||||||
connStr := fmt.Sprintf("host=%s port=%s dbname=%s user=%s password=%s sslmode=disable", host, port, dbname, user, password)
|
connStr := fmt.Sprintf("host=%s port=%s dbname=%s user=%s password=%s sslmode=disable", host, port, dbname, user, password)
|
||||||
|
|
||||||
db, err := sql.Open("postgres", connStr)
|
db, err := sql.Open("postgres", connStr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("%w: %v", ErrInvalidConfiguration, err)
|
return nil, fmt.Errorf("%w: %v", ErrInvalidConfiguration, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Test the connection
|
|
||||||
if err := db.Ping(); err != nil {
|
if err := db.Ping(); err != nil {
|
||||||
return nil, fmt.Errorf("%w: %v", ErrInvalidConfiguration, err)
|
return nil, fmt.Errorf("%w: %v", ErrInvalidConfiguration, err)
|
||||||
}
|
}
|
||||||
|
@ -57,19 +61,26 @@ func NewPostgres(host, port, dbname, user, password string) (*Postgres, error) {
|
||||||
return p, nil
|
return p, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *Postgres) Update(item item.Item, ts time.Time) error {
|
func (p *Postgres) Update(i item.Item, ts time.Time) error {
|
||||||
_, err := p.db.Exec(`
|
if i.Recurrer != nil && i.RecurNext.IsZero() {
|
||||||
INSERT INTO items (id, kind, updated, deleted, body, recurrer, recur_next)
|
i.RecurNext = i.Recurrer.First()
|
||||||
VALUES ($1, $2, $3, $4, $5, $6, $7)
|
}
|
||||||
|
var recurStr string
|
||||||
|
if i.Recurrer != nil {
|
||||||
|
recurStr = i.Recurrer.String()
|
||||||
|
}
|
||||||
|
if _, err := p.db.Exec(`
|
||||||
|
INSERT INTO items (id, kind, updated, deleted, date, recurrer, recur_next, body)
|
||||||
|
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
|
||||||
ON CONFLICT (id) DO UPDATE
|
ON CONFLICT (id) DO UPDATE
|
||||||
SET kind = EXCLUDED.kind,
|
SET kind = EXCLUDED.kind,
|
||||||
updated = EXCLUDED.updated,
|
updated = EXCLUDED.updated,
|
||||||
deleted = EXCLUDED.deleted,
|
deleted = EXCLUDED.deleted,
|
||||||
body = EXCLUDED.body,
|
date = EXCLUDED.date,
|
||||||
recurrer = EXCLUDED.recurrer,
|
recurrer = EXCLUDED.recurrer,
|
||||||
recur_next = EXCLUDED.recur_next`,
|
recur_next = EXCLUDED.recur_next,
|
||||||
item.ID, item.Kind, ts, item.Deleted, item.Body, item.Recurrer, item.RecurNext)
|
body = EXCLUDED.body`,
|
||||||
if err != nil {
|
i.ID, i.Kind, ts, i.Deleted, i.Date.String(), recurStr, i.RecurNext.String(), i.Body); err != nil {
|
||||||
return fmt.Errorf("%w: %v", ErrPostgresFailure, err)
|
return fmt.Errorf("%w: %v", ErrPostgresFailure, err)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
|
@ -77,11 +88,10 @@ func (p *Postgres) Update(item item.Item, ts time.Time) error {
|
||||||
|
|
||||||
func (p *Postgres) Updated(ks []item.Kind, t time.Time) ([]item.Item, error) {
|
func (p *Postgres) Updated(ks []item.Kind, t time.Time) ([]item.Item, error) {
|
||||||
query := `
|
query := `
|
||||||
SELECT id, kind, updated, deleted, body, recurrer, recur_next
|
SELECT id, kind, updated, deleted, date, recurrer, recur_next, body
|
||||||
FROM items
|
FROM items
|
||||||
WHERE updated > $1`
|
WHERE updated > $1`
|
||||||
args := []interface{}{t}
|
args := []interface{}{t}
|
||||||
|
|
||||||
if len(ks) > 0 {
|
if len(ks) > 0 {
|
||||||
placeholder := make([]string, len(ks))
|
placeholder := make([]string, len(ks))
|
||||||
for i := range ks {
|
for i := range ks {
|
||||||
|
@ -99,27 +109,29 @@ func (p *Postgres) Updated(ks []item.Kind, t time.Time) ([]item.Item, error) {
|
||||||
|
|
||||||
result := make([]item.Item, 0)
|
result := make([]item.Item, 0)
|
||||||
for rows.Next() {
|
for rows.Next() {
|
||||||
var item item.Item
|
var i item.Item
|
||||||
var recurNext sql.NullTime
|
var date, recurrer, recurNext string
|
||||||
if err := rows.Scan(&item.ID, &item.Kind, &item.Updated, &item.Deleted, &item.Body, &item.Recurrer, &recurNext); err != nil {
|
if err := rows.Scan(&i.ID, &i.Kind, &i.Updated, &i.Deleted, &date, &recurrer, &recurNext, &i.Body); err != nil {
|
||||||
return nil, fmt.Errorf("%w: %v", ErrPostgresFailure, err)
|
return nil, fmt.Errorf("%w: %v", ErrPostgresFailure, err)
|
||||||
}
|
}
|
||||||
if recurNext.Valid {
|
i.Date = item.NewDateFromString(date)
|
||||||
item.RecurNext = recurNext.Time
|
i.Recurrer = item.NewRecurrer(recurrer)
|
||||||
}
|
i.RecurNext = item.NewDateFromString(recurNext)
|
||||||
result = append(result, item)
|
result = append(result, i)
|
||||||
}
|
}
|
||||||
|
|
||||||
return result, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *Postgres) RecursBefore(date time.Time) ([]item.Item, error) {
|
func (p *Postgres) ShouldRecur(date item.Date) ([]item.Item, error) {
|
||||||
query := `
|
query := `
|
||||||
SELECT id, kind, updated, deleted, body, recurrer, recur_next
|
SELECT id, kind, updated, deleted, date, recurrer, recur_next, body
|
||||||
FROM items
|
FROM items
|
||||||
WHERE recur_next <= $1 AND recurrer IS NOT NULL`
|
WHERE
|
||||||
|
NOT deleted
|
||||||
rows, err := p.db.Query(query, date)
|
AND recurrer <> ''
|
||||||
|
AND recur_next <= $1`
|
||||||
|
rows, err := p.db.Query(query, date.String())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("%w: %v", ErrPostgresFailure, err)
|
return nil, fmt.Errorf("%w: %v", ErrPostgresFailure, err)
|
||||||
}
|
}
|
||||||
|
@ -127,54 +139,20 @@ func (p *Postgres) RecursBefore(date time.Time) ([]item.Item, error) {
|
||||||
|
|
||||||
result := make([]item.Item, 0)
|
result := make([]item.Item, 0)
|
||||||
for rows.Next() {
|
for rows.Next() {
|
||||||
var item item.Item
|
var i item.Item
|
||||||
var recurNext sql.NullTime
|
var date, recurrer, recurNext string
|
||||||
if err := rows.Scan(&item.ID, &item.Kind, &item.Updated, &item.Deleted, &item.Body, &item.Recurrer, &recurNext); err != nil {
|
if err := rows.Scan(&i.ID, &i.Kind, &i.Updated, &i.Deleted, &date, &recurrer, &recurNext, &i.Body); err != nil {
|
||||||
return nil, fmt.Errorf("%w: %v", ErrPostgresFailure, err)
|
return nil, fmt.Errorf("%w: %v", ErrPostgresFailure, err)
|
||||||
}
|
}
|
||||||
if recurNext.Valid {
|
i.Date = item.NewDateFromString(date)
|
||||||
item.RecurNext = recurNext.Time
|
i.Recurrer = item.NewRecurrer(recurrer)
|
||||||
}
|
i.RecurNext = item.NewDateFromString(recurNext)
|
||||||
result = append(result, item)
|
result = append(result, i)
|
||||||
}
|
}
|
||||||
|
|
||||||
return result, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *Postgres) RecursNext(id string, date time.Time, ts time.Time) error {
|
|
||||||
var recurrer *item.Recur
|
|
||||||
err := p.db.QueryRow(`
|
|
||||||
SELECT recurrer
|
|
||||||
FROM items
|
|
||||||
WHERE id = $1`, id).Scan(&recurrer)
|
|
||||||
if err != nil {
|
|
||||||
if err == sql.ErrNoRows {
|
|
||||||
return ErrNotFound
|
|
||||||
}
|
|
||||||
return fmt.Errorf("%w: %v", ErrPostgresFailure, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if recurrer == nil {
|
|
||||||
return ErrNotARecurrer
|
|
||||||
}
|
|
||||||
|
|
||||||
// Verify that the new date is actually a valid recurrence
|
|
||||||
if !recurrer.On(date) {
|
|
||||||
return fmt.Errorf("%w: date %v is not a valid recurrence", ErrPostgresFailure, date)
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err = p.db.Exec(`
|
|
||||||
UPDATE items
|
|
||||||
SET recur_next = $1,
|
|
||||||
updated = $2
|
|
||||||
WHERE id = $3`, date, ts, id)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("%w: %v", ErrPostgresFailure, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *Postgres) migrate(wanted []string) error {
|
func (p *Postgres) migrate(wanted []string) error {
|
||||||
// Create migration table if not exists
|
// Create migration table if not exists
|
||||||
_, err := p.db.Exec(`
|
_, err := p.db.Exec(`
|
||||||
|
|
|
@ -35,34 +35,31 @@ func (r *Recur) Run(interval time.Duration) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *Recur) Recur() error {
|
func (r *Recur) Recur() error {
|
||||||
items, err := r.repoRecur.RecursBefore(time.Now())
|
r.logger.Info("start looking for recurring items")
|
||||||
|
today := item.NewDateFromString(time.Now().Format(item.DateFormat))
|
||||||
|
items, err := r.repoRecur.ShouldRecur(today)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
r.logger.Info("found recurring items", "count", len(items))
|
||||||
for _, i := range items {
|
for _, i := range items {
|
||||||
|
r.logger.Info("processing recurring item", "id", i.ID)
|
||||||
// spawn instance
|
// spawn instance
|
||||||
ne, err := item.NewEvent(i)
|
newItem := i
|
||||||
if err != nil {
|
newItem.ID = uuid.New().String()
|
||||||
return err
|
newItem.Date = i.RecurNext
|
||||||
}
|
newItem.Recurrer = nil
|
||||||
y, m, d := i.RecurNext.Date()
|
newItem.RecurNext = item.Date{}
|
||||||
ne.ID = uuid.New().String()
|
if err := r.repoSync.Update(newItem, time.Now()); err != nil {
|
||||||
ne.Recurrer = nil
|
|
||||||
ne.RecurNext = time.Time{}
|
|
||||||
ne.Start = time.Date(y, m, d, ne.Start.Hour(), ne.Start.Minute(), 0, 0, time.UTC)
|
|
||||||
|
|
||||||
ni, err := ne.Item()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err := r.repoSync.Update(ni, time.Now()); err != nil {
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// set next
|
// update recurrer
|
||||||
if err := r.repoRecur.RecursNext(i.ID, i.Recurrer.NextAfter(i.RecurNext), time.Now()); err != nil {
|
i.RecurNext = item.FirstRecurAfter(i.Recurrer, i.RecurNext)
|
||||||
|
if err := r.repoSync.Update(i, time.Now()); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
r.logger.Info("recurring item processed", "id", i.ID, "recurNext", i.RecurNext.String())
|
||||||
}
|
}
|
||||||
r.logger.Info("processed recurring items", "count", len(items))
|
r.logger.Info("processed recurring items", "count", len(items))
|
||||||
|
|
||||||
|
|
|
@ -12,23 +12,18 @@ import (
|
||||||
func TestRecur(t *testing.T) {
|
func TestRecur(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
now := time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC)
|
now := time.Now()
|
||||||
|
today := item.NewDate(2024, 1, 1)
|
||||||
mem := NewMemory()
|
mem := NewMemory()
|
||||||
rec := NewRecur(mem, mem, slog.New(slog.NewTextHandler(io.Discard, nil)))
|
rec := NewRecur(mem, mem, slog.New(slog.NewTextHandler(io.Discard, nil)))
|
||||||
|
|
||||||
// Create a recurring item
|
|
||||||
recur := &item.Recur{
|
|
||||||
Start: now,
|
|
||||||
Period: item.PeriodDay,
|
|
||||||
Count: 1,
|
|
||||||
}
|
|
||||||
testItem := item.Item{
|
testItem := item.Item{
|
||||||
ID: "test-1",
|
ID: "test-1",
|
||||||
Kind: item.KindEvent,
|
Kind: item.KindEvent,
|
||||||
Updated: now,
|
Updated: now,
|
||||||
Deleted: false,
|
Deleted: false,
|
||||||
Recurrer: recur,
|
Recurrer: item.NewRecurrer("2024-01-01, daily"),
|
||||||
RecurNext: now,
|
RecurNext: today,
|
||||||
Body: `{"title":"Test Event","start":"2024-01-01T10:00:00Z","duration":"30m"}`,
|
Body: `{"title":"Test Event","start":"2024-01-01T10:00:00Z","duration":"30m"}`,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -53,14 +48,14 @@ func TestRecur(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check that RecurNext was updated
|
// Check that RecurNext was updated
|
||||||
recurItems, err := mem.RecursBefore(now.Add(48 * time.Hour))
|
recurItems, err := mem.ShouldRecur(today.Add(1))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
if len(recurItems) != 1 {
|
if len(recurItems) != 1 {
|
||||||
t.Errorf("expected 1 recur item, got %d", len(recurItems))
|
t.Errorf("expected 1 recur item, got %d", len(recurItems))
|
||||||
}
|
}
|
||||||
if !recurItems[0].RecurNext.After(now) {
|
if !recurItems[0].RecurNext.After(today) {
|
||||||
t.Errorf("RecurNext was not updated, still %v", recurItems[0].RecurNext)
|
t.Errorf("RecurNext was not updated, still %v", recurItems[0].RecurNext)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -39,7 +39,7 @@ func main() {
|
||||||
"dbUser": *dbUser,
|
"dbUser": *dbUser,
|
||||||
})
|
})
|
||||||
recurrer := NewRecur(repo, repo, logger)
|
recurrer := NewRecur(repo, repo, logger)
|
||||||
go recurrer.Run(12 * time.Hour)
|
go recurrer.Run(10 * time.Second)
|
||||||
|
|
||||||
srv := NewServer(repo, *apiKey, logger)
|
srv := NewServer(repo, *apiKey, logger)
|
||||||
go http.ListenAndServe(fmt.Sprintf(":%s", *apiPort), srv)
|
go http.ListenAndServe(fmt.Sprintf(":%s", *apiPort), srv)
|
||||||
|
|
|
@ -18,6 +18,5 @@ type Syncer interface {
|
||||||
}
|
}
|
||||||
|
|
||||||
type Recurrer interface {
|
type Recurrer interface {
|
||||||
RecursBefore(date time.Time) ([]item.Item, error)
|
ShouldRecur(date item.Date) ([]item.Item, error)
|
||||||
RecursNext(id string, date time.Time, t time.Time) error
|
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue