runt 0.7.0 → 0.9.0

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