parse-cron-0.1.4/0000755000004100000410000000000012554717216013627 5ustar www-datawww-dataparse-cron-0.1.4/Rakefile0000644000004100000410000000042412554717216015274 0ustar www-datawww-datarequire 'bundler' Bundler::GemHelper.install_tasks require 'rspec/core/rake_task' RSpec::Core::RakeTask.new task :default => :spec desc 'Start IRB with preloaded environment' task :console do exec 'irb', "-I#{File.join(File.dirname(__FILE__), 'lib')}", '-rparse-cron' end parse-cron-0.1.4/Gemfile0000644000004100000410000000020112554717216015113 0ustar www-datawww-datasource "http://rubygems.org" # Specify your gem's dependencies in parse-cron.gemspec gemspec gem "ZenTest", "4.6.0" gem "rake" parse-cron-0.1.4/.rspec0000644000004100000410000000001012554717216014733 0ustar www-datawww-data--color parse-cron-0.1.4/spec/0000755000004100000410000000000012554717216014561 5ustar www-datawww-dataparse-cron-0.1.4/spec/spec_helper.rb0000644000004100000410000000026512554717216017402 0ustar www-datawww-dataspec_dir = File.dirname(__FILE__) lib_dir = File.expand_path(File.join(spec_dir, '..', 'lib')) $:.unshift(lib_dir) $:.uniq! RSpec.configure do |config| end require 'cron_parser' parse-cron-0.1.4/spec/cron_parser_spec.rb0000644000004100000410000002170512554717216020442 0ustar www-datawww-datarequire "time" require "./spec/spec_helper" require "cron_parser" require "date" def parse_date(str) dt = DateTime.strptime(str, "%Y-%m-%d %H:%M") Time.local(dt.year, dt.month, dt.day, dt.hour, dt.min, 0) end describe "CronParser#parse_element" do [ ["*", 0..59, (0..59).to_a], ["*/10", 0..59, [0, 10, 20, 30, 40, 50]], ["10", 0..59, [10]], ["10,30", 0..59, [10, 30]], ["10-15", 0..59, [10, 11, 12, 13, 14, 15]], ["10-40/10", 0..59, [10, 20, 30, 40]], ].each do |element, range, expected| it "should return #{expected} for '#{element}' when range is #{range}" do parser = CronParser.new('* * * * *') parser.parse_element(element, range).first.to_a.sort.should == expected.sort end end end describe "CronParser#next" do [ ["* * * * *", "2011-08-15 12:00", "2011-08-15 12:01"], ["* * * * *", "2011-08-15 02:25", "2011-08-15 02:26"], ["* * * * *", "2011-08-15 02:59", "2011-08-15 03:00"], ["*/15 * * * *", "2011-08-15 02:02", "2011-08-15 02:15"], ["*/15,25 * * * *", "2011-08-15 02:15", "2011-08-15 02:25"], ["30 3,6,9 * * *", "2011-08-15 02:15", "2011-08-15 03:30"], ["30 9 * * *", "2011-08-15 10:15", "2011-08-16 09:30"], ["30 9 * * *", "2011-08-31 10:15", "2011-09-01 09:30"], ["30 9 * * *", "2011-09-30 10:15", "2011-10-01 09:30"], ["0 9 * * *", "2011-12-31 10:15", "2012-01-01 09:00"], ["* * 12 * *", "2010-04-15 10:15", "2010-05-12 00:00"], ["* * * * 1,3", "2010-04-15 10:15", "2010-04-19 00:00"], ["* * * * MON,WED", "2010-04-15 10:15", "2010-04-19 00:00"], ["0 0 1 1 *", "2010-04-15 10:15", "2011-01-01 00:00"], ["0 0 * * 1", "2011-08-01 00:00", "2011-08-08 00:00"], ["0 0 * * 1", "2011-07-25 00:00", "2011-08-01 00:00"], ["45 23 7 3 *", "2011-01-01 00:00", "2011-03-07 23:45"], ["0 0 1 jun *", "2013-05-14 11:20", "2013-06-01 00:00"], ["0 0 1 may,jul *", "2013-05-14 15:00", "2013-07-01 00:00"], ["0 0 1 MAY,JUL *", "2013-05-14 15:00", "2013-07-01 00:00"], ["40 5 * * *", "2014-02-01 15:56", "2014-02-02 05:40"], ["0 5 * * 1", "2014-02-01 15:56", "2014-02-03 05:00"], ["10 8 15 * *", "2014-02-01 15:56", "2014-02-15 08:10"], ["50 6 * * 1", "2014-02-01 15:56", "2014-02-03 06:50"], ["1 2 * apr mOn", "2014-02-01 15:56", "2014-04-07 02:01"], ["1 2 3 4 7", "2014-02-01 15:56", "2014-04-03 02:01"], ["1 2 3 4 7", "2014-04-04 15:56", "2014-04-06 02:01"], ["1-20/3 * * * *", "2014-02-01 15:56", "2014-02-01 16:01"], ["1,2,3 * * * *", "2014-02-01 15:56", "2014-02-01 16:01"], ["1-9,15-30 * * * *", "2014-02-01 15:56", "2014-02-01 16:01"], ["1-9/3,15-30/4 * * * *", "2014-02-01 15:56", "2014-02-01 16:01"], ["1 2 3 jan mon", "2014-02-01 15:56", "2015-01-03 02:01"], ["1 2 3 4 mON", "2014-02-01 15:56", "2014-04-03 02:01"], ["1 2 3 jan 5", "2014-02-01 15:56", "2015-01-02 02:01"], ["@yearly", "2014-02-01 15:56", "2015-01-01 00:00"], ["@annually", "2014-02-01 15:56", "2015-01-01 00:00"], ["@monthly", "2014-02-01 15:56", "2014-03-01 00:00"], ["@weekly", "2014-02-01 15:56", "2014-02-02 00:00"], ["@daily", "2014-02-01 15:56", "2014-02-02 00:00"], ["@midnight", "2014-02-01 15:56", "2014-02-02 00:00"], ["@hourly", "2014-02-01 15:56", "2014-02-01 16:00"], ["*/3 * * * *", "2014-02-01 15:56", "2014-02-01 15:57"], ["0 5 * 2,3 *", "2014-02-01 15:56", "2014-02-02 05:00"], ["15-59/15 * * * *", "2014-02-01 15:56", "2014-02-01 16:15"], ["15-59/15 * * * *", "2014-02-01 15:00", "2014-02-01 15:15"], ["15-59/15 * * * *", "2014-02-01 15:01", "2014-02-01 15:15"], ["15-59/15 * * * *", "2014-02-01 15:16", "2014-02-01 15:30"], ["15-59/15 * * * *", "2014-02-01 15:26", "2014-02-01 15:30"], ["15-59/15 * * * *", "2014-02-01 15:36", "2014-02-01 15:45"], ["15-59/15 * * * *", "2014-02-01 15:45", "2014-02-01 16:15"], ["15-59/15 * * * *", "2014-02-01 15:46", "2014-02-01 16:15"], ].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.next(now).xmlschema.should == expected_next.xmlschema 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) parser = CronParser.new(line) parser.last(now).should == expected_next end end end describe "CronParser#new" do it 'should not raise error when given a valid cronline' do expect { CronParser.new('30 * * * *') }.not_to raise_error end it 'should raise error when given an invalid cronline' do expect { CronParser.new('* * * *') }.to raise_error('not a valid cronline') end end describe "time source" do it "should use an alternate specified time source" do ExtendedTime = Class.new(Time) ExtendedTime.should_receive(:local).once CronParser.new("* * * * *",ExtendedTime).next end end parse-cron-0.1.4/.travis.yml0000644000004100000410000000005412554717216015737 0ustar www-datawww-datalanguage: ruby rvm: - "1.8.7" - "1.9.3" parse-cron-0.1.4/lib/0000755000004100000410000000000012554717216014375 5ustar www-datawww-dataparse-cron-0.1.4/lib/parse-cron.rb0000644000004100000410000000014512554717216016773 0ustar www-datawww-data# More logical way to require 'cron_parser' require File.join(File.dirname(__FILE__), 'cron_parser') parse-cron-0.1.4/lib/parse-cron/0000755000004100000410000000000012554717216016446 5ustar www-datawww-dataparse-cron-0.1.4/lib/parse-cron/version.rb0000644000004100000410000000007312554717216020460 0ustar www-datawww-datamodule Parse module Cron VERSION = "0.1.4" end end parse-cron-0.1.4/lib/cron_parser.rb0000644000004100000410000001603112554717216017240 0ustar www-datawww-datarequire 'set' require 'date' # # Parses cron expressions and computes the next occurence of the "job" # class CronParser # internal "mutable" time representation class InternalTime attr_accessor :year, :month, :day, :hour, :min attr_accessor :time_source def initialize(time,time_source = Time) @year = time.year @month = time.month @day = time.day @hour = time.hour @min = time.min @time_source = time_source end def to_time time_source.local(@year, @month, @day, @hour, @min, 0) end def inspect [year, month, day, hour, min].inspect end end SYMBOLS = { "jan" => "1", "feb" => "2", "mar" => "3", "apr" => "4", "may" => "5", "jun" => "6", "jul" => "7", "aug" => "8", "sep" => "9", "oct" => "10", "nov" => "11", "dec" => "12", "sun" => "0", "mon" => "1", "tue" => "2", "wed" => "3", "thu" => "4", "fri" => "5", "sat" => "6" } def initialize(source,time_source = Time) @source = interpret_vixieisms(source) @time_source = time_source validate_source end def interpret_vixieisms(spec) case spec when '@reboot' raise ArgumentError, "Can't predict last/next run of @reboot" when '@yearly', '@annually' '0 0 1 1 *' when '@monthly' '0 0 1 * *' when '@weekly' '0 0 * * 0' when '@daily', '@midnight' '0 0 * * *' when '@hourly' '0 * * * *' else spec end end # returns the next occurence after the given date def next(now = @time_source.now) t = InternalTime.new(now, @time_source) unless 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) nudge_date(t) t.hour = -1 end unless time_specs[:hour][0].include?(t.hour) nudge_hour(t) t.min = -1 end # always nudge the minute nudge_minute(t) t.to_time end # returns the last occurence before the given date def last(now = @time_source.now) t = InternalTime.new(now,@time_source) unless time_specs[:month][0].include?(t.month) nudge_month(t, :last) t.day = 32 end if t.day == 32 || !interpolate_weekdays(t.year, t.month)[0].include?(t.day) nudge_date(t, :last) t.hour = 24 end unless time_specs[:hour][0].include?(t.hour) nudge_hour(t, :last) t.min = 60 end # always nudge the minute nudge_minute(t, :last) t.to_time end SUBELEMENT_REGEX = %r{^(\d+)(-(\d+)(/(\d+))?)?$} def parse_element(elem, allowed_range) values = elem.split(',').map do |subel| if subel =~ /^\*/ step = subel.length > 1 ? subel[2..-1].to_i : 1 stepped_range(allowed_range, step) else if SUBELEMENT_REGEX === subel if $5 # with range stepped_range($1.to_i..$3.to_i, $5.to_i) elsif $3 # range without step stepped_range($1.to_i..$3.to_i, 1) else # just a numeric [$1.to_i] end else raise ArgumentError, "Bad Vixie-style specification #{subel}" end end end.flatten.sort [Set.new(values), values, elem] end protected # returns a list of days which do both match time_spec[:dom] or time_spec[:dow] def interpolate_weekdays(year, month) @_interpolate_weekdays_cache ||= {} @_interpolate_weekdays_cache["#{year}-#{month}"] ||= interpolate_weekdays_without_cache(year, month) end def interpolate_weekdays_without_cache(year, month) t = Date.new(year, month, 1) valid_mday, _, mday_field = time_specs[:dom] valid_wday, _, wday_field = time_specs[:dow] # 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 == '*' 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) t = t.succ end [Set.new(result), result] end def nudge_year(t, dir = :next) t.year = t.year + (dir == :next ? 1 : -1) end 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 end def date_valid?(t, dir = :next) interpolate_weekdays(t.year, t.month)[0].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 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 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 def time_specs @time_specs ||= begin # tokens now contains the 5 fields tokens = substitute_parse_symbols(@source).split(/\s+/) { :minute => parse_element(tokens[0], 0..59), #minute :hour => parse_element(tokens[1], 0..23), #hour :dom => parse_element(tokens[2], 1..31), #DOM :month => parse_element(tokens[3], 1..12), #mon :dow => parse_element(tokens[4], 0..6) #DOW } end end def substitute_parse_symbols(str) SYMBOLS.inject(str.downcase) do |s, (symbol, replacement)| s.gsub(symbol, replacement) end end 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 # returns the smallest element from allowed which is greater than current # returns nil if no matching value was found def find_best_next(current, allowed, dir) if dir == :next allowed.sort.find { |val| val > current } else allowed.sort.reverse.find { |val| val < current } end end def validate_source unless @source.respond_to?(:split) raise ArgumentError, 'not a valid cronline' end source_length = @source.split(/\s+/).length unless source_length >= 5 && source_length <= 6 raise ArgumentError, 'not a valid cronline' end end end parse-cron-0.1.4/metadata.yml0000644000004100000410000000305312554717216016133 0ustar www-datawww-data--- !ruby/object:Gem::Specification name: parse-cron version: !ruby/object:Gem::Version version: 0.1.4 platform: ruby authors: - Michael Siebert autorequire: bindir: bin cert_chain: [] date: 2014-02-06 00:00:00.000000000 Z dependencies: - !ruby/object:Gem::Dependency name: rspec requirement: !ruby/object:Gem::Requirement requirements: - - ~> - !ruby/object:Gem::Version version: 2.6.0 type: :development prerelease: false version_requirements: !ruby/object:Gem::Requirement requirements: - - ~> - !ruby/object:Gem::Version version: 2.6.0 description: Parses cron expressions and calculates the next occurence email: - siebertm85@googlemail.com executables: [] extensions: [] extra_rdoc_files: [] files: - .gitignore - .rspec - .travis.yml - Gemfile - License - README.md - Rakefile - lib/cron_parser.rb - lib/parse-cron.rb - lib/parse-cron/version.rb - parse-cron.gemspec - spec/cron_parser_spec.rb - spec/spec_helper.rb homepage: https://github.com/siebertm/parse-cron licenses: [] metadata: {} post_install_message: rdoc_options: [] require_paths: - lib required_ruby_version: !ruby/object:Gem::Requirement requirements: - - ! '>=' - !ruby/object:Gem::Version version: '0' required_rubygems_version: !ruby/object:Gem::Requirement requirements: - - ! '>=' - !ruby/object:Gem::Version version: '0' requirements: [] rubyforge_project: parse-cron rubygems_version: 2.1.11 signing_key: specification_version: 4 summary: Parses cron expressions and calculates the next occurence test_files: [] parse-cron-0.1.4/parse-cron.gemspec0000644000004100000410000000154612554717216017253 0ustar www-datawww-data# -*- encoding: utf-8 -*- $:.push File.expand_path("../lib", __FILE__) require "parse-cron/version" Gem::Specification.new do |s| s.name = "parse-cron" s.version = Parse::Cron::VERSION s.platform = Gem::Platform::RUBY s.authors = ["Michael Siebert"] s.email = ["siebertm85@googlemail.com"] s.homepage = "https://github.com/siebertm/parse-cron" s.summary = %q{Parses cron expressions and calculates the next occurence} s.description = %q{Parses cron expressions and calculates the next occurence} s.rubyforge_project = "parse-cron" s.files = `git ls-files`.split("\n") s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n") s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) } s.require_paths = ["lib"] s.add_development_dependency 'rspec', '~>2.6.0' end parse-cron-0.1.4/.gitignore0000644000004100000410000000005612554717216015620 0ustar www-datawww-data*.gem *.swp .bundle Gemfile.lock pkg/* .rvmrc parse-cron-0.1.4/License0000644000004100000410000000211212554717216015130 0ustar www-datawww-dataCopyright (C) 2013 Michael Siebert Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. parse-cron-0.1.4/README.md0000644000004100000410000000103012554717216015100 0ustar www-datawww-data# parse-cron - parse crontab syntax & determine next scheduled run [![Build Status](https://travis-ci.org/siebertm/parse-cron.png)](https://travis-ci.org/siebertm/parse-cron) The goal of this gem is to parse a crontab timing specification and determine when the job should be run. It is not a scheduler, it does not run the jobs. ## API example ``` cron_parser = CronParser.new('30 * * * *') # Comming times next_comming_time = cron_parser.next(Time.now) # Times that have been most_resently_time = cron_parser.last(Time.now) ```