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/internal/cronexpr/cronexpr.go b/internal/cronexpr/cronexpr.go index 41534bd0ec..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 @@ -233,6 +234,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..6570358d49 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], @@ -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 { @@ -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..dceec4b092 100644 --- a/internal/cronexpr/cronexpr_test.go +++ b/internal/cronexpr/cronexpr_test.go @@ -193,8 +193,264 @@ 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"}, + }, + }, - // TODO: more tests + // 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 := 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) { 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) { diff --git a/server/core_tournament.go b/server/core_tournament.go index adebdaff7d..b23dc9a671 100644 --- a/server/core_tournament.go +++ b/server/core_tournament.go @@ -731,28 +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 { + var startActiveUnix int64 + if tUnix < startTime { - // if startTime is in the future, always use startTime - t = time.Unix(startTime, 0).UTC() - tUnix = t.UTC().Unix() - } - - 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) - for { - s := resetSchedule.Next(time.Unix(startActiveUnix, 0).UTC()).UTC().Unix() - if s < tUnix { - startActiveUnix = s + // the supplied time is behind the start time + startActiveUnix = resetSchedule.Next(time.Unix(startTime, 0).UTC()).UTC().Unix() + } else { + // check if we are landing squarely on the reset schedule + landsOnSched := resetSchedule.Next(t.Add(-1*time.Second)) == t + if landsOnSched { + startActiveUnix = tUnix } else { - if s == tUnix { - startActiveUnix = s - } - break + startActiveUnix = resetSchedule.Last(t).UTC().Unix() } } + + // endActiveUnix is when the current iteration ends. endActiveUnix := startActiveUnix + duration - expiryUnix := schedules[0].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 @@ -761,7 +759,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() diff --git a/server/core_tournament_test.go b/server/core_tournament_test.go new file mode 100644 index 0000000000..e2f1512161 --- /dev/null +++ b/server/core_tournament_test.go @@ -0,0 +1,112 @@ +// 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 TestTournamentEveryFourteenDaysFromFirst(t *testing.T) { + sched, err := cronexpr.Parse("0 9 */14 * *") + if err != nil { + t.Fatal("Invalid cron schedule", err) + return + } + + 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() + + // 15 August 2023, 9:00:00 + require.Equal(t, int64(1692090000), startActiveUnix, "Start active times should be equal.") + // 29 August 2023, 7:00:00 + require.Equal(t, int64(1693292400), endActiveUnix, "End active times should be equal.") + // 29 August 2023, 9:00:00 + require.Equal(t, int64(1693299600), nextResetUnix, "Next reset times should be equal.") +} + +func TestTournamentEveryDayMonThruFri(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 + + 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.") + // 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.") +} + +func TestTournamentNowIsResetTime(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.") +} + +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 * 4 // 4 Weeks + + nowUnix := time.Unix(now, 0).UTC() + 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.") + // 12 October 2023, 9:00:00 + require.Equal(t, int64(1697101200), endActiveUnix, "End active times should be equal.") +}