runt19 0.7.6

Sign up to get free protection for your applications and to get access to all the features.
Files changed (63) hide show
  1. data/.gitignore +17 -0
  2. data/Gemfile +4 -0
  3. data/History.txt +153 -0
  4. data/LICENSE +22 -0
  5. data/LICENSE.txt +44 -0
  6. data/Manifest.txt +112 -0
  7. data/README.md +29 -0
  8. data/README.txt +106 -0
  9. data/Rakefile +2 -0
  10. data/TODO +13 -0
  11. data/examples/payment_report.rb +59 -0
  12. data/examples/payment_reporttest.rb +49 -0
  13. data/examples/reminder.rb +63 -0
  14. data/examples/schedule_tutorial.rb +59 -0
  15. data/examples/schedule_tutorialtest.rb +52 -0
  16. data/lib/runt.rb +249 -0
  17. data/lib/runt/daterange.rb +74 -0
  18. data/lib/runt/dprecision.rb +150 -0
  19. data/lib/runt/expressionbuilder.rb +65 -0
  20. data/lib/runt/pdate.rb +165 -0
  21. data/lib/runt/schedule.rb +88 -0
  22. data/lib/runt/sugar.rb +171 -0
  23. data/lib/runt/temporalexpression.rb +795 -0
  24. data/lib/runt/version.rb +3 -0
  25. data/lib/runt19.rb +1 -0
  26. data/runt19.gemspec +17 -0
  27. data/setup.rb +1331 -0
  28. data/site/blue-robot3.css +132 -0
  29. data/site/dcl-small.gif +0 -0
  30. data/site/index.html +72 -0
  31. data/site/logohover.png +0 -0
  32. data/site/runt-logo.gif +0 -0
  33. data/site/runt-logo.psd +0 -0
  34. data/test/aftertetest.rb +31 -0
  35. data/test/baseexpressiontest.rb +110 -0
  36. data/test/beforetetest.rb +31 -0
  37. data/test/collectiontest.rb +63 -0
  38. data/test/combinedexpressionstest.rb +158 -0
  39. data/test/daterangetest.rb +89 -0
  40. data/test/dayintervaltetest.rb +37 -0
  41. data/test/difftest.rb +37 -0
  42. data/test/dimonthtest.rb +59 -0
  43. data/test/diweektest.rb +32 -0
  44. data/test/dprecisiontest.rb +58 -0
  45. data/test/everytetest.rb +36 -0
  46. data/test/expressionbuildertest.rb +64 -0
  47. data/test/icalendartest.rb +1104 -0
  48. data/test/intersecttest.rb +34 -0
  49. data/test/pdatetest.rb +147 -0
  50. data/test/redaytest.rb +40 -0
  51. data/test/remonthtest.rb +37 -0
  52. data/test/reweektest.rb +51 -0
  53. data/test/reyeartest.rb +99 -0
  54. data/test/rspectest.rb +25 -0
  55. data/test/runttest.rb +98 -0
  56. data/test/scheduletest.rb +148 -0
  57. data/test/spectest.rb +36 -0
  58. data/test/sugartest.rb +104 -0
  59. data/test/temporalexpressiontest.rb +76 -0
  60. data/test/uniontest.rb +36 -0
  61. data/test/wimonthtest.rb +54 -0
  62. data/test/yeartetest.rb +22 -0
  63. metadata +137 -0
@@ -0,0 +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
+ # 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 Temporal 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
@@ -0,0 +1,171 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ #
4
+ #
5
+ # == Overview
6
+ #
7
+ # This file provides an optional extension to the Runt module which
8
+ # provides convenient shortcuts for commonly used temporal expressions.
9
+ #
10
+ # Several methods for creating new temporal expression instances are added
11
+ # to a client class by including the Runt module.
12
+ #
13
+ # === Shortcuts
14
+ #
15
+ # Shortcuts are implemented by pattern matching done in method_missing for
16
+ # the Runt module. Generally speaking, range expressions start with "daily_",
17
+ # "weekly_", "yearly_", etc.
18
+ #
19
+ # Times use the format /\d{1,2}_\d{2}[ap]m/ where the first digits represent hours
20
+ # and the second digits represent minutes. Note that hours are always within the
21
+ # range of 1-12 and may be one or two digits. Minutes are always two digits
22
+ # (e.g. '03' not just '3') and are always followed by am or pm (lowercase).
23
+ #
24
+ #
25
+ # class MyClass
26
+ # include Runt
27
+ #
28
+ # def some_method
29
+ # # Daily from 4:02pm to 10:20pm or anytime Tuesday
30
+ # expr = daily_4_02pm_to_10_20pm() | tuesday()
31
+ # ...
32
+ # end
33
+ # ...
34
+ # end
35
+ #
36
+ # The following documents the syntax for particular temporal expression classes.
37
+ #
38
+ # === REDay
39
+ #
40
+ # daily_<start hour>_<start minute>_to_<end hour>_<end minute>
41
+ #
42
+ # Example:
43
+ #
44
+ # self.daily_10_00am_to_1:30pm()
45
+ #
46
+ # is equivilant to
47
+ #
48
+ # REDay.new(10,00,13,30)
49
+ #
50
+ # === REWeek
51
+ #
52
+ # weekly_<start day>_to_<end day>
53
+ #
54
+ # Example:
55
+ #
56
+ # self.weekly_tuesday_to_thrusday()
57
+ #
58
+ # is equivilant to
59
+ #
60
+ # REWeek.new(Tuesday, Thrusday)
61
+ #
62
+ # === REMonth
63
+ #
64
+ # monthly_<start numeric ordinal>_to_<end numeric ordinal>
65
+ #
66
+ # Example:
67
+ #
68
+ # self.monthly_23rd_to_29th()
69
+ #
70
+ # is equivilant to
71
+ #
72
+ # REMonth.new(23, 29)
73
+ #
74
+ # === REYear
75
+ #
76
+ # self.yearly_<start month>_<start day>_to_<end month>_<end day>()
77
+ #
78
+ # Example:
79
+ #
80
+ # self.yearly_march_15_to_june_1()
81
+ #
82
+ # is equivilant to
83
+ #
84
+ # REYear.new(March, 15, June, 1)
85
+ #
86
+ # === DIWeek
87
+ #
88
+ # self.<day name>()
89
+ #
90
+ # Example:
91
+ #
92
+ # self.friday()
93
+ #
94
+ # is equivilant to
95
+ #
96
+ # DIWeek.new(Friday)
97
+ #
98
+ # === DIMonth
99
+ #
100
+ # self.<lowercase ordinal>_<day name>()
101
+ #
102
+ # Example:
103
+ #
104
+ # self.first_saturday()
105
+ # self.last_tuesday()
106
+ #
107
+ # is equivilant to
108
+ #
109
+ # DIMonth.new(First, Saturday)
110
+ # DIMonth.new(Last, Tuesday)
111
+ #
112
+
113
+ require 'runt'
114
+
115
+ module Runt
116
+ MONTHS = '(january|february|march|april|may|june|july|august|september|october|november|december)'
117
+ DAYS = '(sunday|monday|tuesday|wednesday|thursday|friday|saturday)'
118
+ WEEK_OF_MONTH_ORDINALS = '(first|second|third|fourth|last|second_to_last)'
119
+ ORDINAL_SUFFIX = '(?:st|nd|rd|th)'
120
+ ORDINAL_ABBR = '(st|nd|rd|th)'
121
+ class << self
122
+ def const(string)
123
+ self.const_get(string.capitalize)
124
+ end
125
+ end
126
+
127
+ def method_missing(name, *args, &block)
128
+ result = self.build(name, *args, &block)
129
+ return result unless result.nil?
130
+ super
131
+ end
132
+
133
+ def build(name, *args, &block)
134
+ case name.to_s
135
+ when /^daily_(\d{1,2})_(\d{2})([ap]m)_to_(\d{1,2})_(\d{2})([ap]m)$/
136
+ # REDay
137
+ st_hr, st_min, st_m, end_hr, end_min, end_m = $1, $2, $3, $4, $5, $6
138
+ args = parse_time(st_hr, st_min, st_m)
139
+ args.concat(parse_time(end_hr, end_min, end_m))
140
+ return REDay.new(*args)
141
+ when Regexp.new('^weekly_' + DAYS + '_to_' + DAYS + '$')
142
+ # REWeek
143
+ st_day, end_day = $1, $2
144
+ return REWeek.new(Runt.const(st_day), Runt.const(end_day))
145
+ when Regexp.new('^monthly_(\d{1,2})' + ORDINAL_SUFFIX + '_to_(\d{1,2})' \
146
+ + ORDINAL_SUFFIX + '$')
147
+ # REMonth
148
+ st_day, end_day = $1, $2
149
+ return REMonth.new(st_day, end_day)
150
+ when Regexp.new('^yearly_' + MONTHS + '_(\d{1,2})_to_' + MONTHS + '_(\d{1,2})$')
151
+ # REYear
152
+ st_mon, st_day, end_mon, end_day = $1, $2, $3, $4
153
+ return REYear.new(Runt.const(st_mon), st_day, Runt.const(end_mon), end_day)
154
+ when Regexp.new('^' + DAYS + '$')
155
+ # DIWeek
156
+ return DIWeek.new(Runt.const(name.to_s))
157
+ when Regexp.new(WEEK_OF_MONTH_ORDINALS + '_' + DAYS)
158
+ # DIMonth
159
+ ordinal, day = $1, $2
160
+ return DIMonth.new(Runt.const(ordinal), Runt.const(day))
161
+ else
162
+ # You're hosed
163
+ nil
164
+ end
165
+ end
166
+
167
+ def parse_time(hour, minute, ampm)
168
+ hour = hour.to_i + 12 if ampm =~ /pm/
169
+ [hour.to_i, minute.to_i]
170
+ end
171
+ end
@@ -0,0 +1,795 @@
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
+
90
+ def initialize
91
+ @expressions = Array.new
92
+ end
93
+
94
+ def add(anExpression)
95
+ @expressions.push anExpression
96
+ self
97
+ end
98
+
99
+ # Will return true if the supplied object overlaps with the range used to
100
+ # create this instance
101
+ def overlap?(date_expr)
102
+ @expressions.each do | interval |
103
+ return true if date_expr.overlap?(interval)
104
+ end
105
+ false
106
+ end
107
+
108
+ def to_s
109
+ if !@expressions.empty? && block_given?
110
+ first_expr, next_exprs = yield
111
+ result = ''
112
+ @expressions.map do |expr|
113
+ if @expressions.first===expr
114
+ result = first_expr + expr.to_s
115
+ else
116
+ result = result + next_exprs + expr.to_s
117
+ end
118
+ end
119
+ result
120
+ else
121
+ 'empty'
122
+ end
123
+ end
124
+
125
+ def display
126
+ puts "I am a #{self.class} containing:"
127
+ @expressions.each do |ex|
128
+ pp "#{ex.class}"
129
+ end
130
+ end
131
+
132
+
133
+ end
134
+
135
+ # Composite TExpr that will be true if <b>any</b> of it's
136
+ # component expressions are true.
137
+ class Union < Collection
138
+
139
+ def include?(aDate)
140
+ @expressions.each do |expr|
141
+ return true if expr.include?(aDate)
142
+ end
143
+ false
144
+ end
145
+
146
+ def to_s
147
+ super {['every ',' or ']}
148
+ end
149
+ end
150
+
151
+ # Composite TExpr that will be true only if <b>all</b> it's
152
+ # component expressions are true.
153
+ class Intersect < Collection
154
+
155
+ def include?(aDate)
156
+ result = false
157
+ @expressions.each do |expr|
158
+ return false unless (result = expr.include?(aDate))
159
+ end
160
+ result
161
+ end
162
+
163
+ def to_s
164
+ super {['every ', ' and ']}
165
+ end
166
+ end
167
+
168
+ # TExpr that will be true only if the first of
169
+ # its two contained expressions is true and the second is false.
170
+ class Diff
171
+
172
+ include TExpr
173
+
174
+ attr_reader :expr1, :expr2
175
+
176
+ def initialize(expr1, expr2)
177
+ @expr1 = expr1
178
+ @expr2 = expr2
179
+ end
180
+
181
+ def include?(aDate)
182
+ return false unless (@expr1.include?(aDate) && !@expr2.include?(aDate))
183
+ true
184
+ end
185
+
186
+ def to_s
187
+ @expr1.to_s + ' except for ' + @expr2.to_s
188
+ end
189
+ end
190
+
191
+ # TExpr that provides for inclusion of an arbitrary date.
192
+ class Spec
193
+
194
+ include TExpr
195
+
196
+ attr_reader :date_expr
197
+
198
+ def initialize(date_expr)
199
+ @date_expr = date_expr
200
+ end
201
+
202
+ # Will return true if the supplied object is == to that which was used to
203
+ # create this instance
204
+ def include?(date_expr)
205
+ return date_expr.include?(@date_expr) if date_expr.respond_to?(:include?)
206
+ return true if @date_expr == date_expr
207
+ false
208
+ end
209
+
210
+ def to_s
211
+ @date_expr.to_s
212
+ end
213
+
214
+ end
215
+
216
+ # TExpr that provides a thin wrapper around built-in Ruby <tt>Range</tt> functionality
217
+ # facilitating inclusion of an arbitrary range in a temporal expression.
218
+ #
219
+ # See also: Range
220
+ class RSpec < Spec
221
+
222
+ ## Will return true if the supplied object is included in the range used to
223
+ ## create this instance
224
+ def include?(date_expr)
225
+ return @date_expr.include?(date_expr)
226
+ end
227
+
228
+ # Will return true if the supplied object overlaps with the range used to
229
+ # create this instance
230
+ def overlap?(date_expr)
231
+ @date_expr.each do | interval |
232
+ return true if date_expr.include?(interval)
233
+ end
234
+ false
235
+ end
236
+
237
+ end
238
+
239
+ #######################################################################
240
+ # Utility methods common to some expressions
241
+
242
+ module TExprUtils
243
+ def week_in_month(day_in_month)
244
+ ((day_in_month - 1) / 7) + 1
245
+ end
246
+
247
+ def days_left_in_month(date)
248
+ return max_day_of_month(date) - date.day
249
+ end
250
+
251
+ def max_day_of_month(date)
252
+ # Contributed by Justin Cunningham who took it verbatim from the Rails
253
+ # ActiveSupport::CoreExtensions::Time::Calculations::ClassMethods module
254
+ # days_in_month method.
255
+ month = date.month
256
+ year = date.year
257
+ if month == 2
258
+ !year.nil? &&
259
+ (year % 4 == 0) &&
260
+ ((year % 100 != 0) ||
261
+ (year % 400 == 0)) ? 29 : 28
262
+ elsif month <= 7
263
+ month % 2 == 0 ? 30 : 31
264
+ else
265
+ month % 2 == 0 ? 31 : 30
266
+ end
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
+ #
425
+ # TExpr that matches date ranges within a single year. Assumes that the start
426
+ # and end parameters occur within the same year.
427
+ #
428
+ #
429
+ class REYear
430
+
431
+ # Sentinel value used to denote that no specific day was given to create
432
+ # the expression.
433
+ NO_DAY = 0
434
+
435
+ include TExpr
436
+
437
+ attr_accessor :start_month, :start_day, :end_month, :end_day
438
+
439
+ #
440
+ # == Synopsis
441
+ #
442
+ # REYear.new(start_month [, (start_day | end_month), ...]
443
+ #
444
+ # == Args
445
+ #
446
+ # One or two arguments given::
447
+ #
448
+ # +start_month+::
449
+ # Start month. Valid values are 1..12. When no other parameters are given
450
+ # this value will be used for the end month as well. Matches the entire
451
+ # month through the ending month.
452
+ # +end_month+::
453
+ # End month. Valid values are 1..12. When given in two argument form
454
+ # will match through the entire month.
455
+ #
456
+ # Three or four arguments given::
457
+ #
458
+ # +start_month+::
459
+ # Start month. Valid values are 1..12.
460
+ # +start_day+::
461
+ # Start day. Valid values are 1..31, depending on the month.
462
+ # +end_month+::
463
+ # End month. Valid values are 1..12. If a fourth argument is not given,
464
+ # this value will cover through the entire month.
465
+ # +end_day+::
466
+ # End day. Valid values are 1..31, depending on the month.
467
+ #
468
+ # == Description
469
+ #
470
+ # Create a new REYear expression expressing a range of months or days
471
+ # within months within a year.
472
+ #
473
+ # == Usage
474
+ #
475
+ # # Creates the range March 12th through May 23rd
476
+ # expr = REYear.new(3,12,5,23)
477
+ #
478
+ # # Creates the range March 1st through May 31st
479
+ # expr = REYear.new(3,5)
480
+ #
481
+ # # Creates the range March 12th through May 31st
482
+ # expr = REYear.new(3,12,5)
483
+ #
484
+ # # Creates the range March 1st through March 30th
485
+ # expr = REYear.new(3)
486
+ #
487
+ def initialize(start_month, *args)
488
+ @start_month = start_month
489
+ if (args.nil? || args.size == NO_DAY) then
490
+ # One argument given
491
+ @end_month = start_month
492
+ @start_day = NO_DAY
493
+ @end_day = NO_DAY
494
+ else
495
+ case args.size
496
+ when 1
497
+ @end_month = args[0]
498
+ @start_day = NO_DAY
499
+ @end_day = NO_DAY
500
+ when 2
501
+ @start_day = args[0]
502
+ @end_month = args[1]
503
+ @end_day = NO_DAY
504
+ when 3
505
+ @start_day = args[0]
506
+ @end_month = args[1]
507
+ @end_day = args[2]
508
+ else
509
+ raise "Invalid number of var args: 1 or 3 expected, #{args.size} given"
510
+ end
511
+ end
512
+ @same_month_dates_provided = (@start_month == @end_month) && (@start_day!=NO_DAY && @end_day != NO_DAY)
513
+ end
514
+
515
+ def include?(date)
516
+
517
+ return same_start_month_include_day?(date) \
518
+ && same_end_month_include_day?(date) if @same_month_dates_provided
519
+
520
+ is_between_months?(date) ||
521
+ (same_start_month_include_day?(date) ||
522
+ same_end_month_include_day?(date))
523
+ end
524
+
525
+ def save
526
+ "Runt::REYear.new(#{@start_month}, #{@start_day}, #{@end_month}, #{@end_day})"
527
+ end
528
+
529
+ def to_s
530
+ "#{Runt.month_name(@start_month)} #{Runt.ordinalize(@start_day)} " +
531
+ "through #{Runt.month_name(@end_month)} #{Runt.ordinalize(@end_day)}"
532
+ end
533
+
534
+ private
535
+ def is_between_months?(date)
536
+ (date.mon > @start_month) && (date.mon < @end_month)
537
+ end
538
+
539
+ def same_end_month_include_day?(date)
540
+ return false unless (date.mon == @end_month)
541
+ (@end_day == NO_DAY) || (date.day <= @end_day)
542
+ end
543
+
544
+ def same_start_month_include_day?(date)
545
+ return false unless (date.mon == @start_month)
546
+ (@start_day == NO_DAY) || (date.day >= @start_day)
547
+ end
548
+
549
+ end
550
+
551
+ # TExpr that matches periods of the day with minute
552
+ # precision. If the start hour is greater than the end hour, than end hour
553
+ # is assumed to be on the following day.
554
+ #
555
+ # NOTE: By default, this class will match any date expression whose
556
+ # precision is less than or equal to DPrecision::DAY. To override
557
+ # this behavior, pass the optional fifth constructor argument the
558
+ # value: false.
559
+ #
560
+ # See also: Date
561
+ class REDay
562
+
563
+ include TExpr
564
+
565
+ CURRENT=28
566
+ NEXT=29
567
+ ANY_DATE=PDate.day(2002,8,CURRENT)
568
+
569
+ def initialize(start_hour, start_minute, end_hour, end_minute, less_precise_match=true)
570
+
571
+ start_time = PDate.min(ANY_DATE.year,ANY_DATE.month,
572
+ ANY_DATE.day,start_hour,start_minute)
573
+
574
+ if(@spans_midnight = spans_midnight?(start_hour, end_hour)) then
575
+ end_time = get_next(end_hour,end_minute)
576
+ else
577
+ end_time = get_current(end_hour,end_minute)
578
+ end
579
+
580
+ @range = start_time..end_time
581
+ @less_precise_match = less_precise_match
582
+ end
583
+
584
+ def include?(date)
585
+ #
586
+ # If @less_precise_match == true and the precision of the argument
587
+ # is day or greater, then the result is always true
588
+ return true if @less_precise_match && date.date_precision <= DPrecision::DAY
589
+ if(@spans_midnight&&date.hour<12) then
590
+ #Assume next day
591
+ n = get_next(date.hour,date.min)
592
+ return false unless @range.begin <= n
593
+ return false unless @range.end >= n
594
+ true
595
+ end
596
+
597
+ #Same day
598
+ c = get_current(date.hour,date.min)
599
+ return false unless @range.begin <= c
600
+ return false unless @range.end >= c
601
+ true
602
+ end
603
+
604
+ def to_s
605
+ "from #{Runt.format_time(@range.begin)} to #{Runt.format_time(@range.end)} daily"
606
+ end
607
+
608
+ private
609
+ def spans_midnight?(start_hour, end_hour)
610
+ return end_hour < start_hour
611
+ end
612
+
613
+ def get_current(hour,minute)
614
+ PDate.min(ANY_DATE.year,ANY_DATE.month,CURRENT,hour,minute)
615
+ end
616
+
617
+ def get_next(hour,minute)
618
+ PDate.min(ANY_DATE.year,ANY_DATE.month,NEXT,hour,minute)
619
+ end
620
+
621
+ end
622
+
623
+ # TExpr that matches the week in a month. For example:
624
+ #
625
+ # WIMonth.new(1)
626
+ #
627
+ # See also: Date
628
+ # FIXME .dates mixin seems functionally broken
629
+ class WIMonth
630
+
631
+ include TExpr
632
+ include TExprUtils
633
+
634
+ VALID_RANGE = -2..5
635
+
636
+ def initialize(ordinal)
637
+ unless VALID_RANGE.include?(ordinal)
638
+ raise ArgumentError, 'invalid ordinal week of month'
639
+ end
640
+ @ordinal = ordinal
641
+ end
642
+
643
+ def include?(date)
644
+ week_matches?(@ordinal,date)
645
+ end
646
+
647
+ def to_s
648
+ "#{Runt.ordinalize(@ordinal)} week of any month"
649
+ end
650
+
651
+ end
652
+
653
+ # TExpr that matches a range of dates within a month. For example:
654
+ #
655
+ # REMonth.(12,28)
656
+ #
657
+ # matches from the 12th thru the 28th of any month. If end_day==0
658
+ # or is not given, start_day will define the range with that single day.
659
+ #
660
+ # See also: Date
661
+ class REMonth
662
+
663
+ include TExpr
664
+
665
+ def initialize(start_day, end_day=0)
666
+ end_day=start_day if end_day==0
667
+ @range = start_day..end_day
668
+ end
669
+
670
+ def include?(date)
671
+ @range.include? date.mday
672
+ end
673
+
674
+ def to_s
675
+ "from the #{Runt.ordinalize(@range.begin)} to the #{Runt.ordinalize(@range.end)} monthly"
676
+ end
677
+
678
+ end
679
+
680
+ #
681
+ # Using the precision from the supplied start argument and the its date value,
682
+ # matches every n number of time units thereafter.
683
+ #
684
+ class EveryTE
685
+
686
+ include TExpr
687
+
688
+ def initialize(start,n,precision=nil)
689
+ @start=start
690
+ @interval=n
691
+ # Use the precision of the start date by default
692
+ @precision=precision || @start.date_precision
693
+ end
694
+
695
+ def include?(date)
696
+ i=DPrecision.to_p(@start,@precision)
697
+ d=DPrecision.to_p(date,@precision)
698
+ while i<=d
699
+ return true if i.eql?(d)
700
+ i=i+@interval
701
+ end
702
+ false
703
+ end
704
+
705
+ def to_s
706
+ "every #{@interval} #{@precision.label.downcase}s starting #{Runt.format_date(@start)}"
707
+ end
708
+
709
+ end
710
+
711
+ # Using day precision dates, matches every n number of days after a given
712
+ # base date. All date arguments are converted to DPrecision::DAY precision.
713
+ #
714
+ # Contributed by Ira Burton
715
+ class DayIntervalTE
716
+
717
+ include TExpr
718
+
719
+ def initialize(base_date,n)
720
+ @base_date = DPrecision.to_p(base_date,DPrecision::DAY)
721
+ @interval = n
722
+ end
723
+
724
+ def include?(date)
725
+ return ((DPrecision.to_p(date,DPrecision::DAY) - @base_date).to_i % @interval == 0)
726
+ end
727
+
728
+ def to_s
729
+ "every #{Runt.ordinalize(@interval)} day after #{Runt.format_date(@base_date)}"
730
+ end
731
+
732
+ end
733
+
734
+ # Simple expression which returns true if the supplied arguments
735
+ # occur within the given year.
736
+ #
737
+ class YearTE
738
+
739
+ include TExpr
740
+
741
+ def initialize(year)
742
+ @year = year
743
+ end
744
+
745
+ def include?(date)
746
+ return date.year == @year
747
+ end
748
+
749
+ def to_s
750
+ "during the year #{@year}"
751
+ end
752
+
753
+ end
754
+
755
+ # Matches dates that occur before a given date.
756
+ class BeforeTE
757
+
758
+ include TExpr
759
+
760
+ def initialize(date, inclusive=false)
761
+ @date = date
762
+ @inclusive = inclusive
763
+ end
764
+
765
+ def include?(date)
766
+ return (date < @date) || (@inclusive && @date == date)
767
+ end
768
+
769
+ def to_s
770
+ "before #{Runt.format_date(@date)}"
771
+ end
772
+
773
+ end
774
+
775
+ # Matches dates that occur after a given date.
776
+ class AfterTE
777
+
778
+ include TExpr
779
+
780
+ def initialize(date, inclusive=false)
781
+ @date = date
782
+ @inclusive = inclusive
783
+ end
784
+
785
+ def include?(date)
786
+ return (date > @date) || (@inclusive && @date == date)
787
+ end
788
+
789
+ def to_s
790
+ "after #{Runt.format_date(@date)}"
791
+ end
792
+
793
+ end
794
+
795
+ end