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.
@@ -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