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