runt 0.3.0 → 0.5.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.
@@ -1,89 +1,88 @@
1
- #!/usr/bin/env ruby
2
-
3
- module Runt
4
-
5
-
6
- # Implementation of a <tt>pattern</tt>[http://martinfowler.com/apsupp/recurring.pdf]
7
- # for recurring calendar events created by Martin Fowler.
8
- class Schedule
9
-
10
- def initialize
11
- @elems = Hash.new
12
- self
13
- end
14
-
15
- # Schedule event to occur using the given expression.
16
- def add(event, expression)
17
-
18
- if @elems.include?(event)
19
- @elems[event].push(ScheduleElement.new(event, expression))
20
- else
21
- @elems[event] = [ScheduleElement.new(event, expression)]
22
- end
23
-
24
- end
25
-
26
- # For the given date range, returns an Array of PDate objects at which
27
- # the supplied event is scheduled to occur.
28
- def dates(event, date_range)
29
- result = Array.new
30
- date_range.each do |date|
31
- result.push date if include?(event,date)
32
- end
33
- result
34
- end
35
-
36
- # Return true or false depend on if the supplied event is scheduled to occur on the
37
- # given date.
38
- def include?(event, date)
39
- return false unless @elems.include?(event)
40
- result = Array.new
41
- @elems[event].each{|element| result << element.include?(event, date) }
42
- result.inject{|x,y| x && y}
43
- end
44
-
45
- private
46
- def add_element
47
- end
48
- end
49
-
50
- private
51
- class ScheduleElement
52
-
53
- def initialize(event, expression)
54
- @event = event
55
- @expression = expression
56
- end
57
-
58
- def include?(event, date)
59
- return false unless @event == event
60
- @expression.include?(date)
61
- end
62
-
63
- def to_s
64
- "event: #{@event} expr: #{@expression}"
65
- end
66
-
67
- end
68
-
69
- public
70
- class Event
71
-
72
- attr_reader :schedule, :id
73
-
74
- def initialize(id)
75
- raise Exception, "id argument cannot be nil" unless !id.nil?
76
- @id = id
77
- end
78
-
79
-
80
- def to_s; @id.to_s end
81
-
82
- def == (other)
83
- return true if other.kind_of?(Event) && @id == other.id
84
- end
85
-
86
- end
87
-
88
-
89
- end
1
+ #!/usr/bin/env ruby
2
+
3
+ module Runt
4
+
5
+
6
+ # Implementation of a <tt>pattern</tt>[http://martinfowler.com/apsupp/recurring.pdf]
7
+ # for recurring calendar events created by Martin Fowler.
8
+ class Schedule
9
+
10
+ def initialize
11
+ @elems = Hash.new
12
+ self
13
+ end
14
+
15
+ # Schedule event to occur using the given expression.
16
+ # NOTE: version 0.5.0 no longer uses an Array of ScheduleElements
17
+ # internally to hold data. This would only matter to clients if they
18
+ # they depended on the ability to call add multiple times for the same
19
+ # event. Use the update method instead.
20
+ def add(event, expression)
21
+ @elems[event]=expression
22
+ end
23
+
24
+ # For the given date range, returns an Array of PDate objects at which
25
+ # the supplied event is scheduled to occur.
26
+ def dates(event, date_range)
27
+ result=[]
28
+ date_range.each do |date|
29
+ result.push date if include?(event,date)
30
+ end
31
+ result
32
+ end
33
+
34
+ # Return true or false depend on if the supplied event is scheduled to occur on the
35
+ # given date.
36
+ def include?(event, date)
37
+ return false unless @elems.include?(event)
38
+ return 0<(self.select{|ev,xpr| ev.eql?(event)&&xpr.include?(date);}).size
39
+ end
40
+
41
+ #
42
+ # Returns all Events whose Tempoarl Expression includes the given date/expression
43
+ #
44
+ def events(date)
45
+ self.select{|ev,xpr| xpr.include?(date);}
46
+ end
47
+
48
+ #
49
+ # Selects events using the user supplied block/Proc. The Proc must accept
50
+ # two parameters: an Event and a TemporalExpression. It will be called
51
+ # with each existing Event-expression pair at which point it can choose
52
+ # to include the Event in the final result by returning true or to filter
53
+ # it by returning false.
54
+ #
55
+ def select(&block)
56
+ result=[]
57
+ @elems.each_pair{|event,xpr| result.push(event) if block.call(event,xpr);}
58
+ result
59
+ end
60
+
61
+ #
62
+ # Call the supplied block/Proc with the currently configured
63
+ # TemporalExpression associated with the supplied Event.
64
+ #
65
+ def update(event,&block)
66
+ block.call(@elems[event])
67
+ end
68
+
69
+ end
70
+
71
+ class Event
72
+
73
+ attr_reader :id
74
+
75
+ def initialize(id)
76
+ raise Exception, "id argument cannot be nil" unless !id.nil?
77
+ @id=id
78
+ end
79
+
80
+ def to_s; @id.to_s end
81
+
82
+ def == (other)
83
+ return true if other.kind_of?(Event) && @id==other.id
84
+ end
85
+
86
+ end
87
+
88
+ end
@@ -1,524 +1,695 @@
1
- #!/usr/bin/env ruby
2
-
3
- require 'date'
4
- require 'runt/dprecision'
5
- require 'runt/pdate'
6
-
7
- #
8
- # Author:: Matthew Lipper
9
-
10
- module Runt
11
-
12
- #
13
- # 'TExpr' is short for 'TemporalExpression' and are inspired by the recurring event
14
- # <tt>pattern</tt>[http://martinfowler.com/apsupp/recurring.pdf]
15
- # described by Martin Fowler. Essentially, they provide a pattern language for
16
- # specifying recurring events using set expressions.
17
- #
18
- # See also [tutorial_te.rdoc]
19
- module TExpr
20
-
21
- # Returns true or false depending on whether this TExpr includes the supplied
22
- # date expression.
23
- def include?(date_expr); false end
24
-
25
- def to_s; "TExpr" end
26
-
27
- def or (arg)
28
-
29
- if self.kind_of?(Union)
30
- self.add(arg)
31
- else
32
- yield Union.new.add(self).add(arg)
33
- end
34
-
35
- end
36
-
37
- def and (arg)
38
-
39
- if self.kind_of?(Intersect)
40
- self.add(arg)
41
- else
42
- yield Intersect.new.add(self).add(arg)
43
- end
44
-
45
- end
46
-
47
- def minus (arg)
48
- yield Diff.new(self,arg)
49
- end
50
-
51
- def | (expr)
52
- self.or(expr){|adjusted| adjusted }
53
- end
54
-
55
- def & (expr)
56
- self.and(expr){|adjusted| adjusted }
57
- end
58
-
59
- def - (expr)
60
- self.minus(expr){|adjusted| adjusted }
61
- end
62
-
63
- # Contributed by Emmett Shear:
64
- # Returns an Array of Date-like objects which occur within the supplied
65
- # DateRange.
66
- def dates(date_range)
67
- result = []
68
- date_range.each do |date|
69
- result << date if self.include? date
70
- end
71
- result
72
- end
73
-
74
- end
75
-
76
- # Base class for TExpr classes that can be composed of other
77
- # TExpr objects imlpemented using the <tt>Composite(GoF)</tt> pattern.
78
- class Collection
79
-
80
- include TExpr
81
-
82
- attr_reader :expressions
83
- protected :expressions
84
-
85
- def initialize
86
- @expressions = Array.new
87
- end
88
-
89
- def add(anExpression)
90
- @expressions.push anExpression
91
- self
92
- end
93
-
94
- # Will return true if the supplied object overlaps with the range used to
95
- # create this instance
96
- def overlap?(date_expr)
97
- @expressions.each do | interval |
98
- return true if date_expr.overlap?(interval)
99
- end
100
- false
101
- end
102
-
103
-
104
- def to_s; "Collection:" + @expressions.to_s end
105
- end
106
-
107
- # Composite TExpr that will be true if <b>any</b> of it's
108
- # component expressions are true.
109
- class Union < Collection
110
-
111
- def include?(aDate)
112
- @expressions.each do |expr|
113
- return true if expr.include?(aDate)
114
- end
115
- false
116
- end
117
-
118
- def to_s; "Union:" + @expressions.to_s end
119
- end
120
-
121
- # Composite TExpr that will be true only if <b>all</b> it's
122
- # component expressions are true.
123
- class Intersect < Collection
124
-
125
- def include?(aDate)
126
- result = false
127
- @expressions.each do |expr|
128
- return false unless (result = expr.include?(aDate))
129
- end
130
- result
131
- end
132
-
133
- def to_s; "Intersect:" + @expressions.to_s end
134
- end
135
-
136
- # TExpr that will be true only if the first of
137
- # its two contained expressions is true and the second is false.
138
- class Diff
139
-
140
- include TExpr
141
-
142
- def initialize(expr1, expr2)
143
- @expr1 = expr1
144
- @expr2 = expr2
145
- end
146
-
147
- def include?(aDate)
148
- return false unless (@expr1.include?(aDate) && !@expr2.include?(aDate))
149
- true
150
- end
151
-
152
- def to_s; "Diff" end
153
- end
154
-
155
- # TExpr that provides for inclusion of an arbitrary date.
156
- class Spec
157
-
158
- include TExpr
159
-
160
- def initialize(date_expr)
161
- @date_expr = date_expr
162
- end
163
-
164
- # Will return true if the supplied object is == to that which was used to
165
- # create this instance
166
- def include?(date_expr)
167
- return true if @date_expr == date_expr
168
- false
169
- end
170
-
171
- def to_s; "Spec" end
172
-
173
- end
174
-
175
- # TExpr that provides a thin wrapper around built-in Ruby <tt>Range</tt> functionality
176
- # facilitating inclusion of an arbitrary range in a temporal expression.
177
- #
178
- # See also: Range
179
- class RSpec
180
-
181
- include TExpr
182
-
183
- def initialize(date_expr)
184
- @date_expr = date_expr
185
- end
186
-
187
- # Will return true if the supplied object is included in the range used to
188
- # create this instance
189
- def include?(date_expr)
190
- return @date_expr.include?(date_expr)
191
- end
192
-
193
- # Will return true if the supplied object overlaps with the range used to
194
- # create this instance
195
- def overlap?(date_expr)
196
- @date_expr.each do | interval |
197
- return true if date_expr.include?(interval)
198
- end
199
- false
200
- end
201
-
202
- def to_s; "RSpec" end
203
- end
204
-
205
- #######################################################################
206
- # Utility methods common to some expressions
207
-
208
- module TExprUtils
209
- def week_in_month(day_in_month)
210
- ((day_in_month - 1) / 7) + 1
211
- end
212
-
213
- def days_left_in_month(date)
214
- return max_day_of_month(date) - date.day
215
- end
216
-
217
- def max_day_of_month(date)
218
- result = 1
219
- next_month = nil
220
- if(date.mon==12)
221
- next_month = Date.new(date.year+1,1,1)
222
- else
223
- next_month = Date.new(date.year,date.mon+1,1)
224
- end
225
- date.step(next_month,1){ |d| result=d.day unless d.day < result }
226
- result
227
- end
228
-
229
- def week_matches?(index,date)
230
- if(index > 0)
231
- return week_from_start_matches?(index,date)
232
- else
233
- return week_from_end_matches?(index,date)
234
- end
235
- end
236
-
237
- def week_from_start_matches?(index,date)
238
- week_in_month(date.day)==index
239
- end
240
-
241
- def week_from_end_matches?(index,date)
242
- n = days_left_in_month(date) + 1
243
- week_in_month(n)==index.abs
244
- end
245
-
246
- end
247
-
248
- # TExpr that provides support for building a temporal
249
- # expression using the form:
250
- #
251
- # DIMonth.new(1,0)
252
- #
253
- # where the first argument is the week of the month and the second
254
- # argument is the wday of the week as defined by the 'wday' method
255
- # in the standard library class Date.
256
- #
257
- # A negative value for the week of the month argument will count
258
- # backwards from the end of the month. So, to match the last Saturday
259
- # of the month
260
- #
261
- # DIMonth.new(-1,6)
262
- #
263
- # Using constants defined in the base Runt module, you can re-write
264
- # the first example above as:
265
- #
266
- # DIMonth.new(First,Sunday)
267
- #
268
- # and the second as:
269
- #
270
- # DIMonth.new(Last,Saturday)
271
- #
272
- # See also: Date, Runt
273
- class DIMonth
274
-
275
- include TExpr
276
- include TExprUtils
277
-
278
- def initialize(week_of_month_index,day_index)
279
- @day_index = day_index
280
- @week_of_month_index = week_of_month_index
281
- end
282
-
283
- def include?(date)
284
- ( day_matches?(date) ) && ( week_matches?(@week_of_month_index,date) )
285
- end
286
-
287
- def to_s
288
- "DIMonth"
289
- end
290
-
291
- private
292
- def day_matches?(date)
293
- @day_index == date.wday
294
- end
295
-
296
- end
297
-
298
- # TExpr that matches days of the week where the first argument
299
- # is an integer denoting the ordinal day of the week. Valid values are 0..6 where
300
- # 0 == Sunday and 6==Saturday
301
- #
302
- # For example:
303
- #
304
- # DIWeek.new(0)
305
- #
306
- # Using constants defined in the base Runt module, you can re-write
307
- # the first example above as:
308
- #
309
- # DIWeek.new(Sunday)
310
- #
311
- # See also: Date, Runt
312
- class DIWeek
313
-
314
- include TExpr
315
-
316
- VALID_RANGE = 0..6
317
-
318
- def initialize(ordinal_weekday)
319
- unless VALID_RANGE.include?(ordinal_weekday)
320
- raise ArgumentError, 'invalid ordinal day of week'
321
- end
322
- @ordinal_weekday = ordinal_weekday
323
- end
324
-
325
- def include?(date)
326
- @ordinal_weekday == date.wday
327
- end
328
-
329
- end
330
-
331
- # TExpr that matches days of the week within one
332
- # week only.
333
- #
334
- # If start and end day are equal, the entire week will match true.
335
- #
336
- # See also: Date
337
- class REWeek
338
-
339
- include TExpr
340
-
341
- VALID_RANGE = 0..6
342
-
343
- # Creates a REWeek using the supplied start
344
- # day(range = 0..6, where 0=>Sunday) and an optional end
345
- # day. If an end day is not supplied, the maximum value
346
- # (6 => Saturday) is assumed.
347
- #
348
- # If the start day is greater than the end day, an
349
- # ArgumentError will be raised
350
- def initialize(start_day,end_day=6)
351
- validate(start_day,end_day)
352
- @start_day = start_day
353
- @end_day = end_day
354
- end
355
-
356
- def include?(date)
357
- return true if @start_day==@end_day
358
- @start_day<=date.wday && @end_day>=date.wday
359
- end
360
-
361
- def to_s
362
- "REWeek"
363
- end
364
-
365
- private
366
- def validate(start_day,end_day)
367
- unless start_day<=end_day
368
- raise ArgumentError, 'end day of week must be greater than start day'
369
- end
370
- unless VALID_RANGE.include?(start_day)&&VALID_RANGE.include?(end_day)
371
- raise ArgumentError, 'start and end day arguments must be in the range #{VALID_RANGE.to_s}.'
372
- end
373
- end
374
- end
375
-
376
- class REYear
377
-
378
- include TExpr
379
-
380
- def initialize(start_month, start_day=0, end_month=start_month, end_day=0)
381
- @start_month = start_month
382
- @start_day = start_day
383
- @end_month = end_month
384
- @end_day = end_day
385
- end
386
-
387
- def include?(date)
388
- months_include?(date) ||
389
- start_month_include?(date) ||
390
- end_month_include?(date)
391
- end
392
-
393
- def to_s
394
- "REYear"
395
- end
396
-
397
- private
398
- def months_include?(date)
399
- (date.mon > @start_month) && (date.mon < @end_month)
400
- end
401
-
402
- def end_month_include?(date)
403
- return false unless (date.mon == @end_month)
404
- (@end_day == 0) || (date.day <= @end_day)
405
- end
406
-
407
- def start_month_include?(date)
408
- return false unless (date.mon == @start_month)
409
- (@start_day == 0) || (date.day >= @start_day)
410
- end
411
- end
412
-
413
- # TExpr that matches periods of the day with minute
414
- # precision. If the start hour is greater than the end hour, than end hour
415
- # is assumed to be on the following day.
416
- #
417
- # See also: Date
418
- class REDay
419
-
420
- include TExpr
421
-
422
- CURRENT=28
423
- NEXT=29
424
- ANY_DATE=PDate.day(2002,8,CURRENT)
425
-
426
- def initialize(start_hour, start_minute, end_hour, end_minute)
427
-
428
- start_time = PDate.min(ANY_DATE.year,ANY_DATE.month,
429
- ANY_DATE.day,start_hour,start_minute)
430
-
431
- if(@spans_midnight = spans_midnight?(start_hour, end_hour)) then
432
- end_time = get_next(end_hour,end_minute)
433
- else
434
- end_time = get_current(end_hour,end_minute)
435
- end
436
-
437
- @range = start_time..end_time
438
- end
439
-
440
- def include?(date)
441
- # If precision is day or greater, then the result is always true
442
- return true if date.date_precision <= DPrecision::DAY
443
-
444
- if(@spans_midnight&&date.hour<12) then
445
- #Assume next day
446
- return @range.include?(get_next(date.hour,date.min))
447
- end
448
-
449
- #Same day
450
- return @range.include?(get_current(date.hour,date.min))
451
- end
452
-
453
- def to_s
454
- "REDay"
455
- end
456
-
457
- private
458
- def spans_midnight?(start_hour, end_hour)
459
- return end_hour <= start_hour
460
- end
461
-
462
- def get_current(hour,minute)
463
- PDate.min(ANY_DATE.year,ANY_DATE.month,CURRENT,hour,minute)
464
- end
465
-
466
- def get_next(hour,minute)
467
- PDate.min(ANY_DATE.year,ANY_DATE.month,NEXT,hour,minute)
468
- end
469
-
470
- end
471
-
472
- # TExpr that matches the week in a month. For example:
473
- #
474
- # WIMonth.new(1)
475
- #
476
- # See also: Date
477
- class WIMonth
478
-
479
- include TExpr
480
- include TExprUtils
481
-
482
- VALID_RANGE = -2..5
483
-
484
- def initialize(ordinal)
485
- unless VALID_RANGE.include?(ordinal)
486
- raise ArgumentError, 'invalid ordinal week of month'
487
- end
488
- @ordinal = ordinal
489
- end
490
-
491
- def include?(date)
492
- week_matches?(@ordinal,date)
493
- end
494
-
495
- end
496
-
497
- # TExpr that matches a range of dates within a month. For example:
498
- #
499
- # REMonth.(12,28)
500
- #
501
- # matches from the 12th thru the 28th of any month. If end_day==0
502
- # or is not given, start_day will define the range with that single day.
503
- #
504
- # See also: Date
505
- class REMonth
506
-
507
- include TExpr
508
-
509
- def initialize(start_day, end_day=0)
510
- end_day=start_day if end_day==0
511
- @range = start_day..end_day
512
- end
513
-
514
- def include?(date)
515
- @range.include? date.mday
516
- end
517
-
518
- def to_s
519
- "REMonth"
520
- end
521
-
522
- end
523
-
524
- end
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'date'
4
+ require 'runt/dprecision'
5
+ require 'runt/pdate'
6
+ require 'pp'
7
+
8
+ #
9
+ # Author:: Matthew Lipper
10
+
11
+ module Runt
12
+
13
+ #
14
+ # 'TExpr' is short for 'TemporalExpression' and are inspired by the recurring event
15
+ # <tt>pattern</tt>[http://martinfowler.com/apsupp/recurring.pdf]
16
+ # described by Martin Fowler. Essentially, they provide a pattern language for
17
+ # specifying recurring events using set expressions.
18
+ #
19
+ # See also [tutorial_te.rdoc]
20
+ module TExpr
21
+
22
+ # Returns true or false depending on whether this TExpr includes the supplied
23
+ # date expression.
24
+ def include?(date_expr); false end
25
+
26
+ def to_s; "TExpr" end
27
+
28
+ def or (arg)
29
+
30
+ if self.kind_of?(Union)
31
+ self.add(arg)
32
+ else
33
+ yield Union.new.add(self).add(arg)
34
+ end
35
+
36
+ end
37
+
38
+ def and (arg)
39
+
40
+ if self.kind_of?(Intersect)
41
+ self.add(arg)
42
+ else
43
+ yield Intersect.new.add(self).add(arg)
44
+ end
45
+
46
+ end
47
+
48
+ def minus (arg)
49
+ yield Diff.new(self,arg)
50
+ end
51
+
52
+ def | (expr)
53
+ self.or(expr){|adjusted| adjusted }
54
+ end
55
+
56
+ def & (expr)
57
+ self.and(expr){|adjusted| adjusted }
58
+ end
59
+
60
+ def - (expr)
61
+ self.minus(expr){|adjusted| adjusted }
62
+ end
63
+
64
+ # Contributed by Emmett Shear:
65
+ # Returns an Array of Date-like objects which occur within the supplied
66
+ # DateRange. Will stop calculating dates once a number of dates equal
67
+ # to the optional attribute limit are found. (A limit of zero will collect
68
+ # all matching dates in the date range.)
69
+ def dates(date_range, limit=0)
70
+ result = []
71
+ date_range.each do |date|
72
+ result << date if self.include? date
73
+ if limit > 0 and result.size == limit
74
+ break
75
+ end
76
+ end
77
+ result
78
+ end
79
+
80
+ end
81
+
82
+ # Base class for TExpr classes that can be composed of other
83
+ # TExpr objects imlpemented using the <tt>Composite(GoF)</tt> pattern.
84
+ class Collection
85
+
86
+ include TExpr
87
+
88
+ attr_reader :expressions
89
+ #protected :expressions
90
+
91
+ def initialize
92
+ @expressions = Array.new
93
+ end
94
+
95
+ def add(anExpression)
96
+ @expressions.push anExpression
97
+ self
98
+ end
99
+
100
+ # Will return true if the supplied object overlaps with the range used to
101
+ # create this instance
102
+ def overlap?(date_expr)
103
+ @expressions.each do | interval |
104
+ return true if date_expr.overlap?(interval)
105
+ end
106
+ false
107
+ end
108
+
109
+ def to_s
110
+ if !@expressions.empty? && block_given?
111
+ first_expr, next_exprs = yield
112
+ result = ''
113
+ @expressions.map do |expr|
114
+ if @expressions.first===expr
115
+ result = first_expr + expr.to_s
116
+ else
117
+ result = result + next_exprs + expr.to_s
118
+ end
119
+ end
120
+ result
121
+ else
122
+ 'empty'
123
+ end
124
+ end
125
+
126
+ def display
127
+ puts "I am a #{self.class} containing:"
128
+ @expressions.each do |ex|
129
+ pp "#{ex.class}"
130
+ end
131
+ end
132
+
133
+
134
+ end
135
+
136
+ # Composite TExpr that will be true if <b>any</b> of it's
137
+ # component expressions are true.
138
+ class Union < Collection
139
+
140
+ def include?(aDate)
141
+ @expressions.each do |expr|
142
+ return true if expr.include?(aDate)
143
+ end
144
+ false
145
+ end
146
+
147
+ def to_s
148
+ super {['every ',' or ']}
149
+ end
150
+ end
151
+
152
+ # Composite TExpr that will be true only if <b>all</b> it's
153
+ # component expressions are true.
154
+ class Intersect < Collection
155
+
156
+ def include?(aDate)
157
+ result = false
158
+ @expressions.each do |expr|
159
+ return false unless (result = expr.include?(aDate))
160
+ end
161
+ result
162
+ end
163
+
164
+ def to_s
165
+ super {['every ', ' and ']}
166
+ end
167
+ end
168
+
169
+ # TExpr that will be true only if the first of
170
+ # its two contained expressions is true and the second is false.
171
+ class Diff
172
+
173
+ include TExpr
174
+
175
+ def initialize(expr1, expr2)
176
+ @expr1 = expr1
177
+ @expr2 = expr2
178
+ end
179
+
180
+ def include?(aDate)
181
+ return false unless (@expr1.include?(aDate) && !@expr2.include?(aDate))
182
+ true
183
+ end
184
+
185
+ def to_s
186
+ @expr1.to_s + ' except for ' + @expr2.to_s
187
+ end
188
+ end
189
+
190
+ # TExpr that provides for inclusion of an arbitrary date.
191
+ class Spec
192
+
193
+ include TExpr
194
+
195
+ def initialize(date_expr)
196
+ @date_expr = date_expr
197
+ end
198
+
199
+ # Will return true if the supplied object is == to that which was used to
200
+ # create this instance
201
+ def include?(date_expr)
202
+ return date_expr.include?(@date_expr) if date_expr.respond_to?(:include?)
203
+ return true if @date_expr == date_expr
204
+ false
205
+ end
206
+
207
+ def to_s
208
+ @date_expr.to_s
209
+ end
210
+
211
+ end
212
+
213
+ # TExpr that provides a thin wrapper around built-in Ruby <tt>Range</tt> functionality
214
+ # facilitating inclusion of an arbitrary range in a temporal expression.
215
+ #
216
+ # See also: Range
217
+ class RSpec
218
+
219
+ include TExpr
220
+
221
+ def initialize(date_expr)
222
+ @date_expr = date_expr
223
+ end
224
+
225
+ # Will return true if the supplied object is included in the range used to
226
+ # create this instance
227
+ def include?(date_expr)
228
+ return @date_expr.include?(date_expr)
229
+ end
230
+
231
+ # Will return true if the supplied object overlaps with the range used to
232
+ # create this instance
233
+ def overlap?(date_expr)
234
+ @date_expr.each do | interval |
235
+ return true if date_expr.include?(interval)
236
+ end
237
+ false
238
+ end
239
+
240
+ def to_s
241
+ @date_expr.to_s
242
+ end
243
+ end
244
+
245
+ #######################################################################
246
+ # Utility methods common to some expressions
247
+
248
+ module TExprUtils
249
+ def week_in_month(day_in_month)
250
+ ((day_in_month - 1) / 7) + 1
251
+ end
252
+
253
+ def days_left_in_month(date)
254
+ return max_day_of_month(date) - date.day
255
+ end
256
+
257
+ def max_day_of_month(date)
258
+ result = 1
259
+ next_month = nil
260
+ if(date.mon==12)
261
+ next_month = Date.new(date.year+1,1,1)
262
+ else
263
+ next_month = Date.new(date.year,date.mon+1,1)
264
+ end
265
+ date.step(next_month,1){ |d| result=d.day unless d.day < result }
266
+ result
267
+ end
268
+
269
+ def week_matches?(index,date)
270
+ if(index > 0)
271
+ return week_from_start_matches?(index,date)
272
+ else
273
+ return week_from_end_matches?(index,date)
274
+ end
275
+ end
276
+
277
+ def week_from_start_matches?(index,date)
278
+ week_in_month(date.day)==index
279
+ end
280
+
281
+ def week_from_end_matches?(index,date)
282
+ n = days_left_in_month(date) + 1
283
+ week_in_month(n)==index.abs
284
+ end
285
+
286
+ end
287
+
288
+ # TExpr that provides support for building a temporal
289
+ # expression using the form:
290
+ #
291
+ # DIMonth.new(1,0)
292
+ #
293
+ # where the first argument is the week of the month and the second
294
+ # argument is the wday of the week as defined by the 'wday' method
295
+ # in the standard library class Date.
296
+ #
297
+ # A negative value for the week of the month argument will count
298
+ # backwards from the end of the month. So, to match the last Saturday
299
+ # of the month
300
+ #
301
+ # DIMonth.new(-1,6)
302
+ #
303
+ # Using constants defined in the base Runt module, you can re-write
304
+ # the first example above as:
305
+ #
306
+ # DIMonth.new(First,Sunday)
307
+ #
308
+ # and the second as:
309
+ #
310
+ # DIMonth.new(Last,Saturday)
311
+ #
312
+ # See also: Date, Runt
313
+ class DIMonth
314
+
315
+ include TExpr
316
+ include TExprUtils
317
+
318
+ def initialize(week_of_month_index,day_index)
319
+ @day_index = day_index
320
+ @week_of_month_index = week_of_month_index
321
+ end
322
+
323
+ def include?(date)
324
+ ( day_matches?(date) ) && ( week_matches?(@week_of_month_index,date) )
325
+ end
326
+
327
+ def to_s
328
+ "#{Runt.ordinalize(@week_of_month_index)} #{Runt.day_name(@day_index)} of the month"
329
+ end
330
+
331
+ private
332
+ def day_matches?(date)
333
+ @day_index == date.wday
334
+ end
335
+
336
+ end
337
+
338
+ # TExpr that matches days of the week where the first argument
339
+ # is an integer denoting the ordinal day of the week. Valid values are 0..6 where
340
+ # 0 == Sunday and 6==Saturday
341
+ #
342
+ # For example:
343
+ #
344
+ # DIWeek.new(0)
345
+ #
346
+ # Using constants defined in the base Runt module, you can re-write
347
+ # the first example above as:
348
+ #
349
+ # DIWeek.new(Sunday)
350
+ #
351
+ # See also: Date, Runt
352
+ class DIWeek
353
+
354
+ include TExpr
355
+
356
+ VALID_RANGE = 0..6
357
+
358
+ def initialize(ordinal_weekday)
359
+ unless VALID_RANGE.include?(ordinal_weekday)
360
+ raise ArgumentError, 'invalid ordinal day of week'
361
+ end
362
+ @ordinal_weekday = ordinal_weekday
363
+ end
364
+
365
+ def include?(date)
366
+ @ordinal_weekday == date.wday
367
+ end
368
+
369
+ def to_s
370
+ "#{Runt.day_name(@ordinal_weekday)}"
371
+ end
372
+
373
+ end
374
+
375
+ # TExpr that matches days of the week within one
376
+ # week only.
377
+ #
378
+ # If start and end day are equal, the entire week will match true.
379
+ #
380
+ # See also: Date
381
+ class REWeek
382
+
383
+ include TExpr
384
+
385
+ VALID_RANGE = 0..6
386
+
387
+ # Creates a REWeek using the supplied start
388
+ # day(range = 0..6, where 0=>Sunday) and an optional end
389
+ # day. If an end day is not supplied, the maximum value
390
+ # (6 => Saturday) is assumed.
391
+ def initialize(start_day,end_day=6)
392
+ validate(start_day,end_day)
393
+ @start_day = start_day
394
+ @end_day = end_day
395
+ end
396
+
397
+ def include?(date)
398
+ return true if all_week?
399
+ if @start_day < @end_day
400
+ @start_day<=date.wday && @end_day>=date.wday
401
+ else
402
+ (@start_day<=date.wday && 6 >=date.wday) || (0 <=date.wday && @end_day >=date.wday)
403
+ end
404
+ end
405
+
406
+ def to_s
407
+ return "all week" if all_week?
408
+ "#{Runt.day_name(@start_day)} through #{Runt.day_name(@end_day)}"
409
+ end
410
+
411
+ private
412
+
413
+ def all_week?
414
+ return true if @start_day==@end_day
415
+ end
416
+
417
+ def validate(start_day,end_day)
418
+ unless VALID_RANGE.include?(start_day)&&VALID_RANGE.include?(end_day)
419
+ raise ArgumentError, 'start and end day arguments must be in the range #{VALID_RANGE.to_s}.'
420
+ end
421
+ end
422
+ end
423
+
424
+ class REYear
425
+
426
+ include TExpr
427
+
428
+ def initialize(start_month, start_day=0, end_month=start_month, end_day=0)
429
+ @start_month = start_month
430
+ @start_day = start_day
431
+ @end_month = end_month
432
+ @end_day = end_day
433
+ end
434
+
435
+ def include?(date)
436
+ months_include?(date) ||
437
+ start_month_include?(date) ||
438
+ end_month_include?(date)
439
+ end
440
+
441
+ def save
442
+ "Runt::REYear.new(#{@start_month}, #{@start_day}, #{@end_month}, #{@end_day})"
443
+ end
444
+
445
+ def to_s
446
+ "#{Runt.month_name(@start_month)} #{Runt.ordinalize(@start_day)} " +
447
+ "through #{Runt.month_name(@end_month)} #{Runt.ordinalize(@end_day)}"
448
+ end
449
+
450
+ private
451
+ def months_include?(date)
452
+ (date.mon > @start_month) && (date.mon < @end_month)
453
+ end
454
+
455
+ def end_month_include?(date)
456
+ return false unless (date.mon == @end_month)
457
+ (@end_day == 0) || (date.day <= @end_day)
458
+ end
459
+
460
+ def start_month_include?(date)
461
+ return false unless (date.mon == @start_month)
462
+ (@start_day == 0) || (date.day >= @start_day)
463
+ end
464
+ end
465
+
466
+ # TExpr that matches periods of the day with minute
467
+ # precision. If the start hour is greater than the end hour, than end hour
468
+ # is assumed to be on the following day.
469
+ #
470
+ # See also: Date
471
+ class REDay
472
+
473
+ include TExpr
474
+
475
+ CURRENT=28
476
+ NEXT=29
477
+ ANY_DATE=PDate.day(2002,8,CURRENT)
478
+
479
+ def initialize(start_hour, start_minute, end_hour, end_minute)
480
+
481
+ start_time = PDate.min(ANY_DATE.year,ANY_DATE.month,
482
+ ANY_DATE.day,start_hour,start_minute)
483
+
484
+ if(@spans_midnight = spans_midnight?(start_hour, end_hour)) then
485
+ end_time = get_next(end_hour,end_minute)
486
+ else
487
+ end_time = get_current(end_hour,end_minute)
488
+ end
489
+
490
+ @range = start_time..end_time
491
+ end
492
+
493
+ def include?(date)
494
+ # If precision is day or greater, then the result is always true
495
+ return true if date.date_precision <= DPrecision::DAY
496
+
497
+ if(@spans_midnight&&date.hour<12) then
498
+ #Assume next day
499
+ return @range.include?(get_next(date.hour,date.min))
500
+ end
501
+
502
+ #Same day
503
+ return @range.include?(get_current(date.hour,date.min))
504
+ end
505
+
506
+ def to_s
507
+ "from #{Runt.format_time(@range.begin)} to #{Runt.format_time(@range.end)} daily"
508
+ end
509
+
510
+ private
511
+ def spans_midnight?(start_hour, end_hour)
512
+ return end_hour <= start_hour
513
+ end
514
+
515
+ def get_current(hour,minute)
516
+ PDate.min(ANY_DATE.year,ANY_DATE.month,CURRENT,hour,minute)
517
+ end
518
+
519
+ def get_next(hour,minute)
520
+ PDate.min(ANY_DATE.year,ANY_DATE.month,NEXT,hour,minute)
521
+ end
522
+
523
+ end
524
+
525
+ # TExpr that matches the week in a month. For example:
526
+ #
527
+ # WIMonth.new(1)
528
+ #
529
+ # See also: Date
530
+ class WIMonth
531
+
532
+ include TExpr
533
+ include TExprUtils
534
+
535
+ VALID_RANGE = -2..5
536
+
537
+ def initialize(ordinal)
538
+ unless VALID_RANGE.include?(ordinal)
539
+ raise ArgumentError, 'invalid ordinal week of month'
540
+ end
541
+ @ordinal = ordinal
542
+ end
543
+
544
+ def include?(date)
545
+ week_matches?(@ordinal,date)
546
+ end
547
+
548
+ def to_s
549
+ "#{Runt.ordinalize(@ordinal)} week of any month"
550
+ end
551
+
552
+ end
553
+
554
+ # TExpr that matches a range of dates within a month. For example:
555
+ #
556
+ # REMonth.(12,28)
557
+ #
558
+ # matches from the 12th thru the 28th of any month. If end_day==0
559
+ # or is not given, start_day will define the range with that single day.
560
+ #
561
+ # See also: Date
562
+ class REMonth
563
+
564
+ include TExpr
565
+
566
+ def initialize(start_day, end_day=0)
567
+ end_day=start_day if end_day==0
568
+ @range = start_day..end_day
569
+ end
570
+
571
+ def include?(date)
572
+ @range.include? date.mday
573
+ end
574
+
575
+ def to_s
576
+ "from the #{Runt.ordinalize(@range.begin)} to the #{Runt.ordinalize(@range.end)} monthly"
577
+ end
578
+
579
+ end
580
+
581
+ #
582
+ # Using the precision from the supplied start argument and the its date value,
583
+ # matches every n number of time units thereafter.
584
+ #
585
+ class EveryTE
586
+
587
+ include TExpr
588
+
589
+ def initialize(start,n)
590
+ @start=start
591
+ @interval=n
592
+ end
593
+
594
+ def include?(date)
595
+ i=DPrecision.to_p(@start,@start.date_precision)
596
+ # Use the precision of the start date
597
+ d=DPrecision.to_p(date,@start.date_precision)
598
+ while i<=d
599
+ return true if i.eql?(d)
600
+ i=i+@interval
601
+ end
602
+ false
603
+ end
604
+
605
+ def to_s
606
+ "every #{@interval} #{@start.date_precision.label.downcase}s starting #{Runt.format_date(@start)}"
607
+ end
608
+
609
+ end
610
+
611
+ # Using day precision dates, matches every n number of days after a given
612
+ # base date. All date arguments are converted to DPrecision::DAY precision.
613
+ #
614
+ # Contributed by Ira Burton
615
+ class DayIntervalTE
616
+
617
+ include TExpr
618
+
619
+ def initialize(base_date,n)
620
+ @base_date = DPrecision.to_p(base_date,DPrecision::DAY)
621
+ @interval = n
622
+ end
623
+
624
+ def include?(date)
625
+ return ((DPrecision.to_p(date,DPrecision::DAY) - @base_date).to_i % @interval == 0)
626
+ end
627
+
628
+ def to_s
629
+ "every #{Runt.ordinalize(@interval)} day after #{Runt.format_date(@base_date)}"
630
+ end
631
+
632
+ end
633
+
634
+ # Simple expression which returns true if the supplied arguments
635
+ # occur within the given year.
636
+ #
637
+ class YearTE
638
+
639
+ include TExpr
640
+
641
+ def initialize(year)
642
+ @year = year
643
+ end
644
+
645
+ def include?(date)
646
+ return date.year == @year
647
+ end
648
+
649
+ def to_s
650
+ "during the year #{@year}"
651
+ end
652
+
653
+ end
654
+
655
+ # Matches dates that occur before a given date.
656
+ class BeforeTE
657
+
658
+ include TExpr
659
+
660
+ def initialize(date, inclusive=false)
661
+ @date = date
662
+ @inclusive = inclusive
663
+ end
664
+
665
+ def include?(date)
666
+ return (date < @date) || (@inclusive && @date == date)
667
+ end
668
+
669
+ def to_s
670
+ "before #{Runt.format_date(@base_date)}"
671
+ end
672
+
673
+ end
674
+
675
+ # Matches dates that occur after a given date.
676
+ class AfterTE
677
+
678
+ include TExpr
679
+
680
+ def initialize(date, inclusive=false)
681
+ @date = date
682
+ @inclusive = inclusive
683
+ end
684
+
685
+ def include?(date)
686
+ return (date > @date) || (@inclusive && @date == date)
687
+ end
688
+
689
+ def to_s
690
+ "before #{Runt.format_date(@base_date)}"
691
+ end
692
+
693
+ end
694
+
695
+ end