whedon 0.0.0 → 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 ADDED
@@ -0,0 +1,6 @@
1
+ .bundle/
2
+ *.gem
3
+ Gemfile.lock
4
+ pkg/
5
+ coverage/
6
+ *.swp
data/.rspec ADDED
@@ -0,0 +1 @@
1
+ --color
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source "http://rubygems.org"
2
+
3
+ # Specify your gem's dependencies in whedon.gemspec
4
+ gemspec
data/README.md ADDED
@@ -0,0 +1,28 @@
1
+ # whedon - parse crontab syntax
2
+
3
+ The goal of this gem is to parse a crontab timing specification and produce an
4
+ object that can be queried about the schedule.
5
+
6
+ This gem began as an extraction of Rufus::CronLine from the rufus-schedule gem.
7
+
8
+ ## API example
9
+
10
+ ```
11
+ sch = Whedon::Schedule.new('30 * * * *')
12
+
13
+ # Most Recent
14
+ sch.last
15
+
16
+ # Upcoming
17
+ sch.next
18
+
19
+ # Next after date/time argument
20
+ sch.next("2020/07/01")
21
+
22
+ # Given date/time matches cron string
23
+ sch.matches?("2020/07/01 14:00:00")
24
+
25
+ # Give cron string represented as an array
26
+ # [seconds minutes hours days months weekdays monthdays timezone]
27
+ sch.to_a
28
+ ```
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')}", '-rwhedon'
11
+ end
@@ -0,0 +1,374 @@
1
+ #--
2
+ # Copyright (c) 2006-2013, John Mettraux, jmettraux@gmail.com
3
+ #
4
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
5
+ # of this software and associated documentation files (the "Software"), to deal
6
+ # in the Software without restriction, including without limitation the rights
7
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8
+ # copies of 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 be included in
12
+ # all copies or substantial portions of the Software.
13
+ #
14
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
20
+ # THE SOFTWARE.
21
+ #
22
+ # Made in Japan.
23
+ #++
24
+
25
+ require 'tzinfo'
26
+
27
+
28
+ module Whedon
29
+
30
+ #
31
+ # A 'cron line' is a line in the sense of a crontab
32
+ # (man 5 crontab) file line.
33
+ #
34
+ class Schedule
35
+
36
+ DAY_S = 24 * 3600
37
+ WEEK_S = 7 * DAY_S
38
+
39
+ # The string used for creating this cronline instance.
40
+ #
41
+ attr_reader :original
42
+
43
+ attr_reader :seconds
44
+ attr_reader :minutes
45
+ attr_reader :hours
46
+ attr_reader :days
47
+ attr_reader :months
48
+ attr_reader :weekdays
49
+ attr_reader :monthdays
50
+ attr_reader :timezone
51
+
52
+ attr_accessor :raise_error_on_duplicate
53
+
54
+ def initialize(line)
55
+
56
+ super()
57
+
58
+ @original = line
59
+
60
+ items = line.split
61
+
62
+ @timezone = (TZInfo::Timezone.get(items.last) rescue nil)
63
+ items.pop if @timezone
64
+
65
+ raise ArgumentError.new(
66
+ "not a valid cronline : '#{line}'"
67
+ ) unless items.length == 5 or items.length == 6
68
+
69
+ offset = items.length - 5
70
+
71
+ @seconds = offset == 1 ? parse_item(items[0], 0, 59) : [ 0 ]
72
+ @minutes = parse_item(items[0 + offset], 0, 59)
73
+ @hours = parse_item(items[1 + offset], 0, 24)
74
+ @days = parse_item(items[2 + offset], 1, 31)
75
+ @months = parse_item(items[3 + offset], 1, 12)
76
+ @weekdays, @monthdays = parse_weekdays(items[4 + offset])
77
+
78
+ [ @seconds, @minutes, @hours, @months ].each do |es|
79
+
80
+ raise ArgumentError.new(
81
+ "invalid cronline: '#{line}'"
82
+ ) if es && es.find { |e| ! e.is_a?(Fixnum) }
83
+ end
84
+ end
85
+
86
+ # Returns true if the given time matches this cron line.
87
+ #
88
+ def matches?(time)
89
+
90
+ time = as_time(time)
91
+
92
+ return false unless sub_match?(time, :sec, @seconds)
93
+ return false unless sub_match?(time, :min, @minutes)
94
+ return false unless sub_match?(time, :hour, @hours)
95
+ return false unless date_match?(time)
96
+ true
97
+ end
98
+
99
+ def now?(time=Time.now)
100
+ matches?(time)
101
+ end
102
+
103
+ # Returns the next time that this cron line is supposed to 'fire'
104
+ #
105
+ # This is raw, 3 secs to iterate over 1 year on my macbook :( brutal.
106
+ # (Well, I was wrong, takes 0.001 sec on 1.8.7 and 1.9.1)
107
+ #
108
+ # This method accepts an optional Time parameter. It's the starting point
109
+ # for the 'search'. By default, it's Time.now
110
+ #
111
+ # Note that the time instance returned will be in the same time zone that
112
+ # the given start point Time (thus a result in the local time zone will
113
+ # be passed if no start time is specified (search start time set to
114
+ # Time.now))
115
+ #
116
+ # Whedon::CronLine.new('30 7 * * *').next_time(
117
+ # Time.mktime(2008, 10, 24, 7, 29))
118
+ # #=> Fri Oct 24 07:30:00 -0500 2008
119
+ #
120
+ # Whedon::CronLine.new('30 7 * * *').next_time(
121
+ # Time.utc(2008, 10, 24, 7, 29))
122
+ # #=> Fri Oct 24 07:30:00 UTC 2008
123
+ #
124
+ # Whedon::CronLine.new('30 7 * * *').next_time(
125
+ # Time.utc(2008, 10, 24, 7, 29)).localtime
126
+ # #=> Fri Oct 24 02:30:00 -0500 2008
127
+ #
128
+ # (Thanks to K Liu for the note and the examples)
129
+ #
130
+ def next_time(now=Time.now)
131
+
132
+ time = as_time(now)
133
+ time = time - time.usec * 1e-6 + 1
134
+ # small adjustment before starting
135
+
136
+ loop do
137
+
138
+ unless date_match?(time)
139
+ time += (24 - time.hour) * 3600 - time.min * 60 - time.sec; next
140
+ end
141
+ unless sub_match?(time, :hour, @hours)
142
+ time += (60 - time.min) * 60 - time.sec; next
143
+ end
144
+ unless sub_match?(time, :min, @minutes)
145
+ time += 60 - time.sec; next
146
+ end
147
+ unless sub_match?(time, :sec, @seconds)
148
+ time += 1; next
149
+ end
150
+
151
+ break
152
+ end
153
+
154
+ if @timezone
155
+ time = @timezone.local_to_utc(time)
156
+ time = time.getlocal unless now.utc?
157
+ end
158
+
159
+ time
160
+ end
161
+ alias_method :next, :next_time
162
+
163
+ # Returns the previous the cronline matched. It's like next_time, but
164
+ # for the past.
165
+ #
166
+ def previous_time(now=Time.now)
167
+
168
+ # looks back by slices of two hours,
169
+ #
170
+ # finds for '* * * * sun', '* * 13 * *' and '0 12 13 * *'
171
+ # starting 1970, 1, 1 in 1.8 to 2 seconds (says Rspec)
172
+
173
+ start = current = now - 2 * 3600
174
+ result = nil
175
+
176
+ loop do
177
+ nex = next_time(current)
178
+ return (result ? result : previous_time(start)) if nex > now
179
+ result = current = nex
180
+ end
181
+
182
+ # never reached
183
+ end
184
+ alias_method :previous, :previous_time
185
+ alias_method :last, :previous_time
186
+
187
+ # Returns an array of 6 arrays (seconds, minutes, hours, days,
188
+ # months, weekdays).
189
+ # This method is used by the cronline unit tests.
190
+ #
191
+ def to_array
192
+
193
+ [
194
+ @seconds,
195
+ @minutes,
196
+ @hours,
197
+ @days,
198
+ @months,
199
+ @weekdays,
200
+ @monthdays,
201
+ @timezone ? @timezone.name : nil
202
+ ]
203
+ end
204
+ alias_method :to_a, :to_array
205
+
206
+ private
207
+
208
+ WEEKDAYS = %w[ sun mon tue wed thu fri sat ]
209
+
210
+ def raise_error_on_duplicate?
211
+ !!(raise_error_on_duplicate)
212
+ end
213
+
214
+ def as_time(time)
215
+ unless time.kind_of?(Time)
216
+ time = ( time.to_s =~ /^\d+$/ ) ?
217
+ Time.at(time.to_s) : Time.parse(time.to_s)
218
+ end
219
+
220
+ time = @timezone.utc_to_local(time.getutc) if @timezone
221
+ time
222
+ end
223
+
224
+ def parse_weekdays(item)
225
+
226
+ return nil if item == '*'
227
+
228
+ items = item.downcase.split(',')
229
+
230
+ weekdays = nil
231
+ monthdays = nil
232
+
233
+ items.each do |it|
234
+
235
+ if m = it.match(/^(.+)#(l|-?[12345])$/)
236
+
237
+ raise ArgumentError.new(
238
+ "ranges are not supported for monthdays (#{it})"
239
+ ) if m[1].index('-')
240
+
241
+ expr = it.gsub(/#l/, '#-1')
242
+
243
+ (monthdays ||= []) << expr
244
+
245
+ else
246
+
247
+ expr = it.dup
248
+ WEEKDAYS.each_with_index { |a, i| expr.gsub!(/#{a}/, i.to_s) }
249
+
250
+ raise ArgumentError.new(
251
+ "invalid weekday expression (#{it})"
252
+ ) if expr !~ /^0*[0-7](-0*[0-7])?$/
253
+
254
+ its = expr.index('-') ? parse_range(expr, 0, 7) : [ Integer(expr) ]
255
+ its = its.collect { |i| i == 7 ? 0 : i }
256
+
257
+ (weekdays ||= []).concat(its)
258
+ end
259
+ end
260
+
261
+ weekdays = weekdays.uniq if weekdays
262
+
263
+ [ weekdays, monthdays ]
264
+ end
265
+
266
+ def parse_item(item, min, max)
267
+
268
+ return nil if item == '*'
269
+
270
+ r = item.split(',').map { |i| parse_range(i.strip, min, max) }.flatten
271
+
272
+ raise ArgumentError.new(
273
+ "found duplicates in #{item.inspect}"
274
+ ) if raise_error_on_duplicate? && r.uniq.size < r.size
275
+
276
+ r.uniq
277
+ end
278
+
279
+ RANGE_REGEX = /^(\*|\d{1,2})(?:-(\d{1,2}))?(?:\/(\d{1,2}))?$/
280
+
281
+ def parse_range(item, min, max)
282
+
283
+ return %w[ L ] if item == 'L'
284
+
285
+ m = item.match(RANGE_REGEX)
286
+
287
+ raise ArgumentError.new(
288
+ "cannot parse #{item.inspect}"
289
+ ) unless m
290
+
291
+ sta = m[1]
292
+ sta = sta == '*' ? min : sta.to_i
293
+
294
+ edn = m[2]
295
+ edn = edn ? edn.to_i : sta
296
+ edn = max if m[1] == '*'
297
+
298
+ inc = m[3]
299
+ inc = inc ? inc.to_i : 1
300
+
301
+ raise ArgumentError.new(
302
+ "#{item.inspect} is not in range #{min}..#{max}"
303
+ ) if sta < min or edn > max
304
+
305
+ r = []
306
+ val = sta
307
+
308
+ loop do
309
+ v = val
310
+ v = 0 if max == 24 && v == 24
311
+ r << v
312
+ break if inc == 1 && val == edn
313
+ val += inc
314
+ break if inc > 1 && val > edn
315
+ val = min if val > max
316
+ end
317
+
318
+ r.uniq
319
+ end
320
+
321
+ def sub_match?(time, accessor, values)
322
+
323
+ value = time.send(accessor)
324
+
325
+ return true if values.nil?
326
+ return true if values.include?('L') && (time + DAY_S).day == 1
327
+
328
+ return true if value == 0 && accessor == :hour && values.include?(24)
329
+
330
+ values.include?(value)
331
+ end
332
+
333
+ def monthday_match?(date, values)
334
+
335
+ return true if values.nil?
336
+
337
+ today_values = monthdays(date)
338
+
339
+ (today_values & values).any?
340
+ end
341
+
342
+ def date_match?(date)
343
+
344
+ return false unless sub_match?(date, :day, @days)
345
+ return false unless sub_match?(date, :month, @months)
346
+ return false unless sub_match?(date, :wday, @weekdays)
347
+ return false unless monthday_match?(date, @monthdays)
348
+ true
349
+ end
350
+
351
+ def monthdays(date)
352
+
353
+ pos = 1
354
+ d = date.dup
355
+
356
+ loop do
357
+ d = d - WEEK_S
358
+ break if d.month != date.month
359
+ pos = pos + 1
360
+ end
361
+
362
+ neg = -1
363
+ d = date.dup
364
+
365
+ loop do
366
+ d = d + WEEK_S
367
+ break if d.month != date.month
368
+ neg = neg - 1
369
+ end
370
+
371
+ [ "#{WEEKDAYS[date.wday]}##{pos}", "#{WEEKDAYS[date.wday]}##{neg}" ]
372
+ end
373
+ end
374
+ end
@@ -0,0 +1,5 @@
1
+ module Parse
2
+ module Cron
3
+ VERSION = "0.0.1"
4
+ end
5
+ end
data/lib/whedon.rb ADDED
@@ -0,0 +1,2 @@
1
+ require 'whedon/version'
2
+ require 'whedon/schedule'
@@ -0,0 +1,25 @@
1
+ require File.expand_path("../../lib/whedon", __FILE__)
2
+
3
+ class Ex < Whedon::Schedule
4
+ def initialize(line)
5
+ self.raise_error_on_duplicate = true
6
+ super(line)
7
+ end
8
+ end
9
+
10
+ def local(*args)
11
+ Time.local(*args)
12
+ end
13
+ alias lo local
14
+
15
+ def utc(*args)
16
+ Time.utc(*args)
17
+ end
18
+
19
+ def cl(line)
20
+ Whedon::Schedule.new(line)
21
+ end
22
+
23
+ def compare(line, array)
24
+ cl(line).to_array.should == array
25
+ end
@@ -0,0 +1,383 @@
1
+
2
+ #
3
+ # Specifying rufus-scheduler
4
+ #
5
+ # Sat Mar 21 12:55:27 JST 2009
6
+ #
7
+
8
+ #require 'spec_base'
9
+ require "spec_helper"
10
+ require "whedon/schedule"
11
+
12
+ describe Whedon::Schedule do
13
+ #
14
+ # See spec_helper.rb for definitions for the class & methods
15
+ # used in these tests. This includes Ex, local, utc, cl,
16
+ # match, no_match, and compare
17
+
18
+ describe '.new' do
19
+
20
+ it 'interprets cron strings correctly' do
21
+
22
+ compare '* * * * *', [ [0], nil, nil, nil, nil, nil, nil, nil ]
23
+ compare '10-12 * * * *', [ [0], [10, 11, 12], nil, nil, nil, nil, nil, nil ]
24
+ compare '* * * * sun,mon', [ [0], nil, nil, nil, nil, [0, 1], nil, nil ]
25
+ compare '* * * * mon-wed', [ [0], nil, nil, nil, nil, [1, 2, 3], nil, nil ]
26
+ compare '* * * * 7', [ [0], nil, nil, nil, nil, [0], nil, nil ]
27
+ compare '* * * * 0', [ [0], nil, nil, nil, nil, [0], nil, nil ]
28
+ compare '* * * * 0,1', [ [0], nil, nil, nil, nil, [0,1], nil, nil ]
29
+ compare '* * * * 7,1', [ [0], nil, nil, nil, nil, [0,1], nil, nil ]
30
+ compare '* * * * 7,0', [ [0], nil, nil, nil, nil, [0], nil, nil ]
31
+ compare '* * * * sun,2-4', [ [0], nil, nil, nil, nil, [0, 2, 3, 4], nil, nil ]
32
+
33
+ compare '* * * * sun,mon-tue', [ [0], nil, nil, nil, nil, [0, 1, 2], nil, nil ]
34
+
35
+ compare '* * * * * *', [ nil, nil, nil, nil, nil, nil, nil, nil ]
36
+ compare '1 * * * * *', [ [1], nil, nil, nil, nil, nil, nil, nil ]
37
+ compare '7 10-12 * * * *', [ [7], [10, 11, 12], nil, nil, nil, nil, nil, nil ]
38
+ compare '1-5 * * * * *', [ [1,2,3,4,5], nil, nil, nil, nil, nil, nil, nil ]
39
+
40
+ compare '0 0 1 1 *', [ [0], [0], [0], [1], [1], nil, nil, nil ]
41
+
42
+ compare '0 23-24 * * *', [ [0], [0], [23, 0], nil, nil, nil, nil, nil ]
43
+ #
44
+ # as reported by Aimee Rose in
45
+ # https://github.com/jmettraux/rufus-scheduler/issues/56
46
+
47
+ compare '0 23-2 * * *', [ [0], [0], [23, 0, 1, 2], nil, nil, nil, nil, nil ]
48
+ end
49
+
50
+ it 'rejects invalid weekday expressions' do
51
+
52
+ lambda { cl '0 17 * * MON_FRI' }.should raise_error
53
+ # underline instead of dash
54
+
55
+ lambda { cl '* * * * 9' }.should raise_error
56
+ lambda { cl '* * * * 0-12' }.should raise_error
57
+ lambda { cl '* * * * BLABLA' }.should raise_error
58
+ end
59
+
60
+ it 'rejects invalid cronlines' do
61
+
62
+ lambda { cl '* nada * * 9' }.should raise_error(ArgumentError)
63
+ end
64
+
65
+ it 'interprets cron strings with TZ correctly' do
66
+
67
+ compare '* * * * * EST', [ [0], nil, nil, nil, nil, nil, nil, 'EST' ]
68
+ compare '* * * * * * EST', [ nil, nil, nil, nil, nil, nil, nil, 'EST' ]
69
+
70
+ lambda { cl '* * * * * NotATimeZone' }.should raise_error
71
+ lambda { cl '* * * * * * NotATimeZone' }.should raise_error
72
+ end
73
+
74
+ it 'interprets cron strings with / (slashes) correctly' do
75
+
76
+ compare(
77
+ '0 */2 * * *',
78
+ [ [0], [0], (0..11).collect { |e| e * 2 }, nil, nil, nil, nil, nil ])
79
+ compare(
80
+ '0 7-23/2 * * *',
81
+ [ [0], [0], (7..23).select { |e| e.odd? }, nil, nil, nil, nil, nil ])
82
+ compare(
83
+ '*/10 * * * *',
84
+ [ [0], [0, 10, 20, 30, 40, 50], nil, nil, nil, nil, nil, nil ])
85
+ end
86
+
87
+ it 'does not support ranges for monthdays (sun#1-sun#2)' do
88
+
89
+ lambda {
90
+ Whedon::Schedule.new('* * * * sun#1-sun#2')
91
+ }.should raise_error(ArgumentError)
92
+ end
93
+
94
+ it 'accepts items with initial 0' do
95
+
96
+ compare '09 * * * *', [ [0], [9], nil, nil, nil, nil, nil, nil ]
97
+ compare '09-12 * * * *', [ [0], [9, 10, 11, 12], nil, nil, nil, nil, nil, nil ]
98
+ compare '07-08 * * * *', [ [0], [7, 8], nil, nil, nil, nil, nil, nil ]
99
+ compare '* */08 * * *', [ [0], nil, [0, 8, 16], nil, nil, nil, nil, nil ]
100
+ compare '* */07 * * *', [ [0], nil, [0, 7, 14, 21], nil, nil, nil, nil, nil ]
101
+ compare '* 01-09/04 * * *', [ [0], nil, [1, 5, 9], nil, nil, nil, nil, nil ]
102
+ compare '* * * * 06', [ [0], nil, nil, nil, nil, [6], nil, nil ]
103
+ end
104
+
105
+ it 'ignores duplicates by default' do
106
+
107
+ compare '* * L,L * *', [[0], nil, nil, ['L'], nil, nil, nil, nil ]
108
+ compare '*/20,40 * * * *', [ [0], [0, 20, 40 ], nil, nil, nil, nil, nil, nil ]
109
+ end
110
+
111
+ it 'raises an error for duplicates when configured to do so' do
112
+
113
+ lambda { Ex.new('* * L,L * *') }.should raise_error(ArgumentError)
114
+ end
115
+
116
+ it 'interprets cron strings with L correctly' do
117
+
118
+ compare '* * L * *', [[0], nil, nil, ['L'], nil, nil, nil, nil ]
119
+ compare '* * 2-5,L * *', [[0], nil, nil, [2,3,4,5,'L'], nil, nil, nil, nil ]
120
+ compare '* * */8,L * *', [[0], nil, nil, [1,9,17,25,'L'], nil, nil, nil, nil ]
121
+ end
122
+
123
+ it 'does not support ranges for L' do
124
+
125
+ lambda { cl '* * 15-L * *'}.should raise_error(ArgumentError)
126
+ lambda { cl '* * L/4 * *'}.should raise_error(ArgumentError)
127
+ end
128
+
129
+ it 'raises if L is used for something else than days' do
130
+
131
+ lambda { cl '* L * * *'}.should raise_error(ArgumentError)
132
+ end
133
+
134
+ it 'raises for out of range input' do
135
+
136
+ lambda { cl '60-62 * * * *'}.should raise_error(ArgumentError)
137
+ lambda { cl '62 * * * *'}.should raise_error(ArgumentError)
138
+ lambda { cl '60 * * * *'}.should raise_error(ArgumentError)
139
+ lambda { cl '* 25-26 * * *'}.should raise_error(ArgumentError)
140
+ lambda { cl '* 25 * * *'}.should raise_error(ArgumentError)
141
+ #
142
+ # as reported by Aimee Rose in
143
+ # https://github.com/jmettraux/rufus-scheduler/pull/58
144
+ end
145
+ end
146
+
147
+ describe '#next_time' do
148
+
149
+ def nt(cronline, now)
150
+ Whedon::Schedule.new(cronline).next_time(now)
151
+ end
152
+
153
+ it 'computes the next occurence correctly' do
154
+
155
+ now = Time.at(0).getutc # Thu Jan 01 00:00:00 UTC 1970
156
+
157
+ nt('* * * * *', now).should == now + 60
158
+ nt('* * * * sun', now).should == now + 259200
159
+ nt('* * * * * *', now).should == now + 1
160
+ nt('* * 13 * fri', now).should == now + 3715200
161
+
162
+ nt('10 12 13 12 *', now).should == now + 29938200
163
+ # this one is slow (1 year == 3 seconds)
164
+ #
165
+ # historical note:
166
+ # (comment made in 2006 or 2007, the underlying libs got better and
167
+ # that slowness is gone)
168
+
169
+ nt('0 0 * * thu', now).should == now + 604800
170
+
171
+ nt('0 0 * * *', now).should == now + 24 * 3600
172
+ nt('0 24 * * *', now).should == now + 24 * 3600
173
+
174
+ now = local(2008, 12, 31, 23, 59, 59, 0)
175
+
176
+ nt('* * * * *', now).should == now + 1
177
+ end
178
+
179
+ it 'computes the next occurence correctly in UTC (TZ not specified)' do
180
+
181
+ now = utc(1970, 1, 1)
182
+
183
+ nt('* * * * *', now).should == utc(1970, 1, 1, 0, 1)
184
+ nt('* * * * sun', now).should == utc(1970, 1, 4)
185
+ nt('* * * * * *', now).should == utc(1970, 1, 1, 0, 0, 1)
186
+ nt('* * 13 * fri', now).should == utc(1970, 2, 13)
187
+
188
+ nt('10 12 13 12 *', now).should == utc(1970, 12, 13, 12, 10)
189
+ # this one is slow (1 year == 3 seconds)
190
+ nt('* * 1 6 *', now).should == utc(1970, 6, 1)
191
+
192
+ nt('0 0 * * thu', now).should == utc(1970, 1, 8)
193
+ end
194
+
195
+ it 'computes the next occurence correctly in local TZ (TZ not specified)' do
196
+
197
+ now = local(1970, 1, 1)
198
+
199
+ nt('* * * * *', now).should == local(1970, 1, 1, 0, 1)
200
+ nt('* * * * sun', now).should == local(1970, 1, 4)
201
+ nt('* * * * * *', now).should == local(1970, 1, 1, 0, 0, 1)
202
+ nt('* * 13 * fri', now).should == local(1970, 2, 13)
203
+
204
+ nt('10 12 13 12 *', now).should == local(1970, 12, 13, 12, 10)
205
+ # this one is slow (1 year == 3 seconds)
206
+ nt('* * 1 6 *', now).should == local(1970, 6, 1)
207
+
208
+ nt('0 0 * * thu', now).should == local(1970, 1, 8)
209
+ end
210
+
211
+ it 'computes the next occurence correctly in UTC (TZ specified)' do
212
+
213
+ zone = 'Europe/Stockholm'
214
+ tz = TZInfo::Timezone.get(zone)
215
+ now = tz.local_to_utc(local(1970, 1, 1))
216
+ # Midnight in zone, UTC
217
+
218
+ nt("* * * * * #{zone}", now).should == utc(1969, 12, 31, 23, 1)
219
+ nt("* * * * sun #{zone}", now).should == utc(1970, 1, 3, 23)
220
+ nt("* * * * * * #{zone}", now).should == utc(1969, 12, 31, 23, 0, 1)
221
+ nt("* * 13 * fri #{zone}", now).should == utc(1970, 2, 12, 23)
222
+
223
+ nt("10 12 13 12 * #{zone}", now).should == utc(1970, 12, 13, 11, 10)
224
+ nt("* * 1 6 * #{zone}", now).should == utc(1970, 5, 31, 23)
225
+
226
+ nt("0 0 * * thu #{zone}", now).should == utc(1970, 1, 7, 23)
227
+ end
228
+
229
+ #it 'computes the next occurence correctly in local TZ (TZ specified)' do
230
+ # zone = 'Europe/Stockholm'
231
+ # tz = TZInfo::Timezone.get(zone)
232
+ # now = tz.local_to_utc(utc(1970, 1, 1)).localtime
233
+ # # Midnight in zone, local time
234
+ # nt("* * * * * #{zone}", now).should == local(1969, 12, 31, 18, 1)
235
+ # nt("* * * * sun #{zone}", now).should == local(1970, 1, 3, 18)
236
+ # nt("* * * * * * #{zone}", now).should == local(1969, 12, 31, 18, 0, 1)
237
+ # nt("* * 13 * fri #{zone}", now).should == local(1970, 2, 12, 18)
238
+ # nt("10 12 13 12 * #{zone}", now).should == local(1970, 12, 13, 6, 10)
239
+ # nt("* * 1 6 * #{zone}", now).should == local(1970, 5, 31, 19)
240
+ # nt("0 0 * * thu #{zone}", now).should == local(1970, 1, 7, 18)
241
+ #end
242
+
243
+ it 'computes the next time correctly when there is a sun#2 involved' do
244
+
245
+ nt('* * * * sun#1', local(1970, 1, 1)).should == local(1970, 1, 4)
246
+ nt('* * * * sun#2', local(1970, 1, 1)).should == local(1970, 1, 11)
247
+
248
+ nt('* * * * sun#2', local(1970, 1, 12)).should == local(1970, 2, 8)
249
+ end
250
+
251
+ it 'computes the next time correctly when there is a sun#2,sun#3 involved' do
252
+
253
+ nt('* * * * sun#2,sun#3', local(1970, 1, 1)).should == local(1970, 1, 11)
254
+ nt('* * * * sun#2,sun#3', local(1970, 1, 12)).should == local(1970, 1, 18)
255
+ end
256
+
257
+ it 'understands sun#L' do
258
+
259
+ nt('* * * * sun#L', local(1970, 1, 1)).should == local(1970, 1, 25)
260
+ end
261
+
262
+ it 'understands sun#-1' do
263
+
264
+ nt('* * * * sun#-1', local(1970, 1, 1)).should == local(1970, 1, 25)
265
+ end
266
+
267
+ it 'understands sun#-2' do
268
+
269
+ nt('* * * * sun#-2', local(1970, 1, 1)).should == local(1970, 1, 18)
270
+ end
271
+
272
+ it 'computes the next time correctly when "L" (last day of month)' do
273
+
274
+ nt('* * L * *', lo(1970, 1, 1)).should == lo(1970, 1, 31)
275
+ nt('* * L * *', lo(1970, 2, 1)).should == lo(1970, 2, 28)
276
+ nt('* * L * *', lo(1972, 2, 1)).should == lo(1972, 2, 29)
277
+ nt('* * L * *', lo(1970, 4, 1)).should == lo(1970, 4, 30)
278
+ end
279
+ end
280
+
281
+ describe '#previous_time' do
282
+
283
+ def pt(cronline, now)
284
+ Whedon::Schedule.new(cronline).previous_time(now)
285
+ end
286
+
287
+ it 'returns the previous time the cron should have triggered' do
288
+
289
+ pt('* * * * sun', lo(1970, 1, 1)).should == lo(1969, 12, 28, 23, 59, 00)
290
+ pt('* * 13 * *', lo(1970, 1, 1)).should == lo(1969, 12, 13, 23, 59, 00)
291
+ pt('0 12 13 * *', lo(1970, 1, 1)).should == lo(1969, 12, 13, 12, 00)
292
+
293
+ pt('* * * * * sun', lo(1970, 1, 1)).should == lo(1969, 12, 28, 23, 59, 59)
294
+ end
295
+ end
296
+
297
+ describe '#matches?' do
298
+
299
+
300
+ [ ['* * * * *', utc(1970, 1, 1, 0, 1), true],
301
+ ['* * * * sun', utc(1970, 1, 4), true],
302
+ ['* * * * * *', utc(1970, 1, 1, 0, 0, 1), true],
303
+ ['* * 13 * fri', utc(1970, 2, 13), true],
304
+ ['10 12 13 12 *', utc(1970, 12, 13, 12, 10), true],
305
+ ['* * 1 6 *', utc(1970, 6, 1), true],
306
+ ['0 0 * * thu', utc(1970, 1, 8), true],
307
+ ['0 0 1 1 *', utc(2012, 1, 1), true],
308
+ ['0 0 1 1 *', utc(2012, 1, 1, 1, 0), false]
309
+ ].each do |line, time, result|
310
+ it 'matches correctly in UTC (TZ not specified)' do
311
+ cl(line).matches?(time).should eql(result)
312
+ end
313
+ end
314
+
315
+ [ ['* * * * *', local(1970, 1, 1, 0, 1), true],
316
+ ['* * * * sun', local(1970, 1, 4), true],
317
+ ['* * * * * *', local(1970, 1, 1, 0, 0, 1), true],
318
+ ['* * 13 * fri', local(1970, 2, 13), true],
319
+ ['10 12 13 12 *', local(1970, 12, 13, 12, 10), true],
320
+ ['* * 1 6 *', local(1970, 6, 1), true],
321
+ ['0 0 * * thu', local(1970, 1, 8), true],
322
+ ['0 0 1 1 *', local(2012, 1, 1), true],
323
+ ['0 0 1 1 *', local(2012, 1, 1, 1, 0), false]
324
+ ].each do |line, time, result|
325
+ it 'matches correctly in local TZ (TZ not specified)' do
326
+ cl(line).matches?(time).should eql(result)
327
+ end
328
+ end
329
+
330
+ zone = 'Europe/Stockholm'
331
+ [ ["* * * * * #{zone}", utc(1969, 12, 31, 23, 1), true],
332
+ ["* * * * sun #{zone}", utc(1970, 1, 3, 23), true],
333
+ ["* * * * * * #{zone}", utc(1969, 12, 31, 23, 0, 1), true],
334
+ ["* * 13 * fri #{zone}", utc(1970, 2, 12, 23), true],
335
+ ["10 12 13 12 * #{zone}", utc(1970, 12, 13, 11, 10), true],
336
+ ["* * 1 6 * #{zone}", utc(1970, 5, 31, 23), true],
337
+ ["0 0 * * thu #{zone}", utc(1970, 1, 7, 23), true],
338
+ ].each do |line, time, result|
339
+ it 'matches correctly in UTC (TZ specified)' do
340
+ cl(line).matches?(time).should eql(result)
341
+ end
342
+ end
343
+
344
+ it 'matches correctly when there is a sun#2 involved' do
345
+
346
+ cl('* * 13 * fri#2').matches?(utc(1970, 2, 13)).should be_true
347
+ cl('* * 13 * fri#2').matches?(utc(1970, 2, 20)).should be_false
348
+ end
349
+
350
+ it 'matches correctly when there is a L involved' do
351
+
352
+ cl('* * L * *').matches?(utc(1970, 1, 31)).should be_true
353
+ cl('* * L * *').matches?(utc(1970, 1, 30)).should be_false
354
+ end
355
+
356
+ it 'matches correctly when there is a sun#2,sun#3 involved' do
357
+
358
+ cl('* * * * sun#2,sun#3').matches?( local(1970, 1, 4) ).should be_false
359
+ cl('* * * * sun#2,sun#3').matches?( local(1970, 1, 11) ).should be_true
360
+ cl('* * * * sun#2,sun#3').matches?( local(1970, 1, 18) ).should be_true
361
+ cl('* * * * sun#2,sun#3').matches?( local(1970, 1, 25) ).should be_false
362
+ end
363
+ end
364
+
365
+ describe '#monthdays' do
366
+
367
+ it 'returns the appropriate "sun#2"-like string' do
368
+
369
+ class Whedon::Schedule
370
+ public :monthdays
371
+ end
372
+
373
+ cl = Whedon::Schedule.new('* * * * *')
374
+
375
+ cl.monthdays(local(1970, 1, 1)).should == %w[ thu#1 thu#-5 ]
376
+ cl.monthdays(local(1970, 1, 7)).should == %w[ wed#1 wed#-4 ]
377
+ cl.monthdays(local(1970, 1, 14)).should == %w[ wed#2 wed#-3 ]
378
+
379
+ cl.monthdays(local(2011, 3, 11)).should == %w[ fri#2 fri#-3 ]
380
+ end
381
+ end
382
+ end
383
+
data/whedon.gemspec ADDED
@@ -0,0 +1,27 @@
1
+ # -*- encoding: utf-8 -*-
2
+ $:.push File.expand_path("../lib", __FILE__)
3
+ require "whedon/version"
4
+
5
+ Gem::Specification.new do |s|
6
+ s.name = "whedon"
7
+ s.version = Parse::Cron::VERSION
8
+ s.platform = Gem::Platform::RUBY
9
+ s.authors = ["John Mettraux", "Blake Thomas"]
10
+ s.email = ["jmettraux@gmail.com", "bwthomas@gmail.com"]
11
+ s.homepage = "https://github.com/bwthomas/whedon"
12
+ s.summary = %q{Parses cron lines}
13
+ s.description = %q{Parses cron lines into a Schedule instance that can be queried.}
14
+
15
+ s.rubyforge_project = "whedon"
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_dependency 'tzinfo'
23
+
24
+ s.add_development_dependency 'rspec', '~>2.6.0'
25
+ s.add_development_dependency 'rake'
26
+ s.add_development_dependency 'pry'
27
+ end
metadata CHANGED
@@ -1,24 +1,101 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: whedon
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.0
4
+ version: 0.0.1
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
8
+ - John Mettraux
8
9
  - Blake Thomas
9
10
  autorequire:
10
11
  bindir: bin
11
12
  cert_chain: []
12
- date: 2013-06-28 00:00:00.000000000 Z
13
- dependencies: []
14
- description: Nothing to see here.
13
+ date: 2013-06-30 00:00:00.000000000 Z
14
+ dependencies:
15
+ - !ruby/object:Gem::Dependency
16
+ name: tzinfo
17
+ requirement: !ruby/object:Gem::Requirement
18
+ none: false
19
+ requirements:
20
+ - - ! '>='
21
+ - !ruby/object:Gem::Version
22
+ version: '0'
23
+ type: :runtime
24
+ prerelease: false
25
+ version_requirements: !ruby/object:Gem::Requirement
26
+ none: false
27
+ requirements:
28
+ - - ! '>='
29
+ - !ruby/object:Gem::Version
30
+ version: '0'
31
+ - !ruby/object:Gem::Dependency
32
+ name: rspec
33
+ requirement: !ruby/object:Gem::Requirement
34
+ none: false
35
+ requirements:
36
+ - - ~>
37
+ - !ruby/object:Gem::Version
38
+ version: 2.6.0
39
+ type: :development
40
+ prerelease: false
41
+ version_requirements: !ruby/object:Gem::Requirement
42
+ none: false
43
+ requirements:
44
+ - - ~>
45
+ - !ruby/object:Gem::Version
46
+ version: 2.6.0
47
+ - !ruby/object:Gem::Dependency
48
+ name: rake
49
+ requirement: !ruby/object:Gem::Requirement
50
+ none: false
51
+ requirements:
52
+ - - ! '>='
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ type: :development
56
+ prerelease: false
57
+ version_requirements: !ruby/object:Gem::Requirement
58
+ none: false
59
+ requirements:
60
+ - - ! '>='
61
+ - !ruby/object:Gem::Version
62
+ version: '0'
63
+ - !ruby/object:Gem::Dependency
64
+ name: pry
65
+ requirement: !ruby/object:Gem::Requirement
66
+ none: false
67
+ requirements:
68
+ - - ! '>='
69
+ - !ruby/object:Gem::Version
70
+ version: '0'
71
+ type: :development
72
+ prerelease: false
73
+ version_requirements: !ruby/object:Gem::Requirement
74
+ none: false
75
+ requirements:
76
+ - - ! '>='
77
+ - !ruby/object:Gem::Version
78
+ version: '0'
79
+ description: Parses cron lines into a Schedule instance that can be queried.
15
80
  email:
81
+ - jmettraux@gmail.com
16
82
  - bwthomas@gmail.com
17
83
  executables: []
18
84
  extensions: []
19
85
  extra_rdoc_files: []
20
- files: []
21
- homepage: https://rubygems.org/profiles/bwthomas
86
+ files:
87
+ - .gitignore
88
+ - .rspec
89
+ - Gemfile
90
+ - README.md
91
+ - Rakefile
92
+ - lib/whedon.rb
93
+ - lib/whedon/schedule.rb
94
+ - lib/whedon/version.rb
95
+ - spec/spec_helper.rb
96
+ - spec/whedon/schedule_spec.rb
97
+ - whedon.gemspec
98
+ homepage: https://github.com/bwthomas/whedon
22
99
  licenses: []
23
100
  post_install_message:
24
101
  rdoc_options: []
@@ -41,6 +118,8 @@ rubyforge_project: whedon
41
118
  rubygems_version: 1.8.23
42
119
  signing_key:
43
120
  specification_version: 3
44
- summary: Nothing to see here.
45
- test_files: []
121
+ summary: Parses cron lines
122
+ test_files:
123
+ - spec/spec_helper.rb
124
+ - spec/whedon/schedule_spec.rb
46
125
  has_rdoc: