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/lib/texp.rb CHANGED
@@ -1,4 +1,7 @@
1
- require 'texp/core'
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
- private
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
- end
146
- end
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
@@ -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