Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

use Last() implementation when calculating tournament deadlines #1084

Merged
merged 14 commits into from
Sep 19, 2023
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
82 changes: 82 additions & 0 deletions internal/cronexpr/cronexpr.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
202 changes: 199 additions & 3 deletions internal/cronexpr/cronexpr_next.go
Original file line number Diff line number Diff line change
Expand Up @@ -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]),
Expand All @@ -56,14 +56,57 @@ 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],
0,
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 {
Expand Down Expand Up @@ -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 {
Expand All @@ -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 {
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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 {
Expand Down
Loading
Loading