runt 0.7.0 → 0.9.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.
Files changed (49) hide show
  1. data/.gitignore +19 -0
  2. data/.travis.yml +5 -0
  3. data/{CHANGES → CHANGES.txt} +24 -8
  4. data/Gemfile +4 -0
  5. data/LICENSE.txt +22 -44
  6. data/README.md +79 -0
  7. data/Rakefile +6 -119
  8. data/doc/tutorial_schedule.md +365 -0
  9. data/doc/tutorial_sugar.md +170 -0
  10. data/doc/tutorial_te.md +155 -0
  11. data/lib/runt.rb +36 -21
  12. data/lib/runt/dprecision.rb +4 -2
  13. data/lib/runt/pdate.rb +101 -95
  14. data/lib/runt/schedule.rb +18 -0
  15. data/lib/runt/sugar.rb +41 -9
  16. data/lib/runt/temporalexpression.rb +246 -30
  17. data/lib/runt/version.rb +3 -0
  18. data/runt.gemspec +24 -0
  19. data/site/.cvsignore +1 -0
  20. data/site/dcl-small.gif +0 -0
  21. data/site/index-rubforge-www.html +72 -0
  22. data/site/index.html +75 -60
  23. data/site/runt-logo.gif +0 -0
  24. data/site/runt-logo.psd +0 -0
  25. data/test/baseexpressiontest.rb +10 -8
  26. data/test/combinedexpressionstest.rb +166 -158
  27. data/test/daterangetest.rb +4 -6
  28. data/test/diweektest.rb +32 -32
  29. data/test/dprecisiontest.rb +2 -4
  30. data/test/everytetest.rb +6 -0
  31. data/test/expressionbuildertest.rb +2 -3
  32. data/test/icalendartest.rb +3 -6
  33. data/test/minitest_helper.rb +7 -0
  34. data/test/pdatetest.rb +21 -6
  35. data/test/redaytest.rb +3 -0
  36. data/test/reyeartest.rb +1 -1
  37. data/test/runttest.rb +5 -8
  38. data/test/scheduletest.rb +13 -14
  39. data/test/sugartest.rb +28 -6
  40. data/test/{spectest.rb → temporaldatetest.rb} +14 -4
  41. data/test/{rspectest.rb → temporalrangetest.rb} +4 -4
  42. data/test/test_runt.rb +11 -0
  43. data/test/weekintervaltest.rb +106 -0
  44. metadata +161 -116
  45. data/README +0 -106
  46. data/doc/tutorial_schedule.rdoc +0 -393
  47. data/doc/tutorial_sugar.rdoc +0 -143
  48. data/doc/tutorial_te.rdoc +0 -190
  49. data/setup.rb +0 -1331
@@ -16,27 +16,35 @@ module Runt
16
16
  #
17
17
  # Author:: Matthew Lipper
18
18
  class PDate < DateTime
19
+ include Comparable
19
20
  include DPrecision
20
21
 
21
22
  attr_accessor :date_precision
22
-
23
+
23
24
  class << self
24
- alias_method :old_civil, :civil
25
25
 
26
26
  def civil(*args)
27
- precision=nil
27
+ precision=nil
28
28
  if(args[0].instance_of?(DPrecision::Precision))
29
29
  precision = args.shift
30
30
  else
31
31
  return PDate::sec(*args)
32
32
  end
33
- _civil = old_civil(*args)
34
- _civil.date_precision = precision
35
- _civil
33
+ pdate = super(*args)
34
+ pdate.date_precision = precision
35
+ pdate
36
+ end
37
+
38
+ def parse(*args)
39
+ opts = args.last.is_a?(Hash) ? args.pop : {}
40
+ pdate = super(*args)
41
+ pdate.date_precision = opts[:precision] || opts[:date_precision]
42
+ pdate
36
43
  end
37
- end
38
44
 
39
- class << self; alias_method :new, :civil end
45
+ alias_method :new, :civil
46
+
47
+ end
40
48
 
41
49
  def include?(expr)
42
50
  eql?(expr)
@@ -44,122 +52,120 @@ module Runt
44
52
 
45
53
  def + (n)
46
54
  raise TypeError, 'expected numeric' unless n.kind_of?(Numeric)
55
+ ndays = n
47
56
  case @date_precision
48
57
  when YEAR then
49
- return DPrecision::to_p(PDate::civil(year+n,month,day),@date_precision)
58
+ return DPrecision::to_p(PDate::civil(year+n,month,day),@date_precision)
50
59
  when MONTH then
