From 9543d46442a25f1e7df3ffa3a28c384b678e3bd9 Mon Sep 17 00:00:00 2001 From: Luke Gehorsam Date: Tue, 29 Aug 2023 10:55:02 -0400 Subject: [PATCH 01/14] TestCalculateTournamentDeadlines --- server/core_tournament_test.go | 50 ++++++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 server/core_tournament_test.go diff --git a/server/core_tournament_test.go b/server/core_tournament_test.go new file mode 100644 index 0000000000..f4f3629340 --- /dev/null +++ b/server/core_tournament_test.go @@ -0,0 +1,50 @@ +// Copyright 2023 The Nakama Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package server + +import ( + "testing" + "time" + + "github.com/heroiclabs/nakama/v3/internal/cronexpr" + "github.com/stretchr/testify/require" +) + +func TestCalculateTournamentDeadlines(t *testing.T) { + mockSched, err := cronexpr.Parse("0 9 */14 * *") + if err != nil { + t.Fatal("Invalid cron schedule", err) + return + } + + const layout = "2 January 2006, 3:04:05 PM" + mockTimeString := "21 August 2023, 9:00:00 AM" + mockTime, err := time.Parse(layout, mockTimeString) + if err != nil { + t.Fatal("Invalid time", err) + return + } + var startTime int64 = 1683532800 // Mon May 08 2023 08:00:00 AM + var duration int64 = 1202400 // ~2 Weeks + startActiveUnix, endActiveUnix, _ := calculateTournamentDeadlines(startTime, 0, duration, mockSched, mockTime) + nextResetUnix := mockSched.Next(mockTime).Unix() + + // Start Time: Sunday, 15 August 2023, 9:00:00 AM + require.Equal(t, int64(1692090000), startActiveUnix, "Start active times should be equal.") + // End Active: Tues 29 August 2023 7:00:00 AM + require.Equal(t, int64(1693292400), endActiveUnix, "End active times should be equal.") + // Next Reset: Tues 29 August 2023 9:00:00 AM + require.Equal(t, int64(1693299600), nextResetUnix, "Next reset times should be equal.") +} From 1a217f530fdaedeff01534545e8b361c688db82d Mon Sep 17 00:00:00 2001 From: Luke Gehorsam Date: Tue, 29 Aug 2023 16:00:20 -0400 Subject: [PATCH 02/14] update test and add comments --- server/core_tournament.go | 8 +++++++- server/core_tournament_test.go | 10 +++++----- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/server/core_tournament.go b/server/core_tournament.go index adebdaff7d..71f0f43488 100644 --- a/server/core_tournament.go +++ b/server/core_tournament.go @@ -739,8 +739,14 @@ func calculateTournamentDeadlines(startTime, endTime, duration int64, resetSched schedules := resetSchedule.NextN(t, 2) // Roll time back a safe amount, then scan forward looking for the current start active. - startActiveUnix := tUnix - ((schedules[1].UTC().Unix() - schedules[0].UTC().Unix()) * 2) + // We do this by taking the CRON interval and then stepping back twice from the supplied by the interval. + cronInterval := schedules[1].UTC().Unix() - schedules[0].UTC().Unix() + startActiveUnix := tUnix - cronInterval*2 for { + // Now that we've rolled back, get the next start active predicted by the CRON schedule. + // In at least the first iteration, we expect this to be smaller than the supplied time. + // If it's smaller, clamp the startActiveUnix to the + // predicted startActive. s := resetSchedule.Next(time.Unix(startActiveUnix, 0).UTC()).UTC().Unix() if s < tUnix { startActiveUnix = s diff --git a/server/core_tournament_test.go b/server/core_tournament_test.go index f4f3629340..08b1952d01 100644 --- a/server/core_tournament_test.go +++ b/server/core_tournament_test.go @@ -30,16 +30,16 @@ func TestCalculateTournamentDeadlines(t *testing.T) { } const layout = "2 January 2006, 3:04:05 PM" - mockTimeString := "21 August 2023, 9:00:00 AM" - mockTime, err := time.Parse(layout, mockTimeString) + mockNowString := "21 August 2023, 11:00:00 AM" + mockNow, err := time.Parse(layout, mockNowString) if err != nil { t.Fatal("Invalid time", err) return } - var startTime int64 = 1683532800 // Mon May 08 2023 08:00:00 AM + var startTime int64 = 1692090000 // Sunday, 15 August 2023, 9:00:00 AM var duration int64 = 1202400 // ~2 Weeks - startActiveUnix, endActiveUnix, _ := calculateTournamentDeadlines(startTime, 0, duration, mockSched, mockTime) - nextResetUnix := mockSched.Next(mockTime).Unix() + startActiveUnix, endActiveUnix, _ := calculateTournamentDeadlines(startTime, 0, duration, mockSched, mockNow) + nextResetUnix := mockSched.Next(mockNow).Unix() // Start Time: Sunday, 15 August 2023, 9:00:00 AM require.Equal(t, int64(1692090000), startActiveUnix, "Start active times should be equal.") From 3415eabe8951181fd7e711298f7428b56d3cf14d Mon Sep 17 00:00:00 2001 From: Luke Gehorsam Date: Mon, 11 Sep 2023 10:48:43 -0400 Subject: [PATCH 03/14] straight port of Last() from https://github.com/gorhill/cronexpr/pull/28 --- internal/cronexpr/cronexpr.go | 81 +++++ internal/cronexpr/cronexpr_next.go | 198 +++++++++++- internal/cronexpr/cronexpr_test.go | 500 +++++++++++++++++++++++++++++ 3 files changed, 778 insertions(+), 1 deletion(-) diff --git a/internal/cronexpr/cronexpr.go b/internal/cronexpr/cronexpr.go index 41534bd0ec..3beecd2cff 100644 --- a/internal/cronexpr/cronexpr.go +++ b/internal/cronexpr/cronexpr.go @@ -233,6 +233,87 @@ func (expr *Expression) Next(fromTime time.Time) time.Time { return expr.nextSecond(fromTime, actualDaysOfMonthList) } +// Last returns the closest time instant immediately before `fromTime` which +// matches the cron expression `expr`. +// +// The `time.Location` of the returned time instant is the same as that of +// `fromTime`. +// +// The zero value of time.Time is returned if no matching time instant exists +// or if a `fromTime` is itself a zero value. +func (expr *Expression) Last(fromTime time.Time) time.Time { + // Special case + if fromTime.IsZero() { + return fromTime + } + + // year + v := fromTime.Year() + i := sort.SearchInts(expr.yearList, v) + if i == 0 && v != expr.yearList[i] { + return time.Time{} + } + if i == len(expr.yearList) || v != expr.yearList[i] { + return expr.lastYear(fromTime, false) + } + // month + v = int(fromTime.Month()) + i = sort.SearchInts(expr.monthList, v) + if i == 0 && v != expr.monthList[i] { + return expr.lastYear(fromTime, true) + } + if i == len(expr.monthList) || v != expr.monthList[i] { + return expr.lastMonth(fromTime, false) + } + + expr.actualDaysOfMonthList = expr.calculateActualDaysOfMonth(fromTime.Year(), int(fromTime.Month())) + if len(expr.actualDaysOfMonthList) == 0 { + return expr.lastMonth(fromTime, true) + } + + // day of month + v = fromTime.Day() + i = sort.SearchInts(expr.actualDaysOfMonthList, v) + if i == 0 && v != expr.actualDaysOfMonthList[i] { + return expr.lastMonth(fromTime, true) + } + if i == len(expr.actualDaysOfMonthList) || v != expr.actualDaysOfMonthList[i] { + return expr.lastActualDayOfMonth(fromTime, false) + } + // hour + v = fromTime.Hour() + i = sort.SearchInts(expr.hourList, v) + if i == 0 && v != expr.hourList[i] { + return expr.lastActualDayOfMonth(fromTime, true) + } + if i == len(expr.hourList) || v != expr.hourList[i] { + return expr.lastHour(fromTime, false) + } + + // minute + v = fromTime.Minute() + i = sort.SearchInts(expr.minuteList, v) + if i == 0 && v != expr.minuteList[i] { + return expr.lastHour(fromTime, true) + } + if i == len(expr.minuteList) || v != expr.minuteList[i] { + return expr.lastMinute(fromTime, false) + } + // second + v = fromTime.Second() + i = sort.SearchInts(expr.secondList, v) + if i == len(expr.secondList) { + return expr.lastMinute(fromTime, true) + } + + // If we reach this point, there is nothing better to do + // than to move to the next second + + return expr.lastSecond(fromTime) +} + +/******************************************************************************/ + /******************************************************************************/ // NextN returns a slice of `n` closest time instants immediately following diff --git a/internal/cronexpr/cronexpr_next.go b/internal/cronexpr/cronexpr_next.go index 2da9286147..5a51ee97fc 100644 --- a/internal/cronexpr/cronexpr_next.go +++ b/internal/cronexpr/cronexpr_next.go @@ -64,6 +64,49 @@ func (expr *Expression) nextYear(t time.Time) time.Time { t.Location()) } +func (expr *Expression) lastYear(t time.Time, acc bool) time.Time { + // candidate year + v := t.Year() + if acc { + v-- + } + i := sort.SearchInts(expr.yearList, v) + var year int + if i < len(expr.yearList) && v == expr.yearList[i] { + year = expr.yearList[i] + } else if i == 0 { + return time.Time{} + } else { + year = expr.yearList[i-1] + } + // Year changed, need to recalculate actual days of month + expr.actualDaysOfMonthList = expr.calculateActualDaysOfMonth( + year, + expr.monthList[len(expr.monthList)-1]) + + if len(expr.actualDaysOfMonthList) == 0 { + return expr.lastMonth(time.Date( + year, + time.Month(expr.monthList[len(expr.monthList)-1]), + 1, + expr.hourList[len(expr.hourList)-1], + expr.minuteList[len(expr.minuteList)-1], + expr.secondList[len(expr.secondList)-1], + 0, + t.Location()), true) + } + return time.Date( + year, + time.Month(expr.monthList[len(expr.monthList)-1]), + expr.actualDaysOfMonthList[len(expr.actualDaysOfMonthList)-1], + expr.hourList[len(expr.hourList)-1], + expr.minuteList[len(expr.minuteList)-1], + expr.secondList[len(expr.secondList)-1], + 0, + t.Location()) +} + +/******************************************************************************/ /******************************************************************************/ func (expr *Expression) nextMonth(t time.Time) time.Time { @@ -98,6 +141,48 @@ func (expr *Expression) nextMonth(t time.Time) time.Time { t.Location()) } +func (expr *Expression) lastMonth(t time.Time, acc bool) time.Time { + // candidate month + v := int(t.Month()) + if acc { + v-- + } + i := sort.SearchInts(expr.monthList, v) + + var month int + if i < len(expr.monthList) && v == expr.monthList[i] { + month = expr.monthList[i] + } else if i == 0 { + return expr.lastYear(t, true) + } else { + month = expr.monthList[i-1] + } + + // Month changed, need to recalculate actual days of month + expr.actualDaysOfMonthList = expr.calculateActualDaysOfMonth(t.Year(), month) + if len(expr.actualDaysOfMonthList) == 0 { + return expr.lastMonth(time.Date( + t.Year(), + time.Month(month), + 1, + expr.hourList[len(expr.hourList)-1], + expr.minuteList[len(expr.minuteList)-1], + expr.secondList[len(expr.secondList)-1], + 0, + t.Location()), true) + } + + return time.Date( + t.Year(), + time.Month(month), + expr.actualDaysOfMonthList[len(expr.actualDaysOfMonthList)-1], + expr.hourList[len(expr.hourList)-1], + expr.minuteList[len(expr.minuteList)-1], + expr.secondList[len(expr.secondList)-1], + 0, + t.Location()) +} + /******************************************************************************/ func (expr *Expression) nextDayOfMonth(t time.Time, actualDaysOfMonthList []int) time.Time { @@ -119,6 +204,34 @@ func (expr *Expression) nextDayOfMonth(t time.Time, actualDaysOfMonthList []int) t.Location()) } +func (expr *Expression) lastActualDayOfMonth(t time.Time, acc bool) time.Time { + // candidate day of month + v := t.Day() + if acc { + v-- + } + i := sort.SearchInts(expr.actualDaysOfMonthList, v) + + var day int + if i < len(expr.actualDaysOfMonthList) && v == expr.actualDaysOfMonthList[i] { + day = expr.actualDaysOfMonthList[i] + } else if i == 0 { + return expr.lastMonth(t, true) + } else { + day = expr.actualDaysOfMonthList[i-1] + } + + return time.Date( + t.Year(), + t.Month(), + day, + expr.hourList[len(expr.hourList)-1], + expr.minuteList[len(expr.minuteList)-1], + expr.secondList[len(expr.secondList)-1], + 0, + t.Location()) +} + /******************************************************************************/ func (expr *Expression) nextHour(t time.Time, actualDaysOfMonthList []int) time.Time { @@ -126,7 +239,7 @@ func (expr *Expression) nextHour(t time.Time, actualDaysOfMonthList []int) time. // candidate hour i := sort.SearchInts(expr.hourList, t.Hour()+1) if i == len(expr.hourList) { - return expr.nextDayOfMonth(t, actualDaysOfMonthList) + return expr.nextActualDayOfMonth(t) } return time.Date( @@ -142,6 +255,38 @@ func (expr *Expression) nextHour(t time.Time, actualDaysOfMonthList []int) time. /******************************************************************************/ +func (expr *Expression) lastHour(t time.Time, acc bool) time.Time { + // candidate hour + v := t.Hour() + if acc { + v-- + } + i := sort.SearchInts(expr.hourList, v) + + var hour int + if i < len(expr.hourList) && v == expr.hourList[i] { + hour = expr.hourList[i] + } else if i == 0 { + return expr.lastActualDayOfMonth(t, true) + } else { + hour = expr.hourList[i-1] + } + + return time.Date( + t.Year(), + t.Month(), + t.Day(), + hour, + expr.minuteList[len(expr.minuteList)-1], + expr.secondList[len(expr.secondList)-1], + 0, + t.Location()) +} + +/******************************************************************************/ + +/******************************************************************************/ + func (expr *Expression) nextMinute(t time.Time, actualDaysOfMonthList []int) time.Time { // Find index at which item in list is greater or equal to // candidate minute @@ -163,6 +308,35 @@ func (expr *Expression) nextMinute(t time.Time, actualDaysOfMonthList []int) tim /******************************************************************************/ +func (expr *Expression) lastMinute(t time.Time, acc bool) time.Time { + // candidate minute + v := t.Minute() + if !acc { + v-- + } + i := sort.SearchInts(expr.minuteList, v) + var min int + if i < len(expr.minuteList) && v == expr.minuteList[i] { + min = expr.minuteList[i] + } else if i == 0 { + return expr.lastHour(t, true) + } else { + min = expr.minuteList[i-1] + } + + return time.Date( + t.Year(), + t.Month(), + t.Day(), + t.Hour(), + min, + expr.secondList[len(expr.secondList)-1], + 0, + t.Location()) +} + +/******************************************************************************/ + func (expr *Expression) nextSecond(t time.Time, actualDaysOfMonthList []int) time.Time { // nextSecond() assumes all other fields are exactly matched // to the cron expression @@ -185,6 +359,28 @@ func (expr *Expression) nextSecond(t time.Time, actualDaysOfMonthList []int) tim t.Location()) } +/******************************************************************************/ +// lastSecond() assumes all other fields are exactly matched +// to the cron expression +func (expr *Expression) lastSecond(t time.Time) time.Time { + // candidate second + v := t.Second() - 1 + i := sort.SearchInts(expr.secondList, v) + if i == len(expr.secondList) || expr.secondList[i] != v { + return expr.lastMinute(t, false) + } + + return time.Date( + t.Year(), + t.Month(), + t.Day(), + t.Hour(), + t.Minute(), + expr.secondList[i], + 0, + t.Location()) +} + /******************************************************************************/ func (expr *Expression) calculateActualDaysOfMonth(year, month int) []int { diff --git a/internal/cronexpr/cronexpr_test.go b/internal/cronexpr/cronexpr_test.go index 38bd8c0022..0f8a257d7e 100644 --- a/internal/cronexpr/cronexpr_test.go +++ b/internal/cronexpr/cronexpr_test.go @@ -194,9 +194,509 @@ var crontests = []crontest{ }, }, + var cronbackwardtests = []crontest{ + // Seconds + { + "* * * * * * *", + "2006-01-02 15:04:05", + []crontimes{ + {"2013-01-01 00:00:01", "2013-01-01 00:00:00"}, + {"2013-01-01 00:01:00", "2013-01-01 00:00:59"}, + {"2013-01-01 01:00:00", "2013-01-01 00:59:59"}, + {"2013-01-02 00:00:00", "2013-01-01 23:59:59"}, + {"2013-03-01 00:00:00", "2013-02-28 23:59:59"}, + {"2016-02-29 00:00:00", "2016-02-28 23:59:59"}, + {"2013-01-01 00:00:00", "2012-12-31 23:59:59"}, + }, + }, + + // every 5 Second + { + "*/5 * * * * * *", + "2006-01-02 15:04:05", + []crontimes{ + {"2013-01-01 00:00:06", "2013-01-01 00:00:05"}, + {"2013-01-01 00:01:00", "2013-01-01 00:00:55"}, + {"2013-01-01 01:00:00", "2013-01-01 00:59:55"}, + {"2013-01-02 00:00:00", "2013-01-01 23:59:55"}, + {"2013-03-01 00:00:00", "2013-02-28 23:59:55"}, + {"2016-02-29 00:00:00", "2016-02-28 23:59:55"}, + {"2013-01-01 00:00:00", "2012-12-31 23:59:55"}, + }, + }, + + // Minutes + { + "* * * * *", + "2006-01-02 15:04:05", + []crontimes{ + {"2013-01-01 00:00:58", "2013-01-01 00:00:00"}, + {"2013-01-01 00:01:00", "2013-01-01 00:00:00"}, + {"2013-01-01 01:00:00", "2013-01-01 00:59:00"}, + {"2013-01-02 00:00:00", "2013-01-01 23:59:00"}, + {"2013-03-01 00:00:00", "2013-02-28 23:59:00"}, + {"2016-02-29 00:00:00", "2016-02-28 23:59:00"}, + {"2013-01-01 00:00:00", "2012-12-31 23:59:00"}, + }, + }, + + // // Minutes with interval + { + "17-43/5 * * * *", + "2006-01-02 15:04:05", + []crontimes{ + {"2013-01-01 00:17:01", "2013-01-01 00:17:00"}, + {"2013-01-01 00:33:00", "2013-01-01 00:32:00"}, + {"2013-01-01 01:00:00", "2013-01-01 00:42:00"}, + {"2013-01-02 00:01:00", "2013-01-01 23:42:00"}, + {"2013-03-01 00:01:00", "2013-02-28 23:42:00"}, + {"2016-02-29 00:01:00", "2016-02-28 23:42:00"}, + {"2013-01-01 00:01:00", "2012-12-31 23:42:00"}, + }, + }, + + // Minutes interval, list + { + "15-30/4,55 * * * *", + "2006-01-02 15:04:05", + []crontimes{ + {"2013-01-01 00:16:00", "2013-01-01 00:15:00"}, + {"2013-01-01 00:18:59", "2013-01-01 00:15:00"}, + {"2013-01-01 00:19:00", "2013-01-01 00:15:00"}, + {"2013-01-01 00:56:00", "2013-01-01 00:55:00"}, + {"2013-01-01 01:15:00", "2013-01-01 00:55:00"}, + {"2013-01-02 00:15:00", "2013-01-01 23:55:00"}, + {"2013-03-01 00:15:00", "2013-02-28 23:55:00"}, + {"2016-02-29 00:15:00", "2016-02-28 23:55:00"}, + {"2012-12-31 23:54:00", "2012-12-31 23:27:00"}, + {"2013-01-01 00:15:00", "2012-12-31 23:55:00"}, + }, + }, + + // Hour interval + { + "* 9-19/3 * * *", + "2006-01-02 15:04:05", + []crontimes{ + {"2017-01-01 00:10:00", "2016-12-31 18:59:00"}, + {"2017-02-01 00:10:00", "2017-01-31 18:59:00"}, + {"2017-02-12 00:10:00", "2017-02-11 18:59:00"}, + {"2017-02-12 19:10:00", "2017-02-12 18:59:00"}, + {"2017-02-12 12:15:00", "2017-02-12 12:14:00"}, + {"2017-02-12 13:00:00", "2017-02-12 12:59:00"}, + {"2017-02-12 11:00:00", "2017-02-12 09:59:00"}, + }, + }, + + // Hour interval, list + { + "5 12-21/3,23 * * *", + "2006-01-02 15:04:05", + []crontimes{ + {"2017-01-01 00:10:00", "2016-12-31 23:05:00"}, + {"2017-02-01 00:10:00", "2017-01-31 23:05:00"}, + {"2017-02-12 00:10:00", "2017-02-11 23:05:00"}, + {"2017-02-12 19:10:00", "2017-02-12 18:05:00"}, + {"2017-02-12 12:15:00", "2017-02-12 12:05:00"}, + {"2017-02-12 22:00:00", "2017-02-12 21:05:00"}, + }, + }, + + // Day interval + { + "5 10-17 12-25/4 * *", + "2006-01-02 15:04:05", + []crontimes{ + {"2017-01-01 00:10:00", "2016-12-24 17:05:00"}, + {"2017-02-01 10:10:00", "2017-01-24 17:05:00"}, + {"2017-02-27 13:10:00", "2017-02-24 17:05:00"}, + {"2017-02-23 13:10:00", "2017-02-20 17:05:00"}, + {"2017-02-11 13:10:00", "2017-01-24 17:05:00"}, + }, + }, + + // Day interval, list + { + "* * 12-15,20-22 * *", + "2006-01-02 15:04:05", + []crontimes{ + {"2017-01-01 00:20:00", "2016-12-22 23:59:00"}, + {"2017-02-01 10:30:00", "2017-01-22 23:59:00"}, + {"2017-02-27 13:40:00", "2017-02-22 23:59:00"}, + {"2017-02-17 16:10:00", "2017-02-15 23:59:00"}, + {"2017-02-11 13:10:00", "2017-01-22 23:59:00"}, + }, + }, + + // Month + { + "5 10 1 4-6 *", + "2006-01-02 15:04:05", + []crontimes{ + {"2017-01-01 00:10:00", "2016-06-01 10:05:00"}, + {"2017-07-01 10:01:00", "2017-06-01 10:05:00"}, + {"2017-06-03 00:10:00", "2017-06-01 10:05:00"}, + }, + }, + + // Month + { + "0 0 0 12 * * 2017-2020", + "2006-01-02 15:04:05", + []crontimes{ + {"2017-12-11 00:10:00", "2017-11-12 00:00:00"}, + {"2023-01-11 00:10:00", "2020-12-12 00:00:00"}, + {"2021-01-11 00:10:00", "2020-12-12 00:00:00"}, + }, + }, + + // Days of week + { + "0 0 * * MON", + "Mon 2006-01-02 15:04", + []crontimes{ + {"2013-01-10 00:00:00", "Mon 2013-01-07 00:00"}, + {"2017-08-07 00:00:00", "Mon 2017-07-31 00:00"}, + {"2017-01-01 00:30:00", "Mon 2016-12-26 00:00"}, + }, + }, + { + "0 0 * * friday", + "Mon 2006-01-02 15:04", + []crontimes{ + {"2017-08-14 00:00:00", "Fri 2017-08-11 00:00"}, + {"2017-08-02 00:00:00", "Fri 2017-07-28 00:00"}, + {"2018-01-02 00:30:00", "Fri 2017-12-29 00:00"}, + }, + }, + { + "0 0 * * 6,7", + "Mon 2006-01-02 15:04", + []crontimes{ + {"2017-09-04 00:00:00", "Sun 2017-09-03 00:00"}, + {"2017-08-02 00:00:00", "Sun 2017-07-30 00:00"}, + {"2018-01-03 00:30:00", "Sun 2017-12-31 00:00"}, + }, + }, + + // // Specific days of week + { + "0 0 * * 6#5", + "Mon 2006-01-02 15:04", + []crontimes{ + {"2017-03-03 00:00:00", "Sat 2016-12-31 00:00"}, + }, + }, + + // // Work day of month + { + "0 0 18W * *", + "Mon 2006-01-02 15:04", + []crontimes{ + {"2017-12-02 00:00:00", "Fri 2017-11-17 00:00"}, + {"2017-10-12 00:00:00", "Mon 2017-09-18 00:00"}, + {"2017-08-30 00:00:00", "Fri 2017-08-18 00:00"}, + {"2017-06-21 00:00:00", "Mon 2017-06-19 00:00"}, + }, + }, + + // // Work day of month -- end of month + { + "0 0 30W * *", + "Mon 2006-01-02 15:04", + []crontimes{ + {"2017-03-02 00:00:00", "Mon 2017-01-30 00:00"}, + {"2017-06-02 00:00:00", "Tue 2017-05-30 00:00"}, + {"2017-08-02 00:00:00", "Mon 2017-07-31 00:00"}, + {"2017-11-02 00:00:00", "Mon 2017-10-30 00:00"}, + }, + }, + + // // Last day of month + { + "0 0 L * *", + "Mon 2006-01-02 15:04", + []crontimes{ + {"2017-01-02 00:00:00", "Sat 2016-12-31 00:00"}, + {"2017-02-01 00:00:00", "Tue 2017-01-31 00:00"}, + {"2017-03-01 00:00:00", "Tue 2017-02-28 00:00"}, + {"2016-03-15 00:00:00", "Mon 2016-02-29 00:00"}, + }, + }, + + // // Last work day of month + { + "0 0 LW * *", + "Mon 2006-01-02 15:04", + []crontimes{ + {"2016-03-02 00:00:00", "Mon 2016-02-29 00:00"}, + {"2017-11-02 00:00:00", "Tue 2017-10-31 00:00"}, + {"2017-08-15 00:00:00", "Mon 2017-07-31 00:00"}, + }, + }, +} // TODO: more tests } +var cronbackwardtests = []crontest{ + // Seconds + { + "* * * * * * *", + "2006-01-02 15:04:05", + []crontimes{ + {"2013-01-01 00:00:01", "2013-01-01 00:00:00"}, + {"2013-01-01 00:01:00", "2013-01-01 00:00:59"}, + {"2013-01-01 01:00:00", "2013-01-01 00:59:59"}, + {"2013-01-02 00:00:00", "2013-01-01 23:59:59"}, + {"2013-03-01 00:00:00", "2013-02-28 23:59:59"}, + {"2016-02-29 00:00:00", "2016-02-28 23:59:59"}, + {"2013-01-01 00:00:00", "2012-12-31 23:59:59"}, + }, + }, + + // every 5 Second + { + "*/5 * * * * * *", + "2006-01-02 15:04:05", + []crontimes{ + {"2013-01-01 00:00:06", "2013-01-01 00:00:05"}, + {"2013-01-01 00:01:00", "2013-01-01 00:00:55"}, + {"2013-01-01 01:00:00", "2013-01-01 00:59:55"}, + {"2013-01-02 00:00:00", "2013-01-01 23:59:55"}, + {"2013-03-01 00:00:00", "2013-02-28 23:59:55"}, + {"2016-02-29 00:00:00", "2016-02-28 23:59:55"}, + {"2013-01-01 00:00:00", "2012-12-31 23:59:55"}, + }, + }, + + // Minutes + { + "* * * * *", + "2006-01-02 15:04:05", + []crontimes{ + {"2013-01-01 00:00:58", "2013-01-01 00:00:00"}, + {"2013-01-01 00:01:00", "2013-01-01 00:00:00"}, + {"2013-01-01 01:00:00", "2013-01-01 00:59:00"}, + {"2013-01-02 00:00:00", "2013-01-01 23:59:00"}, + {"2013-03-01 00:00:00", "2013-02-28 23:59:00"}, + {"2016-02-29 00:00:00", "2016-02-28 23:59:00"}, + {"2013-01-01 00:00:00", "2012-12-31 23:59:00"}, + }, + }, + + // // Minutes with interval + { + "17-43/5 * * * *", + "2006-01-02 15:04:05", + []crontimes{ + {"2013-01-01 00:17:01", "2013-01-01 00:17:00"}, + {"2013-01-01 00:33:00", "2013-01-01 00:32:00"}, + {"2013-01-01 01:00:00", "2013-01-01 00:42:00"}, + {"2013-01-02 00:01:00", "2013-01-01 23:42:00"}, + {"2013-03-01 00:01:00", "2013-02-28 23:42:00"}, + {"2016-02-29 00:01:00", "2016-02-28 23:42:00"}, + {"2013-01-01 00:01:00", "2012-12-31 23:42:00"}, + }, + }, + + // Minutes interval, list + { + "15-30/4,55 * * * *", + "2006-01-02 15:04:05", + []crontimes{ + {"2013-01-01 00:16:00", "2013-01-01 00:15:00"}, + {"2013-01-01 00:18:59", "2013-01-01 00:15:00"}, + {"2013-01-01 00:19:00", "2013-01-01 00:15:00"}, + {"2013-01-01 00:56:00", "2013-01-01 00:55:00"}, + {"2013-01-01 01:15:00", "2013-01-01 00:55:00"}, + {"2013-01-02 00:15:00", "2013-01-01 23:55:00"}, + {"2013-03-01 00:15:00", "2013-02-28 23:55:00"}, + {"2016-02-29 00:15:00", "2016-02-28 23:55:00"}, + {"2012-12-31 23:54:00", "2012-12-31 23:27:00"}, + {"2013-01-01 00:15:00", "2012-12-31 23:55:00"}, + }, + }, + + // Hour interval + { + "* 9-19/3 * * *", + "2006-01-02 15:04:05", + []crontimes{ + {"2017-01-01 00:10:00", "2016-12-31 18:59:00"}, + {"2017-02-01 00:10:00", "2017-01-31 18:59:00"}, + {"2017-02-12 00:10:00", "2017-02-11 18:59:00"}, + {"2017-02-12 19:10:00", "2017-02-12 18:59:00"}, + {"2017-02-12 12:15:00", "2017-02-12 12:14:00"}, + {"2017-02-12 13:00:00", "2017-02-12 12:59:00"}, + {"2017-02-12 11:00:00", "2017-02-12 09:59:00"}, + }, + }, + + // Hour interval, list + { + "5 12-21/3,23 * * *", + "2006-01-02 15:04:05", + []crontimes{ + {"2017-01-01 00:10:00", "2016-12-31 23:05:00"}, + {"2017-02-01 00:10:00", "2017-01-31 23:05:00"}, + {"2017-02-12 00:10:00", "2017-02-11 23:05:00"}, + {"2017-02-12 19:10:00", "2017-02-12 18:05:00"}, + {"2017-02-12 12:15:00", "2017-02-12 12:05:00"}, + {"2017-02-12 22:00:00", "2017-02-12 21:05:00"}, + }, + }, + + // Day interval + { + "5 10-17 12-25/4 * *", + "2006-01-02 15:04:05", + []crontimes{ + {"2017-01-01 00:10:00", "2016-12-24 17:05:00"}, + {"2017-02-01 10:10:00", "2017-01-24 17:05:00"}, + {"2017-02-27 13:10:00", "2017-02-24 17:05:00"}, + {"2017-02-23 13:10:00", "2017-02-20 17:05:00"}, + {"2017-02-11 13:10:00", "2017-01-24 17:05:00"}, + }, + }, + + // Day interval, list + { + "* * 12-15,20-22 * *", + "2006-01-02 15:04:05", + []crontimes{ + {"2017-01-01 00:20:00", "2016-12-22 23:59:00"}, + {"2017-02-01 10:30:00", "2017-01-22 23:59:00"}, + {"2017-02-27 13:40:00", "2017-02-22 23:59:00"}, + {"2017-02-17 16:10:00", "2017-02-15 23:59:00"}, + {"2017-02-11 13:10:00", "2017-01-22 23:59:00"}, + }, + }, + + // Month + { + "5 10 1 4-6 *", + "2006-01-02 15:04:05", + []crontimes{ + {"2017-01-01 00:10:00", "2016-06-01 10:05:00"}, + {"2017-07-01 10:01:00", "2017-06-01 10:05:00"}, + {"2017-06-03 00:10:00", "2017-06-01 10:05:00"}, + }, + }, + + // Month + { + "0 0 0 12 * * 2017-2020", + "2006-01-02 15:04:05", + []crontimes{ + {"2017-12-11 00:10:00", "2017-11-12 00:00:00"}, + {"2023-01-11 00:10:00", "2020-12-12 00:00:00"}, + {"2021-01-11 00:10:00", "2020-12-12 00:00:00"}, + }, + }, + + // Days of week + { + "0 0 * * MON", + "Mon 2006-01-02 15:04", + []crontimes{ + {"2013-01-10 00:00:00", "Mon 2013-01-07 00:00"}, + {"2017-08-07 00:00:00", "Mon 2017-07-31 00:00"}, + {"2017-01-01 00:30:00", "Mon 2016-12-26 00:00"}, + }, + }, + { + "0 0 * * friday", + "Mon 2006-01-02 15:04", + []crontimes{ + {"2017-08-14 00:00:00", "Fri 2017-08-11 00:00"}, + {"2017-08-02 00:00:00", "Fri 2017-07-28 00:00"}, + {"2018-01-02 00:30:00", "Fri 2017-12-29 00:00"}, + }, + }, + { + "0 0 * * 6,7", + "Mon 2006-01-02 15:04", + []crontimes{ + {"2017-09-04 00:00:00", "Sun 2017-09-03 00:00"}, + {"2017-08-02 00:00:00", "Sun 2017-07-30 00:00"}, + {"2018-01-03 00:30:00", "Sun 2017-12-31 00:00"}, + }, + }, + + // // Specific days of week + { + "0 0 * * 6#5", + "Mon 2006-01-02 15:04", + []crontimes{ + {"2017-03-03 00:00:00", "Sat 2016-12-31 00:00"}, + }, + }, + + // // Work day of month + { + "0 0 18W * *", + "Mon 2006-01-02 15:04", + []crontimes{ + {"2017-12-02 00:00:00", "Fri 2017-11-17 00:00"}, + {"2017-10-12 00:00:00", "Mon 2017-09-18 00:00"}, + {"2017-08-30 00:00:00", "Fri 2017-08-18 00:00"}, + {"2017-06-21 00:00:00", "Mon 2017-06-19 00:00"}, + }, + }, + + // // Work day of month -- end of month + { + "0 0 30W * *", + "Mon 2006-01-02 15:04", + []crontimes{ + {"2017-03-02 00:00:00", "Mon 2017-01-30 00:00"}, + {"2017-06-02 00:00:00", "Tue 2017-05-30 00:00"}, + {"2017-08-02 00:00:00", "Mon 2017-07-31 00:00"}, + {"2017-11-02 00:00:00", "Mon 2017-10-30 00:00"}, + }, + }, + + // // Last day of month + { + "0 0 L * *", + "Mon 2006-01-02 15:04", + []crontimes{ + {"2017-01-02 00:00:00", "Sat 2016-12-31 00:00"}, + {"2017-02-01 00:00:00", "Tue 2017-01-31 00:00"}, + {"2017-03-01 00:00:00", "Tue 2017-02-28 00:00"}, + {"2016-03-15 00:00:00", "Mon 2016-02-29 00:00"}, + }, + }, + + // // Last work day of month + { + "0 0 LW * *", + "Mon 2006-01-02 15:04", + []crontimes{ + {"2016-03-02 00:00:00", "Mon 2016-02-29 00:00"}, + {"2017-11-02 00:00:00", "Tue 2017-10-31 00:00"}, + {"2017-08-15 00:00:00", "Mon 2017-07-31 00:00"}, + }, + }, +} + +func TestBackwardExpressions(t *testing.T) { + for _, test := range cronbackwardtests { + for _, times := range test.times { + from, _ := time.Parse("2006-01-02 15:04:05", times.from) + expr, err := cronexpr.Parse(test.expr) + if err != nil { + t.Errorf(`cronexpr.Parse("%s") returned "%s"`, test.expr, err.Error()) + } + last := expr.Last(from) + laststr := last.Format(test.layout) + if laststr != times.next { + t.Errorf(`("%s").Last("%s") = "%s", got "%s"`, test.expr, times.from, times.next, laststr) + } + } + } +} + func TestExpressions(t *testing.T) { for _, test := range crontests { for _, times := range test.times { From dfb0cf6297bfde70ceef170ed2b5ca375f9e515d Mon Sep 17 00:00:00 2001 From: Luke Gehorsam Date: Mon, 11 Sep 2023 10:55:47 -0400 Subject: [PATCH 04/14] add nextactualdayofmonth and reformat cron test backwards --- internal/cronexpr/cronexpr_next.go | 22 +++ internal/cronexpr/cronexpr_test.go | 246 +---------------------------- 2 files changed, 23 insertions(+), 245 deletions(-) diff --git a/internal/cronexpr/cronexpr_next.go b/internal/cronexpr/cronexpr_next.go index 5a51ee97fc..bba5b46b52 100644 --- a/internal/cronexpr/cronexpr_next.go +++ b/internal/cronexpr/cronexpr_next.go @@ -204,6 +204,28 @@ func (expr *Expression) nextDayOfMonth(t time.Time, actualDaysOfMonthList []int) t.Location()) } +/******************************************************************************/ + +func (expr *Expression) nextActualDayOfMonth(t time.Time) time.Time { + // Find index at which item in list is greater or equal to + // candidate day of month + i := sort.SearchInts(expr.actualDaysOfMonthList, t.Day()+1) + if i == len(expr.actualDaysOfMonthList) { + return expr.nextMonth(t) + } + return time.Date( + t.Year(), + t.Month(), + expr.actualDaysOfMonthList[i], + expr.hourList[0], + expr.minuteList[0], + expr.secondList[0], + 0, + t.Location()) +} + +/******************************************************************************/ + func (expr *Expression) lastActualDayOfMonth(t time.Time, acc bool) time.Time { // candidate day of month v := t.Day() diff --git a/internal/cronexpr/cronexpr_test.go b/internal/cronexpr/cronexpr_test.go index 0f8a257d7e..dceec4b092 100644 --- a/internal/cronexpr/cronexpr_test.go +++ b/internal/cronexpr/cronexpr_test.go @@ -193,251 +193,7 @@ var crontests = []crontest{ {"2014-08-15 00:00:00", "Fri 2014-08-29 00:00"}, }, }, - - var cronbackwardtests = []crontest{ - // Seconds - { - "* * * * * * *", - "2006-01-02 15:04:05", - []crontimes{ - {"2013-01-01 00:00:01", "2013-01-01 00:00:00"}, - {"2013-01-01 00:01:00", "2013-01-01 00:00:59"}, - {"2013-01-01 01:00:00", "2013-01-01 00:59:59"}, - {"2013-01-02 00:00:00", "2013-01-01 23:59:59"}, - {"2013-03-01 00:00:00", "2013-02-28 23:59:59"}, - {"2016-02-29 00:00:00", "2016-02-28 23:59:59"}, - {"2013-01-01 00:00:00", "2012-12-31 23:59:59"}, - }, - }, - - // every 5 Second - { - "*/5 * * * * * *", - "2006-01-02 15:04:05", - []crontimes{ - {"2013-01-01 00:00:06", "2013-01-01 00:00:05"}, - {"2013-01-01 00:01:00", "2013-01-01 00:00:55"}, - {"2013-01-01 01:00:00", "2013-01-01 00:59:55"}, - {"2013-01-02 00:00:00", "2013-01-01 23:59:55"}, - {"2013-03-01 00:00:00", "2013-02-28 23:59:55"}, - {"2016-02-29 00:00:00", "2016-02-28 23:59:55"}, - {"2013-01-01 00:00:00", "2012-12-31 23:59:55"}, - }, - }, - - // Minutes - { - "* * * * *", - "2006-01-02 15:04:05", - []crontimes{ - {"2013-01-01 00:00:58", "2013-01-01 00:00:00"}, - {"2013-01-01 00:01:00", "2013-01-01 00:00:00"}, - {"2013-01-01 01:00:00", "2013-01-01 00:59:00"}, - {"2013-01-02 00:00:00", "2013-01-01 23:59:00"}, - {"2013-03-01 00:00:00", "2013-02-28 23:59:00"}, - {"2016-02-29 00:00:00", "2016-02-28 23:59:00"}, - {"2013-01-01 00:00:00", "2012-12-31 23:59:00"}, - }, - }, - - // // Minutes with interval - { - "17-43/5 * * * *", - "2006-01-02 15:04:05", - []crontimes{ - {"2013-01-01 00:17:01", "2013-01-01 00:17:00"}, - {"2013-01-01 00:33:00", "2013-01-01 00:32:00"}, - {"2013-01-01 01:00:00", "2013-01-01 00:42:00"}, - {"2013-01-02 00:01:00", "2013-01-01 23:42:00"}, - {"2013-03-01 00:01:00", "2013-02-28 23:42:00"}, - {"2016-02-29 00:01:00", "2016-02-28 23:42:00"}, - {"2013-01-01 00:01:00", "2012-12-31 23:42:00"}, - }, - }, - - // Minutes interval, list - { - "15-30/4,55 * * * *", - "2006-01-02 15:04:05", - []crontimes{ - {"2013-01-01 00:16:00", "2013-01-01 00:15:00"}, - {"2013-01-01 00:18:59", "2013-01-01 00:15:00"}, - {"2013-01-01 00:19:00", "2013-01-01 00:15:00"}, - {"2013-01-01 00:56:00", "2013-01-01 00:55:00"}, - {"2013-01-01 01:15:00", "2013-01-01 00:55:00"}, - {"2013-01-02 00:15:00", "2013-01-01 23:55:00"}, - {"2013-03-01 00:15:00", "2013-02-28 23:55:00"}, - {"2016-02-29 00:15:00", "2016-02-28 23:55:00"}, - {"2012-12-31 23:54:00", "2012-12-31 23:27:00"}, - {"2013-01-01 00:15:00", "2012-12-31 23:55:00"}, - }, - }, - - // Hour interval - { - "* 9-19/3 * * *", - "2006-01-02 15:04:05", - []crontimes{ - {"2017-01-01 00:10:00", "2016-12-31 18:59:00"}, - {"2017-02-01 00:10:00", "2017-01-31 18:59:00"}, - {"2017-02-12 00:10:00", "2017-02-11 18:59:00"}, - {"2017-02-12 19:10:00", "2017-02-12 18:59:00"}, - {"2017-02-12 12:15:00", "2017-02-12 12:14:00"}, - {"2017-02-12 13:00:00", "2017-02-12 12:59:00"}, - {"2017-02-12 11:00:00", "2017-02-12 09:59:00"}, - }, - }, - - // Hour interval, list - { - "5 12-21/3,23 * * *", - "2006-01-02 15:04:05", - []crontimes{ - {"2017-01-01 00:10:00", "2016-12-31 23:05:00"}, - {"2017-02-01 00:10:00", "2017-01-31 23:05:00"}, - {"2017-02-12 00:10:00", "2017-02-11 23:05:00"}, - {"2017-02-12 19:10:00", "2017-02-12 18:05:00"}, - {"2017-02-12 12:15:00", "2017-02-12 12:05:00"}, - {"2017-02-12 22:00:00", "2017-02-12 21:05:00"}, - }, - }, - - // Day interval - { - "5 10-17 12-25/4 * *", - "2006-01-02 15:04:05", - []crontimes{ - {"2017-01-01 00:10:00", "2016-12-24 17:05:00"}, - {"2017-02-01 10:10:00", "2017-01-24 17:05:00"}, - {"2017-02-27 13:10:00", "2017-02-24 17:05:00"}, - {"2017-02-23 13:10:00", "2017-02-20 17:05:00"}, - {"2017-02-11 13:10:00", "2017-01-24 17:05:00"}, - }, - }, - - // Day interval, list - { - "* * 12-15,20-22 * *", - "2006-01-02 15:04:05", - []crontimes{ - {"2017-01-01 00:20:00", "2016-12-22 23:59:00"}, - {"2017-02-01 10:30:00", "2017-01-22 23:59:00"}, - {"2017-02-27 13:40:00", "2017-02-22 23:59:00"}, - {"2017-02-17 16:10:00", "2017-02-15 23:59:00"}, - {"2017-02-11 13:10:00", "2017-01-22 23:59:00"}, - }, - }, - - // Month - { - "5 10 1 4-6 *", - "2006-01-02 15:04:05", - []crontimes{ - {"2017-01-01 00:10:00", "2016-06-01 10:05:00"}, - {"2017-07-01 10:01:00", "2017-06-01 10:05:00"}, - {"2017-06-03 00:10:00", "2017-06-01 10:05:00"}, - }, - }, - - // Month - { - "0 0 0 12 * * 2017-2020", - "2006-01-02 15:04:05", - []crontimes{ - {"2017-12-11 00:10:00", "2017-11-12 00:00:00"}, - {"2023-01-11 00:10:00", "2020-12-12 00:00:00"}, - {"2021-01-11 00:10:00", "2020-12-12 00:00:00"}, - }, - }, - - // Days of week - { - "0 0 * * MON", - "Mon 2006-01-02 15:04", - []crontimes{ - {"2013-01-10 00:00:00", "Mon 2013-01-07 00:00"}, - {"2017-08-07 00:00:00", "Mon 2017-07-31 00:00"}, - {"2017-01-01 00:30:00", "Mon 2016-12-26 00:00"}, - }, - }, - { - "0 0 * * friday", - "Mon 2006-01-02 15:04", - []crontimes{ - {"2017-08-14 00:00:00", "Fri 2017-08-11 00:00"}, - {"2017-08-02 00:00:00", "Fri 2017-07-28 00:00"}, - {"2018-01-02 00:30:00", "Fri 2017-12-29 00:00"}, - }, - }, - { - "0 0 * * 6,7", - "Mon 2006-01-02 15:04", - []crontimes{ - {"2017-09-04 00:00:00", "Sun 2017-09-03 00:00"}, - {"2017-08-02 00:00:00", "Sun 2017-07-30 00:00"}, - {"2018-01-03 00:30:00", "Sun 2017-12-31 00:00"}, - }, - }, - - // // Specific days of week - { - "0 0 * * 6#5", - "Mon 2006-01-02 15:04", - []crontimes{ - {"2017-03-03 00:00:00", "Sat 2016-12-31 00:00"}, - }, - }, - - // // Work day of month - { - "0 0 18W * *", - "Mon 2006-01-02 15:04", - []crontimes{ - {"2017-12-02 00:00:00", "Fri 2017-11-17 00:00"}, - {"2017-10-12 00:00:00", "Mon 2017-09-18 00:00"}, - {"2017-08-30 00:00:00", "Fri 2017-08-18 00:00"}, - {"2017-06-21 00:00:00", "Mon 2017-06-19 00:00"}, - }, - }, - - // // Work day of month -- end of month - { - "0 0 30W * *", - "Mon 2006-01-02 15:04", - []crontimes{ - {"2017-03-02 00:00:00", "Mon 2017-01-30 00:00"}, - {"2017-06-02 00:00:00", "Tue 2017-05-30 00:00"}, - {"2017-08-02 00:00:00", "Mon 2017-07-31 00:00"}, - {"2017-11-02 00:00:00", "Mon 2017-10-30 00:00"}, - }, - }, - - // // Last day of month - { - "0 0 L * *", - "Mon 2006-01-02 15:04", - []crontimes{ - {"2017-01-02 00:00:00", "Sat 2016-12-31 00:00"}, - {"2017-02-01 00:00:00", "Tue 2017-01-31 00:00"}, - {"2017-03-01 00:00:00", "Tue 2017-02-28 00:00"}, - {"2016-03-15 00:00:00", "Mon 2016-02-29 00:00"}, - }, - }, - - // // Last work day of month - { - "0 0 LW * *", - "Mon 2006-01-02 15:04", - []crontimes{ - {"2016-03-02 00:00:00", "Mon 2016-02-29 00:00"}, - {"2017-11-02 00:00:00", "Tue 2017-10-31 00:00"}, - {"2017-08-15 00:00:00", "Mon 2017-07-31 00:00"}, - }, - }, } - // TODO: more tests -} - var cronbackwardtests = []crontest{ // Seconds { @@ -684,7 +440,7 @@ func TestBackwardExpressions(t *testing.T) { for _, test := range cronbackwardtests { for _, times := range test.times { from, _ := time.Parse("2006-01-02 15:04:05", times.from) - expr, err := cronexpr.Parse(test.expr) + expr, err := Parse(test.expr) if err != nil { t.Errorf(`cronexpr.Parse("%s") returned "%s"`, test.expr, err.Error()) } From 5943835d4ef48e643fde9d8f6905ed78d32c4974 Mon Sep 17 00:00:00 2001 From: Luke Gehorsam Date: Mon, 11 Sep 2023 11:34:19 -0400 Subject: [PATCH 05/14] add actualdaysofmonthlist --- internal/cronexpr/cronexpr.go | 1 + 1 file changed, 1 insertion(+) diff --git a/internal/cronexpr/cronexpr.go b/internal/cronexpr/cronexpr.go index 3beecd2cff..26189d5bdf 100644 --- a/internal/cronexpr/cronexpr.go +++ b/internal/cronexpr/cronexpr.go @@ -35,6 +35,7 @@ type Expression struct { lastDayOfMonth bool lastWorkdayOfMonth bool daysOfMonthRestricted bool + actualDaysOfMonthList []int monthList []int daysOfWeek map[int]bool specificWeekDaysOfWeek map[int]bool From 4f680d8806d5adec0c06aee8ee7a5c8389a4c495 Mon Sep 17 00:00:00 2001 From: Luke Gehorsam Date: Mon, 11 Sep 2023 11:37:07 -0400 Subject: [PATCH 06/14] use instance member version of actualdaysofmonthlist --- internal/cronexpr/cronexpr_next.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/internal/cronexpr/cronexpr_next.go b/internal/cronexpr/cronexpr_next.go index bba5b46b52..97a14ec4c5 100644 --- a/internal/cronexpr/cronexpr_next.go +++ b/internal/cronexpr/cronexpr_next.go @@ -41,8 +41,8 @@ func (expr *Expression) nextYear(t time.Time) time.Time { return time.Time{} } // Year changed, need to recalculate actual days of month - actualDaysOfMonthList := expr.calculateActualDaysOfMonth(expr.yearList[i], expr.monthList[0]) - if len(actualDaysOfMonthList) == 0 { + expr.actualDaysOfMonthList = expr.calculateActualDaysOfMonth(expr.yearList[i], expr.monthList[0]) + if len(expr.actualDaysOfMonthList) == 0 { return expr.nextMonth(time.Date( expr.yearList[i], time.Month(expr.monthList[0]), @@ -56,7 +56,7 @@ func (expr *Expression) nextYear(t time.Time) time.Time { return time.Date( expr.yearList[i], time.Month(expr.monthList[0]), - actualDaysOfMonthList[0], + expr.actualDaysOfMonthList[0], expr.hourList[0], expr.minuteList[0], expr.secondList[0], From bd328b5140f5000e3786a601606b4d9e42689cf5 Mon Sep 17 00:00:00 2001 From: Luke Gehorsam Date: Mon, 11 Sep 2023 11:41:06 -0400 Subject: [PATCH 07/14] remove nextactualdayofmonth --- internal/cronexpr/cronexpr_next.go | 24 +----------------------- 1 file changed, 1 insertion(+), 23 deletions(-) diff --git a/internal/cronexpr/cronexpr_next.go b/internal/cronexpr/cronexpr_next.go index 97a14ec4c5..6570358d49 100644 --- a/internal/cronexpr/cronexpr_next.go +++ b/internal/cronexpr/cronexpr_next.go @@ -204,28 +204,6 @@ func (expr *Expression) nextDayOfMonth(t time.Time, actualDaysOfMonthList []int) t.Location()) } -/******************************************************************************/ - -func (expr *Expression) nextActualDayOfMonth(t time.Time) time.Time { - // Find index at which item in list is greater or equal to - // candidate day of month - i := sort.SearchInts(expr.actualDaysOfMonthList, t.Day()+1) - if i == len(expr.actualDaysOfMonthList) { - return expr.nextMonth(t) - } - return time.Date( - t.Year(), - t.Month(), - expr.actualDaysOfMonthList[i], - expr.hourList[0], - expr.minuteList[0], - expr.secondList[0], - 0, - t.Location()) -} - -/******************************************************************************/ - func (expr *Expression) lastActualDayOfMonth(t time.Time, acc bool) time.Time { // candidate day of month v := t.Day() @@ -261,7 +239,7 @@ func (expr *Expression) nextHour(t time.Time, actualDaysOfMonthList []int) time. // candidate hour i := sort.SearchInts(expr.hourList, t.Hour()+1) if i == len(expr.hourList) { - return expr.nextActualDayOfMonth(t) + return expr.nextDayOfMonth(t, actualDaysOfMonthList) } return time.Date( From 110b01352afa8f00e43a14c7a5311b6c5976e476 Mon Sep 17 00:00:00 2001 From: Luke Gehorsam Date: Mon, 11 Sep 2023 11:49:05 -0400 Subject: [PATCH 08/14] use new Last() implementation when calculating tournament deadlines --- server/core_tournament.go | 25 +++---------------------- 1 file changed, 3 insertions(+), 22 deletions(-) diff --git a/server/core_tournament.go b/server/core_tournament.go index 71f0f43488..3fc11ed31c 100644 --- a/server/core_tournament.go +++ b/server/core_tournament.go @@ -737,28 +737,9 @@ func calculateTournamentDeadlines(startTime, endTime, duration int64, resetSched tUnix = t.UTC().Unix() } - schedules := resetSchedule.NextN(t, 2) - // Roll time back a safe amount, then scan forward looking for the current start active. - // We do this by taking the CRON interval and then stepping back twice from the supplied by the interval. - cronInterval := schedules[1].UTC().Unix() - schedules[0].UTC().Unix() - startActiveUnix := tUnix - cronInterval*2 - for { - // Now that we've rolled back, get the next start active predicted by the CRON schedule. - // In at least the first iteration, we expect this to be smaller than the supplied time. - // If it's smaller, clamp the startActiveUnix to the - // predicted startActive. - s := resetSchedule.Next(time.Unix(startActiveUnix, 0).UTC()).UTC().Unix() - if s < tUnix { - startActiveUnix = s - } else { - if s == tUnix { - startActiveUnix = s - } - break - } - } + startActiveUnix := resetSchedule.Last(t).UTC().Unix() endActiveUnix := startActiveUnix + duration - expiryUnix := schedules[0].UTC().Unix() + expiryUnix := resetSchedule.Next(t).UTC().Unix() if endActiveUnix > expiryUnix { // Cap the end active to the same time as the expiry. endActiveUnix = expiryUnix @@ -767,7 +748,7 @@ func calculateTournamentDeadlines(startTime, endTime, duration int64, resetSched if startTime > endActiveUnix { // The start time after the end of the current active period but before the next reset. // e.g. Reset schedule is daily at noon, duration is 1 hour, but time is currently 3pm. - schedules = resetSchedule.NextN(time.Unix(startTime, 0).UTC(), 2) + schedules := resetSchedule.NextN(time.Unix(startTime, 0).UTC(), 2) startActiveUnix = schedules[0].UTC().Unix() endActiveUnix = startActiveUnix + duration expiryUnix = schedules[1].UTC().Unix() From 0a013a52420397aaa477f5b352388275bde9e7a1 Mon Sep 17 00:00:00 2001 From: Luke Gehorsam Date: Mon, 11 Sep 2023 19:50:36 -0400 Subject: [PATCH 09/14] add MonThruFri test --- server/core_tournament_test.go | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/server/core_tournament_test.go b/server/core_tournament_test.go index 08b1952d01..567818fb81 100644 --- a/server/core_tournament_test.go +++ b/server/core_tournament_test.go @@ -48,3 +48,24 @@ func TestCalculateTournamentDeadlines(t *testing.T) { // Next Reset: Tues 29 August 2023 9:00:00 AM require.Equal(t, int64(1693299600), nextResetUnix, "Next reset times should be equal.") } + +func TestEveryDayMonThruFri(t *testing.T) { + sched, err := cronexpr.Parse("0 22 * * 1-5") + if err != nil { + t.Fatal("Invalid cron schedule", err) + return + } + + var now int64 = 1692615600 // 21 August 2023, 11:00:00 (Monday) + var startTime int64 = 1692090000 // 15 August 2023, 9:00:00 + var duration int64 = 7200 // 2 Hours + startActiveUnix, endActiveUnix, _ := calculateTournamentDeadlines(startTime, 0, duration, sched, time.Unix(now, 0)) + nextResetUnix := sched.Next(time.Unix(now, 0)).Unix() + + // 18 August 2023, 22:00:00 (Friday) + require.Equal(t, int64(1692396000), startActiveUnix, "Start active times should be equal.") + // 19 August 2023, 0:00:00 + require.Equal(t, int64(1692403200), endActiveUnix, "End active times should be equal.") + // 21 August 2023, 22:00:00 + require.Equal(t, int64(1692655200), nextResetUnix, "Next reset times should be equal.") +} From 631f449872dce4f73aa6752e40bb427a284e72b5 Mon Sep 17 00:00:00 2001 From: Luke Gehorsam Date: Mon, 11 Sep 2023 19:54:15 -0400 Subject: [PATCH 10/14] time -> utc conversion in tests --- server/core_tournament.go | 1 + server/core_tournament_test.go | 34 ++++++++++++++++------------------ 2 files changed, 17 insertions(+), 18 deletions(-) diff --git a/server/core_tournament.go b/server/core_tournament.go index 3fc11ed31c..51618fb809 100644 --- a/server/core_tournament.go +++ b/server/core_tournament.go @@ -737,6 +737,7 @@ func calculateTournamentDeadlines(startTime, endTime, duration int64, resetSched tUnix = t.UTC().Unix() } + // TODO what if it lands on the reset schedule? startActiveUnix := resetSchedule.Last(t).UTC().Unix() endActiveUnix := startActiveUnix + duration expiryUnix := resetSchedule.Next(t).UTC().Unix() diff --git a/server/core_tournament_test.go b/server/core_tournament_test.go index 567818fb81..e5f56b74af 100644 --- a/server/core_tournament_test.go +++ b/server/core_tournament_test.go @@ -22,30 +22,26 @@ import ( "github.com/stretchr/testify/require" ) -func TestCalculateTournamentDeadlines(t *testing.T) { - mockSched, err := cronexpr.Parse("0 9 */14 * *") +func TestEveryFourteenDaysFromFirst(t *testing.T) { + sched, err := cronexpr.Parse("0 9 */14 * *") if err != nil { t.Fatal("Invalid cron schedule", err) return } - const layout = "2 January 2006, 3:04:05 PM" - mockNowString := "21 August 2023, 11:00:00 AM" - mockNow, err := time.Parse(layout, mockNowString) - if err != nil { - t.Fatal("Invalid time", err) - return - } - var startTime int64 = 1692090000 // Sunday, 15 August 2023, 9:00:00 AM - var duration int64 = 1202400 // ~2 Weeks - startActiveUnix, endActiveUnix, _ := calculateTournamentDeadlines(startTime, 0, duration, mockSched, mockNow) - nextResetUnix := mockSched.Next(mockNow).Unix() + var now int64 = 1692608400 // 21 August 2023, 11:00:00 + var startTime int64 = 1692090000 // 15 August 2023, 9:00:00 + var duration int64 = 1202400 // 2 Weeks + + nowUnix := time.Unix(now, 0).UTC() + startActiveUnix, endActiveUnix, _ := calculateTournamentDeadlines(startTime, 0, duration, sched, nowUnix) + nextResetUnix := sched.Next(nowUnix).Unix() - // Start Time: Sunday, 15 August 2023, 9:00:00 AM + // 15 August 2023, 9:00:00 require.Equal(t, int64(1692090000), startActiveUnix, "Start active times should be equal.") - // End Active: Tues 29 August 2023 7:00:00 AM + // 29 August 2023, 7:00:00 require.Equal(t, int64(1693292400), endActiveUnix, "End active times should be equal.") - // Next Reset: Tues 29 August 2023 9:00:00 AM + // 29 August 2023, 9:00:00 require.Equal(t, int64(1693299600), nextResetUnix, "Next reset times should be equal.") } @@ -59,8 +55,10 @@ func TestEveryDayMonThruFri(t *testing.T) { var now int64 = 1692615600 // 21 August 2023, 11:00:00 (Monday) var startTime int64 = 1692090000 // 15 August 2023, 9:00:00 var duration int64 = 7200 // 2 Hours - startActiveUnix, endActiveUnix, _ := calculateTournamentDeadlines(startTime, 0, duration, sched, time.Unix(now, 0)) - nextResetUnix := sched.Next(time.Unix(now, 0)).Unix() + + nowUnix := time.Unix(now, 0).UTC() + startActiveUnix, endActiveUnix, _ := calculateTournamentDeadlines(startTime, 0, duration, sched, nowUnix) + nextResetUnix := sched.Next(nowUnix).Unix() // 18 August 2023, 22:00:00 (Friday) require.Equal(t, int64(1692396000), startActiveUnix, "Start active times should be equal.") From efaa151e877b8234fc2e5d1dc409a68c9c479ffa Mon Sep 17 00:00:00 2001 From: Luke Gehorsam Date: Mon, 11 Sep 2023 20:41:01 -0400 Subject: [PATCH 11/14] handle now() landing on reset schedule --- server/core_tournament.go | 11 +++++++++-- server/core_tournament_test.go | 25 ++++++++++++++++++++++++- 2 files changed, 33 insertions(+), 3 deletions(-) diff --git a/server/core_tournament.go b/server/core_tournament.go index 51618fb809..e627c8a092 100644 --- a/server/core_tournament.go +++ b/server/core_tournament.go @@ -737,8 +737,15 @@ func calculateTournamentDeadlines(startTime, endTime, duration int64, resetSched tUnix = t.UTC().Unix() } - // TODO what if it lands on the reset schedule? - startActiveUnix := resetSchedule.Last(t).UTC().Unix() + var startActiveUnix int64 + // check if we are landing squarely on the reset schedule + if resetSchedule.Next(t.Add(-1*time.Second)) == t { + startActiveUnix = tUnix + } else { + // otherwise assume the supplied time is ahead of the reset schedule. + startActiveUnix = resetSchedule.Last(t).UTC().Unix() + } + endActiveUnix := startActiveUnix + duration expiryUnix := resetSchedule.Next(t).UTC().Unix() if endActiveUnix > expiryUnix { diff --git a/server/core_tournament_test.go b/server/core_tournament_test.go index e5f56b74af..336c0e93d8 100644 --- a/server/core_tournament_test.go +++ b/server/core_tournament_test.go @@ -31,7 +31,7 @@ func TestEveryFourteenDaysFromFirst(t *testing.T) { var now int64 = 1692608400 // 21 August 2023, 11:00:00 var startTime int64 = 1692090000 // 15 August 2023, 9:00:00 - var duration int64 = 1202400 // 2 Weeks + var duration int64 = 1202400 // ~2 Weeks nowUnix := time.Unix(now, 0).UTC() startActiveUnix, endActiveUnix, _ := calculateTournamentDeadlines(startTime, 0, duration, sched, nowUnix) @@ -67,3 +67,26 @@ func TestEveryDayMonThruFri(t *testing.T) { // 21 August 2023, 22:00:00 require.Equal(t, int64(1692655200), nextResetUnix, "Next reset times should be equal.") } + +func TestNowIsResetTime(t *testing.T) { + sched, err := cronexpr.Parse("0 9 14 * *") + if err != nil { + t.Fatal("Invalid cron schedule", err) + return + } + + var now int64 = 1692003600 // 14 August 2023, 9:00:00 + var startTime int64 = 1692003600 // 14 August 2023, 9:00:00 + var duration int64 = 604800 // 1 Week + + nowUnix := time.Unix(now, 0).UTC() + startActiveUnix, endActiveUnix, _ := calculateTournamentDeadlines(startTime, 0, duration, sched, nowUnix) + nextResetUnix := sched.Next(nowUnix).Unix() + + // 14 August 2023, 9:00:00 + require.Equal(t, int64(1692003600), startActiveUnix, "Start active times should be equal.") + // 21 August 2023, 9:00:00 + require.Equal(t, int64(1692608400), endActiveUnix, "End active times should be equal.") + // 14 September 2023, 9:00:00 + require.Equal(t, int64(1694682000), nextResetUnix, "Next reset times should be equal.") +} From dd07ef14f437b4af2be82b5b495bdb2bea12336f Mon Sep 17 00:00:00 2001 From: Luke Gehorsam Date: Tue, 19 Sep 2023 09:12:43 -0400 Subject: [PATCH 12/14] update calculatePrevReset for leaderboards --- server/core_leaderboard.go | 32 +------------------------------- 1 file changed, 1 insertion(+), 31 deletions(-) diff --git a/server/core_leaderboard.go b/server/core_leaderboard.go index f6d328d25c..fab414fc3a 100644 --- a/server/core_leaderboard.go +++ b/server/core_leaderboard.go @@ -735,37 +735,7 @@ func calculatePrevReset(currentTime time.Time, startTime int64, resetSchedule *c return 0 } - nextResets := resetSchedule.NextN(currentTime, 2) - t1 := nextResets[0] - t2 := nextResets[1] - - resetPeriod := t2.Sub(t1) - sTime := t1.Add(resetPeriod * -2) // start from twice the period between the next resets back in time - - nextReset := resetSchedule.Next(currentTime) - if nextReset.IsZero() { - return 0 - } - - var prevReset time.Time - nextResets = resetSchedule.NextN(sTime, 2) - for i, r := range nextResets { - if r.Equal(nextReset) { - if i == 0 { - // No prev reset exists, next reset is the first to occur. - return 0 - } - // Prev reset was found. - prevReset = nextResets[i-1] - break - } - } - - if prevReset.IsZero() { - return 0 - } - - return prevReset.Unix() + return resetSchedule.Last(currentTime).Unix() } func getLeaderboardRecordsHaystack(ctx context.Context, logger *zap.Logger, db *sql.DB, leaderboardCache LeaderboardCache, rankCache LeaderboardRankCache, ownerID uuid.UUID, limit int, leaderboardId, cursor string, sortOrder int, expiryTime time.Time) (*api.LeaderboardRecordList, error) { From b5c9f1f6f31d25592508b3a902bc97a7e5c4ca8f Mon Sep 17 00:00:00 2001 From: Luke Gehorsam Date: Tue, 19 Sep 2023 12:49:21 -0400 Subject: [PATCH 13/14] test tournament start time is after now --- server/core_tournament_test.go | 25 ++++++++++++++++++++++--- 1 file changed, 22 insertions(+), 3 deletions(-) diff --git a/server/core_tournament_test.go b/server/core_tournament_test.go index 336c0e93d8..db11d20c34 100644 --- a/server/core_tournament_test.go +++ b/server/core_tournament_test.go @@ -22,7 +22,7 @@ import ( "github.com/stretchr/testify/require" ) -func TestEveryFourteenDaysFromFirst(t *testing.T) { +func TestTournamentEveryFourteenDaysFromFirst(t *testing.T) { sched, err := cronexpr.Parse("0 9 */14 * *") if err != nil { t.Fatal("Invalid cron schedule", err) @@ -45,7 +45,7 @@ func TestEveryFourteenDaysFromFirst(t *testing.T) { require.Equal(t, int64(1693299600), nextResetUnix, "Next reset times should be equal.") } -func TestEveryDayMonThruFri(t *testing.T) { +func TestTournamentEveryDayMonThruFri(t *testing.T) { sched, err := cronexpr.Parse("0 22 * * 1-5") if err != nil { t.Fatal("Invalid cron schedule", err) @@ -68,7 +68,7 @@ func TestEveryDayMonThruFri(t *testing.T) { require.Equal(t, int64(1692655200), nextResetUnix, "Next reset times should be equal.") } -func TestNowIsResetTime(t *testing.T) { +func TestTournamentNowIsResetTime(t *testing.T) { sched, err := cronexpr.Parse("0 9 14 * *") if err != nil { t.Fatal("Invalid cron schedule", err) @@ -90,3 +90,22 @@ func TestNowIsResetTime(t *testing.T) { // 14 September 2023, 9:00:00 require.Equal(t, int64(1694682000), nextResetUnix, "Next reset times should be equal.") } + +func TestTournamentNowIsBeforeStart(t *testing.T) { + sched, err := cronexpr.Parse("0 9 14 * *") + if err != nil { + t.Fatal("Invalid cron schedule", err) + return + } + var now int64 = 1692003600 // 14 August 2023, 9:00:00 + var startTime int64 = 1693558800 // 1 September 2023, 9:00:00 + var duration int64 = 604800 * 1 // 1 week + nowUnix := time.Unix(now, 0).UTC() + startActiveUnix, endActiveUnix, expiryTime := calculateTournamentDeadlines(startTime, 0, duration, sched, nowUnix) + // September 14, 2023, 9:00:00 + require.Equal(t, int64(1694682000), startActiveUnix, "Start active times should be equal.") + // September 21, 2023, 9:00:00 + require.Equal(t, int64(1695286800), endActiveUnix, "End active times should be equal.") + // October 14, 2023 9:00:00 + require.Equal(t, int64(1697274000), expiryTime, "Next reset times should be equal.") +} From 7833dcfab2fc4e2412b5be66e35126b85d1eeace Mon Sep 17 00:00:00 2001 From: Luke Gehorsam Date: Tue, 19 Sep 2023 14:52:06 -0400 Subject: [PATCH 14/14] fix calculation for future tournaments --- CHANGELOG.md | 4 ++++ server/core_tournament.go | 27 +++++++++++++++------------ server/core_tournament_test.go | 15 ++++++++------- 3 files changed, 27 insertions(+), 19 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b1b466d731..106aded1d3 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,10 @@ The format is based on [keep a changelog](http://keepachangelog.com) and this pr ## [Unreleased] +### Fixed +- Fixes calculation of leaderboard and tournament times for rare types of CRON expressions that don't execute at a fixed interval. +- Improved how start and end times are calculated for tournaments occuring in the future. + ### [3.17.1] - 2023-08-23 ### Added - Add Satori `recompute` optional input parameter to relevant operations. diff --git a/server/core_tournament.go b/server/core_tournament.go index e627c8a092..b23dc9a671 100644 --- a/server/core_tournament.go +++ b/server/core_tournament.go @@ -731,23 +731,26 @@ func TournamentRecordsHaystack(ctx context.Context, logger *zap.Logger, db *sql. func calculateTournamentDeadlines(startTime, endTime, duration int64, resetSchedule *cronexpr.Expression, t time.Time) (int64, int64, int64) { tUnix := t.UTC().Unix() if resetSchedule != nil { - if tUnix < startTime { - // if startTime is in the future, always use startTime - t = time.Unix(startTime, 0).UTC() - tUnix = t.UTC().Unix() - } - var startActiveUnix int64 - // check if we are landing squarely on the reset schedule - if resetSchedule.Next(t.Add(-1*time.Second)) == t { - startActiveUnix = tUnix + + if tUnix < startTime { + // the supplied time is behind the start time + startActiveUnix = resetSchedule.Next(time.Unix(startTime, 0).UTC()).UTC().Unix() } else { - // otherwise assume the supplied time is ahead of the reset schedule. - startActiveUnix = resetSchedule.Last(t).UTC().Unix() + // check if we are landing squarely on the reset schedule + landsOnSched := resetSchedule.Next(t.Add(-1*time.Second)) == t + if landsOnSched { + startActiveUnix = tUnix + } else { + startActiveUnix = resetSchedule.Last(t).UTC().Unix() + } } + // endActiveUnix is when the current iteration ends. endActiveUnix := startActiveUnix + duration - expiryUnix := resetSchedule.Next(t).UTC().Unix() + // expiryUnix represent the start of the next schedule, i.e., when the next iteration begins. It's when the current records "expire". + expiryUnix := resetSchedule.Next(time.Unix(startActiveUnix, 0).UTC()).UTC().Unix() + if endActiveUnix > expiryUnix { // Cap the end active to the same time as the expiry. endActiveUnix = expiryUnix diff --git a/server/core_tournament_test.go b/server/core_tournament_test.go index db11d20c34..e2f1512161 100644 --- a/server/core_tournament_test.go +++ b/server/core_tournament_test.go @@ -97,15 +97,16 @@ func TestTournamentNowIsBeforeStart(t *testing.T) { t.Fatal("Invalid cron schedule", err) return } + var now int64 = 1692003600 // 14 August 2023, 9:00:00 var startTime int64 = 1693558800 // 1 September 2023, 9:00:00 - var duration int64 = 604800 * 1 // 1 week + var duration int64 = 604800 * 4 // 4 Weeks + nowUnix := time.Unix(now, 0).UTC() - startActiveUnix, endActiveUnix, expiryTime := calculateTournamentDeadlines(startTime, 0, duration, sched, nowUnix) - // September 14, 2023, 9:00:00 + startActiveUnix, endActiveUnix, _ := calculateTournamentDeadlines(startTime, 0, duration, sched, nowUnix) + + // 14 September 2023, 9:00:00 require.Equal(t, int64(1694682000), startActiveUnix, "Start active times should be equal.") - // September 21, 2023, 9:00:00 - require.Equal(t, int64(1695286800), endActiveUnix, "End active times should be equal.") - // October 14, 2023 9:00:00 - require.Equal(t, int64(1697274000), expiryTime, "Next reset times should be equal.") + // 12 October 2023, 9:00:00 + require.Equal(t, int64(1697101200), endActiveUnix, "End active times should be equal.") }