diff --git a/src/cron/cron.v b/src/cron/cron.v index 13fc22d..d802dbf 100644 --- a/src/cron/cron.v +++ b/src/cron/cron.v @@ -27,6 +27,29 @@ pub fn cron(conf Config) ? { // } // println(queue) - exp := '10/2 5 *' - println(parse_expression(exp) ?) + // exp := '10/2 5 *' + // println(parse_expression(exp) ?) + ce := parse_expression('0 3 */2') ? + println(ce) + // ce := CronExpression{ + // minutes: [0] + // hours: [3] + // days: [1, 2, 3, 4, 5, 6] + // months: [1, 2] + // } + mut t := time.Time{ + year: 2022 + month: 2 + minute: 9 + hour: 13 + day: 12 + } + + // mut t := time.now() + println(t) + + for _ in 1..25 { + t = ce.next(t) ? + println(t) + } } diff --git a/src/cron/expression.v b/src/cron/expression.v index a29bf53..ff0fcd4 100644 --- a/src/cron/expression.v +++ b/src/cron/expression.v @@ -3,50 +3,117 @@ module cron import math import time +const days_in_month = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31] + struct CronExpression { - minutes []u32 - hours []u32 - days []u32 + minutes []int + hours []int + days []int + months []int } // next calculates the earliest time this cron expression is valid. -pub fn (ce &CronExpression) next(ref &time.Time) time.Time { - res := time.Time{} +pub fn (ce &CronExpression) next(ref time.Time) ?time.Time { + mut minute_index := 0 + mut hour_index := 0 + mut day_index := 0 + mut month_index := 0 - mut day := 0 - mut hour := 0 - mut minute := 0 + for month_index < ce.months.len && ref.month > ce.months[month_index] { + month_index++ + } - // Find the next minute - // If ref.minute is greater than - if ref.minute >= ce.minutes[ce.minutes.len - 1] || ref.minute < ce.minutes[0] { - minute = ce.minutes[0] - }else{ - for i in 0..ce.minutes.len { - if ce.minutes[i] > ref.minute { - minute = ce.minutes[i] - break + if month_index < ce.months.len { + for day_index < ce.days.len && ref.day > ce.days[day_index] { + day_index++ + } + + if day_index < ce.days.len { + for hour_index < ce.hours.len && ref.hour > ce.hours[hour_index] { + hour_index++ + } + + if hour_index < ce.hours.len { + // For each unit, we calculate what the next value is + for minute_index < ce.minutes.len && ref.minute >= ce.minutes[minute_index] { + minute_index++ + } } } } - return res + + // Sometime we have to shift values one more + if minute_index == ce.minutes.len && hour_index < ce.hours.len { + hour_index += 1 + } + + if hour_index == ce.hours.len && day_index < ce.days.len { + day_index += 1 + } + + if day_index == ce.days.len && month_index < ce.months.len { + month_index += 1 + } + + mut minute := ce.minutes[minute_index % ce.minutes.len] + mut hour := ce.hours[hour_index % ce.hours.len] + mut day := ce.days[day_index % ce.days.len] + + mut reset := false + + // If the day can't be planned in the current month, we go to the next one + // and go back to day one + if day > days_in_month[ce.months[month_index % ce.months.len] - 1] { + month_index += 1 + day = ce.days[0] + + // Make sure we only plan in a month that the day occurs in + for day > days_in_month[ce.months[month_index & ce.months.len] - 1] { + month_index += 1 + + // Prevent scenario where there are no months that can be scheduled. + if month_index == 2 * ce.months.len { + return error('No schedulable moment.') + } + } + } + + + month := ce.months[month_index % ce.months.len] + mut year := ref.year + + if month_index >= ce.months.len { + year++ + } + + return time.Time{ + year: year + month: month + day: day + minute: minute + hour: hour + } +} + +fn (ce &CronExpression) next_from_now() ?time.Time { + return ce.next(time.now()) } // parse_range parses a given string into a range of sorted integers, if // possible. -fn parse_range(s string, min u32, max u32) ?[]u32 { - mut out := []u32{} +fn parse_range(s string, min int, max int) ?[]int { + mut out := []int{} mut start := min - mut interval := u32(1) + mut interval := 1 if s != '*' { exps := s.split('/') - start = math.min(max, math.max(exps[0].u32(), min)) + start = math.min(max, math.max(exps[0].int(), min)) if exps.len > 1 { - interval = exps[1].u32() + interval = exps[1].int() } // Here, s solely consists of a number, so that's the only value we // should return. @@ -69,15 +136,22 @@ fn parse_range(s string, min u32, max u32) ?[]u32 { // min hour day month day-of-week fn parse_expression(exp string) ?CronExpression { - parts := exp.split(' ') + mut parts := exp.split(' ') - if parts.len != 3 { - return error('Expression must contain 5 space-separated parts.') + if parts.len < 2 || parts.len > 4 { + return error('Expression must contain between 2 and 4 space-separated parts.') + } + + // For ease of use, we allow the user to only specify as many parts as they + // need. + for parts.len < 4 { + parts << '*' } return CronExpression{ minutes: parse_range(parts[0], 0, 59) ? hours: parse_range(parts[1], 0, 23) ? - days: parse_range(parts[2], 0, 31) ? + days: parse_range(parts[2], 1, 31) ? + months: parse_range(parts[3], 1, 12) ? } } diff --git a/src/cron/expression_test.v b/src/cron/expression_test.v index abd5e5f..9279cce 100644 --- a/src/cron/expression_test.v +++ b/src/cron/expression_test.v @@ -2,27 +2,27 @@ module cron // =====parse_range===== fn test_parse_star_range() ? { - assert parse_range('*', 0, 5) ? == [u32(0), 1, 2, 3, 4, 5] + assert parse_range('*', 0, 5) ? == [0, 1, 2, 3, 4, 5] } fn test_parse_number() ? { - assert parse_range('4', 0, 5) ? == [u32(4)] + assert parse_range('4', 0, 5) ? == [4] } fn test_parse_number_too_large() ? { - assert parse_range('10', 0, 6) ? == [u32(6)] + assert parse_range('10', 0, 6) ? == [6] } fn test_parse_number_too_small() ? { - assert parse_range('0', 2, 6) ? == [u32(2)] + assert parse_range('0', 2, 6) ? == [2] } fn test_parse_step_star() ? { - assert parse_range('*/4', 0, 20) ? == [u32(0), 4, 8, 12, 16, 20] + assert parse_range('*/4', 0, 20) ? == [0, 4, 8, 12, 16, 20] } fn test_parse_step_star_too_large() ? { - assert parse_range('*/21', 0, 20) ? == [u32(0)] + assert parse_range('*/21', 0, 20) ? == [0] } fn test_parse_step_zero() ? { @@ -30,15 +30,15 @@ fn test_parse_step_zero() ? { } fn test_parse_step_number() ? { - assert parse_range('5/4', 0, 20) ? == [u32(5), 9, 13, 17] + assert parse_range('5/4', 0, 20) ? == [5, 9, 13, 17] } fn test_parse_step_number_too_large() ? { - assert parse_range('10/4', 0, 5) ? == [u32(5)] + assert parse_range('10/4', 0, 5) ? == [5] } fn test_parse_step_number_too_small() ? { - assert parse_range('2/4', 5, 10) ? == [u32(5), 9] + assert parse_range('2/4', 5, 10) ? == [5, 9] }