From 8ded69fd7ed23721ad859d951843b242bdb6ee25 Mon Sep 17 00:00:00 2001 From: "Gerasimos (Makis) Maropoulos" Date: Mon, 21 Feb 2022 22:50:27 +0200 Subject: [PATCH] add new x/timex sub-package --- HISTORY.md | 2 + x/timex/weekday.go | 121 ++++++++++ x/timex/weekday_test.go | 511 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 634 insertions(+) create mode 100644 x/timex/weekday.go create mode 100644 x/timex/weekday_test.go diff --git a/HISTORY.md b/HISTORY.md index eac91bf6..4c9b55d8 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -28,6 +28,8 @@ The codebase for Dependency Injection, Internationalization and localization and ## Fixes and Improvements +- New [x/timex](x/timex) sub-package, helps working with weekdays. + - Minor improvements to the [JSON Kitchen Time](x/jsonx/kitchen_time.go). - A session database can now implement the `EndRequest(ctx *context.Context, session *Session)` method which will be fired at the end of the request-response lifecycle. - Improvements on JSON and ReadJSON when `Iris.Configuration.EnableOptimizations` is true. The request's Context is used whenever is necessary. diff --git a/x/timex/weekday.go b/x/timex/weekday.go new file mode 100644 index 00000000..ae7bd7c5 --- /dev/null +++ b/x/timex/weekday.go @@ -0,0 +1,121 @@ +package timex + +import "time" + +// RangeDate returns a function which returns a time +// between "start" and "end". When the iteration finishes +// the returned time is zero. +func RangeDate(start, end time.Time) func() time.Time { + y, m, d := start.Date() + start = time.Date(y, m, d, 0, 0, 0, 0, start.Location()) + y, m, d = end.Date() + end = time.Date(y, m, d, 0, 0, 0, 0, end.Location()) + + return func() time.Time { + if start.After(end) { + return time.Time{} + } + date := start + start = start.AddDate(0, 0, 1) + return date + } +} + +type DateRangeType string + +const ( + DayRange DateRangeType = "day" + MonthRange DateRangeType = "month" + WeekRange DateRangeType = "week" + YearRange DateRangeType = "year" +) + +// Between returns the dates from "start" to "end". +func Between(start, end time.Time) []time.Time { + var dates []time.Time + for df := RangeDate(start, end); ; { + d := df() + if d.IsZero() { + break + } + dates = append(dates, d) + } + return dates +} + +// Backwards returns a list of dates between "end" and -n (years, months, weeks or days). +func Backwards(typ DateRangeType, end time.Time, n int) []time.Time { + var start time.Time + + switch typ { + case DayRange: + n = n - 1 // -7 should be -6 to get the week from today. + start = end.AddDate(0, 0, -n) + case WeekRange: + // 7 should be 6 to get the week. + start = end.AddDate(0, 0, -n*6) + case MonthRange: + start = end.AddDate(0, -n, 0) + case YearRange: + start = end.AddDate(-n, 0, 0) + } + + return Between(start, end) +} + +// BackwardsN returns the dates from back to "n" years, months, weeks or days from today. +func BackwardsN(typ DateRangeType, n int) []time.Time { + end := time.Now() + return Backwards(typ, end, n) +} + +// BackwardsToMonday returns the dates between "end" (including "end") +// until the previous monday of the current week (including monday). +func BackwardsToMonday(end time.Time) []time.Time { + dates := []time.Time{end} + for end.Weekday() != time.Monday { + end = end.AddDate(0, 0, -1) + dates = append(dates, end) + } + return dates +} + +func GetWeekDate(now time.Time, weekday, start, end time.Weekday) time.Time { + dates := GetWeekdays(now, start, end) + for _, d := range dates { + if d.Weekday() == weekday { + return d + } + } + + return time.Time{} +} + +// GetWeekStart returns the date of the first week day (startWeekday) of the current now's week. +func GetWeekStart(now time.Time, startWeekday time.Weekday) time.Time { + offset := (int(startWeekday) - int(now.Weekday()) - 7) % 7 + date := now.Add(time.Duration(offset*24) * time.Hour) + return date +} + +// GetWeekEnd returns the date of the last week day (endWeekday) of the current now's week. +func GetWeekEnd(now time.Time, endWeekday time.Weekday) time.Time { + offset := (int(endWeekday) - int(now.Weekday()) + 7) % 7 + date := now.Add(time.Duration(offset*24) * time.Hour) + return date +} + +// GetWeekdays returns the range between "startWeekday" and "endWeekday" of the current week. +func GetWeekdays(now time.Time, startWeekday, endWeekday time.Weekday) (dates []time.Time) { + return Between(GetWeekStart(now, startWeekday), GetWeekEnd(now, endWeekday)) +} + +// GetMonthStart returns the date of the first month day of the current now's month. +func GetMonthStart(now time.Time) time.Time { + return time.Date(now.Year(), now.Month(), 1, 0, 0, 0, 0, now.Location()) +} + +// GetYearStart returns the date of the first year of the current now's year. +func GetYearStart(now time.Time) time.Time { + return time.Date(now.Year(), 1, 1, 0, 0, 0, 0, now.Location()) +} diff --git a/x/timex/weekday_test.go b/x/timex/weekday_test.go new file mode 100644 index 00000000..2af690a7 --- /dev/null +++ b/x/timex/weekday_test.go @@ -0,0 +1,511 @@ +package timex + +import ( + "fmt" + "testing" + "time" + + "github.com/kataras/iris/v12/x/jsonx" +) + +func TestMonthAndYearStart(t *testing.T) { + now, err := time.Parse(jsonx.ISO8601Layout, "2021-04-21T00:00:00") + if err != nil { + t.Fatal(err) + } + + startMonthDate := GetMonthStart(now) + if expected, got := "2021-04-01 00:00:00 +0000 UTC", startMonthDate.String(); expected != got { + t.Logf("start of the current month: expected: %s but got: %s", expected, got) + } + + startYearDate := GetYearStart(now) + if expected, got := "2021-01-01 00:00:00 +0000 UTC", startYearDate.String(); expected != got { + t.Logf("start of the current year: expected: %s but got: %s", expected, got) + } +} + +func TestGetWeekdays(t *testing.T) { + var tests = []struct { + Date string + ExpectedDates []string + Start time.Weekday + End time.Weekday + }{ + { // 1. + Date: "2021-02-04T00:00:00", + Start: time.Monday, + End: time.Sunday, + ExpectedDates: []string{ + "2021-02-01 00:00:00 +0000 UTC", + "2021-02-02 00:00:00 +0000 UTC", + "2021-02-03 00:00:00 +0000 UTC", + "2021-02-04 00:00:00 +0000 UTC", + "2021-02-05 00:00:00 +0000 UTC", + "2021-02-06 00:00:00 +0000 UTC", + "2021-02-07 00:00:00 +0000 UTC", + }, + }, + { // 2. It's monday. + Date: "2022-01-17T00:00:00", + Start: time.Monday, + End: time.Sunday, + ExpectedDates: []string{ + "2022-01-17 00:00:00 +0000 UTC", + "2022-01-18 00:00:00 +0000 UTC", + "2022-01-19 00:00:00 +0000 UTC", + "2022-01-20 00:00:00 +0000 UTC", + "2022-01-21 00:00:00 +0000 UTC", + "2022-01-22 00:00:00 +0000 UTC", + "2022-01-23 00:00:00 +0000 UTC", + }, + }, + { // 3. Test all other days by order. + Date: "2022-01-18T00:00:00", + Start: time.Monday, + End: time.Sunday, + ExpectedDates: []string{ + "2022-01-17 00:00:00 +0000 UTC", + "2022-01-18 00:00:00 +0000 UTC", + "2022-01-19 00:00:00 +0000 UTC", + "2022-01-20 00:00:00 +0000 UTC", + "2022-01-21 00:00:00 +0000 UTC", + "2022-01-22 00:00:00 +0000 UTC", + "2022-01-23 00:00:00 +0000 UTC", + }, + }, + { // 4. + Date: "2022-01-19T00:00:00", + Start: time.Monday, + End: time.Sunday, + ExpectedDates: []string{ + "2022-01-17 00:00:00 +0000 UTC", + "2022-01-18 00:00:00 +0000 UTC", + "2022-01-19 00:00:00 +0000 UTC", + "2022-01-20 00:00:00 +0000 UTC", + "2022-01-21 00:00:00 +0000 UTC", + "2022-01-22 00:00:00 +0000 UTC", + "2022-01-23 00:00:00 +0000 UTC", + }, + }, + { // 5. + Date: "2022-01-20T00:00:00", + Start: time.Monday, + End: time.Sunday, + ExpectedDates: []string{ + "2022-01-17 00:00:00 +0000 UTC", + "2022-01-18 00:00:00 +0000 UTC", + "2022-01-19 00:00:00 +0000 UTC", + "2022-01-20 00:00:00 +0000 UTC", + "2022-01-21 00:00:00 +0000 UTC", + "2022-01-22 00:00:00 +0000 UTC", + "2022-01-23 00:00:00 +0000 UTC", + }, + }, + { // 6. + Date: "2022-01-21T00:00:00", + Start: time.Monday, + End: time.Sunday, + ExpectedDates: []string{ + "2022-01-17 00:00:00 +0000 UTC", + "2022-01-18 00:00:00 +0000 UTC", + "2022-01-19 00:00:00 +0000 UTC", + "2022-01-20 00:00:00 +0000 UTC", + "2022-01-21 00:00:00 +0000 UTC", + "2022-01-22 00:00:00 +0000 UTC", + "2022-01-23 00:00:00 +0000 UTC", + }, + }, + { // 7. + Date: "2022-01-22T00:00:00", + Start: time.Monday, + End: time.Sunday, + ExpectedDates: []string{ + "2022-01-17 00:00:00 +0000 UTC", + "2022-01-18 00:00:00 +0000 UTC", + "2022-01-19 00:00:00 +0000 UTC", + "2022-01-20 00:00:00 +0000 UTC", + "2022-01-21 00:00:00 +0000 UTC", + "2022-01-22 00:00:00 +0000 UTC", + "2022-01-23 00:00:00 +0000 UTC", + }, + }, + { // 8. Sunday. + Date: "2022-01-23T00:00:00", + Start: time.Monday, + End: time.Sunday, + ExpectedDates: []string{ + "2022-01-17 00:00:00 +0000 UTC", + "2022-01-18 00:00:00 +0000 UTC", + "2022-01-19 00:00:00 +0000 UTC", + "2022-01-20 00:00:00 +0000 UTC", + "2022-01-21 00:00:00 +0000 UTC", + "2022-01-22 00:00:00 +0000 UTC", + "2022-01-23 00:00:00 +0000 UTC", + }, + }, + { // 9. Test 1st Jenuary (Saturday) . + Date: "2022-01-01T00:00:00", + Start: time.Monday, + End: time.Sunday, + ExpectedDates: []string{ + "2021-12-27 00:00:00 +0000 UTC", // monday. + "2021-12-28 00:00:00 +0000 UTC", + "2021-12-29 00:00:00 +0000 UTC", + "2021-12-30 00:00:00 +0000 UTC", + "2021-12-31 00:00:00 +0000 UTC", + "2022-01-01 00:00:00 +0000 UTC", + "2022-01-02 00:00:00 +0000 UTC", // sunday. + }, + }, + { // 10. Test 2021-12-31 (Friday) from sunday to saturday. + Date: "2022-01-01T00:00:00", + Start: time.Sunday, + End: time.Saturday, + ExpectedDates: []string{ + "2021-12-26 00:00:00 +0000 UTC", // sunday. + "2021-12-27 00:00:00 +0000 UTC", // monday. + "2021-12-28 00:00:00 +0000 UTC", + "2021-12-29 00:00:00 +0000 UTC", + "2021-12-30 00:00:00 +0000 UTC", + "2021-12-31 00:00:00 +0000 UTC", // friday. + "2022-01-01 00:00:00 +0000 UTC", // saturday. + }, + }, + } + + for i := range tests { + tt := tests[i] + t.Run(fmt.Sprintf("%s[%d]", t.Name(), i+1), func(t *testing.T) { + now, err := time.Parse(jsonx.ISO8601Layout, tt.Date) + if err != nil { + t.Fatal(err) + } + + dates := GetWeekdays(now, tt.Start, tt.End) + checkDates(t, "", tt.ExpectedDates, dates) + }) + } + + // t.Logf("[%s] Current day of the week: %s", now.String(), now.Weekday().String()) +} + +func TestGetWeekEnd(t *testing.T) { + var tests = []struct { + End time.Weekday + Dates []string + ExpectedDateEnd string + }{ + { // 1. Test sunday as end. + End: time.Sunday, + Dates: []string{ + "2022-01-17T00:00:00", // 1. + "2022-01-18T00:00:00", // 2. + "2022-01-19T00:00:00", // 3. + "2022-01-20T00:00:00", // 4. + "2022-01-21T00:00:00", // 5. + "2022-01-22T00:00:00", // 6. + "2022-01-23T00:00:00", // 7. + }, + ExpectedDateEnd: "2022-01-23T00:00:00", + }, + { // 1. Test saturday as end. + End: time.Saturday, + Dates: []string{ + "2022-01-23T00:00:00", // Sunday. + "2022-01-24T00:00:00", + "2022-01-25T00:00:00", + "2022-01-26T00:00:00", + "2022-01-27T00:00:00", + "2022-01-28T00:00:00", + "2022-01-29T00:00:00", + }, + ExpectedDateEnd: "2022-01-29T00:00:00", + }, + } + + for i := range tests { + tt := tests[i] + t.Run(fmt.Sprintf("%s[%d]", t.Name(), i+1), func(t *testing.T) { + for j, date := range tt.Dates { + now, err := time.Parse(jsonx.ISO8601Layout, date) + if err != nil { + t.Fatal(err) + } + + weekEndDate := GetWeekEnd(now, tt.End) + if got := weekEndDate.Format(jsonx.ISO8601Layout); got != tt.ExpectedDateEnd { + t.Fatalf("[%d] expected week end date: %s but got: %s ", j+1, tt.ExpectedDateEnd, got) + } + } + }) + } +} + +func TestGetWeekDate(t *testing.T) { + now, err := jsonx.ParseSimpleDate("2022-02-10") + if err != nil { + t.Fatal(err) + } + + var tests = []struct { + Now jsonx.SimpleDate + Start time.Weekday + End time.Weekday + Weekday time.Weekday + ExpectedDate string + }{ + { + Now: now, + Start: time.Monday, + End: time.Sunday, + Weekday: time.Monday, + ExpectedDate: "2022-02-07T00:00:00", + }, + { + Now: now, + Start: time.Monday, + End: time.Sunday, + Weekday: time.Tuesday, + ExpectedDate: "2022-02-08T00:00:00", + }, + { + Now: now, + Start: time.Monday, + End: time.Sunday, + Weekday: time.Wednesday, + ExpectedDate: "2022-02-09T00:00:00", + }, + { + Now: now, + Start: time.Monday, + End: time.Sunday, + Weekday: time.Thursday, + ExpectedDate: "2022-02-10T00:00:00", + }, + { + Now: now, + Start: time.Monday, + End: time.Sunday, + Weekday: time.Friday, + ExpectedDate: "2022-02-11T00:00:00", + }, + { + Now: now, + Start: time.Monday, + End: time.Sunday, + Weekday: time.Saturday, + ExpectedDate: "2022-02-12T00:00:00", + }, + { + Now: now, + Start: time.Monday, + End: time.Sunday, + Weekday: time.Sunday, + ExpectedDate: "2022-02-13T00:00:00", + }, + // Test sunday to saturday. + { + Now: now, + Start: time.Sunday, + End: time.Saturday, + Weekday: time.Wednesday, + ExpectedDate: "2022-02-09T00:00:00", + }, + } + + for i := range tests { + tt := tests[i] + t.Run(fmt.Sprintf("%s[%s]", t.Name(), tt.Weekday.String()), func(t *testing.T) { + weekDate := GetWeekDate(tt.Now.ToTime(), tt.Weekday, tt.Start, tt.End) + if got := weekDate.Format(jsonx.ISO8601Layout); got != tt.ExpectedDate { + t.Fatalf("[%d] expected week date: %s but got: %s ", i+1, tt.ExpectedDate, got) + } + }) + } +} + +func TestBackwardsToMonday(t *testing.T) { + end, err := time.Parse(jsonx.ISO8601Layout, "2021-04-05T00:00:00") + if err != nil { + t.Fatal(err) + } + expected := []string{ + "2021-04-05 00:00:00 +0000 UTC", + } + + // Test when when today is monday. + dates := BackwardsToMonday(end) + checkDates(t, "", expected, dates) + + // Test when today is tuesday. + end, err = time.Parse(jsonx.ISO8601Layout, "2021-04-06T00:00:00") + if err != nil { + t.Fatal(err) + } + + expected = []string{ + "2021-04-06 00:00:00 +0000 UTC", + "2021-04-05 00:00:00 +0000 UTC", + } + + dates = BackwardsToMonday(end) + checkDates(t, "", expected, dates) + + // Test when today is thursday. + end, err = time.Parse(jsonx.ISO8601Layout, "2021-04-08T00:00:00") + if err != nil { + t.Fatal(err) + } + + expected = []string{ + "2021-04-08 00:00:00 +0000 UTC", + "2021-04-07 00:00:00 +0000 UTC", + "2021-04-06 00:00:00 +0000 UTC", + "2021-04-05 00:00:00 +0000 UTC", + } + + dates = BackwardsToMonday(end) + checkDates(t, "", expected, dates) + + // Test when today is sunday. + end, err = time.Parse(jsonx.ISO8601Layout, "2021-04-10T00:00:00") + if err != nil { + t.Fatal(err) + } + + expected = []string{ + "2021-04-10 00:00:00 +0000 UTC", + "2021-04-09 00:00:00 +0000 UTC", + "2021-04-08 00:00:00 +0000 UTC", + "2021-04-07 00:00:00 +0000 UTC", + "2021-04-06 00:00:00 +0000 UTC", + "2021-04-05 00:00:00 +0000 UTC", + } + + dates = BackwardsToMonday(end) + checkDates(t, "", expected, dates) +} + +func checkDates(t *testing.T, typ DateRangeType, expected []string, dates []time.Time) { + t.Helper() + + t.Logf("[%s] length of days: %d", typ, len(dates)) + + if expectedLength, gotLength := len(expected), len(dates); expectedLength != gotLength { + t.Logf("[%s] expected days length: %d but got: %d", typ, expectedLength, gotLength) + + if gotLength > expectedLength { + t.Logf("Got %d extra date(s), list of all dates we've got:", gotLength-expectedLength) + for i, gotDate := range dates { + t.Logf("[%d] %s ", i, gotDate.String()) + } + } + + t.FailNow() + } + + for i, date := range dates { + // t.Logf("[%d] %s", i, date.String()) + if expected, got := expected[i], date.String(); expected != got { + t.Fatalf("[%d] [%s] expected date: %s but got: %s", i, typ, expected, got) + } + } +} + +func TestBetweenAndBackwardsN(t *testing.T) { + start, err := time.Parse(jsonx.ISO8601Layout, "2021-03-26T00:00:00") + if err != nil { + t.Fatal(err) + } + + end, err := time.Parse(jsonx.ISO8601Layout, "2021-04-01T00:00:00") + if err != nil { + t.Fatal(err) + } + + expected := []string{ + "2021-03-26 00:00:00 +0000 UTC", + "2021-03-27 00:00:00 +0000 UTC", + "2021-03-28 00:00:00 +0000 UTC", + "2021-03-29 00:00:00 +0000 UTC", + "2021-03-30 00:00:00 +0000 UTC", + "2021-03-31 00:00:00 +0000 UTC", + "2021-04-01 00:00:00 +0000 UTC", + } + + dates := Between(start, end) + checkDates(t, "", expected, dates) + + dates = Backwards(DayRange, end, 7) + checkDates(t, DayRange, expected, dates) + + dates = Backwards(WeekRange, end, 1) + checkDates(t, WeekRange, expected, dates) + + dates = Backwards(MonthRange, end, 2) + expectedMonthDates := []string{ + "2021-02-01 00:00:00 +0000 UTC", + "2021-02-02 00:00:00 +0000 UTC", + "2021-02-03 00:00:00 +0000 UTC", + "2021-02-04 00:00:00 +0000 UTC", + "2021-02-05 00:00:00 +0000 UTC", + "2021-02-06 00:00:00 +0000 UTC", + "2021-02-07 00:00:00 +0000 UTC", + "2021-02-08 00:00:00 +0000 UTC", + "2021-02-09 00:00:00 +0000 UTC", + "2021-02-10 00:00:00 +0000 UTC", + "2021-02-11 00:00:00 +0000 UTC", + "2021-02-12 00:00:00 +0000 UTC", + "2021-02-13 00:00:00 +0000 UTC", + "2021-02-14 00:00:00 +0000 UTC", + "2021-02-15 00:00:00 +0000 UTC", + "2021-02-16 00:00:00 +0000 UTC", + "2021-02-17 00:00:00 +0000 UTC", + "2021-02-18 00:00:00 +0000 UTC", + "2021-02-19 00:00:00 +0000 UTC", + "2021-02-20 00:00:00 +0000 UTC", + "2021-02-21 00:00:00 +0000 UTC", + "2021-02-22 00:00:00 +0000 UTC", + "2021-02-23 00:00:00 +0000 UTC", + "2021-02-24 00:00:00 +0000 UTC", + "2021-02-25 00:00:00 +0000 UTC", + "2021-02-26 00:00:00 +0000 UTC", + "2021-02-27 00:00:00 +0000 UTC", + "2021-02-28 00:00:00 +0000 UTC", + "2021-03-01 00:00:00 +0000 UTC", + "2021-03-02 00:00:00 +0000 UTC", + "2021-03-03 00:00:00 +0000 UTC", + "2021-03-04 00:00:00 +0000 UTC", + "2021-03-05 00:00:00 +0000 UTC", + "2021-03-06 00:00:00 +0000 UTC", + "2021-03-07 00:00:00 +0000 UTC", + "2021-03-08 00:00:00 +0000 UTC", + "2021-03-09 00:00:00 +0000 UTC", + "2021-03-10 00:00:00 +0000 UTC", + "2021-03-11 00:00:00 +0000 UTC", + "2021-03-12 00:00:00 +0000 UTC", + "2021-03-13 00:00:00 +0000 UTC", + "2021-03-14 00:00:00 +0000 UTC", + "2021-03-15 00:00:00 +0000 UTC", + "2021-03-16 00:00:00 +0000 UTC", + "2021-03-17 00:00:00 +0000 UTC", + "2021-03-18 00:00:00 +0000 UTC", + "2021-03-19 00:00:00 +0000 UTC", + "2021-03-20 00:00:00 +0000 UTC", + "2021-03-21 00:00:00 +0000 UTC", + "2021-03-22 00:00:00 +0000 UTC", + "2021-03-23 00:00:00 +0000 UTC", + "2021-03-24 00:00:00 +0000 UTC", + "2021-03-25 00:00:00 +0000 UTC", + "2021-03-26 00:00:00 +0000 UTC", + "2021-03-27 00:00:00 +0000 UTC", + "2021-03-28 00:00:00 +0000 UTC", + "2021-03-29 00:00:00 +0000 UTC", + "2021-03-30 00:00:00 +0000 UTC", + "2021-03-31 00:00:00 +0000 UTC", + "2021-04-01 00:00:00 +0000 UTC", + } + + checkDates(t, MonthRange, expectedMonthDates, dates) +}