runt 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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