diff --git a/evaluator/constants.go b/evaluator/constants.go index ce114c3..9ddede8 100644 --- a/evaluator/constants.go +++ b/evaluator/constants.go @@ -1,26 +1,36 @@ package evaluator -type Value string - -type Unit string - -type Snap string +import "time" const ( - Second Unit = "s" - Minute Unit = "m" - Hour Unit = "h" - Day Unit = "d" - Week Unit = "w" - Month Unit = "M" - Year Unit = "Y" + Second string = "s" + Minute = "m" + Hour = "h" + Day = "d" + Week = "w" + Month = "M" + Year = "Y" + + Monday = "mon" + Tuesday = "tue" + Wednesday = "wed" + Thursday = "thu" + Friday = "fri" + Saturday = "sat" + Sunday = "sun" ) const ( - Start Snap = "/" - End Snap = "@" + Start string = "/" + End string = "@" ) const ( Now = "now" ) + +type valueResolver map[string]func(string) time.Time + +func (vr valueResolver) register(token string, fn func(string) time.Time) { + vr[token] = fn +} diff --git a/evaluator/dates.go b/evaluator/dates.go index 4d5d25d..0cd2d77 100644 --- a/evaluator/dates.go +++ b/evaluator/dates.go @@ -85,26 +85,6 @@ func EndOfYear(date time.Time) time.Time { return BeginningOfYear(date).AddDate(1, 0, 0).Add(-time.Nanosecond) } -// Monday monday -func PreviousMonday(date time.Time) time.Time { - t := BeginningOfDay(date) - weekday := int(t.Weekday()) - if weekday == 0 { - weekday = 7 - } - return t.AddDate(0, 0, -weekday+1) -} - -// Sunday sunday -func PreviousSunday(date time.Time) time.Time { - t := BeginningOfDay(date) - weekday := int(t.Weekday()) - if weekday == 0 { - return t - } - return t.AddDate(0, 0, 7-weekday) -} - // Next sunday func EndOfSunday(date time.Time) time.Time { return EndOfDay(PreviousSunday(date)) @@ -137,3 +117,71 @@ func AddMonths(date time.Time, amount int) time.Time { func AddYears(date time.Time, amount int) time.Time { return date.AddDate(amount, 0, 0) } + +func PreviousWeekDay(date time.Time, targetWeekDay time.Weekday) time.Time { + wd := int(date.Weekday()) + twd := int(targetWeekDay) + return date.AddDate(0, 0, -(((wd-twd)%7 + 7) % 7)) +} + +func PreviousMonday(date time.Time) time.Time { + return PreviousWeekDay(date, time.Monday) +} + +func PreviousTuesday(date time.Time) time.Time { + return PreviousWeekDay(date, time.Tuesday) +} + +func PreviousWednesday(date time.Time) time.Time { + return PreviousWeekDay(date, time.Wednesday) +} + +func PreviousThursday(date time.Time) time.Time { + return PreviousWeekDay(date, time.Thursday) +} + +func PreviousFriday(date time.Time) time.Time { + return PreviousWeekDay(date, time.Friday) +} + +func PreviousSaturday(date time.Time) time.Time { + return PreviousWeekDay(date, time.Saturday) +} + +func PreviousSunday(date time.Time) time.Time { + return PreviousWeekDay(date, time.Sunday) +} + +func NextWeekDay(date time.Time, targetWeekDay time.Weekday) time.Time { + wd := int(date.Weekday()) + twd := int(targetWeekDay) + return date.AddDate(0, 0, ((twd-wd)%7+7)%7) +} + +func NextMonday(date time.Time) time.Time { + return NextWeekDay(date, time.Monday) +} + +func NextTuesday(date time.Time) time.Time { + return NextWeekDay(date, time.Tuesday) +} + +func NextWednesday(date time.Time) time.Time { + return NextWeekDay(date, time.Wednesday) +} + +func NextThursday(date time.Time) time.Time { + return NextWeekDay(date, time.Thursday) +} + +func NextFriday(date time.Time) time.Time { + return NextWeekDay(date, time.Friday) +} + +func NextSaturday(date time.Time) time.Time { + return NextWeekDay(date, time.Saturday) +} + +func NextSunday(date time.Time) time.Time { + return NextWeekDay(date, time.Sunday) +} diff --git a/evaluator/evaluator.go b/evaluator/evaluator.go index 6d80c5d..87e3dc3 100644 --- a/evaluator/evaluator.go +++ b/evaluator/evaluator.go @@ -10,9 +10,6 @@ import ( "github.com/sonirico/datetoken.go/token" ) -type evalArithmeticFunc func(Unit) time.Time -type evalSnapFunc func(Snap) time.Time - type evalConfig struct { WeekStartDay time.Weekday TimeLocation *time.Location @@ -25,9 +22,6 @@ type evaluator struct { current time.Time timeSet bool - - arithmeticRegistry map[Unit]evalArithmeticFunc - snapRegistry map[Snap]evalSnapFunc } func newEvaluator() *evaluator { @@ -35,27 +29,18 @@ func newEvaluator() *evaluator { config: &evalConfig{ WeekStartDay: time.Sunday, }, - timeSet: false, - arithmeticRegistry: make(map[Unit]evalArithmeticFunc), - snapRegistry: make(map[Snap]evalSnapFunc), + timeSet: false, } return evaluator } -func (e *evaluator) registerSnapFunc(snap Snap, fn evalSnapFunc) { - e.snapRegistry[snap] = fn -} - -func (e *evaluator) registerArithmeticFunc(unit Unit, fn evalArithmeticFunc) { - e.arithmeticRegistry[unit] = fn -} - // Override initial node value func (e *evaluator) setInitial(date time.Time) { if e.timeSet { return } e.current = date + e.timeSet = true } func (e *evaluator) evalValueNode(node *ast.ValueNode) { @@ -73,55 +58,88 @@ func (e *evaluator) evalArithmeticNode(node *ast.ArithmeticNode) { } switch node.Unit { - case "s": + case Second: e.addSeconds(amount) - case "m": + case Minute: e.addMinutes(amount) - case "h": + case Hour: e.addHours(amount) - case "d": + case Day: e.addDays(amount) - case "w": + case Week: e.addWeeks(amount) - case "M": + case Month: e.addMonths(amount) - case "Y": + case Year: e.addYears(amount) } } func (e *evaluator) evalStartSnap(node *ast.SnapNode) { switch node.Unit { - case "m": + // time units + case Minute: e.snapStartOfMinute() - case "h": + case Hour: e.snapStartOfHour() - case "d": + case Day: e.snapStartOfDay() - case "w": + case Week: e.snapStartOfWeek() - case "M": + case Month: e.snapStartOfMonth() - case "Y": + case Year: e.snapStartOfYear() + // weekdays + case Monday: + e.previousMonday() + case Tuesday: + e.previousTuesday() + case Wednesday: + e.previousWednesday() + case Thursday: + e.previousThursday() + case Friday: + e.previousFriday() + case Saturday: + e.previousSaturday() + case Sunday: + e.previousSunday() } } func (e *evaluator) evalEndSnap(node *ast.SnapNode) { switch node.Unit { - case "m": + // time unit + case Minute: e.snapEndOfMinute() - case "h": + case Hour: e.snapEndOfHour() - case "d": + case Day: e.snapEndOfDay() - case "w": + case Week: e.snapEndOfWeek() - case "M": + case Month: e.snapEndOfMonth() - case "Y": + case Year: e.snapEndOfYear() + // weekdays + case Monday: + e.nextMonday() + case Tuesday: + e.nextTuesday() + case Wednesday: + e.nextWednesday() + case Thursday: + e.nextThursday() + case Friday: + e.nextFriday() + case Saturday: + e.nextSaturday() + case Sunday: + e.nextSunday() } + } func (e *evaluator) evalSnapNode(node *ast.SnapNode) { diff --git a/evaluator/evaluator_dates.go b/evaluator/evaluator_dates.go index b8cb9d0..b29e4a9 100644 --- a/evaluator/evaluator_dates.go +++ b/evaluator/evaluator_dates.go @@ -91,3 +91,59 @@ func (e *evaluator) addMonths(amount int) { func (e *evaluator) addYears(amount int) { e.current = AddYears(e.current, amount) } + +func (e *evaluator) previousMonday() { + e.current = PreviousMonday(e.current) +} + +func (e *evaluator) previousTuesday() { + e.current = PreviousTuesday(e.current) +} + +func (e *evaluator) previousWednesday() { + e.current = PreviousWednesday(e.current) +} + +func (e *evaluator) previousThursday() { + e.current = PreviousThursday(e.current) +} + +func (e *evaluator) previousFriday() { + e.current = PreviousFriday(e.current) +} + +func (e *evaluator) previousSaturday() { + e.current = PreviousSaturday(e.current) +} + +func (e *evaluator) previousSunday() { + e.current = PreviousSunday(e.current) +} + +func (e *evaluator) nextMonday() { + e.current = NextMonday(e.current) +} + +func (e *evaluator) nextTuesday() { + e.current = NextTuesday(e.current) +} + +func (e *evaluator) nextWednesday() { + e.current = NextWednesday(e.current) +} + +func (e *evaluator) nextThursday() { + e.current = NextThursday(e.current) +} + +func (e *evaluator) nextFriday() { + e.current = NextFriday(e.current) +} + +func (e *evaluator) nextSaturday() { + e.current = NextSaturday(e.current) +} + +func (e *evaluator) nextSunday() { + e.current = NextSunday(e.current) +} diff --git a/evaluator/evaluator_test.go b/evaluator/evaluator_test.go index a7536c4..0ffb3f3 100644 --- a/evaluator/evaluator_test.go +++ b/evaluator/evaluator_test.go @@ -344,3 +344,50 @@ func TestEvaluator_AddYears(t *testing.T) { } } } + +// Week days +func TestEvaluator_PreviousWeekday(t *testing.T) { + tests := []struct { + Token string + Initial string + Expected string + }{ + {"now/mon", "2020-03-11 00:00:00", "2020-03-09 00:00:00"}, + {"now/tue", "2020-03-11 00:00:00", "2020-03-10 00:00:00"}, + {"now/wed", "2020-03-11 00:00:00", "2020-03-11 00:00:00"}, + {"now/thu", "2020-03-11 00:00:00", "2020-03-05 00:00:00"}, + {"now/fri", "2020-03-11 00:00:00", "2020-03-06 00:00:00"}, + {"now/sat", "2020-03-11 00:00:00", "2020-03-07 00:00:00"}, + {"now/sun", "2020-03-11 00:00:00", "2020-03-08 00:00:00"}, + } + + for _, test := range tests { + actual := testEval(t, test.Token, test.Initial) + if !assertTime(t, actual, test.Expected) { + t.FailNow() + } + } +} + +func TestEvaluator_NextWeekday(t *testing.T) { + tests := []struct { + Token string + Initial string + Expected string + }{ + {"now@mon", "2020-03-11 00:00:00", "2020-03-16 00:00:00"}, + {"now@tue", "2020-03-11 00:00:00", "2020-03-17 00:00:00"}, + {"now@wed", "2020-03-11 00:00:00", "2020-03-11 00:00:00"}, + {"now@thu", "2020-03-11 00:00:00", "2020-03-12 00:00:00"}, + {"now@fri", "2020-03-11 00:00:00", "2020-03-13 00:00:00"}, + {"now@sat", "2020-03-11 00:00:00", "2020-03-14 00:00:00"}, + {"now@sun", "2020-03-11 00:00:00", "2020-03-15 00:00:00"}, + } + + for _, test := range tests { + actual := testEval(t, test.Token, test.Initial) + if !assertTime(t, actual, test.Expected) { + t.FailNow() + } + } +} diff --git a/lexer/lexer.go b/lexer/lexer.go index 055eee9..c6ab061 100644 --- a/lexer/lexer.go +++ b/lexer/lexer.go @@ -85,17 +85,20 @@ func (l *Lexer) NextToken() token.Token { number := l.readNumber() tok = newToken(token.Number, number) return tok - } else { - schar := string(l.currentChar) + } + if isLetter(l.currentChar) { + schar := l.readWord() tok = newToken(token.LookupKeyword(schar), schar) + return tok } + tok = newToken(token.Illegal, string(l.currentChar)) } l.readChar() return tok } func isLetter(char byte) bool { - return (char >= 'a' && char <= 'z') || (char >= 'A' && char <= 'Z') || char == '_' + return (char >= 'a' && char <= 'z') || (char >= 'A' && char <= 'Z') } func isDigit(char byte) bool { diff --git a/lexer/lexer_test.go b/lexer/lexer_test.go index 77a5b11..628f5ee 100644 --- a/lexer/lexer_test.go +++ b/lexer/lexer_test.go @@ -29,7 +29,7 @@ func testToken(t *testing.T, payload string, expected []expectedResult) { } } -func TestLexerWithNow(t *testing.T) { +func TestLexer_WithNow(t *testing.T) { input := ` now-s+2m_-3h+234d-w+M-Y/M@w ` @@ -63,7 +63,7 @@ func TestLexerWithNow(t *testing.T) { testToken(t, input, expected) } -func TestLexerWithoutNow(t *testing.T) { +func TestLexer_WithoutNow(t *testing.T) { input := ` -s+2m_-3h+234d-w+M-Y/M@w ` @@ -95,3 +95,27 @@ func TestLexerWithoutNow(t *testing.T) { testToken(t, input, expected) } + +func TestLexer_PreviousWeekdays(t *testing.T) { + input := "now/mon/tue/wed/thu/fri/sat/sun" + expected := []expectedResult{ + {token.Start, "now"}, + {token.SnapStart, "/"}, + {token.Wd, "mon"}, + {token.SnapStart, "/"}, + {token.Wd, "tue"}, + {token.SnapStart, "/"}, + {token.Wd, "wed"}, + {token.SnapStart, "/"}, + {token.Wd, "thu"}, + {token.SnapStart, "/"}, + {token.Wd, "fri"}, + {token.SnapStart, "/"}, + {token.Wd, "sat"}, + {token.SnapStart, "/"}, + {token.Wd, "sun"}, + {token.End, ""}, + } + + testToken(t, input, expected) +} diff --git a/parser/parser.go b/parser/parser.go index 947e663..f494ddc 100644 --- a/parser/parser.go +++ b/parser/parser.go @@ -83,8 +83,8 @@ func (p *Parser) parseSnapNode() *ast.SnapNode { Operator: p.curToken.Literal, } p.nextToken() - if !p.curTokenIs(token.Unit) { - p.addError(fmt.Sprintf("expected token to be Unit, got '%s' instead", p.curToken.Type)) + if !(p.curTokenIs(token.Unit) || p.curTokenIs(token.Wd)) { + p.addError(fmt.Sprintf("expected token to be Unit or Wd, got '%s' instead", p.curToken.Type)) return nil } node.Unit = p.curToken.Literal diff --git a/parser/parser_test.go b/parser/parser_test.go index 0aeef72..167c60a 100644 --- a/parser/parser_test.go +++ b/parser/parser_test.go @@ -22,6 +22,7 @@ func newParser(t *testing.T, payload string) *ast.RootNode { } func testValNode(t *testing.T, node ast.Node, expectedLiteral string) bool { + t.Helper() valnode, ok := node.(*ast.ValueNode) if !ok { t.Fatalf("unexpected node type. want ValueNode, have %v(%T)", @@ -37,6 +38,7 @@ func testValNode(t *testing.T, node ast.Node, expectedLiteral string) bool { } func testArithmeticNode(t *testing.T, node ast.Node, amount int64, unit, sign string) bool { + t.Helper() arnode, ok := node.(*ast.ArithmeticNode) if !ok { t.Fatalf("unexpected node type. want ArithmeticNode, have %v(%T)", @@ -61,6 +63,8 @@ func testArithmeticNode(t *testing.T, node ast.Node, amount int64, unit, sign st } func testSnapNode(t *testing.T, node ast.Node, op, unit string) bool { + t.Helper() + snapnode, ok := node.(*ast.SnapNode) if !ok { t.Fatalf("unexpected node type. want SnapNode, have %v(%T)", @@ -73,7 +77,7 @@ func testSnapNode(t *testing.T, node ast.Node, op, unit string) bool { return false } if snapnode.Unit != unit { - t.Fatalf("snapnode. unexpected unit. want %s, have %s", + t.Fatalf("snapnode. unexpected unit or weekday. want %s, have %s", unit, snapnode.Unit) return false } @@ -90,6 +94,16 @@ func TestParser_SnapStart(t *testing.T) { testSnapNode(t, root.Nodes[1], "/", "d") } +func TestParser_SnapStart_Weekday(t *testing.T) { + payload := "now/thu" + root := newParser(t, payload) + if len(root.Nodes) < 2 { + t.Fatalf("empty node set. expected some") + } + testValNode(t, root.Nodes[0], "now") + testSnapNode(t, root.Nodes[1], "/", "thu") +} + func TestParser_SnapEnd(t *testing.T) { payload := "now@d" root := newParser(t, payload) diff --git a/token/token.go b/token/token.go index d7e3fb6..7fa617b 100644 --- a/token/token.go +++ b/token/token.go @@ -12,20 +12,28 @@ const ( Number = "num" Unit = "unit" Illegal = "ill" + Wd = "wd" ) var keywords = map[string]TokenType{ - "+": Plus, - "-": Minus, - "/": SnapStart, - "@": SnapEnd, - "s": Unit, - "m": Unit, - "h": Unit, - "d": Unit, - "w": Unit, - "M": Unit, - "Y": Unit, + "+": Plus, + "-": Minus, + "/": SnapStart, + "@": SnapEnd, + "s": Unit, + "m": Unit, + "h": Unit, + "d": Unit, + "w": Unit, + "M": Unit, + "Y": Unit, + "mon": Wd, + "tue": Wd, + "wed": Wd, + "thu": Wd, + "fri": Wd, + "sat": Wd, + "sun": Wd, } func LookupKeyword(key string) TokenType {