runt19 0.7.6

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