From 71c77e90bcfa2390ed5f92d19ee3d70d065f4dad Mon Sep 17 00:00:00 2001 From: Chewing_Bever Date: Tue, 4 Oct 2022 16:45:28 +0200 Subject: [PATCH] refactor(cron): expression parser now uses bitfields (closes #148) --- CHANGELOG.md | 1 + src/cron/expression/expression.v | 139 ------------------- src/cron/expression/expression_parse.v | 146 ++++++++++++++++++++ src/cron/expression/expression_parse_test.v | 39 ++---- 4 files changed, 162 insertions(+), 163 deletions(-) create mode 100644 src/cron/expression/expression_parse.v diff --git a/CHANGELOG.md b/CHANGELOG.md index a550524..d2dd760 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed * Migrated codebase to V 0.3.2 +* Cron expression parser now uses bitfields instead of bool arrays ### Fixed diff --git a/src/cron/expression/expression.v b/src/cron/expression/expression.v index 438805d..c3ff8c5 100644 --- a/src/cron/expression/expression.v +++ b/src/cron/expression/expression.v @@ -134,142 +134,3 @@ pub fn (ce &CronExpression) next_n(ref time.Time, n int) ![]time.Time { return times } - -// parse_range parses a given string into a range of sorted integers, if -// possible. -fn parse_range(s string, min int, max int, mut bitv []bool) ! { - mut start := min - mut end := max - mut interval := 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('-') { - bitv[start - min] = true - return - } - - for start <= end { - bitv[start - min] = true - start += interval - } -} - -// bitv_to_ints converts a bit vector into an array containing the -// corresponding values. -fn bitv_to_ints(bitv []bool, min int) []int { - mut out := []int{} - - for i in 0 .. bitv.len { - if bitv[i] { - 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 bitv := []bool{len: max - min + 1, init: false} - - for range in s.split(',') { - parse_range(range, min, max, mut bitv)! - } - - return bitv_to_ints(bitv, 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] - } -} diff --git a/src/cron/expression/expression_parse.v b/src/cron/expression/expression_parse.v new file mode 100644 index 0000000..4aaec5b --- /dev/null +++ b/src/cron/expression/expression_parse.v @@ -0,0 +1,146 @@ +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] + } +} diff --git a/src/cron/expression/expression_parse_test.v b/src/cron/expression/expression_parse_test.v index 5c12329..92e8291 100644 --- a/src/cron/expression/expression_parse_test.v +++ b/src/cron/expression/expression_parse_test.v @@ -3,26 +3,22 @@ module expression // parse_range_error returns the returned error message. If the result is '', // that means the function didn't error. fn parse_range_error(s string, min int, max int) string { - mut bitv := []bool{len: max - min + 1, init: false} - - parse_range(s, min, max, mut bitv) or { return err.msg } + parse_range(s, min, max) or { return err.msg } return '' } // =====parse_range===== fn test_range_star_range() ! { - mut bitv := []bool{len: 6, init: false} - parse_range('*', 0, 5, mut bitv)! + bf := parse_range('*', 0, 5)! - assert bitv == [true, true, true, true, true, true] + assert bf_to_ints(bf, 0) == [0, 1, 2, 3, 4, 5] } fn test_range_number() ! { - mut bitv := []bool{len: 6, init: false} - parse_range('4', 0, 5, mut bitv)! + bf := parse_range('4', 0, 5)! - assert bitv_to_ints(bitv, 0) == [4] + assert bf_to_ints(bf, 0) == [4] } fn test_range_number_too_large() ! { @@ -38,17 +34,15 @@ fn test_range_number_invalid() ! { } fn test_range_step_star_1() ! { - mut bitv := []bool{len: 21, init: false} - parse_range('*/4', 0, 20, mut bitv)! + bf := parse_range('*/4', 0, 20)! - assert bitv_to_ints(bitv, 0) == [0, 4, 8, 12, 16, 20] + assert bf_to_ints(bf, 0) == [0, 4, 8, 12, 16, 20] } fn test_range_step_star_2() ! { - mut bitv := []bool{len: 8, init: false} - parse_range('*/3', 1, 8, mut bitv)! + bf := parse_range('*/3', 1, 8)! - assert bitv_to_ints(bitv, 1) == [1, 4, 7] + assert bf_to_ints(bf, 1) == [1, 4, 7] } fn test_range_step_star_too_large() ! { @@ -60,10 +54,9 @@ fn test_range_step_zero() ! { } fn test_range_step_number() ! { - mut bitv := []bool{len: 21, init: false} - parse_range('5/4', 2, 22, mut bitv)! + bf := parse_range('5/4', 2, 22)! - assert bitv_to_ints(bitv, 2) == [5, 9, 13, 17, 21] + assert bf_to_ints(bf, 2) == [5, 9, 13, 17, 21] } fn test_range_step_number_too_large() ! { @@ -75,17 +68,15 @@ fn test_range_step_number_too_small() ! { } fn test_range_dash() ! { - mut bitv := []bool{len: 10, init: false} - parse_range('4-8', 0, 9, mut bitv)! + bf := parse_range('4-8', 0, 9)! - assert bitv_to_ints(bitv, 0) == [4, 5, 6, 7, 8] + assert bf_to_ints(bf, 0) == [4, 5, 6, 7, 8] } fn test_range_dash_step() ! { - mut bitv := []bool{len: 10, init: false} - parse_range('4-8/2', 0, 9, mut bitv)! + bf := parse_range('4-8/2', 0, 9)! - assert bitv_to_ints(bitv, 0) == [4, 6, 8] + assert bf_to_ints(bf, 0) == [4, 6, 8] } // =====parse_part=====