pangel-chronic 0.3.0.2

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