51
- current_date = self.class.to_date(self)
52
- return DPrecision::to_p((current_date>>n),@date_precision)
60
+ return DPrecision::to_p((self.to_date>>n),@date_precision)
53
61
  when WEEK then
54
- return new_self_plus(n*7)
62
+ ndays = n*7
55
63
  when DAY then
56
- return new_self_plus(n)
64
+ ndays = n
57
65
  when HOUR then
58
- return new_self_plus(n){ |n| n = (n*(1.to_r/24) ) }
66
+ ndays = n*(1.to_r/24)
59
67
  when MIN then
60
- return new_self_plus(n){ |n| n = (n*(1.to_r/1440) ) }
68
+ ndays = n*(1.to_r/1440)
61
69
  when SEC then
62
- return new_self_plus(n){ |n| n = (n*(1.to_r/86400) ) }
70
+ ndays = n*(1.to_r/86400)
63
71
  when MILLI then
64
- return self
72
+ ndays = n*(1.to_r/86400000)
73
+ end
74
+ DPrecision::to_p((self.to_date + ndays),@date_precision)
65
75
  end
66
- end
67
76
 
68
- def - (x)
69
- case x
77
+ def - (x)
78
+ case x
70
79
  when Numeric then
71
- return self+(-x)
72
- #FIXME!!
73
- when Date; return @ajd - x.ajd
80
+ return self+(-x)
81
+ when Date then
82
+ return super(DPrecision::to_p(x,@date_precision))
83
+ end
84
+ raise TypeError, 'expected numeric or date'
74
85
  end
75
- raise TypeError, 'expected numeric or date'
76
- end
77
86
 
78
- def <=> (other)
79
- result = nil
80
- if(other.respond_to?("date_precision") && other.date_precision>@date_precision)
81
- result = super(DPrecision::to_p(other,@date_precision))
82
- else
83
- result = super(other)
87
+ def <=> (other)
88
+ result = nil
89
+ raise "I'm broken #{self.to_s}" if @date_precision.nil?
90
+ if(!other.nil? && other.respond_to?("date_precision") && other.date_precision>@date_precision)
91
+ result = super(DPrecision::to_p(other,@date_precision))
92
+ else
93
+ result = super(other)
94
+ end
95
+ puts "self<#{self.to_s}><=>other<#{other.to_s}> => #{result}" if $DEBUG
96
+ result
84
97
  end
85
- #puts "#{self.to_s}<=>#{other.to_s} => #{result}" if $DEBUG
86
- result
87
- end
88
98
 
89
- def new_self_plus(n)
90
- if(block_given?)
91
- n=yield(n)
99
+ def succ
100
+ result = self + 1
101
+ end
102
+
103
+ def to_date
104
+ (self.date_precision > DAY) ? DateTime.new(self.year,self.month,self.day,self.hour,self.min,self.sec) : Date.new(self.year, self.month, self.day)
92
105
  end
93
- return DPrecision::to_p(self.class.new!(@ajd + n, @of, @sg),@date_precision)
94
- end
95
106
 
96
- def PDate.to_date(pdate)
97
- if( pdate.date_precision > DPrecision::DAY) then
98
- DateTime.new(pdate.year,pdate.month,pdate.day,pdate.hour,pdate.min,pdate.sec)
107
+ def PDate.year(yr,*ignored)
108
+ PDate.civil(YEAR, yr, MONTH.min_value, DAY.min_value )
99
109
  end
100
- return Date.new(pdate.year,pdate.month,pdate.day)
101
- end
102
110
 
103
- def PDate.year(yr,*ignored)
104
- PDate.civil(YEAR, yr, MONTH.min_value, DAY.min_value )
105
- end
111
+ def PDate.month( yr,mon,*ignored )
112
+ PDate.civil(MONTH, yr, mon, DAY.min_value )
113
+ end
106
114
 
107
- def PDate.month( yr,mon,*ignored )
108
- PDate.civil(MONTH, yr, mon, DAY.min_value )
109
- end
115
+ def PDate.week( yr,mon,day,*ignored )
116
+ #LJK: need to calculate which week this day implies,
117
+ #and then move the day back to the *first* day in that week;
118
+ #note that since rfc2445 defaults to weekstart=monday, I'm
119
+ #going to use commercial day-of-week
120
+ raw = PDate.day(yr, mon, day)
121
+ cooked = PDate.commercial(raw.cwyear, raw.cweek, 1)
122
+ PDate.civil(WEEK, cooked.year, cooked.month, cooked.day)
123
+ end
110
124
 
