From 35d1a4e1e58258d05179a418446880055f3e66ca Mon Sep 17 00:00:00 2001 From: Russell Jennings Date: Sun, 20 Mar 2016 18:14:34 -0400 Subject: [PATCH 1/5] adds strict_matching to cron_parser --- lib/cron_parser.rb | 69 ++++++++++++++++++++++++++++++---------------- 1 file changed, 45 insertions(+), 24 deletions(-) diff --git a/lib/cron_parser.rb b/lib/cron_parser.rb index 41a8a5e..980f1ed 100644 --- a/lib/cron_parser.rb +++ b/lib/cron_parser.rb @@ -15,7 +15,6 @@ def initialize(time,time_source = Time) @day = time.day @hour = time.hour @min = time.min - @time_source = time_source end @@ -51,8 +50,9 @@ def inspect "sat" => "6" } - def initialize(source,time_source = Time) + def initialize(source,time_source = Time, strict_match = false) @source = interpret_vixieisms(source) + set_strict_match(strict_match) @time_source = time_source validate_source end @@ -80,18 +80,17 @@ def interpret_vixieisms(spec) # returns the next occurence after the given date def next(now = @time_source.now, num = 1) t = InternalTime.new(now, @time_source) - - unless time_specs[:month][0].include?(t.month) + if !time_specs[:month][0].include?(t.month) nudge_month(t) t.day = 0 end - unless interpolate_weekdays(t.year, t.month)[0].include?(t.day) + if !interpolate_weekdays(t.year, t.month)[0].include?(t.day) nudge_date(t) t.hour = -1 end - - unless time_specs[:hour][0].include?(t.hour) + + if !time_specs[:hour][0].include?(t.hour) nudge_hour(t) t.min = -1 end @@ -184,19 +183,24 @@ def interpolate_weekdays_without_cache(year, month) # Careful, if both DOW and DOM fields are non-wildcard, # then we only need to match *one* for cron to run the job: - if not (mday_field == '*' and wday_field == '*') - valid_mday = [] if mday_field == '*' - valid_wday = [] if wday_field == '*' + if !strict_match? + if not (mday_field == '*' and wday_field == '*') + valid_mday = [] if mday_field == '*' + valid_wday = [] if wday_field == '*' + end end # Careful: crontabs may use either 0 or 7 for Sunday: valid_wday << 0 if valid_wday.include?(7) result = [] while t.month == month - result << t.mday if valid_mday.include?(t.mday) || valid_wday.include?(t.wday) + if strict_match? + result << t.mday if (valid_mday.include?(t.mday) && valid_wday.include?(t.wday)) + else + result << t.mday if (valid_mday.include?(t.mday) || valid_wday.include?(t.wday)) + end t = t.succ end - [Set.new(result), result] end @@ -208,23 +212,32 @@ def nudge_month(t, dir = :next) spec = time_specs[:month][1] next_value = find_best_next(t.month, spec, dir) t.month = next_value || (dir == :next ? spec.first : spec.last) - - nudge_year(t, dir) if next_value.nil? - - # we changed the month, so its likely that the date is incorrect now - valid_days = interpolate_weekdays(t.year, t.month)[1] - t.day = dir == :next ? valid_days.first : valid_days.last + if strict_match? + until date_valid?(t) + next_value = find_best_next(t.month, spec, dir) + t.month = next_value || (dir == :next ? spec.first : spec.last) + nudge_year(t, dir) if next_value.nil? + valid_days = interpolate_weekdays(t.year, t.month)[1] + t.day = dir == :next ? valid_days.first : valid_days.last + end + else + nudge_year(t, dir) if next_value.nil? + # we changed the month, so its likely that the date is incorrect now + valid_days = interpolate_weekdays(t.year, t.month)[1] + t.day = dir == :next ? valid_days.first : valid_days.last + end + end def date_valid?(t, dir = :next) - interpolate_weekdays(t.year, t.month)[0].include?(t.day) + weekdays = interpolate_weekdays(t.year, t.month)[0] + weekdays.include?(t.day) end def nudge_date(t, dir = :next, can_nudge_month = true) spec = interpolate_weekdays(t.year, t.month)[1] next_value = find_best_next(t.day, spec, dir) t.day = next_value || (dir == :next ? spec.first : spec.last) - nudge_month(t, dir) if next_value.nil? && can_nudge_month end @@ -232,7 +245,6 @@ def nudge_hour(t, dir = :next) spec = time_specs[:hour][1] next_value = find_best_next(t.hour, spec, dir) t.hour = next_value || (dir == :next ? spec.first : spec.last) - nudge_date(t, dir) if next_value.nil? end @@ -240,7 +252,6 @@ def nudge_minute(t, dir = :next) spec = time_specs[:minute][1] next_value = find_best_next(t.min, spec, dir) t.min = next_value || (dir == :next ? spec.first : spec.last) - nudge_hour(t, dir) if next_value.nil? end @@ -267,10 +278,8 @@ def substitute_parse_symbols(str) def stepped_range(rng, step = 1) len = rng.last - rng.first - num = len.div(step) result = (0..num).map { |i| rng.first + step * i } - result.pop if result[-1] == rng.last and rng.exclude_end? result end @@ -295,4 +304,16 @@ def validate_source raise ArgumentError, 'not a valid cronline' end end + + def strict_match? + @strict_match + end + + def set_strict_match(strict_match = false) + @strict_match = false + if strict_match && [time_specs[:dom].last, time_specs[:dow].last].all?{|v| v != '*'} + @strict_match = true + end + end + end From b7e8b58704872812fb453ae477d769c30794bf92 Mon Sep 17 00:00:00 2001 From: Russell Jennings Date: Sun, 20 Mar 2016 18:15:48 -0400 Subject: [PATCH 2/5] adds spec coverage for strict_matching behavior ensures that other cron entries are unimpacted by the flag, if they should not be --- spec/cron_parser_spec.rb | 422 +++++++++++++++++++++++++++------------ 1 file changed, 289 insertions(+), 133 deletions(-) diff --git a/spec/cron_parser_spec.rb b/spec/cron_parser_spec.rb index 94f7ad0..99a3eb2 100644 --- a/spec/cron_parser_spec.rb +++ b/spec/cron_parser_spec.rb @@ -24,147 +24,303 @@ def parse_date(str) end end -describe "CronParser#next" do - [ - ["* * * * *", "2011-08-15 12:00", "2011-08-15 12:01",1], - ["* * * * *", "2011-08-15 02:25", "2011-08-15 02:26",1], - ["* * * * *", "2011-08-15 02:59", "2011-08-15 03:00",1], - ["*/15 * * * *", "2011-08-15 02:02", "2011-08-15 02:15",1], - ["*/15,25 * * * *", "2011-08-15 02:15", "2011-08-15 02:25",1], - ["30 3,6,9 * * *", "2011-08-15 02:15", "2011-08-15 03:30",1], - ["30 9 * * *", "2011-08-15 10:15", "2011-08-16 09:30",1], - ["30 9 * * *", "2011-08-31 10:15", "2011-09-01 09:30",1], - ["30 9 * * *", "2011-09-30 10:15", "2011-10-01 09:30",1], - ["0 9 * * *", "2011-12-31 10:15", "2012-01-01 09:00",1], - ["* * 12 * *", "2010-04-15 10:15", "2010-05-12 00:00",1], - ["* * * * 1,3", "2010-04-15 10:15", "2010-04-19 00:00",1], - ["* * * * MON,WED", "2010-04-15 10:15", "2010-04-19 00:00",1], - ["0 0 1 1 *", "2010-04-15 10:15", "2011-01-01 00:00",1], - ["0 0 * * 1", "2011-08-01 00:00", "2011-08-08 00:00",1], - ["0 0 * * 1", "2011-07-25 00:00", "2011-08-01 00:00",1], - ["45 23 7 3 *", "2011-01-01 00:00", "2011-03-07 23:45",1], - ["0 0 1 jun *", "2013-05-14 11:20", "2013-06-01 00:00",1], - ["0 0 1 may,jul *", "2013-05-14 15:00", "2013-07-01 00:00",1], - ["0 0 1 MAY,JUL *", "2013-05-14 15:00", "2013-07-01 00:00",1], - ["40 5 * * *", "2014-02-01 15:56", "2014-02-02 05:40",1], - ["0 5 * * 1", "2014-02-01 15:56", "2014-02-03 05:00",1], - ["10 8 15 * *", "2014-02-01 15:56", "2014-02-15 08:10",1], - ["50 6 * * 1", "2014-02-01 15:56", "2014-02-03 06:50",1], - ["1 2 * apr mOn", "2014-02-01 15:56", "2014-04-07 02:01",1], - ["1 2 3 4 7", "2014-02-01 15:56", "2014-04-03 02:01",1], - ["1 2 3 4 7", "2014-04-04 15:56", "2014-04-06 02:01",1], - ["1-20/3 * * * *", "2014-02-01 15:56", "2014-02-01 16:01",1], - ["1,2,3 * * * *", "2014-02-01 15:56", "2014-02-01 16:01",1], - ["1-9,15-30 * * * *", "2014-02-01 15:56", "2014-02-01 16:01",1], - ["1-9/3,15-30/4 * * * *", "2014-02-01 15:56", "2014-02-01 16:01",1], - ["1 2 3 jan mon", "2014-02-01 15:56", "2015-01-03 02:01",1], - ["1 2 3 4 mON", "2014-02-01 15:56", "2014-04-03 02:01",1], - ["1 2 3 jan 5", "2014-02-01 15:56", "2015-01-02 02:01",1], - ["@yearly", "2014-02-01 15:56", "2015-01-01 00:00",1], - ["@annually", "2014-02-01 15:56", "2015-01-01 00:00",1], - ["@monthly", "2014-02-01 15:56", "2014-03-01 00:00",1], - ["@weekly", "2014-02-01 15:56", "2014-02-02 00:00",1], - ["@daily", "2014-02-01 15:56", "2014-02-02 00:00",1], - ["@midnight", "2014-02-01 15:56", "2014-02-02 00:00",1], - ["@hourly", "2014-02-01 15:56", "2014-02-01 16:00",1], - ["*/3 * * * *", "2014-02-01 15:56", "2014-02-01 15:57",1], - ["0 5 * 2,3 *", "2014-02-01 15:56", "2014-02-02 05:00",1], - ["15-59/15 * * * *", "2014-02-01 15:56", "2014-02-01 16:15",1], - ["15-59/15 * * * *", "2014-02-01 15:00", "2014-02-01 15:15",1], - ["15-59/15 * * * *", "2014-02-01 15:01", "2014-02-01 15:15",1], - ["15-59/15 * * * *", "2014-02-01 15:16", "2014-02-01 15:30",1], - ["15-59/15 * * * *", "2014-02-01 15:26", "2014-02-01 15:30",1], - ["15-59/15 * * * *", "2014-02-01 15:36", "2014-02-01 15:45",1], - ["15-59/15 * * * *", "2014-02-01 15:45", "2014-02-01 16:15",4], - ["15-59/15 * * * *", "2014-02-01 15:46", "2014-02-01 16:15",3], - ["15-59/15 * * * *", "2014-02-01 15:46", "2014-02-01 16:15",2], - ].each do |line, now, expected_next,num| - it "returns #{expected_next} for '#{line}' when now is #{now}" do - parsed_now = parse_date(now) - expected = parse_date(expected_next) - parser = CronParser.new(line) - parser.next(parsed_now).xmlschema.should == expected.xmlschema +describe "Normal matching" do + describe "CronParser#next" do + [ + ["* * * * *", "2011-08-15 12:00", "2011-08-15 12:01",1], + ["* * * * *", "2011-08-15 02:25", "2011-08-15 02:26",1], + ["* * * * *", "2011-08-15 02:59", "2011-08-15 03:00",1], + ["*/15 * * * *", "2011-08-15 02:02", "2011-08-15 02:15",1], + ["*/15,25 * * * *", "2011-08-15 02:15", "2011-08-15 02:25",1], + ["30 3,6,9 * * *", "2011-08-15 02:15", "2011-08-15 03:30",1], + ["30 9 * * *", "2011-08-15 10:15", "2011-08-16 09:30",1], + ["30 9 * * *", "2011-08-31 10:15", "2011-09-01 09:30",1], + ["30 9 * * *", "2011-09-30 10:15", "2011-10-01 09:30",1], + ["0 9 * * *", "2011-12-31 10:15", "2012-01-01 09:00",1], + ["* * 12 * *", "2010-04-15 10:15", "2010-05-12 00:00",1], + ["* * * * 1,3", "2010-04-15 10:15", "2010-04-19 00:00",1], + ["* * * * MON,WED", "2010-04-15 10:15", "2010-04-19 00:00",1], + ["0 0 1 1 *", "2010-04-15 10:15", "2011-01-01 00:00",1], + ["0 0 * * 1", "2011-08-01 00:00", "2011-08-08 00:00",1], + ["0 0 * * 1", "2011-07-25 00:00", "2011-08-01 00:00",1], + ["45 23 7 3 *", "2011-01-01 00:00", "2011-03-07 23:45",1], + ["0 0 1 jun *", "2013-05-14 11:20", "2013-06-01 00:00",1], + ["0 0 1 may,jul *", "2013-05-14 15:00", "2013-07-01 00:00",1], + ["0 0 1 MAY,JUL *", "2013-05-14 15:00", "2013-07-01 00:00",1], + ["40 5 * * *", "2014-02-01 15:56", "2014-02-02 05:40",1], + ["0 5 * * 1", "2014-02-01 15:56", "2014-02-03 05:00",1], + ["10 8 15 * *", "2014-02-01 15:56", "2014-02-15 08:10",1], + ["50 6 * * 1", "2014-02-01 15:56", "2014-02-03 06:50",1], + ["1 2 * apr mOn", "2014-02-01 15:56", "2014-04-07 02:01",1], + ["1 2 3 4 7", "2014-02-01 15:56", "2014-04-03 02:01",1], + ["1 2 3 4 7", "2014-04-04 15:56", "2014-04-06 02:01",1], + ["1-20/3 * * * *", "2014-02-01 15:56", "2014-02-01 16:01",1], + ["1,2,3 * * * *", "2014-02-01 15:56", "2014-02-01 16:01",1], + ["1-9,15-30 * * * *", "2014-02-01 15:56", "2014-02-01 16:01",1], + ["1-9/3,15-30/4 * * * *", "2014-02-01 15:56", "2014-02-01 16:01",1], + ["1 2 3 jan mon", "2014-02-01 15:56", "2015-01-03 02:01",1], + ["1 2 3 4 mON", "2014-02-01 15:56", "2014-04-03 02:01",1], + ["1 2 3 jan 5", "2014-02-01 15:56", "2015-01-02 02:01",1], + ["@yearly", "2014-02-01 15:56", "2015-01-01 00:00",1], + ["@annually", "2014-02-01 15:56", "2015-01-01 00:00",1], + ["@monthly", "2014-02-01 15:56", "2014-03-01 00:00",1], + ["@weekly", "2014-02-01 15:56", "2014-02-02 00:00",1], + ["@daily", "2014-02-01 15:56", "2014-02-02 00:00",1], + ["@midnight", "2014-02-01 15:56", "2014-02-02 00:00",1], + ["@hourly", "2014-02-01 15:56", "2014-02-01 16:00",1], + ["*/3 * * * *", "2014-02-01 15:56", "2014-02-01 15:57",1], + ["0 5 * 2,3 *", "2014-02-01 15:56", "2014-02-02 05:00",1], + ["15-59/15 * * * *", "2014-02-01 15:56", "2014-02-01 16:15",1], + ["15-59/15 * * * *", "2014-02-01 15:00", "2014-02-01 15:15",1], + ["15-59/15 * * * *", "2014-02-01 15:01", "2014-02-01 15:15",1], + ["15-59/15 * * * *", "2014-02-01 15:16", "2014-02-01 15:30",1], + ["15-59/15 * * * *", "2014-02-01 15:26", "2014-02-01 15:30",1], + ["15-59/15 * * * *", "2014-02-01 15:36", "2014-02-01 15:45",1], + ["15-59/15 * * * *", "2014-02-01 15:45", "2014-02-01 16:15",4], + ["15-59/15 * * * *", "2014-02-01 15:46", "2014-02-01 16:15",3], + ["15-59/15 * * * *", "2014-02-01 15:46", "2014-02-01 16:15",2], + ].each do |line, now, expected_next,num| + it "returns #{expected_next} for '#{line}' when now is #{now}" do + parsed_now = parse_date(now) + expected = parse_date(expected_next) + parser = CronParser.new(line) + parser.next(parsed_now).xmlschema.should == expected.xmlschema + end + it "returns the expected class" do + parsed_now = parse_date(now) + expected = parse_date(expected_next) + parser = CronParser.new(line) + result = parser.next(parsed_now,num) + result.class.to_s.should == (num > 1 ? 'Array' : 'Time') + end + it "returns the expected count" do + parsed_now = parse_date(now) + expected = parse_date(expected_next) + parser = CronParser.new(line) + result = parser.next(parsed_now,num) + if result.class.to_s == 'Array' + result.size.should == num + else + result.class.to_s.should == 'Time' + end + end end - it "returns the expected class" do - parsed_now = parse_date(now) - expected = parse_date(expected_next) - parser = CronParser.new(line) - result = parser.next(parsed_now,num) - result.class.to_s.should == (num > 1 ? 'Array' : 'Time') + + describe "without strict matching" do + it "returns any friday OR 13th" do + upcoming = CronParser.new("0 1 13 * 5").next(Time.now, 10) + upcoming.all?{|t| t.mday == 13 || t.wday == 5}.should be true + end end - it "returns the expected count" do - parsed_now = parse_date(now) - expected = parse_date(expected_next) - parser = CronParser.new(line) - result = parser.next(parsed_now,num) - if result.class.to_s == 'Array' - result.size.should == num - else - result.class.to_s.should == 'Time' + describe "with strict matching" do + it "returns only friday the 13th" do + upcoming = CronParser.new("0 1 13 * 5", Time, true).next(Time.now, 10) + upcoming.all?{|t| t.mday == 13 && t.wday == 5}.should be true end end end -end -describe "CronParser#last" do - [ - ["* * * * *", "2011-08-15 12:00", "2011-08-15 11:59"], - ["* * * * *", "2011-08-15 02:25", "2011-08-15 02:24"], - ["* * * * *", "2011-08-15 03:00", "2011-08-15 02:59"], - ["*/15 * * * *", "2011-08-15 02:02", "2011-08-15 02:00"], - ["*/15,45 * * * *", "2011-08-15 02:55", "2011-08-15 02:45"], - ["*/15,25 * * * *", "2011-08-15 02:35", "2011-08-15 02:30"], - ["30 3,6,9 * * *", "2011-08-15 02:15", "2011-08-14 09:30"], - ["30 9 * * *", "2011-08-15 10:15", "2011-08-15 09:30"], - ["30 9 * * *", "2011-09-01 08:15", "2011-08-31 09:30"], - ["30 9 * * *", "2011-10-01 08:15", "2011-09-30 09:30"], - ["0 9 * * *", "2012-01-01 00:15", "2011-12-31 09:00"], - ["* * 12 * *", "2010-04-15 10:15", "2010-04-12 23:59"], - ["* * * * 1,3", "2010-04-15 10:15", "2010-04-14 23:59"], - ["* * * * MON,WED", "2010-04-15 10:15", "2010-04-14 23:59"], - ["0 0 1 1 *", "2010-04-15 10:15", "2010-01-01 00:00"], - ["0 0 1 jun *", "2013-05-14 11:20", "2012-06-01 00:00"], - ["0 0 1 may,jul *", "2013-05-14 15:00", "2013-05-01 00:00"], - ["0 0 1 MAY,JUL *", "2013-05-14 15:00", "2013-05-01 00:00"], - ["40 5 * * *", "2014-02-01 15:56", "2014-02-01 05:40"], - ["0 5 * * 1", "2014-02-01 15:56", "2014-01-27 05:00"], - ["10 8 15 * *", "2014-02-01 15:56", "2014-01-15 08:10"], - ["50 6 * * 1", "2014-02-01 15:56", "2014-01-27 06:50"], - ["1 2 * apr mOn", "2014-02-01 15:56", "2013-04-29 02:01"], - ["1 2 3 4 7", "2014-02-01 15:56", "2013-04-28 02:01"], - ["1 2 3 4 7", "2014-04-04 15:56", "2014-04-03 02:01"], - ["1-20/3 * * * *", "2014-02-01 15:56", "2014-02-01 15:19"], - ["1,2,3 * * * *", "2014-02-01 15:56", "2014-02-01 15:03"], - ["1-9,15-30 * * * *", "2014-02-01 15:56", "2014-02-01 15:30"], - ["1-9/3,15-30/4 * * * *", "2014-02-01 15:56", "2014-02-01 15:27"], - ["1 2 3 jan mon", "2014-02-01 15:56", "2014-01-27 02:01"], - ["1 2 3 4 mON", "2014-02-01 15:56", "2013-04-29 02:01"], - ["1 2 3 jan 5", "2014-02-01 15:56", "2014-01-31 02:01"], - ["@yearly", "2014-02-01 15:56", "2014-01-01 00:00"], - ["@annually", "2014-02-01 15:56", "2014-01-01 00:00"], - ["@monthly", "2014-02-01 15:56", "2014-02-01 00:00"], - ["@weekly", "2014-02-01 15:56", "2014-01-26 00:00"], - ["@daily", "2014-02-01 15:56", "2014-02-01 00:00"], - ["@midnight", "2014-02-01 15:56", "2014-02-01 00:00"], - ["@hourly", "2014-02-01 15:56", "2014-02-01 15:00"], - ["*/3 * * * *", "2014-02-01 15:56", "2014-02-01 15:54"], - ["0 5 * 2,3 *", "2014-02-01 15:56", "2014-02-01 05:00"], - ["15-59/15 * * * *", "2014-02-01 15:56", "2014-02-01 15:45"], - ["15-59/15 * * * *", "2014-02-01 15:00", "2014-02-01 14:45"], - ["15-59/15 * * * *", "2014-02-01 15:01", "2014-02-01 14:45"], - ["15-59/15 * * * *", "2014-02-01 15:16", "2014-02-01 15:15"], - ["15-59/15 * * * *", "2014-02-01 15:26", "2014-02-01 15:15"], - ["15-59/15 * * * *", "2014-02-01 15:36", "2014-02-01 15:30"], - ["15-59/15 * * * *", "2014-02-01 15:45", "2014-02-01 15:30"], - ["15-59/15 * * * *", "2014-02-01 15:46", "2014-02-01 15:45"], - ].each do |line, now, expected_next| - it "should return #{expected_next} for '#{line}' when now is #{now}" do - now = parse_date(now) - expected_next = parse_date(expected_next) + describe "CronParser#last" do + [ + ["* * * * *", "2011-08-15 12:00", "2011-08-15 11:59"], + ["* * * * *", "2011-08-15 02:25", "2011-08-15 02:24"], + ["* * * * *", "2011-08-15 03:00", "2011-08-15 02:59"], + ["*/15 * * * *", "2011-08-15 02:02", "2011-08-15 02:00"], + ["*/15,45 * * * *", "2011-08-15 02:55", "2011-08-15 02:45"], + ["*/15,25 * * * *", "2011-08-15 02:35", "2011-08-15 02:30"], + ["30 3,6,9 * * *", "2011-08-15 02:15", "2011-08-14 09:30"], + ["30 9 * * *", "2011-08-15 10:15", "2011-08-15 09:30"], + ["30 9 * * *", "2011-09-01 08:15", "2011-08-31 09:30"], + ["30 9 * * *", "2011-10-01 08:15", "2011-09-30 09:30"], + ["0 9 * * *", "2012-01-01 00:15", "2011-12-31 09:00"], + ["* * 12 * *", "2010-04-15 10:15", "2010-04-12 23:59"], + ["* * * * 1,3", "2010-04-15 10:15", "2010-04-14 23:59"], + ["* * * * MON,WED", "2010-04-15 10:15", "2010-04-14 23:59"], + ["0 0 1 1 *", "2010-04-15 10:15", "2010-01-01 00:00"], + ["0 0 1 jun *", "2013-05-14 11:20", "2012-06-01 00:00"], + ["0 0 1 may,jul *", "2013-05-14 15:00", "2013-05-01 00:00"], + ["0 0 1 MAY,JUL *", "2013-05-14 15:00", "2013-05-01 00:00"], + ["40 5 * * *", "2014-02-01 15:56", "2014-02-01 05:40"], + ["0 5 * * 1", "2014-02-01 15:56", "2014-01-27 05:00"], + ["10 8 15 * *", "2014-02-01 15:56", "2014-01-15 08:10"], + ["50 6 * * 1", "2014-02-01 15:56", "2014-01-27 06:50"], + ["1 2 * apr mOn", "2014-02-01 15:56", "2013-04-29 02:01"], + ["1 2 3 4 7", "2014-02-01 15:56", "2013-04-28 02:01"], + ["1 2 3 4 7", "2014-04-04 15:56", "2014-04-03 02:01"], + ["1-20/3 * * * *", "2014-02-01 15:56", "2014-02-01 15:19"], + ["1,2,3 * * * *", "2014-02-01 15:56", "2014-02-01 15:03"], + ["1-9,15-30 * * * *", "2014-02-01 15:56", "2014-02-01 15:30"], + ["1-9/3,15-30/4 * * * *", "2014-02-01 15:56", "2014-02-01 15:27"], + ["1 2 3 jan mon", "2014-02-01 15:56", "2014-01-27 02:01"], + ["1 2 3 4 mON", "2014-02-01 15:56", "2013-04-29 02:01"], + ["1 2 3 jan 5", "2014-02-01 15:56", "2014-01-31 02:01"], + ["@yearly", "2014-02-01 15:56", "2014-01-01 00:00"], + ["@annually", "2014-02-01 15:56", "2014-01-01 00:00"], + ["@monthly", "2014-02-01 15:56", "2014-02-01 00:00"], + ["@weekly", "2014-02-01 15:56", "2014-01-26 00:00"], + ["@daily", "2014-02-01 15:56", "2014-02-01 00:00"], + ["@midnight", "2014-02-01 15:56", "2014-02-01 00:00"], + ["@hourly", "2014-02-01 15:56", "2014-02-01 15:00"], + ["*/3 * * * *", "2014-02-01 15:56", "2014-02-01 15:54"], + ["0 5 * 2,3 *", "2014-02-01 15:56", "2014-02-01 05:00"], + ["15-59/15 * * * *", "2014-02-01 15:56", "2014-02-01 15:45"], + ["15-59/15 * * * *", "2014-02-01 15:00", "2014-02-01 14:45"], + ["15-59/15 * * * *", "2014-02-01 15:01", "2014-02-01 14:45"], + ["15-59/15 * * * *", "2014-02-01 15:16", "2014-02-01 15:15"], + ["15-59/15 * * * *", "2014-02-01 15:26", "2014-02-01 15:15"], + ["15-59/15 * * * *", "2014-02-01 15:36", "2014-02-01 15:30"], + ["15-59/15 * * * *", "2014-02-01 15:45", "2014-02-01 15:30"], + ["15-59/15 * * * *", "2014-02-01 15:46", "2014-02-01 15:45"], + ].each do |line, now, expected_next| + it "should return #{expected_next} for '#{line}' when now is #{now}" do + now = parse_date(now) + expected_next = parse_date(expected_next) + parser = CronParser.new(line) + parser.last(now).should == expected_next + end + end + end +end - parser = CronParser.new(line) +describe "Strict matching" do + describe "CronParser#next" do + [ + ["* * * * *", "2011-08-15 12:00", "2011-08-15 12:01",1], + ["* * * * *", "2011-08-15 02:25", "2011-08-15 02:26",1], + ["* * * * *", "2011-08-15 02:59", "2011-08-15 03:00",1], + ["*/15 * * * *", "2011-08-15 02:02", "2011-08-15 02:15",1], + ["*/15,25 * * * *", "2011-08-15 02:15", "2011-08-15 02:25",1], + ["30 3,6,9 * * *", "2011-08-15 02:15", "2011-08-15 03:30",1], + ["30 9 * * *", "2011-08-15 10:15", "2011-08-16 09:30",1], + ["30 9 * * *", "2011-08-31 10:15", "2011-09-01 09:30",1], + ["30 9 * * *", "2011-09-30 10:15", "2011-10-01 09:30",1], + ["0 9 * * *", "2011-12-31 10:15", "2012-01-01 09:00",1], + ["* * 12 * *", "2010-04-15 10:15", "2010-05-12 00:00",1], + ["* * * * 1,3", "2010-04-15 10:15", "2010-04-19 00:00",1], + ["* * * * MON,WED", "2010-04-15 10:15", "2010-04-19 00:00",1], + ["0 0 1 1 *", "2010-04-15 10:15", "2011-01-01 00:00",1], + ["0 0 * * 1", "2011-08-01 00:00", "2011-08-08 00:00",1], + ["0 0 * * 1", "2011-07-25 00:00", "2011-08-01 00:00",1], + ["45 23 7 3 *", "2011-01-01 00:00", "2011-03-07 23:45",1], + ["0 0 1 jun *", "2013-05-14 11:20", "2013-06-01 00:00",1], + ["0 0 1 may,jul *", "2013-05-14 15:00", "2013-07-01 00:00",1], + ["0 0 1 MAY,JUL *", "2013-05-14 15:00", "2013-07-01 00:00",1], + ["40 5 * * *", "2014-02-01 15:56", "2014-02-02 05:40",1], + ["0 5 * * 1", "2014-02-01 15:56", "2014-02-03 05:00",1], + ["10 8 15 * *", "2014-02-01 15:56", "2014-02-15 08:10",1], + ["50 6 * * 1", "2014-02-01 15:56", "2014-02-03 06:50",1], + ["1 2 * apr mOn", "2014-02-01 15:56", "2014-04-07 02:01",1], + ["1 2 3 4 7", "2014-02-01 15:56", "2016-04-03 02:01",1], + ["1-20/3 * * * *", "2014-02-01 15:56", "2014-02-01 16:01",1], + ["1,2,3 * * * *", "2014-02-01 15:56", "2014-02-01 16:01",1], + ["1-9,15-30 * * * *", "2014-02-01 15:56", "2014-02-01 16:01",1], + ["1-9/3,15-30/4 * * * *", "2014-02-01 15:56", "2014-02-01 16:01",1], + ["1 2 3 jan mon", "2014-02-01 15:56", "2022-01-03 02:01",1], + ["1 2 3 4 mON", "2014-02-01 15:56", "2017-04-03 02:01",1], + ["1 2 3 jan 5", "2014-02-01 15:56", "2020-01-03 02:01",1], + ["@yearly", "2014-02-01 15:56", "2015-01-01 00:00",1], + ["@annually", "2014-02-01 15:56", "2015-01-01 00:00",1], + ["@monthly", "2014-02-01 15:56", "2014-03-01 00:00",1], + ["@weekly", "2014-02-01 15:56", "2014-02-02 00:00",1], + ["@daily", "2014-02-01 15:56", "2014-02-02 00:00",1], + ["@midnight", "2014-02-01 15:56", "2014-02-02 00:00",1], + ["@hourly", "2014-02-01 15:56", "2014-02-01 16:00",1], + ["*/3 * * * *", "2014-02-01 15:56", "2014-02-01 15:57",1], + ["0 5 * 2,3 *", "2014-02-01 15:56", "2014-02-02 05:00",1], + ["15-59/15 * * * *", "2014-02-01 15:56", "2014-02-01 16:15",1], + ["15-59/15 * * * *", "2014-02-01 15:00", "2014-02-01 15:15",1], + ["15-59/15 * * * *", "2014-02-01 15:01", "2014-02-01 15:15",1], + ["15-59/15 * * * *", "2014-02-01 15:16", "2014-02-01 15:30",1], + ["15-59/15 * * * *", "2014-02-01 15:26", "2014-02-01 15:30",1], + ["15-59/15 * * * *", "2014-02-01 15:36", "2014-02-01 15:45",1], + ["15-59/15 * * * *", "2014-02-01 15:45", "2014-02-01 16:15",4], + ["15-59/15 * * * *", "2014-02-01 15:46", "2014-02-01 16:15",3], + ["15-59/15 * * * *", "2014-02-01 15:46", "2014-02-01 16:15",2], + ].each do |line, now, expected_next,num| + it "returns #{expected_next} for '#{line}' when now is #{now}" do + parsed_now = parse_date(now) + expected = parse_date(expected_next) + parser = CronParser.new(line,Time,true) + parser.next(parsed_now).xmlschema.should == expected.xmlschema + end + it "returns the expected class" do + parsed_now = parse_date(now) + expected = parse_date(expected_next) + parser = CronParser.new(line,Time,true) + result = parser.next(parsed_now,num) + result.class.to_s.should == (num > 1 ? 'Array' : 'Time') + end + it "returns the expected count" do + parsed_now = parse_date(now) + expected = parse_date(expected_next) + parser = CronParser.new(line,Time,true) + result = parser.next(parsed_now,num) + if result.class.to_s == 'Array' + result.size.should == num + else + result.class.to_s.should == 'Time' + end + end + end + end - parser.last(now).should == expected_next + describe "CronParser#last" do + [ + ["* * * * *", "2011-08-15 12:00", "2011-08-15 11:59"], + ["* * * * *", "2011-08-15 02:25", "2011-08-15 02:24"], + ["* * * * *", "2011-08-15 03:00", "2011-08-15 02:59"], + ["*/15 * * * *", "2011-08-15 02:02", "2011-08-15 02:00"], + ["*/15,45 * * * *", "2011-08-15 02:55", "2011-08-15 02:45"], + ["*/15,25 * * * *", "2011-08-15 02:35", "2011-08-15 02:30"], + ["30 3,6,9 * * *", "2011-08-15 02:15", "2011-08-14 09:30"], + ["30 9 * * *", "2011-08-15 10:15", "2011-08-15 09:30"], + ["30 9 * * *", "2011-09-01 08:15", "2011-08-31 09:30"], + ["30 9 * * *", "2011-10-01 08:15", "2011-09-30 09:30"], + ["0 9 * * *", "2012-01-01 00:15", "2011-12-31 09:00"], + ["* * 12 * *", "2010-04-15 10:15", "2010-04-12 23:59"], + ["* * * * 1,3", "2010-04-15 10:15", "2010-04-14 23:59"], + ["* * * * MON,WED", "2010-04-15 10:15", "2010-04-14 23:59"], + ["0 0 1 1 *", "2010-04-15 10:15", "2010-01-01 00:00"], + ["0 0 1 jun *", "2013-05-14 11:20", "2012-06-01 00:00"], + ["0 0 1 may,jul *", "2013-05-14 15:00", "2013-05-01 00:00"], + ["0 0 1 MAY,JUL *", "2013-05-14 15:00", "2013-05-01 00:00"], + ["40 5 * * *", "2014-02-01 15:56", "2014-02-01 05:40"], + ["0 5 * * 1", "2014-02-01 15:56", "2014-01-27 05:00"], + ["10 8 15 * *", "2014-02-01 15:56", "2014-01-15 08:10"], + ["50 6 * * 1", "2014-02-01 15:56", "2014-01-27 06:50"], + ["1 2 * apr mOn", "2014-02-01 15:56", "2013-04-29 02:01"], + ["1 2 3 4 7", "2014-02-01 15:56", "2011-04-03 02:01"], + ["1-20/3 * * * *", "2014-02-01 15:56", "2014-02-01 15:19"], + ["1,2,3 * * * *", "2014-02-01 15:56", "2014-02-01 15:03"], + ["1-9,15-30 * * * *", "2014-02-01 15:56", "2014-02-01 15:30"], + ["1-9/3,15-30/4 * * * *", "2014-02-01 15:56", "2014-02-01 15:27"], + ["1 2 3 jan mon", "2014-02-01 15:56", "2011-01-03 02:01"], + ["1 2 3 4 mON", "2014-02-01 15:56", "2006-04-03 02:01"], + ["1 2 3 jan 5", "2014-02-01 15:56", "2003-01-03 02:01"], + ["@yearly", "2014-02-01 15:56", "2014-01-01 00:00"], + ["@annually", "2014-02-01 15:56", "2014-01-01 00:00"], + ["@monthly", "2014-02-01 15:56", "2014-02-01 00:00"], + ["@weekly", "2014-02-01 15:56", "2014-01-26 00:00"], + ["@daily", "2014-02-01 15:56", "2014-02-01 00:00"], + ["@midnight", "2014-02-01 15:56", "2014-02-01 00:00"], + ["@hourly", "2014-02-01 15:56", "2014-02-01 15:00"], + ["*/3 * * * *", "2014-02-01 15:56", "2014-02-01 15:54"], + ["0 5 * 2,3 *", "2014-02-01 15:56", "2014-02-01 05:00"], + ["15-59/15 * * * *", "2014-02-01 15:56", "2014-02-01 15:45"], + ["15-59/15 * * * *", "2014-02-01 15:00", "2014-02-01 14:45"], + ["15-59/15 * * * *", "2014-02-01 15:01", "2014-02-01 14:45"], + ["15-59/15 * * * *", "2014-02-01 15:16", "2014-02-01 15:15"], + ["15-59/15 * * * *", "2014-02-01 15:26", "2014-02-01 15:15"], + ["15-59/15 * * * *", "2014-02-01 15:36", "2014-02-01 15:30"], + ["15-59/15 * * * *", "2014-02-01 15:45", "2014-02-01 15:30"], + ["15-59/15 * * * *", "2014-02-01 15:46", "2014-02-01 15:45"], + ].each do |line, now, expected_next| + it "should return #{expected_next} for '#{line}' when now is #{now}" do + now = parse_date(now) + expected_next = parse_date(expected_next) + parser = CronParser.new(line,Time,true) + parser.last(now).should == expected_next + end end end end From 8e31cd1152a60796d31cc8e3f02ded77e3da8dd2 Mon Sep 17 00:00:00 2001 From: Russell Jennings Date: Sun, 20 Mar 2016 18:15:57 -0400 Subject: [PATCH 3/5] updates readme --- README.md | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/README.md b/README.md index d40d529..8641614 100644 --- a/README.md +++ b/README.md @@ -14,5 +14,32 @@ next_time = cron_parser.next(Time.now) # Last occurrence most_recent_time = cron_parser.last(Time.now) + +# next 5 occurences +next_times = cron_parser.next(Time.now,5) + +# last 5 occurences +previous_times = cron_parser.last(Time.now,5) +``` + +## Strict Matching AKA Friday the 13th matching + +From the crontab manpage: +``` +If both fields are restricted (i.e., aren't '*'), the command will be run when either field matches the current time. For example, ``30 4 1,15 * 5'' would cause a command to be run at 4:30 am on the 1st and 15th of each month, plus every Friday. ``` +While this is the designed behavior, Some might not find it to be desireable or expected. If you want to match based on Day of Week AND Day of month, you can turn on strict matching with a parameter when you initialize. + +Let's take a look at the difference between the two modes. + +``` +# Normal behavior +CronParser.new('0 1 13 * 5', Time).next(Time.now, 4) +=> [Fri 25 Mar 2016, Fri 01 Apr 2016, Fri 08 Apr 2016, Wed 13 Apr 2016] + +# Strict Matching +CronParser.new('0 1 13 * 5', Time,true).next(Time.now, 4) +=> [Fri 13 May 2016, Fri 13 Jan 2017, Fri 13 Oct 2017, Fri, 13 Apr 2018] + +``` \ No newline at end of file From 53cd5688831165232e155fc47cac2d050e75346d Mon Sep 17 00:00:00 2001 From: Russell Jennings Date: Sun, 20 Mar 2016 18:29:16 -0400 Subject: [PATCH 4/5] removes draft spec --- spec/cron_parser_spec.rb | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/spec/cron_parser_spec.rb b/spec/cron_parser_spec.rb index 99a3eb2..6c884b8 100644 --- a/spec/cron_parser_spec.rb +++ b/spec/cron_parser_spec.rb @@ -105,19 +105,6 @@ def parse_date(str) end end end - - describe "without strict matching" do - it "returns any friday OR 13th" do - upcoming = CronParser.new("0 1 13 * 5").next(Time.now, 10) - upcoming.all?{|t| t.mday == 13 || t.wday == 5}.should be true - end - end - describe "with strict matching" do - it "returns only friday the 13th" do - upcoming = CronParser.new("0 1 13 * 5", Time, true).next(Time.now, 10) - upcoming.all?{|t| t.mday == 13 && t.wday == 5}.should be true - end - end end describe "CronParser#last" do From 7899c97370b40cc966dbda20f09145906581454e Mon Sep 17 00:00:00 2001 From: Russell Jennings Date: Sat, 16 Apr 2016 15:12:43 -0400 Subject: [PATCH 5/5] replaces inverted if statements with unless --- lib/cron_parser.rb | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/lib/cron_parser.rb b/lib/cron_parser.rb index 980f1ed..3d9a48c 100644 --- a/lib/cron_parser.rb +++ b/lib/cron_parser.rb @@ -80,17 +80,17 @@ def interpret_vixieisms(spec) # returns the next occurence after the given date def next(now = @time_source.now, num = 1) t = InternalTime.new(now, @time_source) - if !time_specs[:month][0].include?(t.month) + unless time_specs[:month][0].include?(t.month) nudge_month(t) t.day = 0 end - if !interpolate_weekdays(t.year, t.month)[0].include?(t.day) + unless interpolate_weekdays(t.year, t.month)[0].include?(t.day) nudge_date(t) t.hour = -1 end - - if !time_specs[:hour][0].include?(t.hour) + + unless time_specs[:hour][0].include?(t.hour) nudge_hour(t) t.min = -1 end @@ -183,8 +183,8 @@ def interpolate_weekdays_without_cache(year, month) # Careful, if both DOW and DOM fields are non-wildcard, # then we only need to match *one* for cron to run the job: - if !strict_match? - if not (mday_field == '*' and wday_field == '*') + unless strict_match? + unless (mday_field == '*' and wday_field == '*') valid_mday = [] if mday_field == '*' valid_wday = [] if wday_field == '*' end @@ -226,7 +226,7 @@ def nudge_month(t, dir = :next) valid_days = interpolate_weekdays(t.year, t.month)[1] t.day = dir == :next ? valid_days.first : valid_days.last end - + end def date_valid?(t, dir = :next)