texp 0.0.3 → 0.0.7
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/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
|