mojombo-chronic 0.3.0

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