sethwalker-chronic 0.3.0

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.
Files changed (47) hide show
  1. data/README +167 -0
  2. data/lib/chronic.rb +127 -0
  3. data/lib/chronic/chronic.rb +249 -0
  4. data/lib/chronic/grabber.rb +26 -0
  5. data/lib/chronic/handlers.rb +541 -0
  6. data/lib/chronic/ordinal.rb +40 -0
  7. data/lib/chronic/pointer.rb +27 -0
  8. data/lib/chronic/repeater.rb +129 -0
  9. data/lib/chronic/repeaters/repeater_day.rb +52 -0
  10. data/lib/chronic/repeaters/repeater_day_name.rb +51 -0
  11. data/lib/chronic/repeaters/repeater_day_portion.rb +94 -0
  12. data/lib/chronic/repeaters/repeater_fortnight.rb +70 -0
  13. data/lib/chronic/repeaters/repeater_hour.rb +57 -0
  14. data/lib/chronic/repeaters/repeater_minute.rb +57 -0
  15. data/lib/chronic/repeaters/repeater_month.rb +66 -0
  16. data/lib/chronic/repeaters/repeater_month_name.rb +98 -0
  17. data/lib/chronic/repeaters/repeater_season.rb +150 -0
  18. data/lib/chronic/repeaters/repeater_season_name.rb +45 -0
  19. data/lib/chronic/repeaters/repeater_second.rb +41 -0
  20. data/lib/chronic/repeaters/repeater_time.rb +124 -0
  21. data/lib/chronic/repeaters/repeater_week.rb +73 -0
  22. data/lib/chronic/repeaters/repeater_weekday.rb +77 -0
  23. data/lib/chronic/repeaters/repeater_weekend.rb +65 -0
  24. data/lib/chronic/repeaters/repeater_year.rb +64 -0
  25. data/lib/chronic/scalar.rb +76 -0
  26. data/lib/chronic/separator.rb +91 -0
  27. data/lib/chronic/time_zone.rb +23 -0
  28. data/lib/numerizer/numerizer.rb +97 -0
  29. data/test/suite.rb +9 -0
  30. data/test/test_Chronic.rb +50 -0
  31. data/test/test_Handler.rb +110 -0
  32. data/test/test_Numerizer.rb +52 -0
  33. data/test/test_RepeaterDayName.rb +52 -0
  34. data/test/test_RepeaterFortnight.rb +63 -0
  35. data/test/test_RepeaterHour.rb +65 -0
  36. data/test/test_RepeaterMonth.rb +47 -0
  37. data/test/test_RepeaterMonthName.rb +57 -0
  38. data/test/test_RepeaterTime.rb +72 -0
  39. data/test/test_RepeaterWeek.rb +63 -0
  40. data/test/test_RepeaterWeekday.rb +56 -0
  41. data/test/test_RepeaterWeekend.rb +75 -0
  42. data/test/test_RepeaterYear.rb +63 -0
  43. data/test/test_Span.rb +24 -0
  44. data/test/test_Time.rb +50 -0
  45. data/test/test_Token.rb +26 -0
  46. data/test/test_parsing.rb +716 -0
  47. metadata +102 -0
