From df085a37b2733ee4e48d41d106f2014fda3ff440 Mon Sep 17 00:00:00 2001 From: Marcos Date: Sun, 3 Oct 2021 18:26:47 +0200 Subject: [PATCH] feat: quarters --- README.md | 7 +++++++ evaluator/constants.go | 5 +++++ evaluator/dates.go | 36 ++++++++++++++++++++++++++++++++++++ evaluator/evaluator.go | 23 ++++++++++++++++++++++- evaluator/evaluator_dates.go | 16 ++++++++++++++++ evaluator/evaluator_test.go | 34 ++++++++++++++++++++++++++++++++++ examples/naive.go | 3 +++ lexer/lexer.go | 2 +- lexer/lexer_test.go | 12 +++++++++++- token/token.go | 5 +++++ 10 files changed, 140 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 15b12ab..9d4d219 100644 --- a/README.md +++ b/README.md @@ -12,6 +12,7 @@ - **Time zone support**. Time zones tend to be a major pending subject to many devs. - **Configure when week start**. Cause not every country cries on Mondays. - **Business weeks**. The previous point allows this. +- **Quarters** ## Motivation @@ -37,6 +38,11 @@ Some common examples of relative tokens: | Custom range | `now+w-2d/h` | `now+2M-10h` | | Last month first business week | `now-M/M+w/bw` | `now-M/+w@bw` | | This year | `now/Y` | `now@Y` | +| This quarter | `now/Q` | `now@Q` | +| This first quarter (Q1) | `now/Q1` | `now@Q1` | +| This second quarter (Q2) | `now/Q2` | `now@Q2` | +| This third quarter (Q3) | `now/Q3` | `now@Q3` | +| This fourth quarter (Q4) | `now/Q4` | `now@Q4` | As you may have noticed, token follow a pattern: @@ -56,6 +62,7 @@ As you may have noticed, token follow a pattern: - `w` weeks - `M` months - `Y` years + - `Q` quarters - Optionally, there exist two extra modifiers to snap dates to the start or the end of any given snapshot unit. Those are: - `/` Snap the date to the start of the snapshot unit. diff --git a/evaluator/constants.go b/evaluator/constants.go index ff90a8b..c382da9 100644 --- a/evaluator/constants.go +++ b/evaluator/constants.go @@ -9,6 +9,11 @@ const ( businessWeek = "bw" month = "M" year = "Y" + quarter = "Q" + quarter1 = "Q1" + quarter2 = "Q2" + quarter3 = "Q3" + quarter4 = "Q4" monday = "mon" tuesday = "tue" diff --git a/evaluator/dates.go b/evaluator/dates.go index 3bd3229..b07a930 100644 --- a/evaluator/dates.go +++ b/evaluator/dates.go @@ -46,6 +46,22 @@ func BeginningOfYear(date time.Time) time.Time { return time.Date(y, time.January, 1, 0, 0, 0, 0, date.Location()) } +// BeginningOfCurrentQuarter returns the given date to the start of then current quarter +func BeginningOfCurrentQuarter(date time.Time) time.Time { + _, m, _ := date.Date() + q := int((m - 1) / 3) + + return BeginningOfQuarter(date, q) +} + +// BeginningOfQuarter returns the given date to the start of the given quarter +func BeginningOfQuarter(date time.Time, q int) time.Time { + y, _, _ := date.Date() + m := time.Month(q*3 + 1) + + return time.Date(y, m, 1, 0, 0, 0, 0, date.Location()) +} + // EndOfMinute returns the given day to the end of the minute, with seconds truncated to 59 func EndOfMinute(date time.Time) time.Time { return BeginningOfMinute(date).Add(time.Minute - time.Nanosecond) @@ -82,6 +98,26 @@ func EndOfMonth(date time.Time) time.Time { return BeginningOfMonth(date).AddDate(0, 1, 0).Add(-time.Nanosecond) } +// EndOfCurrentQuarter returns the given date to the end of then current quarter +func EndOfCurrentQuarter(date time.Time) time.Time { + _, m, _ := date.Date() + q := int((m - 1) / 3) + + return EndOfQuarter(date, q) +} + +// EndOfQuarter returns the given date to the end of the given quarter +func EndOfQuarter(date time.Time, q int) time.Time { + y, _, _ := date.Date() + m := time.Month(q*3 + 3) + d := 30 + if m == 12 || m == 3 { + d = 31 + } + + return time.Date(y, m, d, 23, 59, 59, int(time.Second-time.Nanosecond), date.Location()) +} + // EndOfYear returns the given date snapped to the end of the year, being the day 31th of December. Hours are // truncated to 23, whereas minutes and seconds will be truncated to 59 func EndOfYear(date time.Time) time.Time { diff --git a/evaluator/evaluator.go b/evaluator/evaluator.go index 2236daf..b47857d 100644 --- a/evaluator/evaluator.go +++ b/evaluator/evaluator.go @@ -1,9 +1,10 @@ package evaluator import ( - "github.com/sonirico/datetoken.go/models" "time" + "github.com/sonirico/datetoken.go/models" + "github.com/sonirico/datetoken.go/ast" "github.com/sonirico/datetoken.go/lexer" "github.com/sonirico/datetoken.go/parser" @@ -102,6 +103,16 @@ func (e *Evaluator) evalStartSnap(node *ast.SnapNode) { e.snapStartOfMonth() case year: e.snapStartOfYear() + case quarter: + e.snapStartOfCurrentQuarter() + case quarter1: + e.snapStartOfQuarter(0) + case quarter2: + e.snapStartOfQuarter(1) + case quarter3: + e.snapStartOfQuarter(2) + case quarter4: + e.snapStartOfQuarter(3) // weekdays case monday: e.previousMonday() @@ -137,6 +148,16 @@ func (e *Evaluator) evalEndSnap(node *ast.SnapNode) { e.snapEndOfMonth() case year: e.snapEndOfYear() + case quarter: + e.snapEndOfCurrentQuarter() + case quarter1: + e.snapEndOfQuarter(0) + case quarter2: + e.snapEndOfQuarter(1) + case quarter3: + e.snapEndOfQuarter(2) + case quarter4: + e.snapEndOfQuarter(3) // weekdays case monday: e.nextMonday() diff --git a/evaluator/evaluator_dates.go b/evaluator/evaluator_dates.go index b5bb616..5c2a832 100644 --- a/evaluator/evaluator_dates.go +++ b/evaluator/evaluator_dates.go @@ -32,6 +32,22 @@ func (e *Evaluator) snapStartOfYear() { e.current = BeginningOfYear(e.current) } +func (e *Evaluator) snapStartOfCurrentQuarter() { + e.current = BeginningOfCurrentQuarter(e.current) +} + +func (e *Evaluator) snapStartOfQuarter(q int) { + e.current = BeginningOfQuarter(e.current, q) +} + +func (e *Evaluator) snapEndOfCurrentQuarter() { + e.current = EndOfCurrentQuarter(e.current) +} + +func (e *Evaluator) snapEndOfQuarter(q int) { + e.current = EndOfQuarter(e.current, q) +} + func (e *Evaluator) snapEndOfMinute() { e.current = EndOfMinute(e.current) } diff --git a/evaluator/evaluator_test.go b/evaluator/evaluator_test.go index c38ca99..9bfbf8d 100644 --- a/evaluator/evaluator_test.go +++ b/evaluator/evaluator_test.go @@ -258,6 +258,40 @@ func TestEvaluator_SnapEndYear(t *testing.T) { } } +func TestEvaluator_SnapQuarter(t *testing.T) { + tests := []struct { + Token string + Initial string + Expected string + }{ + // Start + {"now/Q", "2020-02-11 10:13:23", "2020-01-01 00:00:00"}, + {"now/Q", "2020-04-11 10:13:23", "2020-04-01 00:00:00"}, + {"now/Q", "2020-07-11 10:13:23", "2020-07-01 00:00:00"}, + {"now/Q", "2020-10-11 10:13:23", "2020-10-01 00:00:00"}, + {"now/Q1", "2020-02-11 10:13:23", "2020-01-01 00:00:00"}, + {"now/Q2", "2020-02-11 10:59:59", "2020-04-01 00:00:00"}, + {"now/Q3", "2020-02-11 23:59:50", "2020-07-01 00:00:00"}, + {"now/Q4", "2020-02-11 23:59:50", "2020-10-01 00:00:00"}, + // End + {"now@Q", "2020-02-11 10:13:23", "2020-03-31 23:59:59"}, + {"now@Q", "2020-04-11 10:13:23", "2020-06-30 23:59:59"}, + {"now@Q", "2020-07-11 10:13:23", "2020-09-30 23:59:59"}, + {"now@Q", "2020-10-11 10:13:23", "2020-12-31 23:59:59"}, + {"now@Q1", "2020-02-11 10:13:23", "2020-03-31 23:59:59"}, + {"now@Q2", "2020-02-11 10:59:59", "2020-06-30 23:59:59"}, + {"now@Q3", "2020-02-11 23:59:50", "2020-09-30 23:59:59"}, + {"now@Q4", "2020-01-31 23:59:50", "2020-12-31 23:59:59"}, + } + + for _, test := range tests { + actual := testEval(t, test.Token, test.Initial) + if !assertTime(t, actual, test.Expected) { + t.FailNow() + } + } +} + // Arithmetic func TestEvaluator_AddSeconds(t *testing.T) { diff --git a/examples/naive.go b/examples/naive.go index ab43c08..4f451c8 100644 --- a/examples/naive.go +++ b/examples/naive.go @@ -2,6 +2,7 @@ package main import ( "fmt" + "github.com/sonirico/datetoken.go" ) @@ -15,6 +16,7 @@ func main() { "now/bw", "now/M", "now/Y", + "now/Q", } fmt.Println("Snap to start of units") for _, token := range tokens { @@ -32,6 +34,7 @@ func main() { "now@bw", "now@M", "now@Y", + "now@Q", } for _, token := range tokens { date, _ := datetoken.EvalNaive(token) diff --git a/lexer/lexer.go b/lexer/lexer.go index d974cfd..bd4f742 100644 --- a/lexer/lexer.go +++ b/lexer/lexer.go @@ -47,7 +47,7 @@ func (l *Lexer) peekChar() byte { func (l *Lexer) readWord() string { pos := l.currentPointer - for isLetter(l.currentChar) { + for isLetter(l.currentChar) || isDigit(l.currentChar) { l.readChar() } return l.payload[pos:l.currentPointer] diff --git a/lexer/lexer_test.go b/lexer/lexer_test.go index cd2cc58..c9dc157 100644 --- a/lexer/lexer_test.go +++ b/lexer/lexer_test.go @@ -31,7 +31,7 @@ func testToken(t *testing.T, payload string, expected []expectedResult) { } func TestLexer_WithNow(t *testing.T) { input := ` - now-s+2m_-3h+234d-w+M-Y/M@w@bw + now-s+2m_-3h+234d-w+M-Y/M@w@bw/Q/Q1/Q2/Q3/Q4 ` expected := []expectedResult{ {token.Start, "now"}, @@ -59,6 +59,16 @@ func TestLexer_WithNow(t *testing.T) { {token.Unit, "w"}, {token.SnapEnd, "@"}, {token.Unit, "bw"}, + {token.SnapStart, "/"}, + {token.Unit, "Q"}, + {token.SnapStart, "/"}, + {token.Unit, "Q1"}, + {token.SnapStart, "/"}, + {token.Unit, "Q2"}, + {token.SnapStart, "/"}, + {token.Unit, "Q3"}, + {token.SnapStart, "/"}, + {token.Unit, "Q4"}, {token.End, ""}, } diff --git a/token/token.go b/token/token.go index 0b82e94..fd8dbe3 100644 --- a/token/token.go +++ b/token/token.go @@ -46,6 +46,11 @@ var keywords = map[string]Type{ "fri": Wd, "sat": Wd, "sun": Wd, + "Q": Unit, + "Q1": Unit, + "Q2": Unit, + "Q3": Unit, + "Q4": Unit, } // LookupKeyword will return the associated token type for a given token literal. If no one is found, the literal