runt 0.2.0
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/CHANGES +48 -0
- data/LICENSE.txt +44 -0
- data/README +88 -0
- data/Rakefile +117 -0
- data/TODO +19 -0
- data/doc/tutorial_schedule.rdoc +51 -0
- data/doc/tutorial_te.rdoc +190 -0
- data/lib/runt.rb +93 -0
- data/lib/runt/daterange.rb +74 -0
- data/lib/runt/dprecision.rb +137 -0
- data/lib/runt/pdate.rb +127 -0
- data/lib/runt/schedule.rb +89 -0
- data/lib/runt/temporalexpression.rb +467 -0
- data/setup.rb +1331 -0
- data/site/blue-robot3.css +132 -0
- data/site/dcl-small.gif +0 -0
- data/site/index.html +92 -0
- data/site/logohover.png +0 -0
- data/site/runt-logo.gif +0 -0
- data/site/runt-logo.psd +0 -0
- data/test/alltests.rb +12 -0
- data/test/daterangetest.rb +82 -0
- data/test/dprecisiontest.rb +46 -0
- data/test/pdatetest.rb +106 -0
- data/test/scheduletest.rb +56 -0
- data/test/temporalexpressiontest.rb +282 -0
- metadata +62 -0
@@ -0,0 +1,467 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'date'
|
4
|
+
require 'runt/dprecision'
|
5
|
+
|
6
|
+
#
|
7
|
+
# Author:: Matthew Lipper
|
8
|
+
|
9
|
+
module Runt
|
10
|
+
|
11
|
+
# FKA = 'Formally Known As'
|
12
|
+
|
13
|
+
|
14
|
+
# FKA: TemporalExpression
|
15
|
+
#
|
16
|
+
# Base class for all TExpr classes that has proved itself usefull enough to
|
17
|
+
# avoid O's Razor, but that may wind up as a module.
|
18
|
+
#
|
19
|
+
# 'TExpr' is short for 'TemporalExpression' and are inspired by the recurring event
|
20
|
+
# <tt>pattern</tt>[http://martinfowler.com/apsupp/recurring.pdf]
|
21
|
+
# described by Martin Fowler. Essentially, they provide a pattern language for
|
22
|
+
# specifying recurring events using set expressions.
|
23
|
+
#
|
24
|
+
# See also [tutorial_te.rdoc]
|
25
|
+
class TExpr
|
26
|
+
|
27
|
+
# Returns true or false depending on whether this TExpr includes the supplied
|
28
|
+
# date expression.
|
29
|
+
def include?(date_expr); false end
|
30
|
+
def to_s; "TExpr" end
|
31
|
+
|
32
|
+
def or (arg)
|
33
|
+
|
34
|
+
if self.kind_of?(Union)
|
35
|
+
self.add(arg)
|
36
|
+
else
|
37
|
+
yield Union.new.add(self).add(arg)
|
38
|
+
end
|
39
|
+
|
40
|
+
end
|
41
|
+
|
42
|
+
def and (arg)
|
43
|
+
|
44
|
+
if self.kind_of?(Intersect)
|
45
|
+
self.add(arg)
|
46
|
+
else
|
47
|
+
yield Intersect.new.add(self).add(arg)
|
48
|
+
end
|
49
|
+
|
50
|
+
end
|
51
|
+
|
52
|
+
def minus (arg)
|
53
|
+
yield Diff.new(self,arg)
|
54
|
+
end
|
55
|
+
|
56
|
+
def | (expr)
|
57
|
+
self.or(expr){|adjusted| adjusted }
|
58
|
+
end
|
59
|
+
|
60
|
+
def & (expr)
|
61
|
+
self.and(expr){|adjusted| adjusted }
|
62
|
+
end
|
63
|
+
|
64
|
+
def - (expr)
|
65
|
+
self.minus(expr){|adjusted| adjusted }
|
66
|
+
end
|
67
|
+
|
68
|
+
protected
|
69
|
+
def week_in_month(day_in_month)
|
70
|
+
((day_in_month - 1) / 7) + 1
|
71
|
+
end
|
72
|
+
|
73
|
+
def days_left_in_month(date)
|
74
|
+
return max_day_of_month(date) - date.day
|
75
|
+
end
|
76
|
+
|
77
|
+
def max_day_of_month(date)
|
78
|
+
result = 1
|
79
|
+
date.step( Date.new(date.year,date.mon+1,1), 1 ){ |d| result=d.day unless d.day < result }
|
80
|
+
result
|
81
|
+
end
|
82
|
+
|
83
|
+
def week_matches?(index,date)
|
84
|
+
if(index > 0)
|
85
|
+
return week_from_start_matches?(index,date)
|
86
|
+
else
|
87
|
+
return week_from_end_matches?(index,date)
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
def week_from_start_matches?(index,date)
|
92
|
+
week_in_month(date.day)==index
|
93
|
+
end
|
94
|
+
|
95
|
+
def week_from_end_matches?(index,date)
|
96
|
+
n = days_left_in_month(date) + 1
|
97
|
+
week_in_month(n)==index.abs
|
98
|
+
end
|
99
|
+
|
100
|
+
end
|
101
|
+
|
102
|
+
# Base class for TExpr classes that can be composed of other
|
103
|
+
# TExpr objects imlpemented using the <tt>Composite(GoF)</tt> pattern.
|
104
|
+
class Collection < TExpr
|
105
|
+
|
106
|
+
attr_reader :expressions
|
107
|
+
protected :expressions
|
108
|
+
|
109
|
+
def initialize
|
110
|
+
@expressions = Array.new
|
111
|
+
end
|
112
|
+
|
113
|
+
def add(anExpression)
|
114
|
+
@expressions.push anExpression
|
115
|
+
self
|
116
|
+
end
|
117
|
+
|
118
|
+
def to_s; "Collection:" + @expressions.to_s end
|
119
|
+
end
|
120
|
+
|
121
|
+
# Composite TExpr that will be true if <b>any</b> of it's
|
122
|
+
# component expressions are true.
|
123
|
+
class Union < Collection
|
124
|
+
|
125
|
+
def include?(aDate)
|
126
|
+
@expressions.each do |expr|
|
127
|
+
return true if expr.include?(aDate)
|
128
|
+
end
|
129
|
+
false
|
130
|
+
end
|
131
|
+
|
132
|
+
def to_s; "Union:" + @expressions.to_s end
|
133
|
+
end
|
134
|
+
|
135
|
+
# Composite TExpr that will be true only if <b>all</b> it's
|
136
|
+
# component expressions are true.
|
137
|
+
class Intersect < Collection
|
138
|
+
|
139
|
+
def include?(aDate)
|
140
|
+
#Handle @expressions.size==0
|
141
|
+
result = false
|
142
|
+
@expressions.each do |expr|
|
143
|
+
return false unless (result = expr.include?(aDate))
|
144
|
+
end
|
145
|
+
result
|
146
|
+
end
|
147
|
+
|
148
|
+
def to_s; "Intersect:" + @expressions.to_s end
|
149
|
+
end
|
150
|
+
|
151
|
+
# TExpr that will be true only if the first of
|
152
|
+
# it's two contained expressions is true and the second is false.
|
153
|
+
class Diff < TExpr
|
154
|
+
|
155
|
+
def initialize(expr1, expr2)
|
156
|
+
@expr1 = expr1
|
157
|
+
@expr2 = expr2
|
158
|
+
end
|
159
|
+
|
160
|
+
def include?(aDate)
|
161
|
+
return false unless (@expr1.include?(aDate) && !@expr2.include?(aDate))
|
162
|
+
true
|
163
|
+
end
|
164
|
+
|
165
|
+
def to_s; "Diff" end
|
166
|
+
end
|
167
|
+
|
168
|
+
# TExpr that provides for inclusion of an arbitrary date.
|
169
|
+
class Spec < TExpr
|
170
|
+
|
171
|
+
def initialize(date_expr)
|
172
|
+
@date_expr = date_expr
|
173
|
+
end
|
174
|
+
|
175
|
+
# Will return true if the supplied object is == to that which was used to
|
176
|
+
# create this instance
|
177
|
+
def include?(date_expr)
|
178
|
+
return true if @date_expr == date_expr
|
179
|
+
false
|
180
|
+
end
|
181
|
+
|
182
|
+
def to_s; "Spec" end
|
183
|
+
|
184
|
+
end
|
185
|
+
|
186
|
+
# TExpr that provides a thin wrapper around built-in Ruby <tt>Range</tt> functionality
|
187
|
+
# facilitating inclusion of an arbitrary range in a temporal expression.
|
188
|
+
#
|
189
|
+
# See also: Range
|
190
|
+
class RSpec < TExpr
|
191
|
+
|
192
|
+
def initialize(date_expr)
|
193
|
+
raise TypeError, 'expected range' unless date_expr.kind_of?(Range)
|
194
|
+
@date_expr = date_expr
|
195
|
+
end
|
196
|
+
|
197
|
+
# Will return true if the supplied object is included in the range used to
|
198
|
+
# create this instance
|
199
|
+
def include?(date_expr)
|
200
|
+
return @date_expr.include?(date_expr)
|
201
|
+
end
|
202
|
+
|
203
|
+
def to_s; "RSpec" end
|
204
|
+
end
|
205
|
+
|
206
|
+
# TExpr that provides support for building a temporal
|
207
|
+
# expression using the form:
|
208
|
+
#
|
209
|
+
# DIMonth.new(1,0)
|
210
|
+
#
|
211
|
+
# where the first argument is the week of the month and the second
|
212
|
+
# argument is the wday of the week as defined by the 'wday' method
|
213
|
+
# in the standard library class Date.
|
214
|
+
#
|
215
|
+
# A negative value for the week of the month argument will count
|
216
|
+
# backwards from the end of the month. So, to match the last Saturday
|
217
|
+
# of the month
|
218
|
+
#
|
219
|
+
# DIMonth.new(-1,6)
|
220
|
+
#
|
221
|
+
# Using constants defined in the base Runt module, you can re-write
|
222
|
+
# the first example above as:
|
223
|
+
#
|
224
|
+
# DIMonth.new(First,Sunday)
|
225
|
+
#
|
226
|
+
# and the second as:
|
227
|
+
#
|
228
|
+
# DIMonth.new(Last,Saturday)
|
229
|
+
#
|
230
|
+
# See also: Date, Runt
|
231
|
+
class DIMonth < TExpr
|
232
|
+
|
233
|
+
def initialize(week_of_month_index,day_index)
|
234
|
+
@day_index = day_index
|
235
|
+
@week_of_month_index = week_of_month_index
|
236
|
+
end
|
237
|
+
|
238
|
+
def include?(date)
|
239
|
+
( day_matches?(date) ) && ( week_matches?(@week_of_month_index,date) )
|
240
|
+
end
|
241
|
+
|
242
|
+
def to_s
|
243
|
+
"DIMonth"
|
244
|
+
end
|
245
|
+
|
246
|
+
def print(date)
|
247
|
+
puts "DIMonth: #{date}"
|
248
|
+
puts "include? == #{include?(date)}"
|
249
|
+
puts "day_matches? == #{day_matches?(date)}"
|
250
|
+
puts "week_matches? == #{week_matches?(date)}"
|
251
|
+
puts "week_from_start_matches? == #{week_from_start_matches?(date)}"
|
252
|
+
puts "week_from_end_matches? == #{week_from_end_matches?(date)}"
|
253
|
+
puts "days_left_in_month == #{days_left_in_month(date)}"
|
254
|
+
puts "max_day_of_month == #{max_day_of_month(date)}"
|
255
|
+
end
|
256
|
+
|
257
|
+
private
|
258
|
+
def day_matches?(date)
|
259
|
+
@day_index == date.wday
|
260
|
+
end
|
261
|
+
|
262
|
+
end
|
263
|
+
|
264
|
+
# TExpr that matches days of the week where the first argument
|
265
|
+
# is an integer denoting the ordinal day of the week. Valid values are 0..6 where
|
266
|
+
# 0 == Sunday and 6==Saturday
|
267
|
+
#
|
268
|
+
# For example:
|
269
|
+
#
|
270
|
+
# DIWeek.new(0)
|
271
|
+
#
|
272
|
+
# Using constants defined in the base Runt module, you can re-write
|
273
|
+
# the first example above as:
|
274
|
+
#
|
275
|
+
# DIWeek.new(Sunday)
|
276
|
+
#
|
277
|
+
# See also: Date, Runt
|
278
|
+
class DIWeek < TExpr
|
279
|
+
|
280
|
+
VALID_RANGE = 0..6
|
281
|
+
|
282
|
+
def initialize(ordinal_weekday)
|
283
|
+
unless VALID_RANGE.include?(ordinal_weekday)
|
284
|
+
raise ArgumentError, 'invalid ordinal day of week'
|
285
|
+
end
|
286
|
+
@ordinal_weekday = ordinal_weekday
|
287
|
+
end
|
288
|
+
|
289
|
+
def include?(date)
|
290
|
+
@ordinal_weekday == date.wday
|
291
|
+
end
|
292
|
+
|
293
|
+
end
|
294
|
+
|
295
|
+
# TExpr that matches days of the week within one
|
296
|
+
# week only.
|
297
|
+
#
|
298
|
+
# If start and end day are equal, the entire week will match true.
|
299
|
+
#
|
300
|
+
# See also: Date
|
301
|
+
class REWeek < TExpr
|
302
|
+
|
303
|
+
VALID_RANGE = 0..6
|
304
|
+
|
305
|
+
# Creates a REWeek using the supplied start
|
306
|
+
# day(range = 0..6, where 0=>Sunday) and an optional end
|
307
|
+
# day. If an end day is not supplied, the maximum value
|
308
|
+
# (6 => Saturday) is assumed.
|
309
|
+
#
|
310
|
+
# If the start day is greater than the end day, an
|
311
|
+
# ArgumentError will be raised
|
312
|
+
def initialize(start_day,end_day=6)
|
313
|
+
super()
|
314
|
+
validate(start_day,end_day)
|
315
|
+
@start_day = start_day
|
316
|
+
@end_day = end_day
|
317
|
+
end
|
318
|
+
|
319
|
+
def include?(date)
|
320
|
+
return true if @start_day==@end_day
|
321
|
+
@start_day<=date.wday && @end_day>=date.wday
|
322
|
+
end
|
323
|
+
|
324
|
+
def to_s
|
325
|
+
"REWeek"
|
326
|
+
end
|
327
|
+
|
328
|
+
private
|
329
|
+
def validate(start_day,end_day)
|
330
|
+
unless start_day<=end_day
|
331
|
+
raise ArgumentError, 'end day of week must be greater than start day'
|
332
|
+
end
|
333
|
+
unless VALID_RANGE.include?(start_day)&&VALID_RANGE.include?(end_day)
|
334
|
+
raise ArgumentError, 'start and end day arguments must be in the range #{VALID_RANGE.to_s}.'
|
335
|
+
end
|
336
|
+
end
|
337
|
+
end
|
338
|
+
|
339
|
+
class REYear < TExpr
|
340
|
+
|
341
|
+
def initialize(start_month, start_day=0, end_month=start_month, end_day=0)
|
342
|
+
super()
|
343
|
+
@start_month = start_month
|
344
|
+
@start_day = start_day
|
345
|
+
@end_month = end_month
|
346
|
+
@end_day = end_day
|
347
|
+
end
|
348
|
+
|
349
|
+
def include?(date)
|
350
|
+
months_include?(date) ||
|
351
|
+
start_month_include?(date) ||
|
352
|
+
end_month_include?(date)
|
353
|
+
end
|
354
|
+
|
355
|
+
def to_s
|
356
|
+
"REYear"
|
357
|
+
end
|
358
|
+
|
359
|
+
def print(date)
|
360
|
+
puts "DIMonth: #{date}"
|
361
|
+
puts "include? == #{include?(date)}"
|
362
|
+
puts "months_include? == #{months_include?(date)}"
|
363
|
+
puts "end_month_include? == #{end_month_include?(date)}"
|
364
|
+
puts "start_month_include? == #{start_month_include?(date)}"
|
365
|
+
end
|
366
|
+
|
367
|
+
private
|
368
|
+
def months_include?(date)
|
369
|
+
(date.mon > @start_month) && (date.mon < @end_month)
|
370
|
+
end
|
371
|
+
|
372
|
+
def end_month_include?(date)
|
373
|
+
return false unless (date.mon == @end_month)
|
374
|
+
(@end_day == 0) || (date.day <= @end_day)
|
375
|
+
end
|
376
|
+
|
377
|
+
def start_month_include?(date)
|
378
|
+
return false unless (date.mon == @start_month)
|
379
|
+
(@start_day == 0) || (date.day >= @start_day)
|
380
|
+
end
|
381
|
+
end
|
382
|
+
|
383
|
+
# TExpr that matches periods of the day with minute
|
384
|
+
# precision. If the start hour is greater than the end hour, than end hour
|
385
|
+
# is assumed to be on the following day.
|
386
|
+
#
|
387
|
+
# See also: Date
|
388
|
+
class REDay < TExpr
|
389
|
+
|
390
|
+
CURRENT=28
|
391
|
+
NEXT=29
|
392
|
+
ANY_DATE=PDate.day(2002,8,CURRENT)
|
393
|
+
|
394
|
+
def initialize(start_hour, start_minute, end_hour, end_minute)
|
395
|
+
|
396
|
+
start_time = PDate.min(ANY_DATE.year,ANY_DATE.month,
|
397
|
+
ANY_DATE.day,start_hour,start_minute)
|
398
|
+
|
399
|
+
if(@spans_midnight = spans_midnight?(start_hour, end_hour)) then
|
400
|
+
end_time = get_next(end_hour,end_minute)
|
401
|
+
else
|
402
|
+
end_time = get_current(end_hour,end_minute)
|
403
|
+
end
|
404
|
+
|
405
|
+
@range = start_time..end_time
|
406
|
+
end
|
407
|
+
|
408
|
+
def include?(date)
|
409
|
+
raise TypeError, 'expected date' unless date.kind_of?(Date)
|
410
|
+
|
411
|
+
if(@spans_midnight&&date.hour<12) then
|
412
|
+
#Assume next day
|
413
|
+
return @range.include?(get_next(date.hour,date.min))
|
414
|
+
end
|
415
|
+
|
416
|
+
#Same day
|
417
|
+
return @range.include?(get_current(date.hour,date.min))
|
418
|
+
end
|
419
|
+
|
420
|
+
def to_s
|
421
|
+
"REDay"
|
422
|
+
end
|
423
|
+
|
424
|
+
def print(date)
|
425
|
+
puts "DIMonth: #{date}"
|
426
|
+
puts "include? == #{include?(date)}"
|
427
|
+
end
|
428
|
+
|
429
|
+
private
|
430
|
+
def spans_midnight?(start_hour, end_hour)
|
431
|
+
return end_hour <= start_hour
|
432
|
+
end
|
433
|
+
|
434
|
+
private
|
435
|
+
def get_current(hour,minute)
|
436
|
+
PDate.min(ANY_DATE.year,ANY_DATE.month,CURRENT,hour,minute)
|
437
|
+
end
|
438
|
+
|
439
|
+
def get_next(hour,minute)
|
440
|
+
PDate.min(ANY_DATE.year,ANY_DATE.month,NEXT,hour,minute)
|
441
|
+
end
|
442
|
+
|
443
|
+
end
|
444
|
+
|
445
|
+
# TExpr that matches the week in a month. For example:
|
446
|
+
#
|
447
|
+
# WIMonth.new(1)
|
448
|
+
#
|
449
|
+
# See also: Date
|
450
|
+
class WIMonth < TExpr
|
451
|
+
|
452
|
+
VALID_RANGE = -2..5
|
453
|
+
|
454
|
+
def initialize(ordinal)
|
455
|
+
unless VALID_RANGE.include?(ordinal)
|
456
|
+
raise ArgumentError, 'invalid ordinal week of month'
|
457
|
+
end
|
458
|
+
@ordinal = ordinal
|
459
|
+
end
|
460
|
+
|
461
|
+
def include?(date)
|
462
|
+
week_matches?(@ordinal,date)
|
463
|
+
end
|
464
|
+
|
465
|
+
end
|
466
|
+
|
467
|
+
end
|