runt 0.7.0 → 0.9.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +19 -0
- data/.travis.yml +5 -0
- data/{CHANGES → CHANGES.txt} +24 -8
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -44
- data/README.md +79 -0
- data/Rakefile +6 -119
- data/doc/tutorial_schedule.md +365 -0
- data/doc/tutorial_sugar.md +170 -0
- data/doc/tutorial_te.md +155 -0
- data/lib/runt.rb +36 -21
- data/lib/runt/dprecision.rb +4 -2
- data/lib/runt/pdate.rb +101 -95
- data/lib/runt/schedule.rb +18 -0
- data/lib/runt/sugar.rb +41 -9
- data/lib/runt/temporalexpression.rb +246 -30
- data/lib/runt/version.rb +3 -0
- data/runt.gemspec +24 -0
- data/site/.cvsignore +1 -0
- data/site/dcl-small.gif +0 -0
- data/site/index-rubforge-www.html +72 -0
- data/site/index.html +75 -60
- data/site/runt-logo.gif +0 -0
- data/site/runt-logo.psd +0 -0
- data/test/baseexpressiontest.rb +10 -8
- data/test/combinedexpressionstest.rb +166 -158
- data/test/daterangetest.rb +4 -6
- data/test/diweektest.rb +32 -32
- data/test/dprecisiontest.rb +2 -4
- data/test/everytetest.rb +6 -0
- data/test/expressionbuildertest.rb +2 -3
- data/test/icalendartest.rb +3 -6
- data/test/minitest_helper.rb +7 -0
- data/test/pdatetest.rb +21 -6
- data/test/redaytest.rb +3 -0
- data/test/reyeartest.rb +1 -1
- data/test/runttest.rb +5 -8
- data/test/scheduletest.rb +13 -14
- data/test/sugartest.rb +28 -6
- data/test/{spectest.rb → temporaldatetest.rb} +14 -4
- data/test/{rspectest.rb → temporalrangetest.rb} +4 -4
- data/test/test_runt.rb +11 -0
- data/test/weekintervaltest.rb +106 -0
- metadata +161 -116
- data/README +0 -106
- data/doc/tutorial_schedule.rdoc +0 -393
- data/doc/tutorial_sugar.rdoc +0 -143
- data/doc/tutorial_te.rdoc +0 -190
- data/setup.rb +0 -1331
data/lib/runt/pdate.rb
CHANGED
@@ -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
|
-
|
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
|
-
|
34
|
-
|
35
|
-
|
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
|
-
|
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
|
-
|
58
|
+
return DPrecision::to_p(PDate::civil(year+n,month,day),@date_precision)
|
50
59
|
when MONTH then
|
51
|
-
|
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
|
-
|
62
|
+
ndays = n*7
|
55
63
|
when DAY then
|
56
|
-
|
64
|
+
ndays = n
|
57
65
|
when HOUR then
|
58
|
-
|
66
|
+
ndays = n*(1.to_r/24)
|
59
67
|
when MIN then
|
60
|
-
|
68
|
+
ndays = n*(1.to_r/1440)
|
61
69
|
when SEC then
|
62
|
-
|
70
|
+
ndays = n*(1.to_r/86400)
|
63
71
|
when MILLI then
|
64
|
-
|
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
|
-
|
69
|
-
|
77
|
+
def - (x)
|
78
|
+
case x
|
70
79
|
when Numeric then
|
71
|
-
|
72
|
-
|
73
|
-
|
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
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
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
|
-
|
90
|
-
|
91
|
-
|
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
|
-
|
97
|
-
|
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
|
-
|
104
|
-
|
105
|
-
|
111
|
+
def PDate.month( yr,mon,*ignored )
|
112
|
+
PDate.civil(MONTH, yr, mon, DAY.min_value )
|
113
|
+
end
|
106
114
|
|
107
|
-
|
108
|
-
|
109
|
-
|
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
|
-
|
112
|
-
|
113
|
-
|
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
|
-
|
122
|
-
|
123
|
-
|
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
|
-
|
126
|
-
|
127
|
-
|
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
|
-
|
130
|
-
|
131
|
-
|
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
|
-
|
134
|
-
|
135
|
-
|
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
|
-
|
138
|
-
|
139
|
-
|
140
|
-
end
|
146
|
+
def PDate.default(*args)
|
147
|
+
PDate.civil(DEFAULT, *args)
|
148
|
+
end
|
141
149
|
|
142
|
-
|
143
|
-
|
144
|
-
|
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
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
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
|
data/lib/runt/schedule.rb
CHANGED
@@ -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
|
|
data/lib/runt/sugar.rb
CHANGED
@@ -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
|
-
|
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
|
-
|
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 =
|
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
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
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
|
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
|
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
|
-
|
498
|
-
|
499
|
-
|
541
|
+
@end_month = args[0]
|
542
|
+
@start_day = NO_DAY
|
543
|
+
@end_day = NO_DAY
|
500
544
|
when 2
|
501
|
-
|
502
|
-
|
503
|
-
|
545
|
+
@start_day = args[0]
|
546
|
+
@end_month = args[1]
|
547
|
+
@end_day = NO_DAY
|
504
548
|
when 3
|
505
|
-
|
506
|
-
|
507
|
-
|
549
|
+
@start_day = args[0]
|
550
|
+
@end_month = args[1]
|
551
|
+
@end_day = args[2]
|
508
552
|
else
|
509
|
-
|
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
|
589
|
-
|
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(
|
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(
|
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
|