Unverified Commit 81b4c7f0 authored by Luke Gehorsam's avatar Luke Gehorsam Committed by GitHub
Browse files

use Last() implementation when calculating tournament deadlines (#1084)

parent 068c8adf
Loading
Loading
Loading
Loading
+4 −0
Original line number Diff line number Diff line
@@ -10,6 +10,10 @@ The format is based on [keep a changelog](http://keepachangelog.com) and this pr
### Fixed
- Fixed multiple issues found by linter.

### 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.
+82 −0
Original line number Diff line number Diff line
@@ -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
+199 −3
Original line number Diff line number Diff line
@@ -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 {
+257 −1
Original line number Diff line number Diff line
@@ -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) {
+1 −31
Original line number Diff line number Diff line
@@ -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) {
Loading