111
- def PDate.week( yr,mon,day,*ignored )
112
- #LJK: need to calculate which week this day implies,
113
- #and then move the day back to the *first* day in that week;
114
- #note that since rfc2445 defaults to weekstart=monday, I'm
115
- #going to use commercial day-of-week
116
- raw = PDate.day(yr, mon, day)
117
- cooked = PDate.commercial(raw.cwyear, raw.cweek, 1)
118
- PDate.civil(WEEK, cooked.year, cooked.month, cooked.day)
119
- end
125
+ def PDate.day( yr,mon,day,*ignored )
126
+ PDate.civil(DAY, yr, mon, day )
127
+ end
120
128
 
121
- def PDate.day( yr,mon,day,*ignored )
122
- PDate.civil(DAY, yr, mon, day )
123
- end
129
+ def PDate.hour( yr,mon,day,hr=HOUR.min_value,*ignored )
130
+ PDate.civil(HOUR, yr, mon, day,hr,MIN.min_value, SEC.min_value)
131
+ end
124
132
 
125
- def PDate.hour( yr,mon,day,hr=HOUR.min_value,*ignored )
126
- PDate.civil(HOUR, yr, mon, day,hr,MIN.min_value, SEC.min_value)
127
- end
133
+ def PDate.min( yr,mon,day,hr=HOUR.min_value,min=MIN.min_value,*ignored )
134
+ PDate.civil(MIN, yr, mon, day,hr,min, SEC.min_value)
135
+ end
128
136
 
129
- def PDate.min( yr,mon,day,hr=HOUR.min_value,min=MIN.min_value,*ignored )
130
- PDate.civil(MIN, yr, mon, day,hr,min, SEC.min_value)
131
- end
137
+ def PDate.sec( yr,mon,day,hr=HOUR.min_value,min=MIN.min_value,sec=SEC.min_value,*ignored )
138
+ PDate.civil(SEC, yr, mon, day,hr,min, sec)
139
+ end
132
140
 
133
- def PDate.sec( yr,mon,day,hr=HOUR.min_value,min=MIN.min_value,sec=SEC.min_value,*ignored )
134
- PDate.civil(SEC, yr, mon, day,hr,min, sec)
135
- end
141
+ def PDate.millisecond( yr,mon,day,hr,min,sec,ms,*ignored )
142
+ PDate.civil(SEC, yr, mon, day,hr,min, sec, ms, *ignored)
143
+ #raise "Not implemented yet."
144
+ end
136
145
 
137
- def PDate.millisecond( yr,mon,day,hr,min,sec,ms,*ignored )
138
- PDate.civil(SEC, yr, mon, day,hr,min, sec, ms, *ignored)
139
- #raise "Not implemented yet."
140
- end
146
+ def PDate.default(*args)
147
+ PDate.civil(DEFAULT, *args)
148
+ end
141
149
 
142
- def PDate.default(*args)
143
- PDate.civil(DEFAULT, *args)
144
- end
150
+ #FIXME: marshall broken in 1.9
151
+ #
152
+ # Custom dump which preserves DatePrecision
153
+ #
154
+ # Author:: Jodi Showers
155
+ #
156
+ def marshal_dump
157
+ [date_precision, ajd, start, offset]
158
+ end
145
159
 
146
- #
147
- # Custom dump which preserves DatePrecision
148
- #
149
- # Author:: Jodi Showers
150
- #
151
- def marshal_dump
152
- [date_precision, ajd, start, offset]
153
- end
160
+ #FIXME: marshall broken in 1.9
161
+ #
162
+ # Custom load which preserves DatePrecision
163
+ #
164
+ # Author:: Jodi Showers
165
+ #
166
+ def marshal_load(dumped_obj)
167
+ @date_precision, @ajd, @sg, @of=dumped_obj
168
+ end
154
169
 
155
- #
156
- # Custom load which preserves DatePrecision
157
- #
158
- # Author:: Jodi Showers
159
- #
160
- def marshal_load(dumped_obj)
161
- @date_precision, @ajd, @sg, @of=dumped_obj
162
170
  end
163
-
164
- end
165
171
  end
@@ -30,6 +30,10 @@ module Runt
30
30
  end
31
31
  result
32
32
  end
33
+
34
+ def scheduled_dates(date_range)
35
+ @elems.values.collect{|expr| expr.dates(date_range)}.flatten.sort.uniq
36
+ end
33
37
 
34
38
  # Return true or false depend on if the supplied event is scheduled to occur on the
