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