@@ -0,0 +1,26 @@
1
+ #module Chronic
2
+
3
+ class Chronic::Grabber < Chronic::Tag #:nodoc:
4
+ def self.scan(tokens)
5
+ tokens.each_index do |i|
6
+ if t = self.scan_for_all(tokens[i]) then tokens[i].tag(t); next end
7
+ end
8
+ tokens
9
+ end
10
+
11
+ def self.scan_for_all(token)
12
+ scanner = {/last/ => :last,
13
+ /this/ => :this,
14
+ /next/ => :next}
15
+ scanner.keys.each do |scanner_item|
16
+ return self.new(scanner[scanner_item]) if scanner_item =~ token.word
17
+ end
18
+ return nil
19
+ end
20
+
21
+ def to_s
22
+ 'grabber-' << @type.to_s
23
+ end
24
+ end
25
+
26
+ #end
@@ -0,0 +1,541 @@
1
+ module Chronic
2
+
3
+ class << self
4
+
5
+ def definitions(options={}) #:nodoc:
6
+ options[:endian_precedence] = [:middle, :little] if options[:endian_precedence].nil?
7
+
8
+ # ensure the endian precedence is exactly two elements long
9
+ raise ChronicPain, "More than two elements specified for endian precedence array" unless options[:endian_precedence].length == 2
10
+
11
+ # handler for dd/mm/yyyy
12
+ @little_endian_handler ||= Handler.new([:scalar_day, :separator_slash_or_dash, :scalar_month, :separator_slash_or_dash, :scalar_year, :separator_at?, 'time?'], :handle_sd_sm_sy)
13
+
14
+ # handler for mm/dd/yyyy
15
+ @middle_endian_handler ||= Handler.new([:scalar_month, :separator_slash_or_dash, :scalar_day, :separator_slash_or_dash, :scalar_year, :separator_at?, 'time?'], :handle_sm_sd_sy)
16
+
17
+ # ensure we have valid endian values
18
+ options[:endian_precedence].each do |e|
19
+ raise ChronicPain, "Unknown endian type: #{e.to_s}" unless instance_variable_defined?(endian_variable_name_for(e))
20
+ end
21
+
22
+ @definitions ||=
23
+ {:time => [Handler.new([:repeater_time, :repeater_day_portion?], nil)],
24
+
25
+ :date => [Handler.new([:repeater_day_name, :repeater_month_name, :scalar_day, :repeater_time, :separator_slash_or_dash?, :time_zone, :scalar_year], :handle_rdn_rmn_sd_t_tz_sy),
26
+ Handler.new([:repeater_month_name, :scalar_day, :scalar_year], :handle_rmn_sd_sy),
27
+ Handler.new([:repeater_month_name, :scalar_day, :scalar_year, :separator_at?, 'time?'], :handle_rmn_sd_sy),
28
+ Handler.new([:repeater_month_name, :scalar_day, :separator_at?, 'time?'], :handle_rmn_sd),
29
+ Handler.new([:repeater_month_name, :ordinal_day, :scalar_year], :handle_rmn_od_sy),
30
+ Handler.new([:repeater_time, :repeater_day_portion?, :separator_on?, :repeater_month_name, :scalar_day], :handle_rmn_sd_on),
31
+ Handler.new([:repeater_month_name, :ordinal_day, :separator_at?, 'time?'], :handle_rmn_od),
32
+ Handler.new([:repeater_time, :repeater_day_portion?, :separator_on?, :repeater_month_name, :ordinal_day], :handle_rmn_od_on),
33
+ Handler.new([:repeater_month_name, :scalar_year], :handle_rmn_sy),
34
+ Handler.new([:scalar_day, :repeater_month_name, :scalar_year, :separator_at?, 'time?'], :handle_sd_rmn_sy),
35
+ @middle_endian_handler,
36
+ @little_endian_handler,
37
+ Handler.new([:scalar_year, :separator_slash_or_dash, :scalar_month, :separator_slash_or_dash, :scalar_day, :separator_at?, 'time?'], :handle_sy_sm_sd),
38
+ Handler.new([:scalar_month, :separator_slash_or_dash, :scalar_year], :handle_sm_sy)],
39
+
40
+ # tonight at 7pm
41
+ :anchor => [Handler.new([:grabber?, :repeater, :separator_at?, :repeater?, :repeater?], :handle_r),
42
+ Handler.new([:grabber?, :repeater, :repeater, :separator_at?, :repeater?, :repeater?], :handle_r),
43
+ Handler.new([:repeater, :grabber, :repeater], :handle_r_g_r)],
44
+
45
+ # 3 weeks from now, in 2 months
46
+ :arrow => [Handler.new([:scalar, :repeater, :pointer], :handle_s_r_p),
47
+ Handler.new([:pointer, :scalar, :repeater], :handle_p_s_r),
48
+ Handler.new([:scalar, :repeater, :pointer, 'anchor'], :handle_s_r_p_a)],
49
+
50
+ # 3rd week in march
51
+ :narrow => [Handler.new([:ordinal, :repeater, :separator_in, :repeater], :handle_o_r_s_r),
52
+ Handler.new([:ordinal, :repeater, :grabber, :repeater], :handle_o_r_g_r)]
53
+ }
54
+
55
+ apply_endian_precedences(options[:endian_precedence])
56
+
57
+ @definitions
58
+ end
59
+
60
+ def tokens_to_span(tokens, options) #:nodoc:
61
+ # maybe it's a specific date
62
+
63
+ definitions = self.definitions(options)
64
+ definitions[:date].each do |handler|
65
+ if handler.match(tokens, definitions)
66
+ puts "-date" if Chronic.debug
67
+ good_tokens = tokens.select { |o| !o.get_tag Separator }
68
+ return self.send(handler.handler_method, good_tokens, options)
69
+ end
70
+ end
71
+
72
+ # I guess it's not a specific date, maybe it's just an anchor
73
+
74
+ definitions[:anchor].each do |handler|
75
+ if handler.match(tokens, definitions)
76
+ puts "-anchor" if Chronic.debug
77
+ good_tokens = tokens.select { |o| !o.get_tag Separator }
78
+ return self.send(handler.handler_method, good_tokens, options)
79
+ end
80
+ end
81
+
82
+ # not an anchor, perhaps it's an arrow
83
+
84
+ definitions[:arrow].each do |handler|
85
+ if handler.match(tokens, definitions)
86
+ puts "-arrow" if Chronic.debug
87
+ good_tokens = tokens.reject { |o| o.get_tag(SeparatorAt) || o.get_tag(SeparatorSlashOrDash) || o.get_tag(SeparatorComma) }
88
+ return self.send(handler.handler_method, good_tokens, options)
89
+ end
90
+ end
91
+
92
+ # not an arrow, let's hope it's a narrow
93
+
94
+ definitions[:narrow].each do |handler|
95
+ if handler.match(tokens, definitions)
96
+ puts "-narrow" if Chronic.debug
97
+ #good_tokens = tokens.select { |o| !o.get_tag Separator }
98
+ return self.send(handler.handler_method, tokens, options)
99
+ end
100
+ end
101
+
102
+ # I guess you're out of luck!
103
+ puts "-none" if Chronic.debug
104
+ return nil
105
+ end
106
+
107
+ #--------------
108
+
109
+ def apply_endian_precedences(precedences)
110
+ date_defs = @definitions[:date]
111
+
112
+ # map the precedence array to indices on @definitions[:date]
113
+ indices = precedences.map { |e|
114
+ handler = instance_variable_get(endian_variable_name_for(e))
115
+ date_defs.index(handler)
116
+ }
117
+
118
+ # swap the handlers if we discover they are at odds with the desired preferences
119
+ swap(date_defs, indices.first, indices.last) if indices.first > indices.last
120
+ end
121
+
122
+ def endian_variable_name_for(e)
123
+ "@#{e.to_s}_endian_handler".to_sym
124
+ end
125
+
126
+ # exchange two elements in an array
127
+ def swap(arr, a, b); arr[a], arr[b] = arr[b], arr[a]; end
128
+
129
+ def day_or_time(day_start, time_tokens, options)
130
+ outer_span = Span.new(day_start, day_start + (24 * 60 * 60))
131
+
132
+ if !time_tokens.empty?
133
+ @now = outer_span.begin
134
+ time = get_anchor(dealias_and_disambiguate_times(time_tokens, options), options)
135
+ return time
136
+ else
137
+ return outer_span
138
+ end
139
+ end
140
+
141
+ #--------------
142
+
143
+ def handle_m_d(month, day, time_tokens, options) #:nodoc:
144
+ month.start = @now
145
+ span = month.this(options[:context])
146
+
147
+ day_start = Chronic.time_class.local(span.begin.year, span.begin.month, day)
148
+
149
+ day_or_time(day_start, time_tokens, options)
150
+ end
151
+
152
+ def handle_rmn_sd(tokens, options) #:nodoc:
153
+ handle_m_d(tokens[0].get_tag(RepeaterMonthName), tokens[1].get_tag(ScalarDay).type, tokens[2..tokens.size], options)
154
+ end
155
+
156
+ def handle_rmn_sd_on(tokens, options) #:nodoc:
157
+ if tokens.size > 3
158
+ handle_m_d(tokens[2].get_tag(RepeaterMonthName), tokens[3].get_tag(ScalarDay).type, tokens[0..1], options)
159
+ else
160
+ handle_m_d(tokens[1].get_tag(RepeaterMonthName), tokens[2].get_tag(ScalarDay).type, tokens[0..0], options)
161
+ end
162
+ end
163
+
164
+ def handle_rmn_od(tokens, options) #:nodoc:
165
+ handle_m_d(tokens[0].get_tag(RepeaterMonthName), tokens[1].get_tag(OrdinalDay).type, tokens[2..tokens.size], options)
166
+ end
167
+
168
+ def handle_rmn_od_sy(tokens, options) #:nodoc:
169
+ month = tokens[0].get_tag(RepeaterMonthName).index
170
+ day = tokens[1].get_tag(OrdinalDay).type
171
+ year = tokens[2].get_tag(ScalarYear).type
172
+
173
+ time_tokens = tokens.last(tokens.size - 3)
174
+
175
+ begin
176
+ day_start = Chronic.time_class.local(year, month, day)
177
+ day_or_time(day_start, time_tokens, options)
178
+ rescue ArgumentError
179
+ nil
180
+ end
181
+ end
182
+
183
+
184
+ def handle_rmn_od_on(tokens, options) #:nodoc:
185
+ if tokens.size > 3
186
+ handle_m_d(tokens[2].get_tag(RepeaterMonthName), tokens[3].get_tag(OrdinalDay).type, tokens[0..1], options)
187
+ else
188
+ handle_m_d(tokens[1].get_tag(RepeaterMonthName), tokens[2].get_tag(OrdinalDay).type, tokens[0..0], options)
189
+ end
190
+ end
191
+
192
+ def handle_rmn_sy(tokens, options) #:nodoc:
193
+ month = tokens[0].get_tag(RepeaterMonthName).index
194
+ year = tokens[1].get_tag(ScalarYear).type
195
+
196
+ if month == 12
197
+ next_month_year = year + 1
198
+ next_month_month = 1
199
+ else
200
+ next_month_year = year
201
+ next_month_month = month + 1
202
+ end
203
+
204
+ begin
205
+ Span.new(Chronic.time_class.local(year, month), Chronic.time_class.local(next_month_year, next_month_month))
206
+ rescue ArgumentError
207
+ nil
208
+ end
209
+ end
210
+
211
+ def handle_rdn_rmn_sd_t_tz_sy(tokens, options) #:nodoc:
212
+ t = Chronic.time_class.parse(@text)
213
+ Span.new(t, t + 1)
214
+ end
215
+
216
+ def handle_rmn_sd_sy(tokens, options) #:nodoc:
217
+ month = tokens[0].get_tag(RepeaterMonthName).index
218
+ day = tokens[1].get_tag(ScalarDay).type
219
+ year = tokens[2].get_tag(ScalarYear).type
220
+
221
+ time_tokens = tokens.last(tokens.size - 3)
222
+
223
+ begin
224
+ day_start = Chronic.time_class.local(year, month, day)
225
+ day_or_time(day_start, time_tokens, options)
226
+ rescue ArgumentError
227
+ nil
228
+ end
229
+ end
230
+
231
+ def handle_sd_rmn_sy(tokens, options) #:nodoc:
232
+ new_tokens = [tokens[1], tokens[0], tokens[2]]
233
+ time_tokens = tokens.last(tokens.size - 3)
234
+ self.handle_rmn_sd_sy(new_tokens + time_tokens, options)
235
+ end
236
+
237
+ def handle_sm_sd_sy(tokens, options) #:nodoc:
238
+ month = tokens[0].get_tag(ScalarMonth).type
239
+ day = tokens[1].get_tag(ScalarDay).type
240
+ year = tokens[2].get_tag(ScalarYear).type
241
+
242
+ time_tokens = tokens.last(tokens.size - 3)
243
+
244
+ begin
245
+ day_start = Chronic.time_class.local(year, month, day) #:nodoc:
246
+ day_or_time(day_start, time_tokens, options)
247
+ rescue ArgumentError
248
+ nil
249
+ end
250
+ end
251
+
252
+ def handle_sd_sm_sy(tokens, options) #:nodoc:
253
+ new_tokens = [tokens[1], tokens[0], tokens[2]]
254
+ time_tokens = tokens.last(tokens.size - 3)
255
+ self.handle_sm_sd_sy(new_tokens + time_tokens, options)
256
+ end
257
+
258
+ def handle_sy_sm_sd(tokens, options) #:nodoc:
259
+ new_tokens = [tokens[1], tokens[2], tokens[0]]
260
+ time_tokens = tokens.last(tokens.size - 3)
261
+ self.handle_sm_sd_sy(new_tokens + time_tokens, options)
262
+ end
263
+
264
+ def handle_sm_sy(tokens, options) #:nodoc:
265
+ month = tokens[0].get_tag(ScalarMonth).type
266
+ year = tokens[1].get_tag(ScalarYear).type
267
+
268
+ if month == 12
269
+ next_month_year = year + 1
270
+ next_month_month = 1
271
+ else
272
+ next_month_year = year
273
+ next_month_month = month + 1
274
+ end
275
+
276
+ begin
277
+ Span.new(Chronic.time_class.local(year, month), Chronic.time_class.local(next_month_year, next_month_month))
278
+ rescue ArgumentError
279
+ nil
280
+ end
281
+ end
282
+
283
+ # anchors
284
+
285
+ def handle_r(tokens, options) #:nodoc:
286
+ dd_tokens = dealias_and_disambiguate_times(tokens, options)
287
+ self.get_anchor(dd_tokens, options)
288
+ end
289
+
290
+ def handle_r_g_r(tokens, options) #:nodoc:
291
+ new_tokens = [tokens[1], tokens[0], tokens[2]]
292
+ self.handle_r(new_tokens, options)
293
+ end
294
+
295
+ # arrows
296
+
297
+ def handle_srp(tokens, span, options) #:nodoc:
298
+ distance = tokens[0].get_tag(Scalar).type
299
+ repeater = tokens[1].get_tag(Repeater)
300
+ pointer = tokens[2].get_tag(Pointer).type
301
+
302
+ repeater.offset(span, distance, pointer)
303
+ end
304
+
305
+ def handle_s_r_p(tokens, options) #:nodoc:
306
+ repeater = tokens[1].get_tag(Repeater)
307
+
308
+ # span =
309
+ # case true
310
+ # when [RepeaterYear, RepeaterSeason, RepeaterSeasonName, RepeaterMonth, RepeaterMonthName, RepeaterFortnight, RepeaterWeek].include?(repeater.class)
311
+ # self.parse("this hour", :guess => false, :now => @now)
312
+ # when [RepeaterWeekend, RepeaterDay, RepeaterDayName, RepeaterDayPortion, RepeaterHour].include?(repeater.class)
313
+ # self.parse("this minute", :guess => false, :now => @now)
314
+ # when [RepeaterMinute, RepeaterSecond].include?(repeater.class)
315
+ # self.parse("this second", :guess => false, :now => @now)
316
+ # else
317
+ # raise(ChronicPain, "Invalid repeater: #{repeater.class}")
318
+ # end
319
+
320
+ span = self.parse("this second", :guess => false, :now => @now)
321
+
322
+ self.handle_srp(tokens, span, options)
323
+ end
324
+
325
+ def handle_p_s_r(tokens, options) #:nodoc:
326
+ new_tokens = [tokens[1], tokens[2], tokens[0]]
327
+ self.handle_s_r_p(new_tokens, options)
328
+ end
329
+
330
+ def handle_s_r_p_a(tokens, options) #:nodoc:
331
+ anchor_span = get_anchor(tokens[3..tokens.size - 1], options)
332
+ self.handle_srp(tokens, anchor_span, options)
333
+ end
334
+
335
+ # narrows
336
+
337
+ def handle_orr(tokens, outer_span, options) #:nodoc:
338
+ repeater = tokens[1].get_tag(Repeater)
339
+ repeater.start = outer_span.begin - 1
340
+ ordinal = tokens[0].get_tag(Ordinal).type
341
+ span = nil
342
+ ordinal.times do
343
+ span = repeater.next(:future)
344
+ if span.begin > outer_span.end
345
+ span = nil
346
+ break
347
+ end
348
+ end
349
+ span
350
+ end
351
+
352
+ def handle_o_r_s_r(tokens, options) #:nodoc:
353
+ outer_span = get_anchor([tokens[3]], options)
354
+ handle_orr(tokens[0..1], outer_span, options)
355
+ end
356
+
357
+ def handle_o_r_g_r(tokens, options) #:nodoc:
358
+ outer_span = get_anchor(tokens[2..3], options)
359
+ handle_orr(tokens[0..1], outer_span, options)
360
+ end
361
+
362
+ # support methods
363
+
364
+ def get_anchor(tokens, options) #:nodoc:
365
+ grabber = Grabber.new(:this)
366
+ pointer = :future
367
+
368
+ repeaters = self.get_repeaters(tokens)
369
+ repeaters.size.times { tokens.pop }
370
+
371
+ if tokens.first && tokens.first.get_tag(Grabber)
372
+ grabber = tokens.first.get_tag(Grabber)
373
+ tokens.pop
374
+ end
375
+
376
+ head = repeaters.shift
377
+ head.start = @now
378
+
379
+ case grabber.type
380
+ when :last
381
+ outer_span = head.next(:past)
382
+ when :this
383
+ if repeaters.size > 0
384
+ outer_span = head.this(:none)
385
+ else
386
+ outer_span = head.this(options[:context])
387
+ end
388
+ when :next
389
+ outer_span = head.next(:future)
390
+ else raise(ChronicPain, "Invalid grabber")
391
+ end
392
+
393
+ puts "--#{outer_span}" if Chronic.debug
394
+ anchor = find_within(repeaters, outer_span, pointer)
395
+ end
396
+
397
+ def get_repeaters(tokens) #:nodoc:
398
+ repeaters = []
399
+ tokens.each do |token|
400
+ if t = token.get_tag(Repeater)
401
+ repeaters << t
402
+ end
403
+ end
404
+ repeaters.sort.reverse
405
+ end
406
+
407
+ # Recursively finds repeaters within other repeaters.
408
+ # Returns a Span representing the innermost time span
409
+ # or nil if no repeater union could be found
410
+ def find_within(tags, span, pointer) #:nodoc:
411
+ puts "--#{span}" if Chronic.debug
412
+ return span if tags.empty?
413
+
414
+ head, *rest = tags
415
+ head.start = pointer == :future ? span.begin : span.end
416
+ h = head.this(:none)
417
+
418
+ if span.include?(h.begin) || span.include?(h.end)
419
+ return find_within(rest, h, pointer)
420
+ else
421
+ return nil
422
+ end
423
+ end
424
+
425
+ def dealias_and_disambiguate_times(tokens, options) #:nodoc:
426
+ # handle aliases of am/pm
427
+ # 5:00 in the morning -> 5:00 am
428
+ # 7:00 in the evening -> 7:00 pm
429
+
430
+ day_portion_index = nil
431
+ tokens.each_with_index do |t, i|
432
+ if t.get_tag(RepeaterDayPortion)
433
+ day_portion_index = i
434
+ break
435
+ end
436
+ end
437
+
438
+ time_index = nil
439
+ tokens.each_with_index do |t, i|
440
+ if t.get_tag(RepeaterTime)
441
+ time_index = i
442
+ break
443
+ end
444
+ end
445
+
446
+ if (day_portion_index && time_index)
447
+ t1 = tokens[day_portion_index]
448
+ t1tag = t1.get_tag(RepeaterDayPortion)
449
+
450
+ if [:morning].include?(t1tag.type)
451
+ puts '--morning->am' if Chronic.debug
452
+ t1.untag(RepeaterDayPortion)
453
+ t1.tag(RepeaterDayPortion.new(:am))
454
+ elsif [:afternoon, :evening, :night].include?(t1tag.type)
455
+ puts "--#{t1tag.type}->pm" if Chronic.debug
456
+ t1.untag(RepeaterDayPortion)
457
+ t1.tag(RepeaterDayPortion.new(:pm))
458
+ end
459
+ end
460
+
461
+ # tokens.each_with_index do |t0, i|
462
+ # t1 = tokens[i + 1]
463
+ # if t1 && (t1tag = t1.get_tag(RepeaterDayPortion)) && t0.get_tag(RepeaterTime)
464
+ # if [:morning].include?(t1tag.type)
465
+ # puts '--morning->am' if Chronic.debug
466
+ # t1.untag(RepeaterDayPortion)
467
+ # t1.tag(RepeaterDayPortion.new(:am))
468
+ # elsif [:afternoon, :evening, :night].include?(t1tag.type)
469
+ # puts "--#{t1tag.type}->pm" if Chronic.debug
470
+ # t1.untag(RepeaterDayPortion)
471
+ # t1.tag(RepeaterDayPortion.new(:pm))
472
+ # end
473
+ # end
474
+ # end
475
+
476
+ # handle ambiguous times if :ambiguous_time_range is specified
477
+ if options[:ambiguous_time_range] != :none
478
+ ttokens = []
479
+ tokens.each_with_index do |t0, i|
480
+ ttokens << t0
481
+ t1 = tokens[i + 1]
482
+ if t0.get_tag(RepeaterTime) && t0.get_tag(RepeaterTime).type.ambiguous? && (!t1 || !t1.get_tag(RepeaterDayPortion))
483
+ distoken = Token.new('disambiguator')
484
+ distoken.tag(RepeaterDayPortion.new(options[:ambiguous_time_range]))
485
+ ttokens << distoken
486
+ end
487
+ end
488
+ tokens = ttokens
489
+ end
490
+
491
+ tokens
492
+ end
493
+
494
+ end
495
+
496
+ class Handler #:nodoc:
497
+ attr_accessor :pattern, :handler_method
498
+
499
+ def initialize(pattern, handler_method)
500
+ @pattern = pattern
501
+ @handler_method = handler_method
502
+ end
503
+
504
+ def constantize(name)
505
+ camel = name.to_s.gsub(/(^|_)(.)/) { $2.upcase }
506
+ ::Chronic.module_eval(camel, __FILE__, __LINE__)
507
+ end
508
+
509
+ def match(tokens, definitions)
510
+ token_index = 0
511
+ @pattern.each do |element|
512
+ name = element.to_s
513
+ optional = name.reverse[0..0] == '?'
514
+ name = name.chop if optional
515
+ if element.instance_of? Symbol
516
+ klass = constantize(name)
517
+ match = tokens[token_index] && !tokens[token_index].tags.select { |o| o.kind_of?(klass) }.empty?
518
+ return false if !match && !optional
519
+ (token_index += 1; next) if match
520
+ next if !match && optional
521
+ elsif element.instance_of? String
522
+ return true if optional && token_index == tokens.size
523
+ sub_handlers = definitions[name.intern] || raise(ChronicPain, "Invalid subset #{name} specified")
524
+ sub_handlers.each do |sub_handler|
525
+ return true if sub_handler.match(tokens[token_index..tokens.size], definitions)
526
+ end
527
+ return false
528
+ else
529
+ raise(ChronicPain, "Invalid match type: #{element.class}")
530
+ end
531
+ end
532
+ return false if token_index != tokens.size
533
+ return true
534
+ end
535
+
536
+ def ==(other)
537
+ self.pattern == other.pattern
538
+ end
539
+ end
540
+
541
+ end