35
39
  # given date.
@@ -66,7 +70,21 @@ module Runt
66
70
  block.call(@elems[event])
67
71
  end
68
72
 
73
+ def date_to_event_hash(event_attribute=:id)
74
+ start_date = end_date = nil
75
+ @elems.keys.each do |event|
76
+ start_date = event.start_date if start_date.nil? || start_date > event.start_date
77
+ end_date = event.end_date if end_date.nil? || end_date < event.end_date
78
+ end
79
+
80
+ scheduled_dates(DateRange.new(start_date, end_date)).inject({}) do |h, date|
81
+ h[date] = events(date).collect{|e| e.send(event_attribute)}
82
+ h
83
+ end
84
+ end
69
85
  end
86
+
87
+ # TODO: Extend event to take other attributes
70
88
 
71
89
  class Event
72
90
 
@@ -108,8 +108,35 @@
108
108
  #
109
109
  # DIMonth.new(First, Saturday)
110
110
  # DIMonth.new(Last, Tuesday)
111
- #
112
-
111
+ #
112
+ # === AfterTE
113
+ #
114
+ # self.after(date [, inclusive])
115
+ #
116
+ # Example:
117
+ #
118
+ # self.after(date)
119
+ # self.after(date, true)
120
+ #
121
+ # is equivilant to
122
+ #
123
+ # AfterTE.new(date)
124
+ # AfterTE.new(date, true)
125
+ #
126
+ # === BeforeTE
127
+ #
128
+ # self.before(date [, inclusive])
129
+ #
130
+ # Example:
131
+ #
132
+ # self.before(date)
133
+ # self.before(date, true)
134
+ #
135
+ # is equivilant to
136
+ #
137
+ # BeforeTE.new(date)
138
+ # BeforeTE.new(date, true)
139
+ #
113
140
  require 'runt'
114
141
 
115
142
  module Runt
@@ -125,12 +152,7 @@ module Runt
125
152
  end
126
153
 
127
154
  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)
155
+ #puts "method_missing(#{name},#{args},#{block}) => #{result}"
134
156
  case name.to_s
135
157
  when /^daily_(\d{1,2})_(\d{2})([ap]m)_to_(\d{1,2})_(\d{2})([ap]m)$/
136
158
  # REDay
@@ -160,10 +182,20 @@ module Runt
160
182
  return DIMonth.new(Runt.const(ordinal), Runt.const(day))
161
183
  else
162
184
  # You're hosed
163
- nil
185
+ super
164
186
  end
165
187
  end
166
188
 
189
+ # Shortcut for AfterTE(date, ...).new
190
+ def after(date, inclusive=false)
191
+ AfterTE.new(date, inclusive)
192
+ end
193
+
194
+ # Shortcut for BeforeTE(date, ...).new
195
+ def before(date, inclusive=false)
196
+ BeforeTE.new(date, inclusive)
197
+ end
198
+
167
199
  def parse_time(hour, minute, ampm)
168
200
  hour = hour.to_i + 12 if ampm =~ /pm/
169
201
  [hour.to_i, minute.to_i]
@@ -76,7 +76,7 @@ module TExpr
76
76
  end
77
77
  result
78
78
  end
79
-
79
+
80
80
  end
81
81
 
82
82
  # Base class for TExpr classes that can be composed of other
@@ -87,15 +87,29 @@ class Collection
87
87
 
88
88
  attr_reader :expressions
89
89
 
90
- def initialize
91
- @expressions = Array.new
90
+ def initialize(expressions = [])
91
+ @expressions = expressions
92
+ end
93
+
94
+ def ==(other)
95
+ if other.is_a?(Collection)
96
+ o_exprs = other.expressions.dup
97
+ expressions.each do |e|
98
+ return false unless i = o_exprs.index(e)
99
+ o_exprs.delete_at(i)
100
+ end
101
+ o_exprs.each {|e| return false unless i == expressions.index(e)}
102
+ return true
103
+ else
104
+ super(other)
105
+ end
92
106
  end
93
107
 
94
108
  def add(anExpression)
95
109
  @expressions.push anExpression
96
110
  self
97
111
  end
98
-
112
+
99
113
  # Will return true if the supplied object overlaps with the range used to
100
114
  # create this instance
101
115
  def overlap?(date_expr)
@@ -110,11 +124,11 @@ class Collection
110
124
  first_expr, next_exprs = yield
111
125
  result = ''
112
126
  @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
