From fe5b9b8cfdd628829697eed1f8adc2717aeb3ada Mon Sep 17 00:00:00 2001 From: Jef Roosens Date: Thu, 12 Jan 2023 21:52:51 +0100 Subject: [PATCH] WIP replace cron with c impl --- src/cron/expression/c/expression.c | 26 +++- src/cron/expression/c/expression.h | 4 + src/cron/expression/expression.c.v | 18 ++- src/cron/expression/expression.v | 161 +++++++------------------ src/cron/expression/expression_parse.v | 146 ---------------------- 5 files changed, 86 insertions(+), 269 deletions(-) delete mode 100644 src/cron/expression/expression_parse.v diff --git a/src/cron/expression/c/expression.c b/src/cron/expression/c/expression.c index 3f65b6a..3d12604 100644 --- a/src/cron/expression/c/expression.c +++ b/src/cron/expression/c/expression.c @@ -1,7 +1,12 @@ #include "expression.h" +#include const uint8_t month_days[] = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}; +CronExpression *ce_init() { + return malloc(sizeof(CronExpression)); +} + int ce_next(SimpleTime *out, CronExpression *ce, SimpleTime *ref) { // For all of these values, the rule is the following: if their value is // the length of their respective array in the CronExpression object, that @@ -26,12 +31,12 @@ int ce_next(SimpleTime *out, CronExpression *ce, SimpleTime *ref) { day_index++; } - if (day_index < ce->days_count && ref->day == ce->days[day_index]) { + if (day_index < ce->day_count && ref->day == ce->days[day_index]) { while (hour_index < ce->hour_count && ref->hour > ce->hours[hour_index]) { hour_index++; } - if (hour_index < ce->hours_count && ref->hour == ce->hours[hour_index]) { + if (hour_index < ce->hour_count && ref->hour == ce->hours[hour_index]) { // Minute is the only value where we explicitely make sure we // can't match sref's value exactly. This is to ensure we only // return values in the future. @@ -88,3 +93,20 @@ int ce_next(SimpleTime *out, CronExpression *ce, SimpleTime *ref) { return 0; } + +int ce_next_from_now(SimpleTime *out, CronExpression *ce) { + time_t t = time(NULL); + struct tm gm; + gmtime_r(&t, &gm); + + SimpleTime ref = { + .year = gm.tm_year, + // tm_mon goes from 0 to 11 + .month = gm.tm_mon + 1, + .day = gm.tm_mday, + .hour = gm.tm_hour, + .minute = gm.tm_min + }; + + return ce_next(out, ce, &ref); +} diff --git a/src/cron/expression/c/expression.h b/src/cron/expression/c/expression.h index 7abb189..12dbdec 100644 --- a/src/cron/expression/c/expression.h +++ b/src/cron/expression/c/expression.h @@ -29,9 +29,13 @@ typedef struct simple_time { int minute; } SimpleTime; +CronExpression *ce_init(); + /** * Given a */ int ce_next(SimpleTime *out, CronExpression *ce, SimpleTime *ref); +int ce_next_from_now(SimpleTime *out, CronExpression *ce); + ParseError ce_parse_expression(CronExpression *out, char *s); diff --git a/src/cron/expression/expression.c.v b/src/cron/expression/expression.c.v index ec39fd6..27e6193 100644 --- a/src/cron/expression/expression.c.v +++ b/src/cron/expression/expression.c.v @@ -15,4 +15,20 @@ pub struct C.CronExpression { month_count u8 } -/* pub type CronExpression = C.CronExpression */ +struct C.SimpleTime { + year int + month int + day int + hour int + minute int +} + +pub type CronExpression = C.CronExpression + +fn C.ce_init() &CronExpression + +fn C.ce_next(out &C.SimpleTime, ce &C.CronExpression, ref &C.SimpleTime) int + +fn C.ce_next_from_now(out &C.SimpleTime, ce &C.CronExpression) int + +fn C.ce_parse_expression(out &C.CronExpression, s &char) int diff --git a/src/cron/expression/expression.v b/src/cron/expression/expression.v index c3ff8c5..095acf0 100644 --- a/src/cron/expression/expression.v +++ b/src/cron/expression/expression.v @@ -2,135 +2,56 @@ module expression import time -pub struct CronExpression { - minutes []int - hours []int - days []int - months []int +pub fn parse_expression(exp string) !&CronExpression { + out := C.ce_init() + + res := C.ce_parse_expression(out, exp.str) + + if res != 0 { + return error('yuhh') + } + + return out } -// next calculates the earliest time this cron expression is valid. It will -// always pick a moment in the future, even if ref matches completely up to the -// minute. This function conciously does not take gap years into account. pub fn (ce &CronExpression) next(ref time.Time) !time.Time { - // If the given ref matches the next cron occurence up to the minute, it - // will return that value. Because we always want to return a value in the - // future, we artifically shift the ref 60 seconds to make sure we always - // match in the future. A shift of 60 seconds is enough because the cron - // expression does not allow for accuracy smaller than one minute. - sref := ref - - // For all of these values, the rule is the following: if their value is - // the length of their respective array in the CronExpression object, that - // means we've looped back around. This means that the "bigger" value has - // to be incremented by one. For example, if the minutes have looped - // around, that means that the hour has to be incremented as well. - mut minute_index := 0 - mut hour_index := 0 - mut day_index := 0 - mut month_index := 0 - - // This chain is the same logic multiple times, namely that if a "bigger" - // value loops around, then the smaller value will always reset as well. - // For example, if we're going to a new day, the hour & minute will always - // be their smallest value again. - for month_index < ce.months.len && sref.month > ce.months[month_index] { - month_index++ + st := C.SimpleTime{ + year: ref.year + month: ref.month + day: ref.day + hour: ref.hour + minute: ref.minute } - if month_index < ce.months.len && sref.month == ce.months[month_index] { - for day_index < ce.days.len && sref.day > ce.days[day_index] { - day_index++ - } - - if day_index < ce.days.len && ce.days[day_index] == sref.day { - for hour_index < ce.hours.len && sref.hour > ce.hours[hour_index] { - hour_index++ - } - - if hour_index < ce.hours.len && ce.hours[hour_index] == sref.hour { - // Minute is the only value where we explicitely make sure we - // can't match sref's value exactly. This is to ensure we only - // return values in the future. - for minute_index < ce.minutes.len && sref.minute >= ce.minutes[minute_index] { - minute_index++ - } - } - } - } - - // Here, we increment the "bigger" values by one if the smaller ones loop - // around. The order is important, as it allows a sort-of waterfall effect - // to occur which updates all values if required. - 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] - - // Sometimes, we end up with a day that does not exist within the selected - // month, e.g. day 30 in February. When this occurs, we reset day back to - // the smallest value & loop over to the next month that does have this - // day. - if day > time.month_days[ce.months[month_index % ce.months.len] - 1] { - day = ce.days[0] - month_index += 1 - - for day > time.month_days[ce.months[month_index & ce.months.len] - 1] { - month_index += 1 - - // If for whatever reason the day value ends up being something - // that can't be scheduled in any month, we have to make sure we - // don't create an infinite loop. - if month_index == 2 * ce.months.len { - return error('No schedulable moment.') - } - } - } - - month := ce.months[month_index % ce.months.len] - mut year := sref.year - - // If the month loops over, we need to increment the year. - if month_index >= ce.months.len { - year++ - } + out := C.SimpleTime{} + res := C.ce_next(&out, ce, &st) + if res != 0 { + return error('yuhh') + } + return time.new_time(time.Time{ - year: year - month: month - day: day - minute: minute - hour: hour + year: out.year + month: out.month + day: out.day + hour: out.hour + minute: out.minute }) } -// next_from_now returns the result of ce.next(ref) where ref is the result of -// time.now(). pub fn (ce &CronExpression) next_from_now() !time.Time { - return ce.next(time.now()) -} - -// next_n returns the n next occurences of the expression, given a starting -// time. -pub fn (ce &CronExpression) next_n(ref time.Time, n int) ![]time.Time { - mut times := []time.Time{cap: n} - - times << ce.next(ref)! - - for i in 1 .. n { - times << ce.next(times[i - 1])! - } - - return times + out := C.SimpleTime{} + res := C.ce_next_from_now(&out, ce) + + if res != 0 { + return error('yuhh') + } + + return time.new_time(time.Time{ + year: out.year + month: out.month + day: out.day + hour: out.hour + minute: out.minute + }) } diff --git a/src/cron/expression/expression_parse.v b/src/cron/expression/expression_parse.v deleted file mode 100644 index 4aaec5b..0000000 --- a/src/cron/expression/expression_parse.v +++ /dev/null @@ -1,146 +0,0 @@ -module expression - -import bitfield - -// parse_range parses a given string into a range of sorted integers. Its -// result is a BitField with set bits for all numbers in the result. -fn parse_range(s string, min int, max int) !bitfield.BitField { - mut start := min - mut end := max - mut interval := 1 - mut bf := bitfield.new(max - min + 1) - - exps := s.split('/') - - if exps.len > 2 { - return error('Invalid expression.') - } - - if exps[0] != '*' { - dash_parts := exps[0].split('-') - - if dash_parts.len > 2 { - return error('Invalid expression.') - } - - start = dash_parts[0].int() - - // The builtin parsing functions return zero if the string can't be - // parsed into a number, so we have to explicitely check whether they - // actually entered zero or if it's an invalid number. - if start == 0 && dash_parts[0] != '0' { - return error('Invalid number.') - } - - // Check whether the start value is out of range - if start < min || start > max { - return error('Out of range.') - } - - if dash_parts.len == 2 { - end = dash_parts[1].int() - - if end == 0 && dash_parts[1] != '0' { - return error('Invalid number.') - } - - if end < start || end > max { - return error('Out of range.') - } - } - } - - if exps.len > 1 { - interval = exps[1].int() - - // interval being zero is always invalid, but we want to check why - // it's invalid for better error messages. - if interval == 0 { - if exps[1] != '0' { - return error('Invalid number.') - } else { - return error('Step size zero not allowed.') - } - } - - if interval > max - min { - return error('Step size too large.') - } - } - // Here, s solely consists of a number, so that's the only value we - // should return. - else if exps[0] != '*' && !exps[0].contains('-') { - bf.set_bit(start - min) - return bf - } - - for start <= end { - bf.set_bit(start - min) - start += interval - } - - return bf -} - -// bf_to_ints takes a BitField and converts it into the expected list of actual -// integers. -fn bf_to_ints(bf bitfield.BitField, min int) []int { - mut out := []int{} - - for i in 0 .. bf.get_size() { - if bf.get_bit(i) == 1 { - out << min + i - } - } - - return out -} - -// parse_part parses a given part of a cron expression & returns the -// corresponding array of ints. -fn parse_part(s string, min int, max int) ![]int { - mut bf := bitfield.new(max - min + 1) - - for range in s.split(',') { - bf2 := parse_range(range, min, max)! - bf = bitfield.bf_or(bf, bf2) - } - - return bf_to_ints(bf, min) -} - -// parse_expression parses an entire cron expression string into a -// CronExpression object, if possible. -pub fn parse_expression(exp string) !CronExpression { - // The filter allows for multiple spaces between parts - mut parts := exp.split(' ').filter(it != '') - - 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 << '*' - } - - mut part_results := [][]int{} - - mins := [0, 0, 1, 1] - maxs := [59, 23, 31, 12] - - // This for loop allows us to more clearly propagate the error to the user. - for i, min in mins { - part_results << parse_part(parts[i], min, maxs[i]) or { - return error('An error occurred with part $i: $err.msg()') - } - } - - return CronExpression{ - minutes: part_results[0] - hours: part_results[1] - days: part_results[2] - months: part_results[3] - } -}