├── .rspec ├── .gitignore ├── .travis.yml ├── lib ├── parse-cron │ └── version.rb ├── parse-cron.rb └── cron_parser.rb ├── Gemfile ├── spec ├── spec_helper.rb └── cron_parser_spec.rb ├── Rakefile ├── README.md ├── parse-cron.gemspec └── License /.rspec: -------------------------------------------------------------------------------- 1 | --color 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.gem 2 | *.swp 3 | .bundle 4 | Gemfile.lock 5 | pkg/* 6 | .rvmrc 7 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: ruby 2 | rvm: 3 | - "2.1.8" 4 | - "2.2.4" 5 | - "2.3.0" 6 | -------------------------------------------------------------------------------- /lib/parse-cron/version.rb: -------------------------------------------------------------------------------- 1 | module Parse 2 | module Cron 3 | VERSION = "0.1.4" 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /lib/parse-cron.rb: -------------------------------------------------------------------------------- 1 | # More logical way to require 'cron_parser' 2 | require File.join(File.dirname(__FILE__), 'cron_parser') 3 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "http://rubygems.org" 2 | 3 | # Specify your gem's dependencies in parse-cron.gemspec 4 | gemspec 5 | 6 | gem "ZenTest", "4.6.0" 7 | gem "rake" 8 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | spec_dir = File.dirname(__FILE__) 2 | lib_dir = File.expand_path(File.join(spec_dir, '..', 'lib')) 3 | $:.unshift(lib_dir) 4 | $:.uniq! 5 | 6 | RSpec.configure do |config| 7 | end 8 | 9 | require 'cron_parser' 10 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'bundler' 2 | Bundler::GemHelper.install_tasks 3 | 4 | require 'rspec/core/rake_task' 5 | RSpec::Core::RakeTask.new 6 | task :default => :spec 7 | 8 | desc 'Start IRB with preloaded environment' 9 | task :console do 10 | exec 'irb', "-I#{File.join(File.dirname(__FILE__), 'lib')}", '-rparse-cron' 11 | end 12 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Parse-Cron 2 | ## Parse crontab syntax to determine scheduled run times [![Build Status](https://travis-ci.org/siebertm/parse-cron.png)](https://travis-ci.org/siebertm/parse-cron) 3 | 4 | The goal of this gem is to parse a crontab timing specification and determine when the 5 | job should be run. It is not a scheduler, it does not run the jobs. 6 | 7 | ## API example 8 | 9 | ``` 10 | cron_parser = CronParser.new('30 * * * *') 11 | 12 | # Next occurrence 13 | next_time = cron_parser.next(Time.now) 14 | 15 | # Last occurrence 16 | most_recent_time = cron_parser.last(Time.now) 17 | ``` 18 | 19 | -------------------------------------------------------------------------------- /parse-cron.gemspec: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | $:.push File.expand_path("../lib", __FILE__) 3 | require "parse-cron/version" 4 | 5 | Gem::Specification.new do |s| 6 | s.name = "parse-cron" 7 | s.version = Parse::Cron::VERSION 8 | s.platform = Gem::Platform::RUBY 9 | s.authors = ["Michael Siebert"] 10 | s.email = ["siebertm85@googlemail.com"] 11 | s.homepage = "https://github.com/siebertm/parse-cron" 12 | s.summary = %q{Parses cron expressions and calculates the next occurence} 13 | s.description = %q{Parses cron expressions and calculates the next occurence} 14 | 15 | s.rubyforge_project = "parse-cron" 16 | 17 | s.files = `git ls-files`.split("\n") 18 | s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n") 19 | s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) } 20 | s.require_paths = ["lib"] 21 | 22 | s.add_development_dependency 'rspec', '~>2.6.0' 23 | end 24 | -------------------------------------------------------------------------------- /License: -------------------------------------------------------------------------------- 1 | Copyright (C) 2013 Michael Siebert 2 | 3 | Permission is hereby granted, free of charge, to any person 4 | obtaining a copy of this software and associated documentation 5 | files (the "Software"), to deal in the Software without restriction, 6 | including without limitation the rights to use, copy, modify, 7 | merge, publish, distribute, sublicense, and/or sell copies of 8 | the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall 12 | be included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 16 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 18 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 19 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 20 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 21 | OTHER DEALINGS IN THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /lib/cron_parser.rb: -------------------------------------------------------------------------------- 1 | require 'set' 2 | require 'date' 3 | 4 | # Parses cron expressions and computes the next occurence of the "job" 5 | # 6 | class CronParser 7 | # internal "mutable" time representation 8 | class InternalTime 9 | attr_accessor :year, :month, :day, :hour, :min 10 | attr_accessor :time_source 11 | 12 | def initialize(time,time_source = Time) 13 | @year = time.year 14 | @month = time.month 15 | @day = time.day 16 | @hour = time.hour 17 | @min = time.min 18 | 19 | @time_source = time_source 20 | end 21 | 22 | def to_time 23 | time_source.local(@year, @month, @day, @hour, @min, 0) 24 | end 25 | 26 | def inspect 27 | [year, month, day, hour, min].inspect 28 | end 29 | end 30 | 31 | SYMBOLS = { 32 | "jan" => "1", 33 | "feb" => "2", 34 | "mar" => "3", 35 | "apr" => "4", 36 | "may" => "5", 37 | "jun" => "6", 38 | "jul" => "7", 39 | "aug" => "8", 40 | "sep" => "9", 41 | "oct" => "10", 42 | "nov" => "11", 43 | "dec" => "12", 44 | 45 | "sun" => "0", 46 | "mon" => "1", 47 | "tue" => "2", 48 | "wed" => "3", 49 | "thu" => "4", 50 | "fri" => "5", 51 | "sat" => "6" 52 | } 53 | 54 | def initialize(source,time_source = Time) 55 | @source = interpret_vixieisms(source) 56 | @time_source = time_source 57 | validate_source 58 | end 59 | 60 | def interpret_vixieisms(spec) 61 | case spec 62 | when '@reboot' 63 | raise ArgumentError, "Can't predict last/next run of @reboot" 64 | when '@yearly', '@annually' 65 | '0 0 1 1 *' 66 | when '@monthly' 67 | '0 0 1 * *' 68 | when '@weekly' 69 | '0 0 * * 0' 70 | when '@daily', '@midnight' 71 | '0 0 * * *' 72 | when '@hourly' 73 | '0 * * * *' 74 | else 75 | spec 76 | end 77 | end 78 | 79 | 80 | # returns the next occurence after the given date 81 | def next(now = @time_source.now, num = 1) 82 | t = InternalTime.new(now, @time_source) 83 | 84 | unless time_specs[:month][0].include?(t.month) 85 | nudge_month(t) 86 | t.day = 0 87 | end 88 | 89 | unless interpolate_weekdays(t.year, t.month)[0].include?(t.day) 90 | nudge_date(t) 91 | t.hour = -1 92 | end 93 | 94 | unless time_specs[:hour][0].include?(t.hour) 95 | nudge_hour(t) 96 | t.min = -1 97 | end 98 | 99 | # always nudge the minute 100 | nudge_minute(t) 101 | t = t.to_time 102 | if num > 1 103 | recursive_calculate(:next,t,num) 104 | else 105 | t 106 | end 107 | end 108 | 109 | # returns the last occurence before the given date 110 | def last(now = @time_source.now, num=1) 111 | t = InternalTime.new(now,@time_source) 112 | 113 | unless time_specs[:month][0].include?(t.month) 114 | nudge_month(t, :last) 115 | t.day = 32 116 | end 117 | 118 | if t.day == 32 || !interpolate_weekdays(t.year, t.month)[0].include?(t.day) 119 | nudge_date(t, :last) 120 | t.hour = 24 121 | end 122 | 123 | unless time_specs[:hour][0].include?(t.hour) 124 | nudge_hour(t, :last) 125 | t.min = 60 126 | end 127 | 128 | # always nudge the minute 129 | nudge_minute(t, :last) 130 | t = t.to_time 131 | if num > 1 132 | recursive_calculate(:last,t,num) 133 | else 134 | t 135 | end 136 | end 137 | 138 | 139 | SUBELEMENT_REGEX = %r{^(\d+)(-(\d+)(/(\d+))?)?$} 140 | def parse_element(elem, allowed_range) 141 | values = elem.split(',').map do |subel| 142 | if subel =~ /^\*/ 143 | step = subel.length > 1 ? subel[2..-1].to_i : 1 144 | stepped_range(allowed_range, step) 145 | else 146 | if SUBELEMENT_REGEX === subel 147 | if $5 # with range 148 | stepped_range($1.to_i..$3.to_i, $5.to_i) 149 | elsif $3 # range without step 150 | stepped_range($1.to_i..$3.to_i, 1) 151 | else # just a numeric 152 | [$1.to_i] 153 | end 154 | else 155 | raise ArgumentError, "Bad Vixie-style specification #{subel}" 156 | end 157 | end 158 | end.flatten.sort 159 | 160 | [Set.new(values), values, elem] 161 | end 162 | 163 | 164 | protected 165 | 166 | def recursive_calculate(meth,time,num) 167 | array = [time] 168 | num.-(1).times do |num| 169 | array << self.send(meth, array.last) 170 | end 171 | array 172 | end 173 | 174 | # returns a list of days which do both match time_spec[:dom] or time_spec[:dow] 175 | def interpolate_weekdays(year, month) 176 | @_interpolate_weekdays_cache ||= {} 177 | @_interpolate_weekdays_cache["#{year}-#{month}"] ||= interpolate_weekdays_without_cache(year, month) 178 | end 179 | 180 | def interpolate_weekdays_without_cache(year, month) 181 | t = Date.new(year, month, 1) 182 | valid_mday, _, mday_field = time_specs[:dom] 183 | valid_wday, _, wday_field = time_specs[:dow] 184 | 185 | # Careful, if both DOW and DOM fields are non-wildcard, 186 | # then we only need to match *one* for cron to run the job: 187 | if not (mday_field == '*' and wday_field == '*') 188 | valid_mday = [] if mday_field == '*' 189 | valid_wday = [] if wday_field == '*' 190 | end 191 | # Careful: crontabs may use either 0 or 7 for Sunday: 192 | valid_wday << 0 if valid_wday.include?(7) 193 | 194 | result = [] 195 | while t.month == month 196 | result << t.mday if valid_mday.include?(t.mday) || valid_wday.include?(t.wday) 197 | t = t.succ 198 | end 199 | 200 | [Set.new(result), result] 201 | end 202 | 203 | def nudge_year(t, dir = :next) 204 | t.year = t.year + (dir == :next ? 1 : -1) 205 | end 206 | 207 | def nudge_month(t, dir = :next) 208 | spec = time_specs[:month][1] 209 | next_value = find_best_next(t.month, spec, dir) 210 | t.month = next_value || (dir == :next ? spec.first : spec.last) 211 | 212 | nudge_year(t, dir) if next_value.nil? 213 | 214 | # we changed the month, so its likely that the date is incorrect now 215 | valid_days = interpolate_weekdays(t.year, t.month)[1] 216 | t.day = dir == :next ? valid_days.first : valid_days.last 217 | end 218 | 219 | def date_valid?(t, dir = :next) 220 | interpolate_weekdays(t.year, t.month)[0].include?(t.day) 221 | end 222 | 223 | def nudge_date(t, dir = :next, can_nudge_month = true) 224 | spec = interpolate_weekdays(t.year, t.month)[1] 225 | next_value = find_best_next(t.day, spec, dir) 226 | t.day = next_value || (dir == :next ? spec.first : spec.last) 227 | 228 | nudge_month(t, dir) if next_value.nil? && can_nudge_month 229 | end 230 | 231 | def nudge_hour(t, dir = :next) 232 | spec = time_specs[:hour][1] 233 | next_value = find_best_next(t.hour, spec, dir) 234 | t.hour = next_value || (dir == :next ? spec.first : spec.last) 235 | 236 | nudge_date(t, dir) if next_value.nil? 237 | end 238 | 239 | def nudge_minute(t, dir = :next) 240 | spec = time_specs[:minute][1] 241 | next_value = find_best_next(t.min, spec, dir) 242 | t.min = next_value || (dir == :next ? spec.first : spec.last) 243 | 244 | nudge_hour(t, dir) if next_value.nil? 245 | end 246 | 247 | def time_specs 248 | @time_specs ||= begin 249 | # tokens now contains the 5 fields 250 | tokens = substitute_parse_symbols(@source).split(/\s+/) 251 | { 252 | :minute => parse_element(tokens[0], 0..59), #minute 253 | :hour => parse_element(tokens[1], 0..23), #hour 254 | :dom => parse_element(tokens[2], 1..31), #DOM 255 | :month => parse_element(tokens[3], 1..12), #mon 256 | :dow => parse_element(tokens[4], 0..6) #DOW 257 | } 258 | end 259 | end 260 | 261 | def substitute_parse_symbols(str) 262 | SYMBOLS.inject(str.downcase) do |s, (symbol, replacement)| 263 | s.gsub(symbol, replacement) 264 | end 265 | end 266 | 267 | 268 | def stepped_range(rng, step = 1) 269 | len = rng.last - rng.first 270 | 271 | num = len.div(step) 272 | result = (0..num).map { |i| rng.first + step * i } 273 | 274 | result.pop if result[-1] == rng.last and rng.exclude_end? 275 | result 276 | end 277 | 278 | 279 | # returns the smallest element from allowed which is greater than current 280 | # returns nil if no matching value was found 281 | def find_best_next(current, allowed, dir) 282 | if dir == :next 283 | allowed.sort.find { |val| val > current } 284 | else 285 | allowed.sort.reverse.find { |val| val < current } 286 | end 287 | end 288 | 289 | def validate_source 290 | unless @source.respond_to?(:split) 291 | raise ArgumentError, 'not a valid cronline' 292 | end 293 | source_length = @source.split(/\s+/).length 294 | unless source_length >= 5 && source_length <= 6 295 | raise ArgumentError, 'not a valid cronline' 296 | end 297 | end 298 | end 299 | -------------------------------------------------------------------------------- /spec/cron_parser_spec.rb: -------------------------------------------------------------------------------- 1 | require "time" 2 | require "./spec/spec_helper" 3 | require "cron_parser" 4 | require "date" 5 | 6 | def parse_date(str) 7 | dt = DateTime.strptime(str, "%Y-%m-%d %H:%M") 8 | Time.local(dt.year, dt.month, dt.day, dt.hour, dt.min, 0) 9 | end 10 | 11 | describe "CronParser#parse_element" do 12 | [ 13 | ["*", 0..59, (0..59).to_a], 14 | ["*/10", 0..59, [0, 10, 20, 30, 40, 50]], 15 | ["10", 0..59, [10]], 16 | ["10,30", 0..59, [10, 30]], 17 | ["10-15", 0..59, [10, 11, 12, 13, 14, 15]], 18 | ["10-40/10", 0..59, [10, 20, 30, 40]], 19 | ].each do |element, range, expected| 20 | it "should return #{expected} for '#{element}' when range is #{range}" do 21 | parser = CronParser.new('* * * * *') 22 | parser.parse_element(element, range).first.to_a.sort.should == expected.sort 23 | end 24 | end 25 | end 26 | 27 | describe "CronParser#next" do 28 | [ 29 | ["* * * * *", "2011-08-15 12:00", "2011-08-15 12:01",1], 30 | ["* * * * *", "2011-08-15 02:25", "2011-08-15 02:26",1], 31 | ["* * * * *", "2011-08-15 02:59", "2011-08-15 03:00",1], 32 | ["*/15 * * * *", "2011-08-15 02:02", "2011-08-15 02:15",1], 33 | ["*/15,25 * * * *", "2011-08-15 02:15", "2011-08-15 02:25",1], 34 | ["30 3,6,9 * * *", "2011-08-15 02:15", "2011-08-15 03:30",1], 35 | ["30 9 * * *", "2011-08-15 10:15", "2011-08-16 09:30",1], 36 | ["30 9 * * *", "2011-08-31 10:15", "2011-09-01 09:30",1], 37 | ["30 9 * * *", "2011-09-30 10:15", "2011-10-01 09:30",1], 38 | ["0 9 * * *", "2011-12-31 10:15", "2012-01-01 09:00",1], 39 | ["* * 12 * *", "2010-04-15 10:15", "2010-05-12 00:00",1], 40 | ["* * * * 1,3", "2010-04-15 10:15", "2010-04-19 00:00",1], 41 | ["* * * * MON,WED", "2010-04-15 10:15", "2010-04-19 00:00",1], 42 | ["0 0 1 1 *", "2010-04-15 10:15", "2011-01-01 00:00",1], 43 | ["0 0 * * 1", "2011-08-01 00:00", "2011-08-08 00:00",1], 44 | ["0 0 * * 1", "2011-07-25 00:00", "2011-08-01 00:00",1], 45 | ["45 23 7 3 *", "2011-01-01 00:00", "2011-03-07 23:45",1], 46 | ["0 0 1 jun *", "2013-05-14 11:20", "2013-06-01 00:00",1], 47 | ["0 0 1 may,jul *", "2013-05-14 15:00", "2013-07-01 00:00",1], 48 | ["0 0 1 MAY,JUL *", "2013-05-14 15:00", "2013-07-01 00:00",1], 49 | ["40 5 * * *", "2014-02-01 15:56", "2014-02-02 05:40",1], 50 | ["0 5 * * 1", "2014-02-01 15:56", "2014-02-03 05:00",1], 51 | ["10 8 15 * *", "2014-02-01 15:56", "2014-02-15 08:10",1], 52 | ["50 6 * * 1", "2014-02-01 15:56", "2014-02-03 06:50",1], 53 | ["1 2 * apr mOn", "2014-02-01 15:56", "2014-04-07 02:01",1], 54 | ["1 2 3 4 7", "2014-02-01 15:56", "2014-04-03 02:01",1], 55 | ["1 2 3 4 7", "2014-04-04 15:56", "2014-04-06 02:01",1], 56 | ["1-20/3 * * * *", "2014-02-01 15:56", "2014-02-01 16:01",1], 57 | ["1,2,3 * * * *", "2014-02-01 15:56", "2014-02-01 16:01",1], 58 | ["1-9,15-30 * * * *", "2014-02-01 15:56", "2014-02-01 16:01",1], 59 | ["1-9/3,15-30/4 * * * *", "2014-02-01 15:56", "2014-02-01 16:01",1], 60 | ["1 2 3 jan mon", "2014-02-01 15:56", "2015-01-03 02:01",1], 61 | ["1 2 3 4 mON", "2014-02-01 15:56", "2014-04-03 02:01",1], 62 | ["1 2 3 jan 5", "2014-02-01 15:56", "2015-01-02 02:01",1], 63 | ["@yearly", "2014-02-01 15:56", "2015-01-01 00:00",1], 64 | ["@annually", "2014-02-01 15:56", "2015-01-01 00:00",1], 65 | ["@monthly", "2014-02-01 15:56", "2014-03-01 00:00",1], 66 | ["@weekly", "2014-02-01 15:56", "2014-02-02 00:00",1], 67 | ["@daily", "2014-02-01 15:56", "2014-02-02 00:00",1], 68 | ["@midnight", "2014-02-01 15:56", "2014-02-02 00:00",1], 69 | ["@hourly", "2014-02-01 15:56", "2014-02-01 16:00",1], 70 | ["*/3 * * * *", "2014-02-01 15:56", "2014-02-01 15:57",1], 71 | ["0 5 * 2,3 *", "2014-02-01 15:56", "2014-02-02 05:00",1], 72 | ["15-59/15 * * * *", "2014-02-01 15:56", "2014-02-01 16:15",1], 73 | ["15-59/15 * * * *", "2014-02-01 15:00", "2014-02-01 15:15",1], 74 | ["15-59/15 * * * *", "2014-02-01 15:01", "2014-02-01 15:15",1], 75 | ["15-59/15 * * * *", "2014-02-01 15:16", "2014-02-01 15:30",1], 76 | ["15-59/15 * * * *", "2014-02-01 15:26", "2014-02-01 15:30",1], 77 | ["15-59/15 * * * *", "2014-02-01 15:36", "2014-02-01 15:45",1], 78 | ["15-59/15 * * * *", "2014-02-01 15:45", "2014-02-01 16:15",4], 79 | ["15-59/15 * * * *", "2014-02-01 15:46", "2014-02-01 16:15",3], 80 | ["15-59/15 * * * *", "2014-02-01 15:46", "2014-02-01 16:15",2], 81 | ].each do |line, now, expected_next,num| 82 | it "returns #{expected_next} for '#{line}' when now is #{now}" do 83 | parsed_now = parse_date(now) 84 | expected = parse_date(expected_next) 85 | parser = CronParser.new(line) 86 | parser.next(parsed_now).xmlschema.should == expected.xmlschema 87 | end 88 | it "returns the expected class" do 89 | parsed_now = parse_date(now) 90 | expected = parse_date(expected_next) 91 | parser = CronParser.new(line) 92 | result = parser.next(parsed_now,num) 93 | result.class.to_s.should == (num > 1 ? 'Array' : 'Time') 94 | end 95 | it "returns the expected count" do 96 | parsed_now = parse_date(now) 97 | expected = parse_date(expected_next) 98 | parser = CronParser.new(line) 99 | result = parser.next(parsed_now,num) 100 | if result.class.to_s == 'Array' 101 | result.size.should == num 102 | else 103 | result.class.to_s.should == 'Time' 104 | end 105 | end 106 | end 107 | end 108 | 109 | describe "CronParser#last" do 110 | [ 111 | ["* * * * *", "2011-08-15 12:00", "2011-08-15 11:59"], 112 | ["* * * * *", "2011-08-15 02:25", "2011-08-15 02:24"], 113 | ["* * * * *", "2011-08-15 03:00", "2011-08-15 02:59"], 114 | ["*/15 * * * *", "2011-08-15 02:02", "2011-08-15 02:00"], 115 | ["*/15,45 * * * *", "2011-08-15 02:55", "2011-08-15 02:45"], 116 | ["*/15,25 * * * *", "2011-08-15 02:35", "2011-08-15 02:30"], 117 | ["30 3,6,9 * * *", "2011-08-15 02:15", "2011-08-14 09:30"], 118 | ["30 9 * * *", "2011-08-15 10:15", "2011-08-15 09:30"], 119 | ["30 9 * * *", "2011-09-01 08:15", "2011-08-31 09:30"], 120 | ["30 9 * * *", "2011-10-01 08:15", "2011-09-30 09:30"], 121 | ["0 9 * * *", "2012-01-01 00:15", "2011-12-31 09:00"], 122 | ["* * 12 * *", "2010-04-15 10:15", "2010-04-12 23:59"], 123 | ["* * * * 1,3", "2010-04-15 10:15", "2010-04-14 23:59"], 124 | ["* * * * MON,WED", "2010-04-15 10:15", "2010-04-14 23:59"], 125 | ["0 0 1 1 *", "2010-04-15 10:15", "2010-01-01 00:00"], 126 | ["0 0 1 jun *", "2013-05-14 11:20", "2012-06-01 00:00"], 127 | ["0 0 1 may,jul *", "2013-05-14 15:00", "2013-05-01 00:00"], 128 | ["0 0 1 MAY,JUL *", "2013-05-14 15:00", "2013-05-01 00:00"], 129 | ["40 5 * * *", "2014-02-01 15:56", "2014-02-01 05:40"], 130 | ["0 5 * * 1", "2014-02-01 15:56", "2014-01-27 05:00"], 131 | ["10 8 15 * *", "2014-02-01 15:56", "2014-01-15 08:10"], 132 | ["50 6 * * 1", "2014-02-01 15:56", "2014-01-27 06:50"], 133 | ["1 2 * apr mOn", "2014-02-01 15:56", "2013-04-29 02:01"], 134 | ["1 2 3 4 7", "2014-02-01 15:56", "2013-04-28 02:01"], 135 | ["1 2 3 4 7", "2014-04-04 15:56", "2014-04-03 02:01"], 136 | ["1-20/3 * * * *", "2014-02-01 15:56", "2014-02-01 15:19"], 137 | ["1,2,3 * * * *", "2014-02-01 15:56", "2014-02-01 15:03"], 138 | ["1-9,15-30 * * * *", "2014-02-01 15:56", "2014-02-01 15:30"], 139 | ["1-9/3,15-30/4 * * * *", "2014-02-01 15:56", "2014-02-01 15:27"], 140 | ["1 2 3 jan mon", "2014-02-01 15:56", "2014-01-27 02:01"], 141 | ["1 2 3 4 mON", "2014-02-01 15:56", "2013-04-29 02:01"], 142 | ["1 2 3 jan 5", "2014-02-01 15:56", "2014-01-31 02:01"], 143 | ["@yearly", "2014-02-01 15:56", "2014-01-01 00:00"], 144 | ["@annually", "2014-02-01 15:56", "2014-01-01 00:00"], 145 | ["@monthly", "2014-02-01 15:56", "2014-02-01 00:00"], 146 | ["@weekly", "2014-02-01 15:56", "2014-01-26 00:00"], 147 | ["@daily", "2014-02-01 15:56", "2014-02-01 00:00"], 148 | ["@midnight", "2014-02-01 15:56", "2014-02-01 00:00"], 149 | ["@hourly", "2014-02-01 15:56", "2014-02-01 15:00"], 150 | ["*/3 * * * *", "2014-02-01 15:56", "2014-02-01 15:54"], 151 | ["0 5 * 2,3 *", "2014-02-01 15:56", "2014-02-01 05:00"], 152 | ["15-59/15 * * * *", "2014-02-01 15:56", "2014-02-01 15:45"], 153 | ["15-59/15 * * * *", "2014-02-01 15:00", "2014-02-01 14:45"], 154 | ["15-59/15 * * * *", "2014-02-01 15:01", "2014-02-01 14:45"], 155 | ["15-59/15 * * * *", "2014-02-01 15:16", "2014-02-01 15:15"], 156 | ["15-59/15 * * * *", "2014-02-01 15:26", "2014-02-01 15:15"], 157 | ["15-59/15 * * * *", "2014-02-01 15:36", "2014-02-01 15:30"], 158 | ["15-59/15 * * * *", "2014-02-01 15:45", "2014-02-01 15:30"], 159 | ["15-59/15 * * * *", "2014-02-01 15:46", "2014-02-01 15:45"], 160 | ].each do |line, now, expected_next| 161 | it "should return #{expected_next} for '#{line}' when now is #{now}" do 162 | now = parse_date(now) 163 | expected_next = parse_date(expected_next) 164 | 165 | parser = CronParser.new(line) 166 | 167 | parser.last(now).should == expected_next 168 | end 169 | end 170 | end 171 | 172 | describe "CronParser#new" do 173 | it 'should not raise error when given a valid cronline' do 174 | expect { CronParser.new('30 * * * *') }.not_to raise_error 175 | end 176 | 177 | it 'should raise error when given an invalid cronline' do 178 | expect { CronParser.new('* * * *') }.to raise_error('not a valid cronline') 179 | end 180 | end 181 | 182 | describe "time source" do 183 | it "should use an alternate specified time source" do 184 | ExtendedTime = Class.new(Time) 185 | ExtendedTime.should_receive(:local).once 186 | CronParser.new("* * * * *",ExtendedTime).next 187 | end 188 | end 189 | --------------------------------------------------------------------------------