parse-cron 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,5 @@
1
+ *.gem
2
+ *.swp
3
+ .bundle
4
+ Gemfile.lock
5
+ pkg/*
data/.rspec ADDED
@@ -0,0 +1 @@
1
+ --color
data/Gemfile ADDED
@@ -0,0 +1,6 @@
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"
data/README ADDED
@@ -0,0 +1,5 @@
1
+ # parse-cron - parse crontab syntax & determine next scheduled run
2
+
3
+ The goal of this gem is to parse a crontab timing specification and determine when the
4
+ job should be run. It is not a scheduler, it does not run the jobs.
5
+
@@ -0,0 +1,11 @@
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
@@ -0,0 +1,184 @@
1
+ #
2
+ # Parses cron expressions and computes the next occurence of the "job"
3
+ #
4
+ class CronParser
5
+ # internal "mutable" time representation
6
+ class InternalTime
7
+ attr_accessor :year, :month, :day, :hour, :min
8
+
9
+ def initialize(time)
10
+ @year = time.year
11
+ @month = time.month
12
+ @day = time.day
13
+ @hour = time.hour
14
+ @min = time.min
15
+ end
16
+
17
+ def to_time
18
+ Time.utc(@year, @month, @day, @hour, @min, 0)
19
+ end
20
+ alias :inspect :to_time
21
+ end
22
+
23
+ SYMBOLS = {
24
+ "jan" => "0",
25
+ "feb" => "1",
26
+ "mar" => "2",
27
+ "apr" => "3",
28
+ "may" => "4",
29
+ "jun" => "5",
30
+ "jul" => "6",
31
+ "aug" => "7",
32
+ "sep" => "8",
33
+ "oct" => "9",
34
+ "nov" => "10",
35
+ "dec" => "11",
36
+
37
+ "sun" => "0",
38
+ "mon" => "1",
39
+ "tue" => "2",
40
+ "wed" => "3",
41
+ "thu" => "4",
42
+ "fri" => "5",
43
+ "sat" => "6"
44
+ }
45
+
46
+ def initialize(source)
47
+ @source = source
48
+ end
49
+
50
+
51
+ # returns the next occurence after the given date
52
+ def next(now = Time.now)
53
+ t = InternalTime.new(now)
54
+
55
+ unless time_specs[:month].include?(t.month)
56
+ nudge_month(t)
57
+ t.day = 0
58
+ end
59
+
60
+ unless t.day == 0 || interpolate_weekdays(t.year, t.month).include?(t.day)
61
+ nudge_date(t)
62
+ t.hour = -1
63
+ end
64
+
65
+ unless time_specs[:hour].include?(t.hour)
66
+ nudge_hour(t)
67
+ t.min = -1
68
+ end
69
+
70
+ # always nudge the minute
71
+ nudge_minute(t)
72
+ t.to_time
73
+ end
74
+
75
+ SUBELEMENT_REGEX = %r{^(\d+)(-(\d+)(/(\d+))?)?$}
76
+ def parse_element(elem, allowed_range)
77
+ elem.split(',').map do |subel|
78
+ if subel =~ /^\*/
79
+ step = subel.length > 1 ? subel[2..-1].to_i : 1
80
+ stepped_range(allowed_range, step)
81
+ else
82
+ if SUBELEMENT_REGEX === subel
83
+ if $5 # with range
84
+ stepped_range($1.to_i..($3.to_i + 1), $5.to_i)
85
+ elsif $3 # range without step
86
+ stepped_range($1.to_i..($3.to_i + 1), 1)
87
+ else # just a numeric
88
+ [$1.to_i]
89
+ end
90
+ else
91
+ raise "Bad Vixie-style specification #{subel}"
92
+ end
93
+ end
94
+ end.flatten.sort
95
+ end
96
+
97
+
98
+ protected
99
+
100
+ # returns a list of days which do both match time_spec[:dom] and time_spec[:dow]
101
+ def interpolate_weekdays(year, month)
102
+ t = Date.new(year, month, 1)
103
+ valid_mday = time_specs[:dom]
104
+ valid_wday = time_specs[:dow]
105
+
106
+ result = []
107
+ while t.month == month
108
+ result << t.mday if valid_mday.include?(t.mday) && valid_wday.include?(t.wday)
109
+ t = t.succ
110
+ end
111
+
112
+ result
113
+ end
114
+
115
+ def nudge_year(t)
116
+ t.year = t.year + 1
117
+ end
118
+
119
+ def nudge_month(t)
120
+ spec = time_specs[:month]
121
+ next_value = find_best_next(t.month, spec)
122
+ t.month = next_value || spec.first
123
+ next_value.nil? ? nudge_year(t) : t
124
+ end
125
+
126
+ def nudge_date(t)
127
+ spec = interpolate_weekdays(t.year, t.month)
128
+ next_value = find_best_next(t.day, spec)
129
+ t.day = next_value || spec.first
130
+ next_value.nil? ? nudge_month(t) : t
131
+ end
132
+
133
+ def nudge_hour(t)
134
+ spec = time_specs[:hour]
135
+ next_value = find_best_next(t.hour, spec)
136
+ t.hour = next_value || spec.first
137
+ next_value.nil? ? nudge_date(t) : t
138
+ end
139
+
140
+ def nudge_minute(t)
141
+ spec = time_specs[:minute]
142
+ next_value = find_best_next(t.min, spec)
143
+ t.min = next_value || spec.first
144
+ next_value.nil? ? nudge_hour(t) : t
145
+ end
146
+
147
+ def time_specs
148
+ @time_specs ||= begin
149
+ # tokens now contains the 5 fields
150
+ tokens = substitute_parse_symbols(@source).split(/\s+/)
151
+ {
152
+ :minute => parse_element(tokens[0], 0..59), #minute
153
+ :hour => parse_element(tokens[1], 0..23), #hour
154
+ :dom => parse_element(tokens[2], 1..31), #DOM
155
+ :month => parse_element(tokens[3], 1..12), #mon
156
+ :dow => parse_element(tokens[4], 0..6) #DOW
157
+ }
158
+ end
159
+ end
160
+
161
+ def substitute_parse_symbols(str)
162
+ SYMBOLS.inject(str) do |s, (symbol, replacement)|
163
+ s.gsub(symbol, replacement)
164
+ end
165
+ end
166
+
167
+
168
+ def stepped_range(rng, step = 1)
169
+ len = rng.last - rng.first
170
+
171
+ num = len.div(step)
172
+ result = (0..num).map { |i| rng.first + step * i }
173
+
174
+ result.pop if result[-1] == rng.last and rng.exclude_end?
175
+ result
176
+ end
177
+
178
+
179
+ # returns the smallest element from allowed which is greater than current
180
+ # returns nil if no matching value was found
181
+ def find_best_next(current, allowed)
182
+ allowed.sort.find { |val| val > current }
183
+ end
184
+ end
@@ -0,0 +1,5 @@
1
+ module Parse
2
+ module Cron
3
+ VERSION = "0.0.1"
4
+ end
5
+ end
@@ -0,0 +1,23 @@
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 = ""
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
@@ -0,0 +1,46 @@
1
+ require "time"
2
+ require "./spec/spec_helper"
3
+ require "cron_parser"
4
+
5
+ describe "CronParser#parse_element" do
6
+ [
7
+ ["*", 0..60, (0..60).to_a],
8
+ ["*/10", 0..60, [0, 10, 20, 30, 40, 50]],
9
+ ["10", 0..60, [10]],
10
+ ["10,30", 0..60, [10, 30]],
11
+ ["10-15", 0..60, [10, 11, 12, 13, 14, 15]],
12
+ ["10-40/10", 0..60, [10, 20, 30, 40]],
13
+ ].each do |element, range, expected|
14
+ it "should return #{expected} for '#{element}' when range is #{range}" do
15
+ parser = CronParser.new('')
16
+ parser.parse_element(element, range) == expected
17
+ end
18
+ end
19
+ end
20
+
21
+ describe "CronParser#next" do
22
+ [
23
+ ["* * * * *", "2011-08-15T12:00", "2011-08-15T12:01"],
24
+ ["* * * * *", "2011-08-15T02:25", "2011-08-15T02:26"],
25
+ ["* * * * *", "2011-08-15T02:59", "2011-08-15T03:00"],
26
+ ["*/15 * * * *", "2011-08-15T02:02", "2011-08-15T02:15"],
27
+ ["*/15,25 * * * *", "2011-08-15T02:15", "2011-08-15T02:25"],
28
+ ["30 3,6,9 * * *", "2011-08-15T02:15", "2011-08-15T03:30"],
29
+ ["30 9 * * *", "2011-08-15T10:15", "2011-08-16T09:30"],
30
+ ["30 9 * * *", "2011-08-31T10:15", "2011-09-01T09:30"],
31
+ ["30 9 * * *", "2011-09-30T10:15", "2011-10-01T09:30"],
32
+ ["0 9 * * *", "2011-12-31T10:15", "2012-01-01T09:00"],
33
+ ["* * 12 * *", "2010-04-15T10:15", "2010-05-12T00:00"],
34
+ ["* * * * 1,3", "2010-04-15T10:15", "2010-04-19T00:00"],
35
+ ["0 0 1 1 *", "2010-04-15T10:15", "2011-01-01T00:00"],
36
+ ].each do |line, now, expected_next|
37
+ it "should return #{expected_next} for '#{line}' when now is #{now}" do
38
+ now = Time.xmlschema(now + ":00+00:00")
39
+ expected_next = Time.xmlschema(expected_next + ":00+00:00")
40
+
41
+ parser = CronParser.new(line)
42
+
43
+ parser.next(now).xmlschema.should == expected_next.xmlschema
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,9 @@
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'
metadata ADDED
@@ -0,0 +1,93 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: parse-cron
3
+ version: !ruby/object:Gem::Version
4
+ hash: 29
5
+ prerelease:
6
+ segments:
7
+ - 0
8
+ - 0
9
+ - 1
10
+ version: 0.0.1
11
+ platform: ruby
12
+ authors:
13
+ - Michael Siebert
14
+ autorequire:
15
+ bindir: bin
16
+ cert_chain: []
17
+
18
+ date: 2011-08-17 00:00:00 +02:00
19
+ default_executable:
20
+ dependencies:
21
+ - !ruby/object:Gem::Dependency
22
+ name: rspec
23
+ prerelease: false
24
+ requirement: &id001 !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ~>
28
+ - !ruby/object:Gem::Version
29
+ hash: 23
30
+ segments:
31
+ - 2
32
+ - 6
33
+ - 0
34
+ version: 2.6.0
35
+ type: :development
36
+ version_requirements: *id001
37
+ description: Parses cron expressions and calculates the next occurence
38
+ email:
39
+ - siebertm85@googlemail.com
40
+ executables: []
41
+
42
+ extensions: []
43
+
44
+ extra_rdoc_files: []
45
+
46
+ files:
47
+ - .gitignore
48
+ - .rspec
49
+ - Gemfile
50
+ - README
51
+ - Rakefile
52
+ - lib/cron_parser.rb
53
+ - lib/parse-cron/version.rb
54
+ - parse-cron.gemspec
55
+ - spec/cron_parser_spec.rb
56
+ - spec/spec_helper.rb
57
+ has_rdoc: true
58
+ homepage: ""
59
+ licenses: []
60
+
61
+ post_install_message:
62
+ rdoc_options: []
63
+
64
+ require_paths:
65
+ - lib
66
+ required_ruby_version: !ruby/object:Gem::Requirement
67
+ none: false
68
+ requirements:
69
+ - - ">="
70
+ - !ruby/object:Gem::Version
71
+ hash: 3
72
+ segments:
73
+ - 0
74
+ version: "0"
75
+ required_rubygems_version: !ruby/object:Gem::Requirement
76
+ none: false
77
+ requirements:
78
+ - - ">="
79
+ - !ruby/object:Gem::Version
80
+ hash: 3
81
+ segments:
82
+ - 0
83
+ version: "0"
84
+ requirements: []
85
+
86
+ rubyforge_project: parse-cron
87
+ rubygems_version: 1.4.2
88
+ signing_key:
89
+ specification_version: 3
90
+ summary: Parses cron expressions and calculates the next occurence
91
+ test_files:
92
+ - spec/cron_parser_spec.rb
93
+ - spec/spec_helper.rb