127
+ if @expressions.first===expr
128
+ result = first_expr + expr.to_s
129
+ else
130
+ result = result + next_exprs + expr.to_s
131
+ end
118
132
  end
119
133
  result
120
134
  else
@@ -142,7 +156,7 @@ class Union < Collection
142
156
  end
143
157
  false
144
158
  end
145
-
159
+
146
160
  def to_s
147
161
  super {['every ',' or ']}
148
162
  end
@@ -159,7 +173,7 @@ class Intersect < Collection
159
173
  end
160
174
  result
161
175
  end
162
-
176
+
163
177
  def to_s
164
178
  super {['every ', ' and ']}
165
179
  end
@@ -178,6 +192,10 @@ class Diff
178
192
  @expr2 = expr2
179
193
  end
180
194
 
195
+ def ==(o)
196
+ o.is_a?(Diff) ? expr1 == o.expr1 && expr2 == o.expr2 : super(o)
197
+ end
198
+
181
199
  def include?(aDate)
182
200
  return false unless (@expr1.include?(aDate) && !@expr2.include?(aDate))
183
201
  true
@@ -189,7 +207,7 @@ class Diff
189
207
  end
190
208
 
191
209
  # TExpr that provides for inclusion of an arbitrary date.
192
- class Spec
210
+ class TemporalDate
193
211
 
194
212
  include TExpr
195
213
 
@@ -199,6 +217,10 @@ class Spec
199
217
  @date_expr = date_expr
200
218
  end
201
219
 
220
+ def ==(o)
221
+ o.is_a?(TemporalDate) ? date_expr == o.date_expr : super(o)
222
+ end
223
+
202
224
  # Will return true if the supplied object is == to that which was used to
203
225
  # create this instance
204
226
  def include?(date_expr)
@@ -217,7 +239,7 @@ end
217
239
  # facilitating inclusion of an arbitrary range in a temporal expression.
218
240
  #
219
241
  # See also: Range
220
- class RSpec < Spec
242
+ class TemporalRange < TemporalDate
221
243
 
222
244
  ## Will return true if the supplied object is included in the range used to
223
245
  ## create this instance
@@ -225,6 +247,10 @@ class RSpec < Spec
225
247
  return @date_expr.include?(date_expr)
226
248
  end
227
249
 
250
+ def ==(o)
251
+ o.is_a?(TemporalRange) ? date_expr == o.date_expr : super(o)
252
+ end
253
+
228
254
  # Will return true if the supplied object overlaps with the range used to
229
255
  # create this instance
230
256
  def overlap?(date_expr)
@@ -315,11 +341,17 @@ class DIMonth
315
341
  include TExpr
316
342
  include TExprUtils
317
343
 
344
+ attr_reader :day_index, :week_of_month_index
345
+
318
346
  def initialize(week_of_month_index,day_index)
319
347
  @day_index = day_index
320
348
  @week_of_month_index = week_of_month_index
321
349
  end
322
350
 
351
+ def ==(o)
352
+ o.is_a?(DIMonth) ? day_index == o.day_index && week_of_month_index == o.week_of_month_index : super(o)
353
+ end
354
+
323
355
  def include?(date)
324
356
  ( day_matches?(date) ) && ( week_matches?(@week_of_month_index,date) )
325
357
  end
@@ -354,6 +386,8 @@ class DIWeek
354
386
  include TExpr
355
387
 
356
388
  VALID_RANGE = 0..6
389
+
390
+ attr_reader :ordinal_weekday
357
391
 
358
392
  def initialize(ordinal_weekday)
359
393
  unless VALID_RANGE.include?(ordinal_weekday)
@@ -361,6 +395,10 @@ class DIWeek
361
395
  end
362
396
  @ordinal_weekday = ordinal_weekday
363
397
  end
398
+
399
+ def ==(o)
400
+ o.is_a?(DIWeek) ? ordinal_weekday == o.ordinal_weekday : super(o)
401
+ end
364
402
 
365
403
  def include?(date)
366
404
  @ordinal_weekday == date.wday
@@ -384,6 +422,8 @@ class REWeek
384
422
 
385
423
  VALID_RANGE = 0..6
386
424
 
425
+ attr_reader :start_day, :end_day
426
+
387
427
  # Creates a REWeek using the supplied start
388
428
  # day(range = 0..6, where 0=>Sunday) and an optional end
389
429
  # day. If an end day is not supplied, the maximum value
@@ -393,6 +433,10 @@ class REWeek
393
433
  @start_day = start_day
394
434
  @end_day = end_day
395
435
  end
