texp 0.0.3 → 0.0.7
Sign up to get free protection for your applications and to get access to all the features.
- data/ChangeLog +36 -2
- data/README +92 -0
- data/Rakefile +26 -1
- data/TAGS +369 -0
- data/lib/texp.rb +6 -1
- data/lib/texp/base.rb +139 -10
- data/lib/texp/builder.rb +254 -0
- data/lib/texp/day_interval.rb +19 -15
- data/lib/texp/day_of_month.rb +1 -7
- data/lib/texp/day_of_week.rb +1 -7
- data/lib/texp/dsl.rb +338 -0
- data/lib/texp/errors.rb +18 -0
- data/lib/texp/every_day.rb +1 -5
- data/lib/texp/logic.rb +16 -40
- data/lib/texp/month.rb +1 -6
- data/lib/texp/operators.rb +53 -0
- data/lib/texp/parse.rb +9 -27
- data/lib/texp/time_ext.rb +7 -0
- data/lib/texp/version.rb +3 -0
- data/lib/texp/week.rb +1 -7
- data/lib/texp/window.rb +30 -12
- data/lib/texp/year.rb +1 -6
- data/test/texp/base_test.rb +82 -0
- data/test/texp/day_interval_test.rb +24 -12
- data/test/texp/day_of_month_test.rb +10 -10
- data/test/texp/day_of_week_test.rb +14 -14
- data/test/texp/dsl_test.rb +288 -0
- data/test/texp/every_day_test.rb +1 -1
- data/test/texp/ext_test.rb +3 -3
- data/test/texp/logic_test.rb +18 -18
- data/test/texp/month_test.rb +3 -3
- data/test/texp/operators_test.rb +52 -0
- data/test/texp/parse_test.rb +39 -39
- data/test/texp/time_ext_test.rb +8 -0
- data/test/texp/week_test.rb +19 -19
- data/test/texp/window_test.rb +41 -12
- data/test/texp/year_test.rb +7 -7
- data/test/texp_tests.rb +27 -0
- metadata +14 -6
- data/lib/texp/core.rb +0 -41
- data/lib/texp/hash_builder.rb +0 -44
- data/test/texp/hash_test.rb +0 -26
- data/test/texp/logic_text_test.rb +0 -0
data/lib/texp/day_interval.rb
CHANGED
@@ -2,14 +2,25 @@ module TExp
|
|
2
2
|
class DayInterval < Base
|
3
3
|
register_parse_callback('i')
|
4
4
|
|
5
|
+
attr_reader :base_date
|
6
|
+
|
5
7
|
def initialize(base_date, interval)
|
6
|
-
@base_date = base_date
|
8
|
+
@base_date = base_date.kind_of?(Date) ? base_date : nil
|
7
9
|
@interval = interval
|
8
10
|
end
|
9
11
|
|
10
12
|
# Is +date+ included in the temporal expression.
|
11
|
-
def
|
12
|
-
|
13
|
+
def includes?(date)
|
14
|
+
if @base_date.nil? || date < @base_date
|
15
|
+
false
|
16
|
+
else
|
17
|
+
((date.mjd - base_mjd) % @interval) == 0
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
# Create a new temporal expression with a new anchor date.
|
22
|
+
def reanchor(new_anchor_date)
|
23
|
+
self.class.new(new_anchor_date, @interval)
|
13
24
|
end
|
14
25
|
|
15
26
|
# Human readable version of the temporal expression.
|
@@ -23,15 +34,12 @@ module TExp
|
|
23
34
|
|
24
35
|
# Encode the temporal expression into +codes+.
|
25
36
|
def encode(codes)
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
def to_hash
|
31
|
-
build_hash do |b|
|
32
|
-
b.with(@base_date)
|
33
|
-
b.with(@interval)
|
37
|
+
if @base_date
|
38
|
+
encode_date(codes, @base_date)
|
39
|
+
else
|
40
|
+
codes << 0
|
34
41
|
end
|
42
|
+
codes << ',' << @interval << encoding_token
|
35
43
|
end
|
36
44
|
|
37
45
|
private
|
@@ -40,10 +48,6 @@ module TExp
|
|
40
48
|
@base_date.mjd
|
41
49
|
end
|
42
50
|
|
43
|
-
def formatted_date
|
44
|
-
@base_date.strftime("%Y-%m-%d")
|
45
|
-
end
|
46
|
-
|
47
51
|
class << self
|
48
52
|
def parse_callback(stack)
|
49
53
|
interval = stack.pop
|
data/lib/texp/day_of_month.rb
CHANGED
@@ -7,7 +7,7 @@ module TExp
|
|
7
7
|
end
|
8
8
|
|
9
9
|
# Is +date+ included in the temporal expression.
|
10
|
-
def
|
10
|
+
def includes?(date)
|
11
11
|
@days.include?(date.day)
|
12
12
|
end
|
13
13
|
|
@@ -22,11 +22,5 @@ module TExp
|
|
22
22
|
encode_list(codes, @days)
|
23
23
|
codes << encoding_token
|
24
24
|
end
|
25
|
-
|
26
|
-
def to_hash
|
27
|
-
build_hash do |b|
|
28
|
-
b.with(@days.map { |d| d.to_s })
|
29
|
-
end
|
30
|
-
end
|
31
25
|
end
|
32
26
|
end
|
data/lib/texp/day_of_week.rb
CHANGED
@@ -7,7 +7,7 @@ module TExp
|
|
7
7
|
end
|
8
8
|
|
9
9
|
# Is +date+ included in the temporal expression.
|
10
|
-
def
|
10
|
+
def includes?(date)
|
11
11
|
@days.include?(date.wday)
|
12
12
|
end
|
13
13
|
|
@@ -17,12 +17,6 @@ module TExp
|
|
17
17
|
humanize_list(@days) { |d| Date::DAYNAMES[d] }
|
18
18
|
end
|
19
19
|
|
20
|
-
def to_hash
|
21
|
-
build_hash do |b|
|
22
|
-
b.with(@days)
|
23
|
-
end
|
24
|
-
end
|
25
|
-
|
26
20
|
# Encode the temporal expression into +codes+.
|
27
21
|
def encode(codes)
|
28
22
|
encode_list(codes, @days)
|
data/lib/texp/dsl.rb
ADDED
@@ -0,0 +1,338 @@
|
|
1
|
+
module TExp
|
2
|
+
# DSL methods are available as methods on TExp (e.g. +TExp.day()+).
|
3
|
+
# Alternatively, you can include the +TExp::Builder+ module into
|
4
|
+
# whatever namespace to get direct access to these methods.
|
5
|
+
#
|
6
|
+
module DSL
|
7
|
+
|
8
|
+
# Return a temporal expression that matches any date that falls on
|
9
|
+
# a day of the month given in the argument list.
|
10
|
+
# Examples:
|
11
|
+
#
|
12
|
+
# day(1) # Match any date that falls on the 1st of any month
|
13
|
+
# day(1, 15) # Match any date that falls on the 1st or 15th of any month
|
14
|
+
#
|
15
|
+
def day(*days_of_month)
|
16
|
+
TExp::DayOfMonth.new(days_of_month)
|
17
|
+
end
|
18
|
+
|
19
|
+
# Return a temporal expression that matches any date in the
|
20
|
+
# specified week of the month. Days 1 through 7 are considered
|
21
|
+
# the first week of the month; days 8 through 14 are the second
|
22
|
+
# week; and so on.
|
23
|
+
#
|
24
|
+
# The week is specified by a numeric argument. Negative arguments
|
25
|
+
# are calculated from the end of the month (e.g. -1 would request
|
26
|
+
# the _last_ 7 days of the month). The symbols <tt>:first</tt>,
|
27
|
+
# <tt>:second</tt>, <tt>:third</tt>, <tt>:fourth</tt>,
|
28
|
+
# <tt>:fifth</tt>, and <tt>:last</tt> are also recognized.
|
29
|
+
#
|
30
|
+
# Examples:
|
31
|
+
#
|
32
|
+
# week(1) # Match any date in the first 7 days of any month.
|
33
|
+
# week(1, 2) # Match any date in the first or second 7 days of any month.
|
34
|
+
# week(:first) # Match any date in the first 7 days of any month.
|
35
|
+
# week(:last) # Match any date in the last 7 days of any month.
|
36
|
+
#
|
37
|
+
def week(*weeks)
|
38
|
+
TExp::Week.new(normalize_weeks(weeks))
|
39
|
+
end
|
40
|
+
|
41
|
+
# Return a temporal expression that matches any date in the list
|
42
|
+
# of given months.
|
43
|
+
#
|
44
|
+
# <b>Examples:</b>
|
45
|
+
#
|
46
|
+
# month(2) # Match any date in February
|
47
|
+
# month(2, 8) # Match any date in February or August
|
48
|
+
# month("February") # Match any date in February
|
49
|
+
# month("Sep", "Apr", "Jun", "Nov")
|
50
|
+
# # Match any date in any month with 30 days.
|
51
|
+
#
|
52
|
+
def month(*month)
|
53
|
+
TExp::Month.new(normalize_months(month))
|
54
|
+
end
|
55
|
+
|
56
|
+
# Return a temporal expression that matches the given list of
|
57
|
+
# years.
|
58
|
+
#
|
59
|
+
# <b>Examples:</b>
|
60
|
+
#
|
61
|
+
# year(2008) # Match any date in 2008
|
62
|
+
# year(2000, 2004, 2008) # Match any date in any of the three years
|
63
|
+
def year(*years)
|
64
|
+
TExp::Year.new(years)
|
65
|
+
end
|
66
|
+
|
67
|
+
# :call-seq:
|
68
|
+
# on(day, month)
|
69
|
+
# on(day, month_string)
|
70
|
+
# on(day, month, year)
|
71
|
+
# on(day, month_string, year)
|
72
|
+
# on(date)
|
73
|
+
# on(date_string)
|
74
|
+
# on(time)
|
75
|
+
# on(object_with_to_date)
|
76
|
+
# on(object_with_to_s)
|
77
|
+
#
|
78
|
+
# Return a temporal expression that matches a particular date of
|
79
|
+
# the year. The temporal expression will be pinned to a
|
80
|
+
# particular year if a year is given (either explicity as a
|
81
|
+
# parameter or implicitly via a +Date+ object). If no year is
|
82
|
+
# given, then the temporal expression will match that date in any
|
83
|
+
# year.
|
84
|
+
#
|
85
|
+
# If only a single argument is given, then the argument may be a
|
86
|
+
# string (which is parsed), a Date, a Time, or an object that
|
87
|
+
# responds to +to_date+. If the single argument is none of the
|
88
|
+
# above, then it is converted to a string (via +to_s+) and given
|
89
|
+
# to <tt>Date.parse()</tt>.
|
90
|
+
#
|
91
|
+
# Invalid arguments will cause +on+ to throw an ArgumentError
|
92
|
+
# exception.
|
93
|
+
#
|
94
|
+
# <b>Examples:</b>
|
95
|
+
#
|
96
|
+
# The following examples all match Feb 14 of any year.
|
97
|
+
#
|
98
|
+
# on(14, 2)
|
99
|
+
# on(14, "February")
|
100
|
+
# on(14, "Feb")
|
101
|
+
# on(14, :feb)
|
102
|
+
#
|
103
|
+
# The following examples all match Feb 14 of the year 2008.
|
104
|
+
#
|
105
|
+
# on(14, 2, 2008)
|
106
|
+
# on(14, "February", 2008)
|
107
|
+
# on(14, "Feb", 2008)
|
108
|
+
# on(14, :feb, 2008)
|
109
|
+
# on("Feb 14, 2008")
|
110
|
+
# on(Date.new(2008, 2, 14))
|
111
|
+
#
|
112
|
+
def on(*args)
|
113
|
+
if args.size == 1
|
114
|
+
arg = args.first
|
115
|
+
case arg
|
116
|
+
when String
|
117
|
+
date = Date.parse(arg)
|
118
|
+
when Date
|
119
|
+
date = arg
|
120
|
+
else
|
121
|
+
if arg.respond_to?(:to_date)
|
122
|
+
date = arg.to_date
|
123
|
+
else
|
124
|
+
date = try_parsing(arg.to_s)
|
125
|
+
end
|
126
|
+
end
|
127
|
+
on(date.day, date.month, date.year)
|
128
|
+
elsif args.size == 2
|
129
|
+
day, month = dm_args(args)
|
130
|
+
TExp::And.new(
|
131
|
+
TExp::DayOfMonth.new(day),
|
132
|
+
TExp::Month.new(month))
|
133
|
+
elsif args.size == 3
|
134
|
+
day, month, year = dmy_args(args)
|
135
|
+
TExp::And.new(
|
136
|
+
TExp::DayOfMonth.new(day),
|
137
|
+
TExp::Month.new(normalize_month(month)),
|
138
|
+
TExp::Year.new(year))
|
139
|
+
else
|
140
|
+
fail DateArgumentError
|
141
|
+
end
|
142
|
+
rescue DateArgumentError
|
143
|
+
fail ArgumentError, "Invalid arguents for on(): #{args.inspect}"
|
144
|
+
end
|
145
|
+
|
146
|
+
# Return a temporal expression matching the given days of the
|
147
|
+
# week.
|
148
|
+
#
|
149
|
+
# <b>Examples:</b>
|
150
|
+
#
|
151
|
+
# dow(2) # Match any date on a Tuesday
|
152
|
+
# dow("Tuesday") # Match any date on a Tuesday
|
153
|
+
# dow(:mon, :wed, :fr) # Match any date on a Monday, Wednesday or Friday
|
154
|
+
#
|
155
|
+
def dow(*dow)
|
156
|
+
TExp::DayOfWeek.new(normalize_dows(dow))
|
157
|
+
end
|
158
|
+
|
159
|
+
def every(n, unit, start_date=Date.today)
|
160
|
+
value = apply_units(unit, n)
|
161
|
+
TExp::DayInterval.new(start_date, value)
|
162
|
+
end
|
163
|
+
|
164
|
+
# Evaluate a temporal expression in the TExp environment.
|
165
|
+
# Redirect missing method calls to the containing environment.
|
166
|
+
def evaluate_expression_in_environment(&block) # :nodoc:
|
167
|
+
env = EvalEnvironment.new(block)
|
168
|
+
env.instance_eval(&block)
|
169
|
+
end
|
170
|
+
|
171
|
+
def normalize_units(args) # :nodoc:
|
172
|
+
result = []
|
173
|
+
while ! args.empty?
|
174
|
+
arg = args.shift
|
175
|
+
case arg
|
176
|
+
when Numeric
|
177
|
+
result.push(arg)
|
178
|
+
when Symbol
|
179
|
+
result.push(apply_units(arg, result.pop))
|
180
|
+
else
|
181
|
+
fail ArgumentError, "Unabled to recognize #{arg.inspect}"
|
182
|
+
end
|
183
|
+
end
|
184
|
+
result
|
185
|
+
end
|
186
|
+
|
187
|
+
private
|
188
|
+
|
189
|
+
WEEKNAMES = {
|
190
|
+
"fi" => 1,
|
191
|
+
"se" => 2,
|
192
|
+
"th" => 3,
|
193
|
+
"fo" => 4,
|
194
|
+
"fi" => 5,
|
195
|
+
"la" => -1
|
196
|
+
}
|
197
|
+
MONTHNAMES = Date::MONTHNAMES.collect { |mn| mn ? mn[0,3].downcase : nil }
|
198
|
+
DAYNAMES = Date::DAYNAMES.collect { |dn| dn[0,2].downcase }
|
199
|
+
|
200
|
+
UNIT_MULTIPLIERS = {
|
201
|
+
:day => 1, :days => 1,
|
202
|
+
:week => 7, :weeks => 7,
|
203
|
+
:month => 30, :months => 30,
|
204
|
+
:year => 365, :years => 365,
|
205
|
+
}
|
206
|
+
|
207
|
+
def apply_units(unit, value)
|
208
|
+
UNIT_MULTIPLIERS[unit] * value
|
209
|
+
end
|
210
|
+
|
211
|
+
def try_parsing(string)
|
212
|
+
Date.parse(string)
|
213
|
+
rescue ArgumentError
|
214
|
+
fail DateArgumentError
|
215
|
+
end
|
216
|
+
|
217
|
+
def dm_args(args)
|
218
|
+
day, month = args
|
219
|
+
month = normalize_month(month)
|
220
|
+
check_valid_day_month(day, month)
|
221
|
+
[day, month]
|
222
|
+
end
|
223
|
+
|
224
|
+
def dmy_args(args)
|
225
|
+
day, month, year = args
|
226
|
+
month = normalize_month(month)
|
227
|
+
check_valid_day_month(day, month)
|
228
|
+
[day, month, year]
|
229
|
+
end
|
230
|
+
|
231
|
+
def check_valid_day_month(day, month)
|
232
|
+
unless day.kind_of?(Integer) &&
|
233
|
+
month.kind_of?(Integer) &&
|
234
|
+
month >= 1 &&
|
235
|
+
month <= 12 &&
|
236
|
+
day >= 1 &&
|
237
|
+
day <= 31
|
238
|
+
fail DateArgumentError
|
239
|
+
end
|
240
|
+
end
|
241
|
+
|
242
|
+
def normalize_weeks(weeks)
|
243
|
+
weeks.collect { |w| normalize_week(w) }
|
244
|
+
end
|
245
|
+
|
246
|
+
def normalize_week(week)
|
247
|
+
case week
|
248
|
+
when Integer
|
249
|
+
week
|
250
|
+
else
|
251
|
+
WEEKNAMES[week.to_s[0,2].downcase]
|
252
|
+
end
|
253
|
+
end
|
254
|
+
|
255
|
+
def normalize_months(months)
|
256
|
+
months.collect { |m| normalize_month(m) }
|
257
|
+
end
|
258
|
+
|
259
|
+
def normalize_month(month_thing)
|
260
|
+
case month_thing
|
261
|
+
when Integer
|
262
|
+
month_thing
|
263
|
+
else
|
264
|
+
MONTHNAMES.index(month_thing.to_s[0,3].downcase)
|
265
|
+
end
|
266
|
+
end
|
267
|
+
|
268
|
+
def normalize_dows(dow_list)
|
269
|
+
dow_list.collect { |dow| normalize_dow(dow) }
|
270
|
+
end
|
271
|
+
|
272
|
+
def normalize_dow(dow_thing)
|
273
|
+
case dow_thing
|
274
|
+
when Integer
|
275
|
+
dow_thing
|
276
|
+
else
|
277
|
+
DAYNAMES.index(dow_thing.to_s[0,2].downcase)
|
278
|
+
end
|
279
|
+
end
|
280
|
+
|
281
|
+
end
|
282
|
+
|
283
|
+
|
284
|
+
# Internal class that provides an evaluation environment for
|
285
|
+
# temporal expressions. The temporal expression DSL methods are
|
286
|
+
# not generally exposed outside of the TExp module, meaning that
|
287
|
+
# any coded temporal expression must be prefixed with the TExp
|
288
|
+
# module name. This is cumbersome for a DSL.
|
289
|
+
#
|
290
|
+
# One solution is to just include the TExp module wherever you
|
291
|
+
# wish to use temporal expressions. Including at the top level
|
292
|
+
# would make them available everywhere, but would also pollute the
|
293
|
+
# top level namespace with all of TExp's methods.
|
294
|
+
#
|
295
|
+
# An alternate solution is to use a block that is instance_eval'ed
|
296
|
+
# in the TExp namespace. Within in the block you can reference the
|
297
|
+
# TExp DSL methods freely, but they do not pollute any namespaces
|
298
|
+
# outside the block.
|
299
|
+
#
|
300
|
+
# Unfortunately, the instance_eval technique will cut off access to
|
301
|
+
# methods defined in the calling namespace (since we are using
|
302
|
+
# TExp's environment instead). The EvalEnvironment handles this by
|
303
|
+
# directing any unknown methods to the calling environment, making
|
304
|
+
# the instance_eval technique much more friendly.
|
305
|
+
#
|
306
|
+
# See the +texp+ method to see how this class is used.
|
307
|
+
#
|
308
|
+
class EvalEnvironment
|
309
|
+
include DSL
|
310
|
+
|
311
|
+
# Create a TExp environment for evaluating temporal expressions.
|
312
|
+
# Use the block binding to find the object where the block is
|
313
|
+
# embeded.
|
314
|
+
def initialize(containing_env)
|
315
|
+
@container = eval "self", containing_env
|
316
|
+
end
|
317
|
+
|
318
|
+
# The methods identified by +sym+ is not found in the TExp
|
319
|
+
# namespace. Try calling it in the containing namespace.
|
320
|
+
def method_missing(sym, *args, &block)
|
321
|
+
@container.send(sym, *args, &block)
|
322
|
+
end
|
323
|
+
end
|
324
|
+
|
325
|
+
extend DSL
|
326
|
+
end
|
327
|
+
|
328
|
+
# Evaluate a temporal expression in the TExp environment. Methods
|
329
|
+
# that are not found in the TExp environment will be redirected to the
|
330
|
+
# calling environment automatically.
|
331
|
+
#
|
332
|
+
# <b>Example:</b>
|
333
|
+
#
|
334
|
+
# texp { day(1) * month(:feb) } # Match the first of February (any year)
|
335
|
+
#
|
336
|
+
def texp(&block)
|
337
|
+
TExp.evaluate_expression_in_environment(&block)
|
338
|
+
end
|
data/lib/texp/errors.rb
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
module TExp
|
2
|
+
|
3
|
+
# Base error for TExp specific exceptions.
|
4
|
+
class Error < StandardError
|
5
|
+
end
|
6
|
+
|
7
|
+
|
8
|
+
# Thrown by internal methods in the date() builder. These
|
9
|
+
# exceptions are caught and uniformly reraised as Ruby
|
10
|
+
# ArgumentErrors.
|
11
|
+
class DateArgumentError < Error
|
12
|
+
end
|
13
|
+
|
14
|
+
# Thrown if an error is encountered during the parsing of a temporal
|
15
|
+
# expression.
|
16
|
+
class ParseError < Error
|
17
|
+
end
|
18
|
+
end
|