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.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
|