436
+
437
+ def ==(o)
438
+ o.is_a?(REWeek) ? start_day == o.start_day && end_day == o.end_day : super(o)
439
+ end
396
440
 
397
441
  def include?(date)
398
442
  return true if all_week?
@@ -494,24 +538,28 @@ class REYear
494
538
  else
495
539
  case args.size
496
540
  when 1
497
- @end_month = args[0]
498
- @start_day = NO_DAY
499
- @end_day = NO_DAY
541
+ @end_month = args[0]
542
+ @start_day = NO_DAY
543
+ @end_day = NO_DAY
500
544
  when 2
501
- @start_day = args[0]
502
- @end_month = args[1]
503
- @end_day = NO_DAY
545
+ @start_day = args[0]
546
+ @end_month = args[1]
547
+ @end_day = NO_DAY
504
548
  when 3
505
- @start_day = args[0]
506
- @end_month = args[1]
507
- @end_day = args[2]
549
+ @start_day = args[0]
550
+ @end_month = args[1]
551
+ @end_day = args[2]
508
552
  else
509
- raise "Invalid number of var args: 1 or 3 expected, #{args.size} given"
553
+ raise "Invalid number of var args: 1 or 3 expected, #{args.size} given"
510
554
  end
511
555
  end
512
556
  @same_month_dates_provided = (@start_month == @end_month) && (@start_day!=NO_DAY && @end_day != NO_DAY)
513
557
  end
514
558
 
559
+ def ==(o)
560
+ o.is_a?(REYear) ? start_day == o.start_day && end_day == o.end_day && start_month == o.start_month && end_month == o.end_month : super(o)
561
+ end
562
+
515
563
  def include?(date)
516
564
 
517
565
  return same_start_month_include_day?(date) \
@@ -555,9 +603,13 @@ end
555
603
  # NOTE: By default, this class will match any date expression whose
556
604
  # precision is less than or equal to DPrecision::DAY. To override
557
605
  # this behavior, pass the optional fifth constructor argument the
558
- # value: false.
606
+ # value: false.
607
+ #
608
+ # When the less_precise_match argument is true, the
609
+ # date-like object passed to :include? will be "promoted" to
610
+ # DPrecision::MINUTE if it has a precision of DPrecision::DAY or
611
+ # less.
559
612
  #
560
- # See also: Date
561
613
  class REDay
562
614
 
563
615
  include TExpr
@@ -565,6 +617,8 @@ class REDay
565
617
  CURRENT=28
566
618
  NEXT=29
567
619
  ANY_DATE=PDate.day(2002,8,CURRENT)
620
+
621
+ attr_reader :range, :spans_midnight
568
622
 
569
623
  def initialize(start_hour, start_minute, end_hour, end_minute, less_precise_match=true)
570
624
 
@@ -580,19 +634,26 @@ class REDay
580
634
  @range = start_time..end_time
581
635
  @less_precise_match = less_precise_match
582
636
  end
583
-
637
+
638
+ def ==(o)
639
+ o.is_a?(REDay) ? spans_midnight == o.spans_midnight && range == o.range : super(o)
640
+ end
641
+
584
642
  def include?(date)
585
643
  #
586
644
  # If @less_precise_match == true and the precision of the argument
587
645
  # 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
646
+ return true if @less_precise_match && less_precise?(date)
647
+
648
+ date_to_use = ensure_precision(date)
649
+
650
+ if(@spans_midnight&&date_to_use.hour<12) then
590
651
  #Assume next day
591
- return @range.include?(get_next(date.hour,date.min))
652
+ return @range.include?(get_next(date_to_use.hour,date_to_use.min))
592
653
  end
593
654
 
594
655
  #Same day
595
- return @range.include?(get_current(date.hour,date.min))
656
+ return @range.include?(get_current(date_to_use.hour,date_to_use.min))
596
657
  end
597
658
 
598
659
  def to_s
@@ -600,6 +661,16 @@ class REDay
600
661
  end
601
662
 
602
663
  private
664
+
665
+ def less_precise?(date)
666
+ date.date_precision <= DPrecision::DAY
667
+ end
668
+
669
+ def ensure_precision(date)
670
+ return date unless less_precise?(date)
671
+ DPrecision.to_p(date,DPrecision::MIN)
672
+ end
673
+
603
674
  def spans_midnight?(start_hour, end_hour)
604
675
  return end_hour < start_hour
605
676
  end
@@ -627,12 +698,18 @@ class WIMonth
627
698
 
