texp 0.0.3 → 0.0.7

Sign up to get free protection for your applications and to get access to all the features.
@@ -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 include?(date)
12
- ((date.mjd - base_mjd) % @interval) == 0
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
- encode_date(codes, @base_date)
27
- codes << ',' << @interval << encoding_token
28
- end
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
@@ -7,7 +7,7 @@ module TExp
7
7
  end
8
8
 
9
9
  # Is +date+ included in the temporal expression.
10
- def include?(date)
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
@@ -7,7 +7,7 @@ module TExp
7
7
  end
8
8
 
9
9
  # Is +date+ included in the temporal expression.
10
- def include?(date)
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
@@ -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