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.
- data/.gitignore +5 -0
- data/.rspec +1 -0
- data/Gemfile +6 -0
- data/README +5 -0
- data/Rakefile +11 -0
- data/lib/cron_parser.rb +184 -0
- data/lib/parse-cron/version.rb +5 -0
- data/parse-cron.gemspec +23 -0
- data/spec/cron_parser_spec.rb +46 -0
- data/spec/spec_helper.rb +9 -0
- metadata +93 -0
data/.rspec
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
--color
|
data/Gemfile
ADDED
data/README
ADDED
data/Rakefile
ADDED
@@ -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
|
data/lib/cron_parser.rb
ADDED
@@ -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
|
data/parse-cron.gemspec
ADDED
@@ -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
|
data/spec/spec_helper.rb
ADDED
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
|