parse_cron 0.1.6

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,251 @@
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:%S")
8
+ Time.local(dt.year, dt.month, dt.day, dt.hour, dt.min, dt.sec)
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
+ expect(parser.parse_element(element, range).first.to_a.sort).to eq expected.sort
23
+ end
24
+ end
25
+ end
26
+
27
+ describe "CronParser#next" do
28
+ [
29
+ ["* * * * *", "2011-08-15 12:00:00", "2011-08-15 12:01:00",1],
30
+ ["* * * * *", "2011-08-15 02:25:00", "2011-08-15 02:26:00",1],
31
+ ["* * * * *", "2011-08-15 02:59:00", "2011-08-15 03:00:00",1],
32
+ ["*/15 * * * *", "2011-08-15 02:02:00", "2011-08-15 02:15:00",1],
33
+ ["*/15,25 * * * *", "2011-08-15 02:15:00", "2011-08-15 02:25:00",1],
34
+ ["30 3,6,9 * * *", "2011-08-15 02:15:00", "2011-08-15 03:30:00",1],
35
+ ["30 9 * * *", "2011-08-15 10:15:00", "2011-08-16 09:30:00",1],
36
+ ["30 9 * * *", "2011-08-31 10:15:00", "2011-09-01 09:30:00",1],
37
+ ["30 9 * * *", "2011-09-30 10:15:00", "2011-10-01 09:30:00",1],
38
+ ["0 9 * * *", "2011-12-31 10:15:00", "2012-01-01 09:00:00",1],
39
+ ["* * 12 * *", "2010-04-15 10:15:00", "2010-05-12 00:00:00",1],
40
+ ["* * * * 1,3", "2010-04-15 10:15:00", "2010-04-19 00:00:00",1],
41
+ ["* * * * MON,WED", "2010-04-15 10:15:00", "2010-04-19 00:00:00",1],
42
+ ["0 0 1 1 *", "2010-04-15 10:15:00", "2011-01-01 00:00:00",1],
43
+ ["0 0 * * 1", "2011-08-01 00:00:00", "2011-08-08 00:00:00",1],
44
+ ["0 0 * * 1", "2011-07-25 00:00:00", "2011-08-01 00:00:00",1],
45
+ ["45 23 7 3 *", "2011-01-01 00:00:00", "2011-03-07 23:45:00",1],
46
+ ["0 0 1 jun *", "2013-05-14 11:20:00", "2013-06-01 00:00:00",1],
47
+ ["0 0 1 may,jul *", "2013-05-14 15:00:00", "2013-07-01 00:00:00",1],
48
+ ["0 0 1 MAY,JUL *", "2013-05-14 15:00:00", "2013-07-01 00:00:00",1],
49
+ ["40 5 * * *", "2014-02-01 15:56:00", "2014-02-02 05:40:00",1],
50
+ ["0 5 * * 1", "2014-02-01 15:56:00", "2014-02-03 05:00:00",1],
51
+ ["10 8 15 * *", "2014-02-01 15:56:00", "2014-02-15 08:10:00",1],
52
+ ["50 6 * * 1", "2014-02-01 15:56:00", "2014-02-03 06:50:00",1],
53
+ ["1 2 * apr mOn", "2014-02-01 15:56:00", "2014-04-07 02:01:00",1],
54
+ ["1 2 3 4 7", "2014-02-01 15:56:00", "2014-04-03 02:01:00",1],
55
+ ["1 2 3 4 7", "2014-04-04 15:56:00", "2014-04-06 02:01:00",1],
56
+ ["1-20/3 * * * *", "2014-02-01 15:56:00", "2014-02-01 16:01:00",1],
57
+ ["1,2,3 * * * *", "2014-02-01 15:56:00", "2014-02-01 16:01:00",1],
58
+ ["1-9,15-30 * * * *", "2014-02-01 15:56:00", "2014-02-01 16:01:00",1],
59
+ ["1-9/3,15-30/4 * * * *", "2014-02-01 15:56:00", "2014-02-01 16:01:00",1],
60
+ ["1 2 3 jan mon", "2014-02-01 15:56:00", "2015-01-03 02:01:00",1],
61
+ ["1 2 3 4 mON", "2014-02-01 15:56:00", "2014-04-03 02:01:00",1],
62
+ ["1 2 3 jan 5", "2014-02-01 15:56:00", "2015-01-02 02:01:00",1],
63
+ ["@yearly", "2014-02-01 15:56:00", "2015-01-01 00:00:00",1],
64
+ ["@annually", "2014-02-01 15:56:00", "2015-01-01 00:00:00",1],
65
+ ["@monthly", "2014-02-01 15:56:00", "2014-03-01 00:00:00",1],
66
+ ["@weekly", "2014-02-01 15:56:00", "2014-02-02 00:00:00",1],
67
+ ["@daily", "2014-02-01 15:56:00", "2014-02-02 00:00:00",1],
68
+ ["@midnight", "2014-02-01 15:56:00", "2014-02-02 00:00:00",1],
69
+ ["@hourly", "2014-02-01 15:56:00", "2014-02-01 16:00:00",1],
70
+ ["@minutely", "2014-02-01 15:56:00", "2014-02-01 15:57:00",1],
71
+ ["*/3 * * * *", "2014-02-01 15:56:00", "2014-02-01 15:57:00",1],
72
+ ["0 5 * 2,3 *", "2014-02-01 15:56:00", "2014-02-02 05:00:00",1],
73
+ ["15-59/15 * * * *", "2014-02-01 15:56:00", "2014-02-01 16:15:00",1],
74
+ ["15-59/15 * * * *", "2014-02-01 15:00:00", "2014-02-01 15:15:00",1],
75
+ ["15-59/15 * * * *", "2014-02-01 15:01:00", "2014-02-01 15:15:00",1],
76
+ ["15-59/15 * * * *", "2014-02-01 15:16:00", "2014-02-01 15:30:00",1],
77
+ ["15-59/15 * * * *", "2014-02-01 15:26:00", "2014-02-01 15:30:00",1],
78
+ ["15-59/15 * * * *", "2014-02-01 15:36:00", "2014-02-01 15:45:00",1],
79
+ ["15-59/15 * * * *", "2014-02-01 15:45:00", "2014-02-01 16:15:00",4],
80
+ ["15-59/15 * * * *", "2014-02-01 15:46:00", "2014-02-01 16:15:00",3],
81
+ ["15-59/15 * * * *", "2014-02-01 15:46:00", "2014-02-01 16:15:00",2],
82
+ ["30 * * * * * *", "2011-08-15 12:00:00", "2011-08-15 12:00:30",1],
83
+ ["*/15 * * * * * *", "2011-08-15 12:00:00", "2011-08-15 12:00:15",1],
84
+ ["20-40 * * * * * *", "2011-08-15 12:00:00", "2011-08-15 12:00:20",1],
85
+ ["12 15-59/15 * * * * *", "2014-02-01 15:46:00", "2014-02-01 16:15:12",2],
86
+ ["* * * * * * 2018", "2014-02-01 15:46:00", "2018-01-01 00:00:00",1],
87
+ ["1 1 1 1 1 * 2018", "2014-02-01 15:46:00", "2018-01-01 01:01:01",1],
88
+ ["* * * * * * 2016-2018", "2014-02-01 15:46:00", "2016-01-01 00:00:00",1],
89
+ ["* * * * * * */20", "2014-02-01 15:46:00", "2020-01-01 00:00:00",1],
90
+ # tw testing
91
+ # 6 digit dkron
92
+ ["0 * * * * *", "2019-06-01 08:00:00", "2019-06-01 08:01:00",1],
93
+ ["0 0 * * * *", "2019-06-01 08:00:00", "2019-06-01 09:00:00",1],
94
+ ["0 0 0 * * *", "2019-06-01 08:00:00", "2019-06-02 00:00:00",1],
95
+ ["0 0 0 1 * *", "2019-06-01 08:00:00", "2019-07-01 00:00:00",1],
96
+ ["0 0 0 5 7 *", "2019-06-01 08:00:00", "2019-07-05 00:00:00",1],
97
+ ["0 0 * * 8 SUN", "2019-06-01 08:00:00", "2019-08-04 00:00:00",1],
98
+
99
+ ["15 0,15,30,45 * * * *", "2019-06-01 08:16:16", "2019-06-01 08:30:15",1],
100
+ ["0 */15 */3 * * *", "2019-06-01 08:00:00", "2019-06-01 09:00:00",1],
101
+ ["0 0 * ? * *", "2019-06-01 08:00:00", "2019-06-01 09:00:00",1],
102
+ ["0 0 * * * ?", "2019-06-01 08:00:00", "2019-06-01 09:00:00",1],
103
+ ["0 0 * ? * ?", "2019-06-01 08:00:00", "2019-06-01 09:00:00",1],
104
+ ].each do |line, now, expected_next,num|
105
+ it "returns #{expected_next} for '#{line}' when now is #{now}" do
106
+ parsed_now = parse_date(now)
107
+ expected = parse_date(expected_next)
108
+ parser = CronParser.new(line)
109
+ expect(parser.next(parsed_now).xmlschema).to eq expected.xmlschema
110
+ end
111
+ it "returns the expected class" do
112
+ parsed_now = parse_date(now)
113
+ expected = parse_date(expected_next)
114
+ parser = CronParser.new(line)
115
+ result = parser.next(parsed_now,num)
116
+ expect(result.class.to_s).to eq (num > 1 ? 'Array' : 'Time')
117
+ end
118
+ it "returns the expected count" do
119
+ parsed_now = parse_date(now)
120
+ expected = parse_date(expected_next)
121
+ parser = CronParser.new(line)
122
+ result = parser.next(parsed_now,num)
123
+ if result.class.to_s == 'Array'
124
+ expect(result.size).to eq num
125
+ else
126
+ expect(result.class.to_s).to eq 'Time'
127
+ end
128
+ end
129
+ end
130
+
131
+ [
132
+ ["* * * * * * 2010", "2014-02-01 15:46:00"]
133
+ ].each do |line, now|
134
+ it "should raise an error for '#{line}' when now is #{now}" do
135
+ now = parse_date(now)
136
+
137
+ parser = CronParser.new(line)
138
+
139
+ expect{parser.next(now)}.to raise_error "No matching dates exist"
140
+ end
141
+ end
142
+ end
143
+
144
+ describe "CronParser#last" do
145
+ [
146
+ ["* * * * *", "2011-08-15 12:00:00", "2011-08-15 11:59:00"],
147
+ ["* * * * *", "2011-08-15 02:25:00", "2011-08-15 02:24:00"],
148
+ ["* * * * *", "2011-08-15 03:00:00", "2011-08-15 02:59:00"],
149
+ ["*/15 * * * *", "2011-08-15 02:02:00", "2011-08-15 02:00:00"],
150
+ ["*/15,45 * * * *", "2011-08-15 02:55:00", "2011-08-15 02:45:00"],
151
+ ["*/15,25 * * * *", "2011-08-15 02:35:00", "2011-08-15 02:30:00"],
152
+ ["30 3,6,9 * * *", "2011-08-15 02:15:00", "2011-08-14 09:30:00"],
153
+ ["30 9 * * *", "2011-08-15 10:15:00", "2011-08-15 09:30:00"],
154
+ ["30 9 * * *", "2011-09-01 08:15:00", "2011-08-31 09:30:00"],
155
+ ["30 9 * * *", "2011-10-01 08:15:00", "2011-09-30 09:30:00"],
156
+ ["0 9 * * *", "2012-01-01 00:15:00", "2011-12-31 09:00:00"],
157
+ ["* * 12 * *", "2010-04-15 10:15:00", "2010-04-12 23:59:00"],
158
+ ["* * * * 1,3", "2010-04-15 10:15:00", "2010-04-14 23:59:00"],
159
+ ["* * * * MON,WED", "2010-04-15 10:15:00", "2010-04-14 23:59:00"],
160
+ ["0 0 1 1 *", "2010-04-15 10:15:00", "2010-01-01 00:00:00"],
161
+ ["0 0 1 jun *", "2013-05-14 11:20:00", "2012-06-01 00:00:00"],
162
+ ["0 0 1 may,jul *", "2013-05-14 15:00:00", "2013-05-01 00:00:00"],
163
+ ["0 0 1 MAY,JUL *", "2013-05-14 15:00:00", "2013-05-01 00:00:00"],
164
+ ["40 5 * * *", "2014-02-01 15:56:00", "2014-02-01 05:40:00"],
165
+ ["0 5 * * 1", "2014-02-01 15:56:00", "2014-01-27 05:00:00"],
166
+ ["10 8 15 * *", "2014-02-01 15:56:00", "2014-01-15 08:10:00"],
167
+ ["50 6 * * 1", "2014-02-01 15:56:00", "2014-01-27 06:50:00"],
168
+ ["1 2 * apr mOn", "2014-02-01 15:56:00", "2013-04-29 02:01:00"],
169
+ ["1 2 3 4 7", "2014-02-01 15:56:00", "2013-04-28 02:01:00"],
170
+ ["1 2 3 4 7", "2014-04-04 15:56:00", "2014-04-03 02:01:00"],
171
+ ["1-20/3 * * * *", "2014-02-01 15:56:00", "2014-02-01 15:19:00"],
172
+ ["1,2,3 * * * *", "2014-02-01 15:56:00", "2014-02-01 15:03:00"],
173
+ ["1-9,15-30 * * * *", "2014-02-01 15:56:00", "2014-02-01 15:30:00"],
174
+ ["1-9/3,15-30/4 * * * *", "2014-02-01 15:56:00", "2014-02-01 15:27:00"],
175
+ ["1 2 3 jan mon", "2014-02-01 15:56:00", "2014-01-27 02:01:00"],
176
+ ["1 2 3 4 mON", "2014-02-01 15:56:00", "2013-04-29 02:01:00"],
177
+ ["1 2 3 jan 5", "2014-02-01 15:56:00", "2014-01-31 02:01:00"],
178
+ ["@yearly", "2014-02-01 15:56:00", "2014-01-01 00:00:00"],
179
+ ["@annually", "2014-02-01 15:56:00", "2014-01-01 00:00:00"],
180
+ ["@monthly", "2014-02-01 15:56:00", "2014-02-01 00:00:00"],
181
+ ["@weekly", "2014-02-01 15:56:00", "2014-01-26 00:00:00"],
182
+ ["@daily", "2014-02-01 15:56:00", "2014-02-01 00:00:00"],
183
+ ["@midnight", "2014-02-01 15:56:00", "2014-02-01 00:00:00"],
184
+ ["@hourly", "2014-02-01 15:56:00", "2014-02-01 15:00:00"],
185
+ ["*/3 * * * *", "2014-02-01 15:56:00", "2014-02-01 15:54:00"],
186
+ ["0 5 * 2,3 *", "2014-02-01 15:56:00", "2014-02-01 05:00:00"],
187
+ ["15-59/15 * * * *", "2014-02-01 15:56:00", "2014-02-01 15:45:00"],
188
+ ["15-59/15 * * * *", "2014-02-01 15:00:00", "2014-02-01 14:45:00"],
189
+ ["15-59/15 * * * *", "2014-02-01 15:01:00", "2014-02-01 14:45:00"],
190
+ ["15-59/15 * * * *", "2014-02-01 15:16:00", "2014-02-01 15:15:00"],
191
+ ["15-59/15 * * * *", "2014-02-01 15:26:00", "2014-02-01 15:15:00"],
192
+ ["15-59/15 * * * *", "2014-02-01 15:36:00", "2014-02-01 15:30:00"],
193
+ ["15-59/15 * * * *", "2014-02-01 15:45:00", "2014-02-01 15:30:00"],
194
+ ["15-59/15 * * * *", "2014-02-01 15:46:00", "2014-02-01 15:45:00"],
195
+ ["30 * * * * * *", "2011-08-15 12:00:00", "2011-08-15 11:59:30"],
196
+ ["*/15 * * * * * *", "2011-08-15 12:00:00", "2011-08-15 11:59:45"],
197
+ ["20-40 * * * * * *", "2011-08-15 12:00:00", "2011-08-15 11:59:40"],
198
+ ["12 15-59/15 * * * * *", "2014-02-01 15:46:00", "2014-02-01 15:45:12"],
199
+ ["* * * * * * 1970", "2014-02-01 15:46:00", "1970-12-31 23:59:59"],
200
+ ["1 1 1 1 1 * 1970", "2014-02-01 15:46:00", "1970-01-01 01:01:01"],
201
+ ["* * * * * * 1970-1978", "2014-02-01 15:46:00", "1978-12-31 23:59:59"],
202
+ ["* * * * * * */20", "2014-02-01 15:46:00", "2000-12-31 23:59:59"],
203
+ ].each do |line, now, expected_next|
204
+ it "should return #{expected_next} for '#{line}' when now is #{now}" do
205
+ now = parse_date(now)
206
+ expected_next = parse_date(expected_next)
207
+
208
+ parser = CronParser.new(line)
209
+
210
+ expect(parser.last(now)).to eq expected_next
211
+ end
212
+ end
213
+
214
+ [
215
+ ["* * * * * * 2018", "2014-02-01 15:46:00"]
216
+ ].each do |line, now|
217
+ it "should raise an error for '#{line}' when now is #{now}" do
218
+ now = parse_date(now)
219
+
220
+ parser = CronParser.new(line)
221
+
222
+ expect{parser.last(now)}.to raise_error "No matching dates exist"
223
+ end
224
+ end
225
+ end
226
+
227
+ describe "CronParser#new" do
228
+ it 'should not raise error when given a valid cronline' do
229
+ expect { CronParser.new('30 * * * *') }.not_to raise_error
230
+ end
231
+
232
+
233
+ [
234
+ ["* * * *"],
235
+ ["? ? ? ? ? ?"],
236
+ ["? * * * * *"],
237
+ ["* * * * * * * *"],
238
+ ].each do |line|
239
+ it 'should raise error when given an invalid cronline' do
240
+ expect { CronParser.new(line) }.to raise_error('not a valid cronline')
241
+ end
242
+ end
243
+ end
244
+
245
+ describe "time source" do
246
+ it "should use an alternate specified time source" do
247
+ ExtendedTime = Class.new(Time)
248
+ expect(ExtendedTime).to receive(:local).once
249
+ CronParser.new("* * * * *",ExtendedTime).next
250
+ end
251
+ end
@@ -0,0 +1,362 @@
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, :sec
10
+ attr_accessor :time_source
11
+
12
+ def initialize(time = Time.now, time_source = Time)
13
+ @year = time.year
14
+ @month = time.month
15
+ @day = time.day
16
+ @hour = time.hour
17
+ @min = time.min
18
+ @sec = time.sec
19
+
20
+ @time_source = time_source
21
+ end
22
+
23
+ def to_time
24
+ time_source.local(@year, @month, @day, @hour, @min, @sec)
25
+ end
26
+
27
+ def inspect
28
+ [year, month, day, hour, min, sec].inspect
29
+ end
30
+ end
31
+
32
+ SYMBOLS = {
33
+ "jan" => "1",
34
+ "feb" => "2",
35
+ "mar" => "3",
36
+ "apr" => "4",
37
+ "may" => "5",
38
+ "jun" => "6",
39
+ "jul" => "7",
40
+ "aug" => "8",
41
+ "sep" => "9",
42
+ "oct" => "10",
43
+ "nov" => "11",
44
+ "dec" => "12",
45
+
46
+ "sun" => "0",
47
+ "mon" => "1",
48
+ "tue" => "2",
49
+ "wed" => "3",
50
+ "thu" => "4",
51
+ "fri" => "5",
52
+ "sat" => "6"
53
+ }
54
+
55
+ def initialize(source,time_source = Time)
56
+ @source = interpret_vixieisms(source)
57
+ @time_source = time_source
58
+ validate_source
59
+ end
60
+
61
+ def interpret_vixieisms(spec)
62
+ case spec
63
+ when '@reboot'
64
+ raise ArgumentError, "Can't predict last/next run of @reboot"
65
+ when '@yearly', '@annually'
66
+ '0 0 1 1 *'
67
+ when '@monthly'
68
+ '0 0 1 * *'
69
+ when '@weekly'
70
+ '0 0 * * 0'
71
+ when '@daily', '@midnight'
72
+ '0 0 * * *'
73
+ when '@hourly'
74
+ '0 * * * *'
75
+ when '@minutely'
76
+ '* * * * *'
77
+ else
78
+ spec
79
+ end
80
+ end
81
+
82
+
83
+ # returns the next occurence after the given date
84
+ def next(now = @time_source.now, num = 1)
85
+ t = InternalTime.new(now, @time_source)
86
+
87
+ unless time_specs[:year][0].include?(t.year)
88
+ nudge_year(t)
89
+ t.month = 0
90
+ end
91
+
92
+ unless time_specs[:month][0].include?(t.month)
93
+ nudge_month(t)
94
+ t.day = 0
95
+ end
96
+
97
+ unless interpolate_weekdays(t.year, t.month)[0].include?(t.day)
98
+ nudge_date(t)
99
+ t.hour = -1
100
+ end
101
+
102
+ unless time_specs[:hour][0].include?(t.hour)
103
+ nudge_hour(t)
104
+ t.min = -1
105
+ end
106
+
107
+ unless time_specs[:minute][0].include?(t.min)
108
+ nudge_minute(t)
109
+ t.sec = -1
110
+ end
111
+
112
+ # always nudge the second
113
+ nudge_second(t)
114
+ t = t.to_time
115
+ if num > 1
116
+ recursive_calculate(:next,t,num)
117
+ else
118
+ t
119
+ end
120
+ end
121
+
122
+ # returns the last occurence before the given date
123
+ def last(now = @time_source.now, num=1)
124
+ t = InternalTime.new(now,@time_source)
125
+
126
+ unless time_specs[:year][0].include?(t.year)
127
+ nudge_year(t, :last)
128
+ t.month = 13
129
+ end
130
+
131
+ unless time_specs[:month][0].include?(t.month)
132
+ nudge_month(t, :last)
133
+ t.day = 32
134
+ end
135
+
136
+ if t.day == 32 || !interpolate_weekdays(t.year, t.month)[0].include?(t.day)
137
+ nudge_date(t, :last)
138
+ t.hour = 24
139
+ end
140
+
141
+ unless time_specs[:hour][0].include?(t.hour)
142
+ nudge_hour(t, :last)
143
+ t.min = 60
144
+ end
145
+
146
+ unless time_specs[:minute][0].include?(t.min)
147
+ nudge_minute(t, :last)
148
+ t.sec = 60
149
+ end
150
+
151
+ # always nudge the second
152
+ nudge_second(t, :last)
153
+ t = t.to_time
154
+ if num > 1
155
+ recursive_calculate(:last,t,num)
156
+ else
157
+ t
158
+ end
159
+ end
160
+
161
+
162
+ SUBELEMENT_REGEX = %r{^(\d+)(-(\d+)(/(\d+))?)?$}
163
+ def parse_element(elem, allowed_range)
164
+ values = elem.split(',').map do |subel|
165
+ if subel =~ /^\*/
166
+ step = subel.length > 1 ? subel[2..-1].to_i : 1
167
+ stepped_range(allowed_range, step)
168
+ elsif subel =~ /^\?$/ && (allowed_range == (1..31) || allowed_range == (0..6))
169
+ step = subel.length > 1 ? subel[2..-1].to_i : 1
170
+ stepped_range(allowed_range, step)
171
+ else
172
+ if SUBELEMENT_REGEX === subel
173
+ if $5 # with range
174
+ stepped_range($1.to_i..$3.to_i, $5.to_i)
175
+ elsif $3 # range without step
176
+ stepped_range($1.to_i..$3.to_i, 1)
177
+ else # just a numeric
178
+ [$1.to_i]
179
+ end
180
+ else
181
+ raise ArgumentError, "Bad Vixie-style specification #{subel}"
182
+ end
183
+ end
184
+ end.flatten.sort
185
+
186
+ [Set.new(values), values, elem]
187
+ end
188
+
189
+
190
+ protected
191
+
192
+ def recursive_calculate(meth,time,num)
193
+ array = [time]
194
+ num.-(1).times do |num|
195
+ array << self.send(meth, array.last)
196
+ end
197
+ array
198
+ end
199
+
200
+ # returns a list of days which do both match time_spec[:dom] or time_spec[:dow]
201
+ def interpolate_weekdays(year, month)
202
+ @_interpolate_weekdays_cache ||= {}
203
+ @_interpolate_weekdays_cache["#{year}-#{month}"] ||= interpolate_weekdays_without_cache(year, month)
204
+ end
205
+
206
+ def interpolate_weekdays_without_cache(year, month)
207
+ t = Date.new(year, month, 1)
208
+ valid_mday, _, mday_field = time_specs[:dom]
209
+ valid_wday, _, wday_field = time_specs[:dow]
210
+
211
+ # Careful, if both DOW and DOM fields are non-wildcard,
212
+ # then we only need to match *one* for cron to run the job:
213
+ if not (mday_field == '*' and wday_field == '*')
214
+ valid_mday = [] if mday_field == '*'
215
+ valid_wday = [] if wday_field == '*'
216
+ end
217
+ # Careful: crontabs may use either 0 or 7 for Sunday:
218
+ valid_wday << 0 if valid_wday.include?(7)
219
+
220
+ result = []
221
+ while t.month == month
222
+ result << t.mday if valid_mday.include?(t.mday) || valid_wday.include?(t.wday)
223
+ t = t.succ
224
+ end
225
+
226
+ [Set.new(result), result]
227
+ end
228
+
229
+ def nudge_year(t, dir = :next)
230
+ spec = time_specs[:year][1]
231
+ next_value = find_best_next(t.year, spec, dir)
232
+ t.year = next_value || (dir == :next ? spec.first : spec.last)
233
+
234
+ # We've exhausted all years in the range
235
+ raise "No matching dates exist" if next_value.nil?
236
+ end
237
+
238
+ def nudge_month(t, dir = :next)
239
+ spec = time_specs[:month][1]
240
+ next_value = find_best_next(t.month, spec, dir)
241
+ t.month = next_value || (dir == :next ? spec.first : spec.last)
242
+
243
+ nudge_year(t, dir) if next_value.nil?
244
+
245
+ # we changed the month, so its likely that the date is incorrect now
246
+ valid_days = interpolate_weekdays(t.year, t.month)[1]
247
+ t.day = dir == :next ? valid_days.first : valid_days.last
248
+ end
249
+
250
+ def date_valid?(t, dir = :next)
251
+ interpolate_weekdays(t.year, t.month)[0].include?(t.day)
252
+ end
253
+
254
+ def nudge_date(t, dir = :next, can_nudge_month = true)
255
+ spec = interpolate_weekdays(t.year, t.month)[1]
256
+ next_value = find_best_next(t.day, spec, dir)
257
+ t.day = next_value || (dir == :next ? spec.first : spec.last)
258
+
259
+ nudge_month(t, dir) if next_value.nil? && can_nudge_month
260
+ end
261
+
262
+ def nudge_hour(t, dir = :next)
263
+ spec = time_specs[:hour][1]
264
+ next_value = find_best_next(t.hour, spec, dir)
265
+ t.hour = next_value || (dir == :next ? spec.first : spec.last)
266
+
267
+ nudge_date(t, dir) if next_value.nil?
268
+ end
269
+
270
+ def nudge_minute(t, dir = :next)
271
+ spec = time_specs[:minute][1]
272
+ next_value = find_best_next(t.min, spec, dir)
273
+ t.min = next_value || (dir == :next ? spec.first : spec.last)
274
+
275
+ nudge_hour(t, dir) if next_value.nil?
276
+ end
277
+
278
+ def nudge_second(t, dir = :next)
279
+ spec = time_specs[:second][1]
280
+ next_value = find_best_next(t.sec, spec, dir)
281
+ t.sec = next_value || (dir == :next ? spec.first : spec.last)
282
+
283
+ nudge_minute(t, dir) if next_value.nil?
284
+ end
285
+
286
+ def time_specs
287
+ @time_specs ||= begin
288
+ tokens = substitute_parse_symbols(@source).split(/\s+/)
289
+ # tokens now contains the 5 or 7 fields
290
+
291
+ if tokens.count == 5
292
+ {
293
+ :second => parse_element("0", 0..59), #second
294
+ :minute => parse_element(tokens[0], 0..59), #minute
295
+ :hour => parse_element(tokens[1], 0..23), #hour
296
+ :dom => parse_element(tokens[2], 1..31), #DOM
297
+ :month => parse_element(tokens[3], 1..12), #mon
298
+ :dow => parse_element(tokens[4], 0..6), #DOW
299
+ :year => parse_element("*", 2000..2050) #year
300
+ }
301
+ elsif tokens.count == 6
302
+ {
303
+ :second => parse_element(tokens[0], 0..59), #second
304
+ :minute => parse_element(tokens[1], 0..59), #minute
305
+ :hour => parse_element(tokens[2], 0..23), #hour
306
+ :dom => parse_element(tokens[3], 1..31), #DOM
307
+ :month => parse_element(tokens[4], 1..12), #mon
308
+ :dow => parse_element(tokens[5], 0..6), #DOW
309
+ :year => parse_element("*", 2000..2050) #year
310
+ }
311
+ else
312
+ {
313
+ :second => parse_element(tokens[0], 0..59), #second
314
+ :minute => parse_element(tokens[1], 0..59), #minute
315
+ :hour => parse_element(tokens[2], 0..23), #hour
316
+ :dom => parse_element(tokens[3], 1..31), #DOM
317
+ :month => parse_element(tokens[4], 1..12), #mon
318
+ :dow => parse_element(tokens[5], 0..6), #DOW
319
+ :year => parse_element(tokens[6], 2000..2050) #year
320
+ }
321
+ end
322
+ end
323
+ end
324
+
325
+ def substitute_parse_symbols(str)
326
+ SYMBOLS.inject(str.downcase) do |s, (symbol, replacement)|
327
+ s.gsub(symbol, replacement)
328
+ end
329
+ end
330
+
331
+
332
+ def stepped_range(rng, step = 1)
333
+ len = rng.last - rng.first
334
+
335
+ num = len.div(step)
336
+ result = (0..num).map { |i| rng.first + step * i }
337
+
338
+ result.pop if result[-1] == rng.last and rng.exclude_end?
339
+ result
340
+ end
341
+
342
+
343
+ # returns the smallest element from allowed which is greater than current
344
+ # returns nil if no matching value was found
345
+ def find_best_next(current, allowed, dir)
346
+ if dir == :next
347
+ allowed.sort.find { |val| val > current }
348
+ else
349
+ allowed.sort.reverse.find { |val| val < current }
350
+ end
351
+ end
352
+
353
+ def validate_source
354
+ unless @source.respond_to?(:split)
355
+ raise ArgumentError, 'not a valid cronline'
356
+ end
357
+ source_length = @source.split(/\s+/).length
358
+ unless (source_length >= 5 && source_length < 8)
359
+ raise ArgumentError, 'not a valid cronline'
360
+ end
361
+ end
362
+ end