628
699
  VALID_RANGE = -2..5
629
700
 
701
+ attr_reader :ordinal
702
+
630
703
  def initialize(ordinal)
631
704
  unless VALID_RANGE.include?(ordinal)
632
705
  raise ArgumentError, 'invalid ordinal week of month'
633
706
  end
634
707
  @ordinal = ordinal
635
708
  end
709
+
710
+ def ==(o)
711
+ o.is_a?(WIMonth) ? ordinal == o.ordinal : super(o)
712
+ end
636
713
 
637
714
  def include?(date)
638
715
  week_matches?(@ordinal,date)
@@ -656,11 +733,17 @@ class REMonth
656
733
 
657
734
  include TExpr
658
735
 
736
+ attr_reader :range
737
+
659
738
  def initialize(start_day, end_day=0)
660
739
  end_day=start_day if end_day==0
661
740
  @range = start_day..end_day
662
741
  end
663
742
 
743
+ def ==(o)
744
+ o.is_a?(REMonth) ? range == o.range : super(o)
745
+ end
746
+
664
747
  def include?(date)
665
748
  @range.include? date.mday
666
749
  end
@@ -678,6 +761,8 @@ end
678
761
  class EveryTE
679
762
 
680
763
  include TExpr
764
+
765
+ attr_reader :start, :interval, :precision
681
766
 
682
767
  def initialize(start,n,precision=nil)
683
768
  @start=start
@@ -686,6 +771,10 @@ class EveryTE
686
771
  @precision=precision || @start.date_precision
687
772
  end
688
773
 
774
+ def ==(o)
775
+ o.is_a?(EveryTE) ? start == o.start && precision == o.precision && interval == o.interval : super(o)
776
+ end
777
+
689
778
  def include?(date)
690
779
  i=DPrecision.to_p(@start,@precision)
691
780
  d=DPrecision.to_p(date,@precision)
@@ -710,10 +799,16 @@ class DayIntervalTE
710
799
 
711
800
  include TExpr
712
801
 
802
+ attr_reader :interval, :base_date
803
+
713
804
  def initialize(base_date,n)
714
805
  @base_date = DPrecision.to_p(base_date,DPrecision::DAY)
715
806
  @interval = n
716
807
  end
808
+
809
+ def ==(o)
810
+ o.is_a?(DayIntervalTE) ? base_date == o.base_date && interval == o.interval : super(o)
811
+ end
717
812
 
718
813
  def include?(date)
719
814
  return ((DPrecision.to_p(date,DPrecision::DAY) - @base_date).to_i % @interval == 0)
@@ -725,6 +820,107 @@ class DayIntervalTE
725
820
 
726
821
  end
727
822
 
