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.rb
CHANGED
@@ -1,4 +1,7 @@
|
|
1
|
-
require '
|
1
|
+
require 'date'
|
2
|
+
require 'texp/time_ext'
|
3
|
+
require 'texp/version'
|
4
|
+
require 'texp/errors'
|
2
5
|
require 'texp/base'
|
3
6
|
require 'texp/parse'
|
4
7
|
require 'texp/day_of_week'
|
@@ -10,4 +13,6 @@ require 'texp/day_interval'
|
|
10
13
|
require 'texp/every_day'
|
11
14
|
require 'texp/window'
|
12
15
|
require 'texp/logic'
|
16
|
+
require 'texp/dsl'
|
17
|
+
require 'texp/operators'
|
13
18
|
require 'texp/ext'
|
data/lib/texp/base.rb
CHANGED
@@ -1,9 +1,10 @@
|
|
1
|
-
require 'texp/hash_builder'
|
2
|
-
|
3
1
|
module TExp
|
2
|
+
|
3
|
+
####################################################################
|
4
4
|
# Abstract Base class for all Texp Temporal Expressions.
|
5
5
|
class Base
|
6
|
-
|
6
|
+
include Enumerable
|
7
|
+
|
7
8
|
# Convert the temporal expression into an encoded string (that can
|
8
9
|
# be parsed by TExp.parse).
|
9
10
|
def to_s
|
@@ -12,8 +13,75 @@ module TExp
|
|
12
13
|
codes.join("")
|
13
14
|
end
|
14
15
|
|
15
|
-
|
16
|
+
# Create a new temporal expression with a new anchor date.
|
17
|
+
def reanchor(date)
|
18
|
+
self
|
19
|
+
end
|
20
|
+
|
21
|
+
# Return the first day of the window for the temporal expression.
|
22
|
+
# If the temporal expression is not a windowed expression, then
|
23
|
+
# the first day of the window is the given date.
|
24
|
+
def first_day_of_window(date)
|
25
|
+
includes?(date) ? date : nil
|
26
|
+
end
|
27
|
+
|
28
|
+
# Return the last day of the window for the temporal expression.
|
29
|
+
# If the temporal expression is not a windowed expression, then
|
30
|
+
# the last day of the window is the given date.
|
31
|
+
def last_day_of_window(date)
|
32
|
+
includes?(date) ? date : nil
|
33
|
+
end
|
16
34
|
|
35
|
+
# Iterate over all temporal expressions and subexpressions (in
|
36
|
+
# post order).
|
37
|
+
def each() # :yield: temporal_expression
|
38
|
+
yield self
|
39
|
+
end
|
40
|
+
|
41
|
+
# :call-seq:
|
42
|
+
# window(days)
|
43
|
+
# window(predays, postdays)
|
44
|
+
# window(n, units)
|
45
|
+
# window(pre, pre_units, post, post_units)
|
46
|
+
#
|
47
|
+
# Create a new temporal expression that matches a window around
|
48
|
+
# any date matched by the current expression.
|
49
|
+
#
|
50
|
+
# If a single numeric value is given, then a symetrical window of
|
51
|
+
# the given number of days is created around each date matched by
|
52
|
+
# the current expression. If a symbol representing units is given
|
53
|
+
# in addition to the numeric, then the appropriate scale factor is
|
54
|
+
# applied to the numeric value.
|
55
|
+
#
|
56
|
+
# If two numberic values are given (with or without unit symbols),
|
57
|
+
# then the window will be asymmetric. The firsts numeric value
|
58
|
+
# will be the pre-window, and the second numeric value will be the
|
59
|
+
# post window.
|
60
|
+
#
|
61
|
+
# The following unit symbols are recognized:
|
62
|
+
#
|
63
|
+
# * :day, :days (scale by 1)
|
64
|
+
# * :week, :weeks (scale by 7)
|
65
|
+
# * :month, :months (scale by 30)
|
66
|
+
# * :year, :years (scale by 365)
|
67
|
+
#
|
68
|
+
# <b>Examples:</b>
|
69
|
+
#
|
70
|
+
# texp.window(3) # window of 3 days on either side
|
71
|
+
# texp.window(3, :days) # window of 3 days on either side
|
72
|
+
# texp.window(1, :week) # window of 1 week on either side
|
73
|
+
# texp.window(3, :days, 2, :weeks)
|
74
|
+
# # window of 3 days before any match,
|
75
|
+
# # and 2 weeks after any match.
|
76
|
+
#
|
77
|
+
def window(*args)
|
78
|
+
prewindow, postwindow = TExp.normalize_units(args)
|
79
|
+
postwindow ||= prewindow
|
80
|
+
TExp::Window.new(self, prewindow, postwindow)
|
81
|
+
end
|
82
|
+
|
83
|
+
private
|
84
|
+
|
17
85
|
# Coerce +arg+ into a list (i.e. Array) if it is not one already.
|
18
86
|
def listize(arg)
|
19
87
|
case arg
|
@@ -23,12 +91,12 @@ module TExp
|
|
23
91
|
[arg]
|
24
92
|
end
|
25
93
|
end
|
26
|
-
|
94
|
+
|
27
95
|
# Encode the date into the codes receiver.
|
28
96
|
def encode_date(codes, date)
|
29
97
|
codes << date.strftime("%Y-%m-%d")
|
30
98
|
end
|
31
|
-
|
99
|
+
|
32
100
|
# Encode the list into the codes receiver. All
|
33
101
|
def encode_list(codes, list)
|
34
102
|
if list.empty?
|
@@ -46,7 +114,7 @@ module TExp
|
|
46
114
|
codes << "]"
|
47
115
|
end
|
48
116
|
end
|
49
|
-
|
117
|
+
|
50
118
|
# For the list of integers as a list of ordinal numbers. By
|
51
119
|
# default, use 'or' as a connectingin word. (e.g. [1,2,3] => "1st,
|
52
120
|
# 2nd, or 3rd")
|
@@ -91,7 +159,7 @@ module TExp
|
|
91
159
|
11 => "th",
|
92
160
|
12 => "th",
|
93
161
|
13 => "th",
|
94
|
-
}
|
162
|
+
} # :nodoc:
|
95
163
|
|
96
164
|
# Return the ordinal abbreviation for the integer +n+. (e.g. 1 =>
|
97
165
|
# "1st", 3 => "3rd")
|
@@ -141,6 +209,67 @@ module TExp
|
|
141
209
|
def parse_callback(stack)
|
142
210
|
stack.push new(stack.pop)
|
143
211
|
end
|
212
|
+
end # class << self
|
213
|
+
end # class Base
|
214
|
+
|
215
|
+
####################################################################
|
216
|
+
# Base class for temporal expressions with a single sub-expressions
|
217
|
+
# (i.e. term).
|
218
|
+
class SingleTermBase < Base
|
219
|
+
# Create a single term temporal expression.
|
220
|
+
def initialize(term)
|
221
|
+
@term = term
|
222
|
+
end
|
223
|
+
|
224
|
+
# Create a new temporal expression with a new anchor date.
|
225
|
+
def reanchor(date)
|
226
|
+
new_term = @term.reanchor(date)
|
227
|
+
(@term == new_term) ? self : self.class.new(new_term)
|
228
|
+
end
|
229
|
+
|
230
|
+
# Iterate over all temporal expressions and subexpressions (in
|
231
|
+
# post order).
|
232
|
+
def each() # :yield: temporal_expression
|
233
|
+
yield @term
|
234
|
+
yield self
|
235
|
+
end
|
236
|
+
end # class SingleTermBase
|
237
|
+
|
238
|
+
####################################################################
|
239
|
+
# Base class for temporal expressions with multiple sub-expressions
|
240
|
+
# (i.e. terms).
|
241
|
+
class MultiTermBase < Base
|
242
|
+
|
243
|
+
# Create an multi-term temporal expression.
|
244
|
+
def initialize(*terms)
|
245
|
+
@terms = terms
|
246
|
+
end
|
247
|
+
|
248
|
+
# Create a new temporal expression with a new anchor date.
|
249
|
+
def reanchor(date)
|
250
|
+
new_terms = @terms.collect { |term| term.reanchor(date) }
|
251
|
+
if new_terms == @terms
|
252
|
+
self
|
253
|
+
else
|
254
|
+
self.class.new(*new_terms)
|
255
|
+
end
|
256
|
+
end
|
257
|
+
|
258
|
+
# Iterate over all temporal expressions and subexpressions (in
|
259
|
+
# post order).
|
260
|
+
def each() # :yield: temporal_expression
|
261
|
+
@terms.each do |term| yield term end
|
262
|
+
yield self
|
144
263
|
end
|
145
|
-
|
146
|
-
|
264
|
+
|
265
|
+
class << self
|
266
|
+
# Parsing callback for terms based temporal expressions. The
|
267
|
+
# top of the stack is assumed to be a list that is *-expanded to
|
268
|
+
# the temporal expression's constructor.
|
269
|
+
def parse_callback(stack)
|
270
|
+
stack.push self.new(*stack.pop)
|
271
|
+
end
|
272
|
+
end
|
273
|
+
end # class << self
|
274
|
+
|
275
|
+
end # class MultiTermBase
|
data/lib/texp/builder.rb
ADDED
@@ -0,0 +1,254 @@
|
|
1
|
+
module TExp
|
2
|
+
|
3
|
+
# Builder methods are available as methods on TExp
|
4
|
+
# (e.g. +TExp.day()+). Alternatively, you can include the
|
5
|
+
# +TExp::Builder+ module into whatever namespace to get direct
|
6
|
+
# access to these methods.
|
7
|
+
module Builder
|
8
|
+
|
9
|
+
# Return a temporal expression that matches any date that falls on
|
10
|
+
# a day of the month given in the argument list.
|
11
|
+
# Examples:
|
12
|
+
#
|
13
|
+
# day(1) # Match any date that falls on the 1st of any month
|
14
|
+
# day(1, 15) # Match any date that falls on the 1st or 15th of any month
|
15
|
+
#
|
16
|
+
def day(*days_of_month)
|
17
|
+
TExp::DayOfMonth.new(days_of_month)
|
18
|
+
end
|
19
|
+
|
20
|
+
# Return a temporal expression that matches any date in the
|
21
|
+
# specified week of the month. Days 1 through 7 are considered
|
22
|
+
# the first week of the month; days 8 through 14 are the second
|
23
|
+
# week; and so on.
|
24
|
+
#
|
25
|
+
# The week is specified by a numeric argument. Negative arguments
|
26
|
+
# are calculated from the end of the month (e.g. -1 would request
|
27
|
+
# the _last_ 7 days of the month). The symbols <tt>:first</tt>,
|
28
|
+
# <tt>:second</tt>, <tt>:third</tt>, <tt>:fourth</tt>,
|
29
|
+
# <tt>:fifth</tt>, and <tt>:last</tt> are also recognized.
|
30
|
+
#
|
31
|
+
# Examples:
|
32
|
+
#
|
33
|
+
# week(1) # Match any date in the first 7 days of any month.
|
34
|
+
# week(1, 2) # Match any date in the first or second 7 days of any month.
|
35
|
+
# week(:first) # Match any date in the first 7 days of any month.
|
36
|
+
# week(:last) # Match any date in the last 7 days of any month.
|
37
|
+
#
|
38
|
+
def week(*weeks)
|
39
|
+
TExp::Week.new(weeks)
|
40
|
+
end
|
41
|
+
|
42
|
+
# Return a temporal expression that matches any date in the list
|
43
|
+
# of given months.
|
44
|
+
#
|
45
|
+
# <b>Examples:</b>
|
46
|
+
#
|
47
|
+
# month(2) # Match any date in February
|
48
|
+
# month(2, 8) # Match any date in February or August
|
49
|
+
# month("February") # Match any date in February
|
50
|
+
# month("Sep", "Apr", "Jun", "Nov")
|
51
|
+
# # Match any date in any month with 30 days.
|
52
|
+
#
|
53
|
+
def month(*month)
|
54
|
+
TExp::Month.new(month)
|
55
|
+
end
|
56
|
+
|
57
|
+
# Return a temporal expression that matches the given list of
|
58
|
+
# years.
|
59
|
+
#
|
60
|
+
# <b>Examples:</b>
|
61
|
+
#
|
62
|
+
# year(2008) # Match any date in 2008
|
63
|
+
# year(2000, 2004, 2008) # Match any date in any of the three years
|
64
|
+
def year(*years)
|
65
|
+
TExp::Year.new(years)
|
66
|
+
end
|
67
|
+
|
68
|
+
# :call-seq:
|
69
|
+
# on(day, month)
|
70
|
+
# on(day, month_string)
|
71
|
+
# on(day, month, year)
|
72
|
+
# on(day, month_string, year)
|
73
|
+
# on(date)
|
74
|
+
# on(date_string)
|
75
|
+
# on(time)
|
76
|
+
# on(object_with_to_date)
|
77
|
+
# on(object_with_to_s)
|
78
|
+
#
|
79
|
+
# Return a temporal expression that matches a particular date of
|
80
|
+
# the year. The temporal expression will be pinned to a
|
81
|
+
# particular year if a year is given (either explicity as a
|
82
|
+
# parameter or implicitly via a +Date+ object). If no year is
|
83
|
+
# given, then the temporal expression will match that date in any
|
84
|
+
# year.
|
85
|
+
#
|
86
|
+
# If only a single argument is given, then the argument may be a
|
87
|
+
# string (which is parsed), a Date, a Time, or an object that
|
88
|
+
# responds to +to_date+. If the single argument is none of the
|
89
|
+
# above, then it is converted to a string (via +to_s+) and given
|
90
|
+
# to <tt>Date.parse()</tt>.
|
91
|
+
#
|
92
|
+
# Invalid arguments will cause +on+ to throw an ArgumentError
|
93
|
+
# exception.
|
94
|
+
#
|
95
|
+
# <b>Examples:</b>
|
96
|
+
#
|
97
|
+
# The following examples all match Feb 14 of any year.
|
98
|
+
#
|
99
|
+
# on(14, 2)
|
100
|
+
# on(14, "February")
|
101
|
+
# on(14, "Feb")
|
102
|
+
# on(14, :feb)
|
103
|
+
#
|
104
|
+
# The following examples all match Feb 14 of the year 2008.
|
105
|
+
#
|
106
|
+
# on(14, 2, 2008)
|
107
|
+
# on(14, "February", 2008)
|
108
|
+
# on(14, "Feb", 2008)
|
109
|
+
# on(14, :feb, 2008)
|
110
|
+
# on("Feb 14, 2008")
|
111
|
+
# on(Date.new(2008, 2, 14))
|
112
|
+
#
|
113
|
+
def on(*args)
|
114
|
+
if args.size == 1
|
115
|
+
arg = args.first
|
116
|
+
case arg
|
117
|
+
when String
|
118
|
+
date = Date.parse(arg)
|
119
|
+
when Date
|
120
|
+
date = arg
|
121
|
+
else
|
122
|
+
if arg.respond_to?(:to_date)
|
123
|
+
date = arg.to_date
|
124
|
+
else
|
125
|
+
date = try_parsing(arg.to_s)
|
126
|
+
end
|
127
|
+
end
|
128
|
+
on(date.day, date.month, date.year)
|
129
|
+
elsif args.size == 2
|
130
|
+
day, month = dm_args(args)
|
131
|
+
TExp::And.new(
|
132
|
+
TExp::DayOfMonth.new(day),
|
133
|
+
TExp::Month.new(month))
|
134
|
+
elsif args.size == 3
|
135
|
+
day, month, year = dmy_args(args)
|
136
|
+
TExp::And.new(
|
137
|
+
TExp::DayOfMonth.new(day),
|
138
|
+
TExp::Month.new(normalize_month(month)),
|
139
|
+
TExp::Year.new(year))
|
140
|
+
else
|
141
|
+
fail DateArgumentError
|
142
|
+
end
|
143
|
+
rescue DateArgumentError
|
144
|
+
fail ArgumentError, "Invalid arguents for on(): #{args.inspect}"
|
145
|
+
end
|
146
|
+
|
147
|
+
# Return a temporal expression matching the given days of the
|
148
|
+
# week.
|
149
|
+
#
|
150
|
+
# <b>Examples:</b>
|
151
|
+
#
|
152
|
+
# dow(2) # Match any date on a Tuesday
|
153
|
+
# dow("Tuesday") # Match any date on a Tuesday
|
154
|
+
# dow(:mon, :wed, :fr) # Match any date on a Monday, Wednesday or Friday
|
155
|
+
#
|
156
|
+
def dow(*dow)
|
157
|
+
TExp::DayOfWeek.new(normalize_dows(dow))
|
158
|
+
end
|
159
|
+
|
160
|
+
# Return a temporal expression that matches
|
161
|
+
def every(n, unit, start_date=Date.today)
|
162
|
+
value = apply_units(unit, n)
|
163
|
+
TExp::DayInterval.new(start_date, value)
|
164
|
+
end
|
165
|
+
|
166
|
+
def normalize_units(args) # :nodoc:
|
167
|
+
result = []
|
168
|
+
while ! args.empty?
|
169
|
+
arg = args.shift
|
170
|
+
case arg
|
171
|
+
when Numeric
|
172
|
+
result.push(arg)
|
173
|
+
when Symbol
|
174
|
+
result.push(apply_units(arg, result.pop))
|
175
|
+
else
|
176
|
+
fail ArgumentError, "Unabled to recognize #{arg}"
|
177
|
+
end
|
178
|
+
end
|
179
|
+
result
|
180
|
+
end
|
181
|
+
|
182
|
+
private
|
183
|
+
|
184
|
+
MONTHNAMES = Date::MONTHNAMES.collect { |mn| mn ? mn[0,3].downcase : nil }
|
185
|
+
DAYNAMES = Date::DAYNAMES.collect { |dn| dn[0,2].downcase }
|
186
|
+
|
187
|
+
UNIT_MULTIPLIERS = {
|
188
|
+
:day => 1, :days => 1,
|
189
|
+
:week => 7, :weeks => 7,
|
190
|
+
:month => 30, :months => 30,
|
191
|
+
:year => 365, :years => 365,
|
192
|
+
}
|
193
|
+
|
194
|
+
def apply_units(unit, value)
|
195
|
+
UNIT_MULTIPLIERS[unit] * value
|
196
|
+
end
|
197
|
+
|
198
|
+
def try_parsing(string)
|
199
|
+
Date.parse(string)
|
200
|
+
rescue ArgumentError
|
201
|
+
fail DateArgumentError
|
202
|
+
end
|
203
|
+
|
204
|
+
def dm_args(args)
|
205
|
+
day, month = args
|
206
|
+
month = normalize_month(month)
|
207
|
+
check_valid_day_month(day, month)
|
208
|
+
[day, month]
|
209
|
+
end
|
210
|
+
|
211
|
+
def dmy_args(args)
|
212
|
+
day, month, year = args
|
213
|
+
month = normalize_month(month)
|
214
|
+
check_valid_day_month(day, month)
|
215
|
+
[day, month, year]
|
216
|
+
end
|
217
|
+
|
218
|
+
def check_valid_day_month(day, month)
|
219
|
+
unless day.kind_of?(Integer) &&
|
220
|
+
month.kind_of?(Integer) &&
|
221
|
+
month >= 1 &&
|
222
|
+
month <= 12 &&
|
223
|
+
day >= 1 &&
|
224
|
+
day <= 31
|
225
|
+
fail DateArgumentError
|
226
|
+
end
|
227
|
+
end
|
228
|
+
|
229
|
+
def normalize_month(month_thing)
|
230
|
+
case month_thing
|
231
|
+
when Integer
|
232
|
+
month_thing
|
233
|
+
else
|
234
|
+
MONTHNAMES.index(month_thing.to_s[0,3].downcase)
|
235
|
+
end
|
236
|
+
end
|
237
|
+
|
238
|
+
def normalize_dows(dow_list)
|
239
|
+
dow_list.collect { |dow| normalize_dow(dow) }
|
240
|
+
end
|
241
|
+
|
242
|
+
def normalize_dow(dow_thing)
|
243
|
+
case dow_thing
|
244
|
+
when Integer
|
245
|
+
dow_thing
|
246
|
+
else
|
247
|
+
DAYNAMES.index(dow_thing.to_s[0,2].downcase)
|
248
|
+
end
|
249
|
+
end
|
250
|
+
|
251
|
+
end
|
252
|
+
|
253
|
+
extend Builder
|
254
|
+
end
|