runt 0.6.0 → 0.7.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.
- data/CHANGES +153 -125
- data/LICENSE.txt +43 -43
- data/README +106 -100
- data/Rakefile +122 -122
- data/TODO +13 -13
- data/doc/tutorial_schedule.rdoc +393 -393
- data/doc/tutorial_sugar.rdoc +143 -0
- data/doc/tutorial_te.rdoc +190 -190
- data/examples/payment_report.rb +59 -0
- data/examples/payment_reporttest.rb +49 -0
- data/examples/reminder.rb +63 -63
- data/lib/runt.rb +237 -219
- data/lib/runt/daterange.rb +74 -74
- data/lib/runt/dprecision.rb +150 -141
- data/lib/runt/expressionbuilder.rb +65 -0
- data/lib/runt/pdate.rb +165 -153
- data/lib/runt/schedule.rb +88 -88
- data/lib/runt/sugar.rb +171 -0
- data/lib/runt/temporalexpression.rb +789 -777
- data/setup.rb +1331 -1331
- data/site/blue-robot3.css +131 -131
- data/site/dcl-small.gif +0 -0
- data/site/index.html +72 -94
- data/site/runt-logo.gif +0 -0
- data/site/runt-logo.psd +0 -0
- data/test/aftertetest.rb +31 -0
- data/test/beforetetest.rb +31 -0
- data/test/daterangetest.rb +89 -89
- data/test/dprecisiontest.rb +58 -55
- data/test/expressionbuildertest.rb +64 -0
- data/test/icalendartest.rb +621 -41
- data/test/pdatetest.rb +147 -117
- data/test/redaytest.rb +10 -0
- data/test/reyeartest.rb +99 -98
- data/test/runttest.rb +98 -101
- data/test/scheduletest.rb +148 -148
- data/test/sugartest.rb +104 -0
- data/test/temporalexpressiontest.rb +76 -76
- metadata +112 -95
data/lib/runt/sugar.rb
ADDED
@@ -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
|
@@ -1,777 +1,789 @@
|
|
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
|
-
|
253
|
-
|
254
|
-
|
255
|
-
|
256
|
-
|
257
|
-
|
258
|
-
|
259
|
-
|
260
|
-
|
261
|
-
|
262
|
-
|
263
|
-
|
264
|
-
|
265
|
-
|
266
|
-
|
267
|
-
|
268
|
-
|
269
|
-
|
270
|
-
|
271
|
-
|
272
|
-
|
273
|
-
|
274
|
-
|
275
|
-
|
276
|
-
|
277
|
-
|
278
|
-
|
279
|
-
|
280
|
-
|
281
|
-
|
282
|
-
|
283
|
-
|
284
|
-
|
285
|
-
|
286
|
-
|
287
|
-
|
288
|
-
#
|
289
|
-
#
|
290
|
-
#
|
291
|
-
#
|
292
|
-
#
|
293
|
-
# of the month
|
294
|
-
#
|
295
|
-
#
|
296
|
-
#
|
297
|
-
#
|
298
|
-
# the
|
299
|
-
#
|
300
|
-
#
|
301
|
-
#
|
302
|
-
#
|
303
|
-
#
|
304
|
-
#
|
305
|
-
#
|
306
|
-
#
|
307
|
-
|
308
|
-
|
309
|
-
|
310
|
-
|
311
|
-
|
312
|
-
|
313
|
-
|
314
|
-
|
315
|
-
|
316
|
-
|
317
|
-
|
318
|
-
|
319
|
-
|
320
|
-
|
321
|
-
|
322
|
-
|
323
|
-
|
324
|
-
|
325
|
-
|
326
|
-
|
327
|
-
|
328
|
-
|
329
|
-
|
330
|
-
|
331
|
-
|
332
|
-
|
333
|
-
|
334
|
-
|
335
|
-
|
336
|
-
|
337
|
-
|
338
|
-
#
|
339
|
-
#
|
340
|
-
#
|
341
|
-
#
|
342
|
-
#
|
343
|
-
#
|
344
|
-
#
|
345
|
-
#
|
346
|
-
|
347
|
-
|
348
|
-
|
349
|
-
|
350
|
-
|
351
|
-
|
352
|
-
|
353
|
-
|
354
|
-
|
355
|
-
|
356
|
-
|
357
|
-
|
358
|
-
|
359
|
-
|
360
|
-
|
361
|
-
|
362
|
-
|
363
|
-
|
364
|
-
|
365
|
-
|
366
|
-
|
367
|
-
end
|
368
|
-
|
369
|
-
|
370
|
-
#
|
371
|
-
|
372
|
-
|
373
|
-
|
374
|
-
|
375
|
-
|
376
|
-
|
377
|
-
|
378
|
-
|
379
|
-
|
380
|
-
|
381
|
-
|
382
|
-
|
383
|
-
|
384
|
-
|
385
|
-
|
386
|
-
|
387
|
-
|
388
|
-
|
389
|
-
end
|
390
|
-
|
391
|
-
def
|
392
|
-
|
393
|
-
|
394
|
-
|
395
|
-
|
396
|
-
|
397
|
-
|
398
|
-
|
399
|
-
|
400
|
-
|
401
|
-
|
402
|
-
|
403
|
-
|
404
|
-
|
405
|
-
|
406
|
-
|
407
|
-
|
408
|
-
|
409
|
-
end
|
410
|
-
|
411
|
-
|
412
|
-
|
413
|
-
|
414
|
-
|
415
|
-
end
|
416
|
-
|
417
|
-
|
418
|
-
|
419
|
-
|
420
|
-
|
421
|
-
|
422
|
-
|
423
|
-
|
424
|
-
|
425
|
-
|
426
|
-
|
427
|
-
|
428
|
-
|
429
|
-
|
430
|
-
|
431
|
-
|
432
|
-
|
433
|
-
|
434
|
-
|
435
|
-
|
436
|
-
|
437
|
-
|
438
|
-
|
439
|
-
#
|
440
|
-
#
|
441
|
-
#
|
442
|
-
#
|
443
|
-
#
|
444
|
-
#
|
445
|
-
#
|
446
|
-
#
|
447
|
-
#
|
448
|
-
#
|
449
|
-
#
|
450
|
-
#
|
451
|
-
#
|
452
|
-
# +
|
453
|
-
#
|
454
|
-
#
|
455
|
-
#
|
456
|
-
#
|
457
|
-
#
|
458
|
-
#
|
459
|
-
#
|
460
|
-
#
|
461
|
-
#
|
462
|
-
#
|
463
|
-
#
|
464
|
-
#
|
465
|
-
#
|
466
|
-
#
|
467
|
-
#
|
468
|
-
#
|
469
|
-
#
|
470
|
-
#
|
471
|
-
#
|
472
|
-
#
|
473
|
-
#
|
474
|
-
#
|
475
|
-
# # Creates the range March 12th through May
|
476
|
-
# expr = REYear.new(3,12,5)
|
477
|
-
#
|
478
|
-
# # Creates the range March 1st through
|
479
|
-
# expr = REYear.new(3)
|
480
|
-
#
|
481
|
-
|
482
|
-
|
483
|
-
|
484
|
-
|
485
|
-
|
486
|
-
|
487
|
-
|
488
|
-
|
489
|
-
|
490
|
-
|
491
|
-
|
492
|
-
|
493
|
-
|
494
|
-
|
495
|
-
|
496
|
-
|
497
|
-
@
|
498
|
-
|
499
|
-
@
|
500
|
-
|
501
|
-
@
|
502
|
-
|
503
|
-
|
504
|
-
|
505
|
-
|
506
|
-
|
507
|
-
|
508
|
-
|
509
|
-
|
510
|
-
|
511
|
-
|
512
|
-
|
513
|
-
|
514
|
-
|
515
|
-
|
516
|
-
|
517
|
-
|
518
|
-
|
519
|
-
|
520
|
-
|
521
|
-
|
522
|
-
|
523
|
-
end
|
524
|
-
|
525
|
-
|
526
|
-
|
527
|
-
|
528
|
-
|
529
|
-
|
530
|
-
|
531
|
-
|
532
|
-
|
533
|
-
|
534
|
-
|
535
|
-
def
|
536
|
-
|
537
|
-
|
538
|
-
|
539
|
-
|
540
|
-
|
541
|
-
|
542
|
-
|
543
|
-
|
544
|
-
|
545
|
-
|
546
|
-
|
547
|
-
|
548
|
-
|
549
|
-
|
550
|
-
|
551
|
-
|
552
|
-
|
553
|
-
|
554
|
-
|
555
|
-
|
556
|
-
|
557
|
-
|
558
|
-
|
559
|
-
|
560
|
-
|
561
|
-
|
562
|
-
|
563
|
-
|
564
|
-
|
565
|
-
|
566
|
-
|
567
|
-
|
568
|
-
|
569
|
-
def
|
570
|
-
|
571
|
-
|
572
|
-
|
573
|
-
|
574
|
-
|
575
|
-
|
576
|
-
|
577
|
-
|
578
|
-
|
579
|
-
|
580
|
-
|
581
|
-
|
582
|
-
|
583
|
-
|
584
|
-
|
585
|
-
|
586
|
-
|
587
|
-
|
588
|
-
|
589
|
-
|
590
|
-
|
591
|
-
|
592
|
-
|
593
|
-
|
594
|
-
|
595
|
-
|
596
|
-
|
597
|
-
|
598
|
-
|
599
|
-
|
600
|
-
|
601
|
-
|
602
|
-
|
603
|
-
|
604
|
-
|
605
|
-
|
606
|
-
|
607
|
-
|
608
|
-
|
609
|
-
|
610
|
-
|
611
|
-
|
612
|
-
|
613
|
-
|
614
|
-
|
615
|
-
|
616
|
-
|
617
|
-
|
618
|
-
|
619
|
-
|
620
|
-
|
621
|
-
|
622
|
-
|
623
|
-
|
624
|
-
|
625
|
-
|
626
|
-
|
627
|
-
|
628
|
-
|
629
|
-
|
630
|
-
def
|
631
|
-
|
632
|
-
|
633
|
-
|
634
|
-
|
635
|
-
|
636
|
-
|
637
|
-
|
638
|
-
|
639
|
-
|
640
|
-
|
641
|
-
|
642
|
-
#
|
643
|
-
|
644
|
-
|
645
|
-
|
646
|
-
|
647
|
-
|
648
|
-
|
649
|
-
|
650
|
-
|
651
|
-
|
652
|
-
|
653
|
-
|
654
|
-
|
655
|
-
|
656
|
-
|
657
|
-
|
658
|
-
|
659
|
-
|
660
|
-
|
661
|
-
|
662
|
-
|
663
|
-
|
664
|
-
|
665
|
-
|
666
|
-
|
667
|
-
|
668
|
-
|
669
|
-
|
670
|
-
|
671
|
-
|
672
|
-
|
673
|
-
|
674
|
-
|
675
|
-
|
676
|
-
|
677
|
-
|
678
|
-
|
679
|
-
|
680
|
-
|
681
|
-
|
682
|
-
|
683
|
-
|
684
|
-
|
685
|
-
|
686
|
-
|
687
|
-
|
688
|
-
|
689
|
-
|
690
|
-
|
691
|
-
|
692
|
-
|
693
|
-
|
694
|
-
|
695
|
-
|
696
|
-
|
697
|
-
|
698
|
-
|
699
|
-
|
700
|
-
|
701
|
-
|
702
|
-
|
703
|
-
|
704
|
-
|
705
|
-
|
706
|
-
|
707
|
-
|
708
|
-
|
709
|
-
|
710
|
-
|
711
|
-
|
712
|
-
|
713
|
-
|
714
|
-
|
715
|
-
|
716
|
-
|
717
|
-
|
718
|
-
|
719
|
-
|
720
|
-
|
721
|
-
|
722
|
-
|
723
|
-
|
724
|
-
|
725
|
-
|
726
|
-
|
727
|
-
|
728
|
-
|
729
|
-
|
730
|
-
|
731
|
-
|
732
|
-
|
733
|
-
|
734
|
-
|
735
|
-
|
736
|
-
|
737
|
-
|
738
|
-
|
739
|
-
|
740
|
-
|
741
|
-
|
742
|
-
|
743
|
-
|
744
|
-
|
745
|
-
end
|
746
|
-
|
747
|
-
|
748
|
-
|
749
|
-
|
750
|
-
|
751
|
-
|
752
|
-
|
753
|
-
|
754
|
-
|
755
|
-
|
756
|
-
|
757
|
-
|
758
|
-
|
759
|
-
|
760
|
-
|
761
|
-
|
762
|
-
|
763
|
-
|
764
|
-
@
|
765
|
-
end
|
766
|
-
|
767
|
-
|
768
|
-
|
769
|
-
|
770
|
-
|
771
|
-
|
772
|
-
|
773
|
-
|
774
|
-
|
775
|
-
|
776
|
-
|
777
|
-
end
|
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
|
+
return @range.include?(get_next(date.hour,date.min))
|
592
|
+
end
|
593
|
+
|
594
|
+
#Same day
|
595
|
+
return @range.include?(get_current(date.hour,date.min))
|
596
|
+
end
|
597
|
+
|
598
|
+
def to_s
|
599
|
+
"from #{Runt.format_time(@range.begin)} to #{Runt.format_time(@range.end)} daily"
|
600
|
+
end
|
601
|
+
|
602
|
+
private
|
603
|
+
def spans_midnight?(start_hour, end_hour)
|
604
|
+
return end_hour < start_hour
|
605
|
+
end
|
606
|
+
|
607
|
+
def get_current(hour,minute)
|
608
|
+
PDate.min(ANY_DATE.year,ANY_DATE.month,CURRENT,hour,minute)
|
609
|
+
end
|
610
|
+
|
611
|
+
def get_next(hour,minute)
|
612
|
+
PDate.min(ANY_DATE.year,ANY_DATE.month,NEXT,hour,minute)
|
613
|
+
end
|
614
|
+
|
615
|
+
end
|
616
|
+
|
617
|
+
# TExpr that matches the week in a month. For example:
|
618
|
+
#
|
619
|
+
# WIMonth.new(1)
|
620
|
+
#
|
621
|
+
# See also: Date
|
622
|
+
# FIXME .dates mixin seems functionally broken
|
623
|
+
class WIMonth
|
624
|
+
|
625
|
+
include TExpr
|
626
|
+
include TExprUtils
|
627
|
+
|
628
|
+
VALID_RANGE = -2..5
|
629
|
+
|
630
|
+
def initialize(ordinal)
|
631
|
+
unless VALID_RANGE.include?(ordinal)
|
632
|
+
raise ArgumentError, 'invalid ordinal week of month'
|
633
|
+
end
|
634
|
+
@ordinal = ordinal
|
635
|
+
end
|
636
|
+
|
637
|
+
def include?(date)
|
638
|
+
week_matches?(@ordinal,date)
|
639
|
+
end
|
640
|
+
|
641
|
+
def to_s
|
642
|
+
"#{Runt.ordinalize(@ordinal)} week of any month"
|
643
|
+
end
|
644
|
+
|
645
|
+
end
|
646
|
+
|
647
|
+
# TExpr that matches a range of dates within a month. For example:
|
648
|
+
#
|
649
|
+
# REMonth.(12,28)
|
650
|
+
#
|
651
|
+
# matches from the 12th thru the 28th of any month. If end_day==0
|
652
|
+
# or is not given, start_day will define the range with that single day.
|
653
|
+
#
|
654
|
+
# See also: Date
|
655
|
+
class REMonth
|
656
|
+
|
657
|
+
include TExpr
|
658
|
+
|
659
|
+
def initialize(start_day, end_day=0)
|
660
|
+
end_day=start_day if end_day==0
|
661
|
+
@range = start_day..end_day
|
662
|
+
end
|
663
|
+
|
664
|
+
def include?(date)
|
665
|
+
@range.include? date.mday
|
666
|
+
end
|
667
|
+
|
668
|
+
def to_s
|
669
|
+
"from the #{Runt.ordinalize(@range.begin)} to the #{Runt.ordinalize(@range.end)} monthly"
|
670
|
+
end
|
671
|
+
|
672
|
+
end
|
673
|
+
|
674
|
+
#
|
675
|
+
# Using the precision from the supplied start argument and the its date value,
|
676
|
+
# matches every n number of time units thereafter.
|
677
|
+
#
|
678
|
+
class EveryTE
|
679
|
+
|
680
|
+
include TExpr
|
681
|
+
|
682
|
+
def initialize(start,n,precision=nil)
|
683
|
+
@start=start
|
684
|
+
@interval=n
|
685
|
+
# Use the precision of the start date by default
|
686
|
+
@precision=precision || @start.date_precision
|
687
|
+
end
|
688
|
+
|
689
|
+
def include?(date)
|
690
|
+
i=DPrecision.to_p(@start,@precision)
|
691
|
+
d=DPrecision.to_p(date,@precision)
|
692
|
+
while i<=d
|
693
|
+
return true if i.eql?(d)
|
694
|
+
i=i+@interval
|
695
|
+
end
|
696
|
+
false
|
697
|
+
end
|
698
|
+
|
699
|
+
def to_s
|
700
|
+
"every #{@interval} #{@precision.label.downcase}s starting #{Runt.format_date(@start)}"
|
701
|
+
end
|
702
|
+
|
703
|
+
end
|
704
|
+
|
705
|
+
# Using day precision dates, matches every n number of days after a given
|
706
|
+
# base date. All date arguments are converted to DPrecision::DAY precision.
|
707
|
+
#
|
708
|
+
# Contributed by Ira Burton
|
709
|
+
class DayIntervalTE
|
710
|
+
|
711
|
+
include TExpr
|
712
|
+
|
713
|
+
def initialize(base_date,n)
|
714
|
+
@base_date = DPrecision.to_p(base_date,DPrecision::DAY)
|
715
|
+
@interval = n
|
716
|
+
end
|
717
|
+
|
718
|
+
def include?(date)
|
719
|
+
return ((DPrecision.to_p(date,DPrecision::DAY) - @base_date).to_i % @interval == 0)
|
720
|
+
end
|
721
|
+
|
722
|
+
def to_s
|
723
|
+
"every #{Runt.ordinalize(@interval)} day after #{Runt.format_date(@base_date)}"
|
724
|
+
end
|
725
|
+
|
726
|
+
end
|
727
|
+
|
728
|
+
# Simple expression which returns true if the supplied arguments
|
729
|
+
# occur within the given year.
|
730
|
+
#
|
731
|
+
class YearTE
|
732
|
+
|
733
|
+
include TExpr
|
734
|
+
|
735
|
+
def initialize(year)
|
736
|
+
@year = year
|
737
|
+
end
|
738
|
+
|
739
|
+
def include?(date)
|
740
|
+
return date.year == @year
|
741
|
+
end
|
742
|
+
|
743
|
+
def to_s
|
744
|
+
"during the year #{@year}"
|
745
|
+
end
|
746
|
+
|
747
|
+
end
|
748
|
+
|
749
|
+
# Matches dates that occur before a given date.
|
750
|
+
class BeforeTE
|
751
|
+
|
752
|
+
include TExpr
|
753
|
+
|
754
|
+
def initialize(date, inclusive=false)
|
755
|
+
@date = date
|
756
|
+
@inclusive = inclusive
|
757
|
+
end
|
758
|
+
|
759
|
+
def include?(date)
|
760
|
+
return (date < @date) || (@inclusive && @date == date)
|
761
|
+
end
|
762
|
+
|
763
|
+
def to_s
|
764
|
+
"before #{Runt.format_date(@date)}"
|
765
|
+
end
|
766
|
+
|
767
|
+
end
|
768
|
+
|
769
|
+
# Matches dates that occur after a given date.
|
770
|
+
class AfterTE
|
771
|
+
|
772
|
+
include TExpr
|
773
|
+
|
774
|
+
def initialize(date, inclusive=false)
|
775
|
+
@date = date
|
776
|
+
@inclusive = inclusive
|
777
|
+
end
|
778
|
+
|
779
|
+
def include?(date)
|
780
|
+
return (date > @date) || (@inclusive && @date == date)
|
781
|
+
end
|
782
|
+
|
783
|
+
def to_s
|
784
|
+
"after #{Runt.format_date(@date)}"
|
785
|
+
end
|
786
|
+
|
787
|
+
end
|
788
|
+
|
789
|
+
end
|