823
+ #
824
+ # This class creates an expression which matches dates occuring during the weeks
825
+ # alternating at the given interval begining on the week containing the date
826
+ # used to create the instance.
827
+ #
828
+ # WeekInterval.new(starting_date, interval)
829
+ #
830
+ # Weeks are defined as Sunday to Saturday, as opposed to the commercial week
831
+ # which starts on a Monday. For example,
832
+ #
833
+ # every_other_week = WeekInterval.new(Date.new(2013,04,24), 2)
834
+ #
835
+ # will match any date that occurs during every other week begining with the
836
+ # week of 2013-04-21 (2013-04-24 is a Wednesday and 2013-04-21 is the Sunday
837
+ # that begins the containing week).
838
+ #
839
+ # # Sunday of starting week
840
+ # every_other_week.include?(Date.new(2013,04,21)) #==> true
841
+ # # Saturday of starting week
842
+ # every_other_week.include?(Date.new(2013,04,27)) #==> true
843
+ # # First week _after_ start week
844
+ # every_other_week.include?(Date.new(2013,05,01)) #==> false
845
+ # # Second week _after_ start week
846
+ # every_other_week.include?(Date.new(2013,05,06)) #==> true
847
+ #
848
+ # NOTE: The idea and tests for this class were originally contributed as the
849
+ # REWeekWithIntervalTE class by Jeff Whitmire. The behavior of the original class
850
+ # provided both the matching of every n weeks and the specification of specific
851
+ # days of that week in a single class. This class only provides the matching
852
+ # of every n weeks. The exact functionality of the original class is easy to create
853
+ # using the Runt set operators and the DIWeek class:
854
+ #
855
+ # # Old way
856
+ # tu_thurs_every_third_week = REWeekWithIntervalTE.new(Date.new(2013,04,24),2,[2,4])
857
+ #
858
+ # # New way
859
+ # tu_thurs_every_third_week =
860
+ # WeekInterval.new(Date.new(2013,04,24),2) & (DIWeek.new(Tuesday) | DIWeek.new(Thursday))
861
+ #
862
+ # Notice that the compound expression (in parens after the "&") can be replaced
863
+ # or combined with any other appropriate temporal expression to provide different
864
+ # functionality (REWeek to provide a range of days, REDay to provide certain times, etc...).
865
+ #
866
+ # Contributed by Jeff Whitmire
867
+ class WeekInterval
868
+ include TExpr
869
+ def initialize(start_date,interval=2)
870
+ @start_date = DPrecision.to_p(start_date,DPrecision::DAY)
871
+ # convert base_date to the start of the week
872
+ @base_date = @start_date - @start_date.wday
873
+ @interval = interval
874
+ end
875
+
876
+ def include?(date)
877
+ return false if @base_date > date
878
+ ((adjust_for_year(date) - week_num(@base_date)) % @interval) == 0
879
+ end
880
+
881
+ def to_s
882
+ "every #{Runt.ordinalize(@interval)} week starting with the week containing #{Runt.format_date(@start_date)}"
883
+ end
884
+
885
+ private
886
+ def week_num(date)
887
+ # %U - Week number of the year. The week starts with Sunday. (00..53)
888
+ date.strftime("%U").to_i
889
+ end
890
+ def max_week_num(year)
891
+ d = Date.new(year,12,31)
892
+ max = week_num(d)
893
+ while max < 52
894
+ d = d - 1
895
+ max = week_num(d)
896
+ end
897
+ max
898
+ end
899
+ def adjust_for_year(date)
900
+ # Exclusive range: if date.year == @base_date.year, this will be empty
901
+ range_of_years = @base_date.year...date.year
902
+ in_same_year = range_of_years.to_a.empty?
903
+ # Week number of the given date argument
904
+ week_number = week_num(date)
905
+ # Default (most common case) date argument is in same year as @base_date
906
+ # and the week number is also part of the same year. This starting value
907
+ # is also necessary for the case where they're not in the same year.
908
+ adjustment = week_number
909
+ if in_same_year && (week_number < week_num(@base_date)) then
910
+ # The given date occurs within the same year
911
+ # but is actually week number 1 of the next year
912
+ adjustment = adjustment + max_week_num(date.year)
913
+ elsif !in_same_year then
914
+ # Date occurs in different year
915
+ range_of_years.each do |year|
916
+ # Max week number taking into account we are not using commercial week
917
+ adjustment = adjustment + max_week_num(year)
918
+ end
919
+ end
920
+ adjustment
921
+ end
922
+ end
923
+
728
924
  # Simple expression which returns true if the supplied arguments
729
925
  # occur within the given year.
730
926
  #
@@ -732,10 +928,16 @@ class YearTE
732
928
 
733
929
  include TExpr
734
930
 
931
+ attr_reader :year
932
+
735
933
  def initialize(year)
736
934
  @year = year
737
935
  end
738
936
 
937
+ def ==(o)
938
+ o.is_a?(YearTE) ? year == o.year : super(o)
939
+ end
940
+
739
941
  def include?(date)
740
942
  return date.year == @year
741
943
  end
@@ -750,13 +952,20 @@ end
750
952
  class BeforeTE
751
953
 
752
954
  include TExpr
955
+
956
+ attr_reader :date, :inclusive
753
957
 
754
958
  def initialize(date, inclusive=false)
755
959
  @date = date
756
960
  @inclusive = inclusive
757
961
  end
962
+
963
+ def ==(o)
964
+ o.is_a?(BeforeTE) ? date == o.date && inclusive == o.inclusive : super(o)
965
+ end
758
966
 
759
967
  def include?(date)
968
+ return false unless date
760
969
  return (date < @date) || (@inclusive && @date == date)
761
970
  end
762
971
 
@@ -771,11 +980,18 @@ class AfterTE
771
980
 
772
981
  include TExpr
773
982
 
983
+ attr_reader :date, :inclusive
984
+
774
985
  def initialize(date, inclusive=false)
775
986
  @date = date
776
987
  @inclusive = inclusive
777
988
  end
778
989
 
990
+ def ==(o)
991
+ o.is_a?(AfterTE) ? date == o.date && inclusive == o.inclusive : super(o)
992
+ end
993
+
994
+
779
995
  def include?(date)
780
996
  return (date > @date) || (@inclusive && @date == date)
781
997
  end