chronic-mmlac 0.6.4.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (64) hide show
  1. checksums.yaml +7 -0
  2. data/.gemtest +0 -0
  3. data/.gitignore +5 -0
  4. data/.yardopts +3 -0
  5. data/HISTORY.md +174 -0
  6. data/LICENSE +21 -0
  7. data/README.md +177 -0
  8. data/Rakefile +46 -0
  9. data/chronic.gemspec +17 -0
  10. data/lib/chronic/chronic.rb +325 -0
  11. data/lib/chronic/grabber.rb +31 -0
  12. data/lib/chronic/handler.rb +90 -0
  13. data/lib/chronic/handlers.rb +465 -0
  14. data/lib/chronic/mini_date.rb +38 -0
  15. data/lib/chronic/numerizer.rb +121 -0
  16. data/lib/chronic/ordinal.rb +44 -0
  17. data/lib/chronic/pointer.rb +30 -0
  18. data/lib/chronic/repeater.rb +135 -0
  19. data/lib/chronic/repeaters/repeater_day.rb +53 -0
  20. data/lib/chronic/repeaters/repeater_day_name.rb +52 -0
  21. data/lib/chronic/repeaters/repeater_day_portion.rb +94 -0
  22. data/lib/chronic/repeaters/repeater_fortnight.rb +71 -0
  23. data/lib/chronic/repeaters/repeater_hour.rb +58 -0
  24. data/lib/chronic/repeaters/repeater_minute.rb +58 -0
  25. data/lib/chronic/repeaters/repeater_month.rb +79 -0
  26. data/lib/chronic/repeaters/repeater_month_name.rb +94 -0
  27. data/lib/chronic/repeaters/repeater_season.rb +109 -0
  28. data/lib/chronic/repeaters/repeater_season_name.rb +43 -0
  29. data/lib/chronic/repeaters/repeater_second.rb +42 -0
  30. data/lib/chronic/repeaters/repeater_time.rb +128 -0
  31. data/lib/chronic/repeaters/repeater_week.rb +74 -0
  32. data/lib/chronic/repeaters/repeater_weekday.rb +85 -0
  33. data/lib/chronic/repeaters/repeater_weekend.rb +66 -0
  34. data/lib/chronic/repeaters/repeater_year.rb +77 -0
  35. data/lib/chronic/scalar.rb +109 -0
  36. data/lib/chronic/season.rb +37 -0
  37. data/lib/chronic/separator.rb +88 -0
  38. data/lib/chronic/span.rb +31 -0
  39. data/lib/chronic/tag.rb +42 -0
  40. data/lib/chronic/time_zone.rb +30 -0
  41. data/lib/chronic/token.rb +45 -0
  42. data/lib/chronic.rb +118 -0
  43. data/test/helper.rb +6 -0
  44. data/test/test_Chronic.rb +148 -0
  45. data/test/test_DaylightSavings.rb +118 -0
  46. data/test/test_Handler.rb +104 -0
  47. data/test/test_MiniDate.rb +32 -0
  48. data/test/test_Numerizer.rb +72 -0
  49. data/test/test_RepeaterDayName.rb +51 -0
  50. data/test/test_RepeaterFortnight.rb +62 -0
  51. data/test/test_RepeaterHour.rb +68 -0
  52. data/test/test_RepeaterMinute.rb +34 -0
  53. data/test/test_RepeaterMonth.rb +50 -0
  54. data/test/test_RepeaterMonthName.rb +56 -0
  55. data/test/test_RepeaterSeason.rb +40 -0
  56. data/test/test_RepeaterTime.rb +70 -0
  57. data/test/test_RepeaterWeek.rb +62 -0
  58. data/test/test_RepeaterWeekday.rb +55 -0
  59. data/test/test_RepeaterWeekend.rb +74 -0
  60. data/test/test_RepeaterYear.rb +69 -0
  61. data/test/test_Span.rb +23 -0
  62. data/test/test_Token.rb +25 -0
  63. data/test/test_parsing.rb +886 -0
  64. metadata +132 -0
@@ -0,0 +1,465 @@
1
+ module Chronic
2
+ module Handlers
3
+ module_function
4
+
5
+ # Handle month/day
6
+ def handle_m_d(month, day, time_tokens, options)
7
+ month.start = Chronic.now
8
+ span = month.this(options[:context])
9
+ year, month = span.begin.year, span.begin.month
10
+ day_start = Chronic.time_class.local(year, month, day)
11
+
12
+ day_or_time(day_start, time_tokens, options)
13
+ end
14
+
15
+ # Handle repeater-month-name/scalar-day
16
+ def handle_rmn_sd(tokens, options)
17
+ month = tokens[0].get_tag(RepeaterMonthName)
18
+ day = tokens[1].get_tag(ScalarDay).type
19
+
20
+ return if month_overflow?(Chronic.now.year, month.index, day)
21
+
22
+ handle_m_d(month, day, tokens[2..tokens.size], options)
23
+ end
24
+
25
+ # Handle repeater-month-name/scalar-day with separator-on
26
+ def handle_rmn_sd_on(tokens, options)
27
+ if tokens.size > 3
28
+ month = tokens[2].get_tag(RepeaterMonthName)
29
+ day = tokens[3].get_tag(ScalarDay).type
30
+ token_range = 0..1
31
+ else
32
+ month = tokens[1].get_tag(RepeaterMonthName)
33
+ day = tokens[2].get_tag(ScalarDay).type
34
+ token_range = 0..0
35
+ end
36
+
37
+ return if month_overflow?(Chronic.now.year, month.index, day)
38
+
39
+ handle_m_d(month, day, tokens[token_range], options)
40
+ end
41
+
42
+ # Handle repeater-month-name/ordinal-day
43
+ def handle_rmn_od(tokens, options)
44
+ month = tokens[0].get_tag(RepeaterMonthName)
45
+ day = tokens[1].get_tag(OrdinalDay).type
46
+
47
+ return if month_overflow?(Chronic.now.year, month.index, day)
48
+
49
+ handle_m_d(month, day, tokens[2..tokens.size], options)
50
+ end
51
+
52
+ # Handle ordinal-day/repeater-month-name
53
+ def handle_od_rmn(tokens, options)
54
+ month = tokens[1].get_tag(RepeaterMonthName)
55
+ day = tokens[0].get_tag(OrdinalDay).type
56
+
57
+ return if month_overflow?(Chronic.now.year, month.index, day)
58
+
59
+ handle_m_d(month, day, tokens[2..tokens.size], options)
60
+ end
61
+
62
+ def handle_sy_rmn_od(tokens, options)
63
+ year = tokens[0].get_tag(ScalarYear).type
64
+ month = tokens[1].get_tag(RepeaterMonthName).index
65
+ day = tokens[2].get_tag(OrdinalDay).type
66
+ time_tokens = tokens.last(tokens.size - 3)
67
+
68
+ return if month_overflow?(year, month, day)
69
+
70
+ begin
71
+ day_start = Chronic.time_class.local(year, month, day)
72
+ day_or_time(day_start, time_tokens, options)
73
+ rescue ArgumentError
74
+ nil
75
+ end
76
+ end
77
+
78
+ # Handle scalar-day/repeater-month-name
79
+ def handle_sd_rmn(tokens, options)
80
+ month = tokens[1].get_tag(RepeaterMonthName)
81
+ day = tokens[0].get_tag(ScalarDay).type
82
+
83
+ return if month_overflow?(Chronic.now.year, month.index, day)
84
+
85
+ handle_m_d(month, day, tokens[2..tokens.size], options)
86
+ end
87
+
88
+ # Handle repeater-month-name/ordinal-day with separator-on
89
+ def handle_rmn_od_on(tokens, options)
90
+ if tokens.size > 3
91
+ month = tokens[2].get_tag(RepeaterMonthName)
92
+ day = tokens[3].get_tag(OrdinalDay).type
93
+ token_range = 0..1
94
+ else
95
+ month = tokens[1].get_tag(RepeaterMonthName)
96
+ day = tokens[2].get_tag(OrdinalDay).type
97
+ token_range = 0..0
98
+ end
99
+
100
+ return if month_overflow?(Chronic.now.year, month.index, day)
101
+
102
+ handle_m_d(month, day, tokens[token_range], options)
103
+ end
104
+
105
+ # Handle repeater-month-name/scalar-year
106
+ def handle_rmn_sy(tokens, options)
107
+ month = tokens[0].get_tag(RepeaterMonthName).index
108
+ year = tokens[1].get_tag(ScalarYear).type
109
+
110
+ if month == 12
111
+ next_month_year = year + 1
112
+ next_month_month = 1
113
+ else
114
+ next_month_year = year
115
+ next_month_month = month + 1
116
+ end
117
+
118
+ begin
119
+ end_time = Chronic.time_class.local(next_month_year, next_month_month)
120
+ Span.new(Chronic.time_class.local(year, month), end_time)
121
+ rescue ArgumentError
122
+ nil
123
+ end
124
+ end
125
+
126
+ # Handle generic timestamp (ruby 1.8)
127
+ def handle_rdn_rmn_sd_t_tz_sy(tokens, options)
128
+ t = Chronic.time_class.parse(options[:text])
129
+ Span.new(t, t + 1)
130
+ end
131
+
132
+ # Handle generic timestamp (ruby 1.9)
133
+ def handle_sy_sm_sd_t_tz(tokens, options)
134
+ t = Chronic.time_class.parse(options[:text])
135
+ Span.new(t, t + 1)
136
+ end
137
+
138
+ # Handle repeater-month-name/scalar-day/scalar-year
139
+ def handle_rmn_sd_sy(tokens, options)
140
+ month = tokens[0].get_tag(RepeaterMonthName).index
141
+ day = tokens[1].get_tag(ScalarDay).type
142
+ year = tokens[2].get_tag(ScalarYear).type
143
+ time_tokens = tokens.last(tokens.size - 3)
144
+
145
+ return if month_overflow?(year, month, day)
146
+
147
+ begin
148
+ day_start = Chronic.time_class.local(year, month, day)
149
+ day_or_time(day_start, time_tokens, options)
150
+ rescue ArgumentError
151
+ nil
152
+ end
153
+ end
154
+
155
+ # Handle repeater-month-name/ordinal-day/scalar-year
156
+ def handle_rmn_od_sy(tokens, options)
157
+ month = tokens[0].get_tag(RepeaterMonthName).index
158
+ day = tokens[1].get_tag(OrdinalDay).type
159
+ year = tokens[2].get_tag(ScalarYear).type
160
+ time_tokens = tokens.last(tokens.size - 3)
161
+
162
+ return if month_overflow?(year, month, day)
163
+
164
+ begin
165
+ day_start = Chronic.time_class.local(year, month, day)
166
+ day_or_time(day_start, time_tokens, options)
167
+ rescue ArgumentError
168
+ nil
169
+ end
170
+ end
171
+
172
+ # Handle oridinal-day/repeater-month-name/scalar-year
173
+ def handle_od_rmn_sy(tokens, options)
174
+ day = tokens[0].get_tag(OrdinalDay).type
175
+ month = tokens[1].get_tag(RepeaterMonthName).index
176
+ year = tokens[2].get_tag(ScalarYear).type
177
+ time_tokens = tokens.last(tokens.size - 3)
178
+
179
+ return if month_overflow?(year, month, day)
180
+
181
+ begin
182
+ day_start = Chronic.time_class.local(year, month, day)
183
+ day_or_time(day_start, time_tokens, options)
184
+ rescue ArgumentError
185
+ nil
186
+ end
187
+ end
188
+
189
+ # Handle scalar-day/repeater-month-name/scalar-year
190
+ def handle_sd_rmn_sy(tokens, options)
191
+ new_tokens = [tokens[1], tokens[0], tokens[2]]
192
+ time_tokens = tokens.last(tokens.size - 3)
193
+ handle_rmn_sd_sy(new_tokens + time_tokens, options)
194
+ end
195
+
196
+ # Handle scalar-month/scalar-day/scalar-year (endian middle)
197
+ def handle_sm_sd_sy(tokens, options)
198
+ month = tokens[0].get_tag(ScalarMonth).type
199
+ day = tokens[1].get_tag(ScalarDay).type
200
+ year = tokens[2].get_tag(ScalarYear).type
201
+ time_tokens = tokens.last(tokens.size - 3)
202
+
203
+ return if month_overflow?(year, month, day)
204
+
205
+ begin
206
+ day_start = Chronic.time_class.local(year, month, day)
207
+ day_or_time(day_start, time_tokens, options)
208
+ rescue ArgumentError
209
+ nil
210
+ end
211
+ end
212
+
213
+ # Handle scalar-day/scalar-month/scalar-year (endian little)
214
+ def handle_sd_sm_sy(tokens, options)
215
+ new_tokens = [tokens[1], tokens[0], tokens[2]]
216
+ time_tokens = tokens.last(tokens.size - 3)
217
+ handle_sm_sd_sy(new_tokens + time_tokens, options)
218
+ end
219
+
220
+ # Handle scalar-year/scalar-month/scalar-day
221
+ def handle_sy_sm_sd(tokens, options)
222
+ new_tokens = [tokens[1], tokens[2], tokens[0]]
223
+ time_tokens = tokens.last(tokens.size - 3)
224
+ handle_sm_sd_sy(new_tokens + time_tokens, options)
225
+ end
226
+
227
+ # Handle scalar-month/scalar-year
228
+ def handle_sm_sy(tokens, options)
229
+ month = tokens[0].get_tag(ScalarMonth).type
230
+ year = tokens[1].get_tag(ScalarYear).type
231
+
232
+ if month == 12
233
+ next_month_year = year + 1
234
+ next_month_month = 1
235
+ else
236
+ next_month_year = year
237
+ next_month_month = month + 1
238
+ end
239
+
240
+ begin
241
+ end_time = Chronic.time_class.local(next_month_year, next_month_month)
242
+ Span.new(Chronic.time_class.local(year, month), end_time)
243
+ rescue ArgumentError
244
+ nil
245
+ end
246
+ end
247
+
248
+ # anchors
249
+
250
+ # Handle repeaters
251
+ def handle_r(tokens, options)
252
+ dd_tokens = dealias_and_disambiguate_times(tokens, options)
253
+ get_anchor(dd_tokens, options)
254
+ end
255
+
256
+ # Handle repeater/grabber/repeater
257
+ def handle_r_g_r(tokens, options)
258
+ new_tokens = [tokens[1], tokens[0], tokens[2]]
259
+ handle_r(new_tokens, options)
260
+ end
261
+
262
+ # arrows
263
+
264
+ # Handle scalar/repeater/pointer helper
265
+ def handle_srp(tokens, span, options)
266
+ distance = tokens[0].get_tag(Scalar).type
267
+ repeater = tokens[1].get_tag(Repeater)
268
+ pointer = tokens[2].get_tag(Pointer).type
269
+
270
+ repeater.offset(span, distance, pointer)
271
+ end
272
+
273
+ # Handle scalar/repeater/pointer
274
+ def handle_s_r_p(tokens, options)
275
+ repeater = tokens[1].get_tag(Repeater)
276
+ span = Span.new(Chronic.now, Chronic.now + 1)
277
+
278
+ handle_srp(tokens, span, options)
279
+ end
280
+
281
+ # Handle pointer/scalar/repeater
282
+ def handle_p_s_r(tokens, options)
283
+ new_tokens = [tokens[1], tokens[2], tokens[0]]
284
+ handle_s_r_p(new_tokens, options)
285
+ end
286
+
287
+ # Handle scalar/repeater/pointer/anchor
288
+ def handle_s_r_p_a(tokens, options)
289
+ anchor_span = get_anchor(tokens[3..tokens.size - 1], options)
290
+ handle_srp(tokens, anchor_span, options)
291
+ end
292
+
293
+ # narrows
294
+
295
+ # Handle oridinal repeaters
296
+ def handle_orr(tokens, outer_span, options)
297
+ repeater = tokens[1].get_tag(Repeater)
298
+ repeater.start = outer_span.begin - 1
299
+ ordinal = tokens[0].get_tag(Ordinal).type
300
+ span = nil
301
+
302
+ ordinal.times do
303
+ span = repeater.next(:future)
304
+
305
+ if span.begin > outer_span.end
306
+ span = nil
307
+ break
308
+ end
309
+ end
310
+
311
+ span
312
+ end
313
+
314
+ # Handle ordinal/repeater/separator/repeater
315
+ def handle_o_r_s_r(tokens, options)
316
+ outer_span = get_anchor([tokens[3]], options)
317
+ handle_orr(tokens[0..1], outer_span, options)
318
+ end
319
+
320
+ # Handle ordinal/repeater/grabber/repeater
321
+ def handle_o_r_g_r(tokens, options)
322
+ outer_span = get_anchor(tokens[2..3], options)
323
+ handle_orr(tokens[0..1], outer_span, options)
324
+ end
325
+
326
+ # support methods
327
+
328
+ def day_or_time(day_start, time_tokens, options)
329
+ outer_span = Span.new(day_start, day_start + (24 * 60 * 60))
330
+
331
+ if !time_tokens.empty?
332
+ Chronic.now = outer_span.begin
333
+ get_anchor(dealias_and_disambiguate_times(time_tokens, options), options)
334
+ else
335
+ outer_span
336
+ end
337
+ end
338
+
339
+ def get_anchor(tokens, options)
340
+ grabber = Grabber.new(:this)
341
+ pointer = :future
342
+
343
+ repeaters = get_repeaters(tokens)
344
+ repeaters.size.times { tokens.pop }
345
+
346
+ if tokens.first && tokens.first.get_tag(Grabber)
347
+ grabber = tokens.shift.get_tag(Grabber)
348
+ end
349
+
350
+ head = repeaters.shift
351
+ head.start = Chronic.now
352
+
353
+ case grabber.type
354
+ when :last
355
+ outer_span = head.next(:past)
356
+ when :this
357
+ if options[:context] != :past and repeaters.size > 0
358
+ outer_span = head.this(:none)
359
+ else
360
+ outer_span = head.this(options[:context])
361
+ end
362
+ when :next
363
+ outer_span = head.next(:future)
364
+ else
365
+ raise ChronicPain, "Invalid grabber"
366
+ end
367
+
368
+ if Chronic.debug
369
+ puts "Handler-class: #{head.class}"
370
+ puts "--#{outer_span}"
371
+ end
372
+
373
+ find_within(repeaters, outer_span, pointer)
374
+ end
375
+
376
+ def get_repeaters(tokens)
377
+ tokens.map { |token| token.get_tag(Repeater) }.compact.sort.reverse
378
+ end
379
+
380
+ def month_overflow?(year, month, day)
381
+ if Date.leap?(year)
382
+ day > RepeaterMonth::MONTH_DAYS_LEAP[month - 1]
383
+ else
384
+ day > RepeaterMonth::MONTH_DAYS[month - 1]
385
+ end
386
+ end
387
+
388
+ # Recursively finds repeaters within other repeaters.
389
+ # Returns a Span representing the innermost time span
390
+ # or nil if no repeater union could be found
391
+ def find_within(tags, span, pointer)
392
+ puts "--#{span}" if Chronic.debug
393
+ return span if tags.empty?
394
+
395
+ head = tags.shift
396
+ head.start = (pointer == :future ? span.begin : span.end)
397
+ h = head.this(:none)
398
+
399
+ if span.cover?(h.begin) || span.cover?(h.end)
400
+ find_within(tags, h, pointer)
401
+ end
402
+ end
403
+
404
+ def dealias_and_disambiguate_times(tokens, options)
405
+ # handle aliases of am/pm
406
+ # 5:00 in the morning -> 5:00 am
407
+ # 7:00 in the evening -> 7:00 pm
408
+
409
+ day_portion_index = nil
410
+ tokens.each_with_index do |t, i|
411
+ if t.get_tag(RepeaterDayPortion)
412
+ day_portion_index = i
413
+ break
414
+ end
415
+ end
416
+
417
+ time_index = nil
418
+ tokens.each_with_index do |t, i|
419
+ if t.get_tag(RepeaterTime)
420
+ time_index = i
421
+ break
422
+ end
423
+ end
424
+
425
+ if day_portion_index && time_index
426
+ t1 = tokens[day_portion_index]
427
+ t1tag = t1.get_tag(RepeaterDayPortion)
428
+
429
+ case t1tag.type
430
+ when :morning
431
+ puts '--morning->am' if Chronic.debug
432
+ t1.untag(RepeaterDayPortion)
433
+ t1.tag(RepeaterDayPortion.new(:am))
434
+ when :afternoon, :evening, :night
435
+ puts "--#{t1tag.type}->pm" if Chronic.debug
436
+ t1.untag(RepeaterDayPortion)
437
+ t1.tag(RepeaterDayPortion.new(:pm))
438
+ end
439
+ end
440
+
441
+ # handle ambiguous times if :ambiguous_time_range is specified
442
+ if options[:ambiguous_time_range] != :none
443
+ ambiguous_tokens = []
444
+
445
+ tokens.each_with_index do |token, i|
446
+ ambiguous_tokens << token
447
+ next_token = tokens[i + 1]
448
+
449
+ if token.get_tag(RepeaterTime) && token.get_tag(RepeaterTime).type.ambiguous? && (!next_token || !next_token.get_tag(RepeaterDayPortion))
450
+ distoken = Token.new('disambiguator')
451
+
452
+ distoken.tag(RepeaterDayPortion.new(options[:ambiguous_time_range]))
453
+ ambiguous_tokens << distoken
454
+ end
455
+ end
456
+
457
+ tokens = ambiguous_tokens
458
+ end
459
+
460
+ tokens
461
+ end
462
+
463
+ end
464
+
465
+ end
@@ -0,0 +1,38 @@
1
+ module Chronic
2
+ class MiniDate
3
+ attr_accessor :month, :day
4
+
5
+ def self.from_time(time)
6
+ new(time.month, time.day)
7
+ end
8
+
9
+ def initialize(month, day)
10
+ unless (1..12).include?(month)
11
+ raise ArgumentError, "1..12 are valid months"
12
+ end
13
+
14
+ @month = month
15
+ @day = day
16
+ end
17
+
18
+ def is_between?(md_start, md_end)
19
+ return false if (@month == md_start.month && @month == md_end.month) &&
20
+ (@day < md_start.day || @day > md_end.day)
21
+ return true if (@month == md_start.month && @day >= md_start.day) ||
22
+ (@month == md_end.month && @day <= md_end.day)
23
+
24
+ i = (md_start.month % 12) + 1
25
+
26
+ until i == md_end.month
27
+ return true if @month == i
28
+ i = (i % 12) + 1
29
+ end
30
+
31
+ return false
32
+ end
33
+
34
+ def equals?(other)
35
+ @month == other.month and @day == other.day
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,121 @@
1
+ require 'strscan'
2
+
3
+ module Chronic
4
+ class Numerizer
5
+
6
+ DIRECT_NUMS = [
7
+ ['eleven', '11'],
8
+ ['twelve', '12'],
9
+ ['thirteen', '13'],
10
+ ['fourteen', '14'],
11
+ ['fifteen', '15'],
12
+ ['sixteen', '16'],
13
+ ['seventeen', '17'],
14
+ ['eighteen', '18'],
15
+ ['nineteen', '19'],
16
+ ['ninteen', '19'], # Common mis-spelling
17
+ ['zero', '0'],
18
+ ['one', '1'],
19
+ ['two', '2'],
20
+ ['three', '3'],
21
+ ['four(\W|$)', '4\1'], # The weird regex is so that it matches four but not fourty
22
+ ['five', '5'],
23
+ ['six(\W|$)', '6\1'],
24
+ ['seven(\W|$)', '7\1'],
25
+ ['eight(\W|$)', '8\1'],
26
+ ['nine(\W|$)', '9\1'],
27
+ ['ten', '10'],
28
+ ['\ba[\b^$]', '1'] # doesn't make sense for an 'a' at the end to be a 1
29
+ ]
30
+
31
+ ORDINALS = [
32
+ ['first', '1'],
33
+ ['third', '3'],
34
+ ['fourth', '4'],
35
+ ['fifth', '5'],
36
+ ['sixth', '6'],
37
+ ['seventh', '7'],
38
+ ['eighth', '8'],
39
+ ['ninth', '9'],
40
+ ['tenth', '10']
41
+ ]
42
+
43
+ TEN_PREFIXES = [
44
+ ['twenty', 20],
45
+ ['thirty', 30],
46
+ ['forty', 40],
47
+ ['fourty', 40], # Common mis-spelling
48
+ ['fifty', 50],
49
+ ['sixty', 60],
50
+ ['seventy', 70],
51
+ ['eighty', 80],
52
+ ['ninety', 90]
53
+ ]
54
+
55
+ BIG_PREFIXES = [
56
+ ['hundred', 100],
57
+ ['thousand', 1000],
58
+ ['million', 1_000_000],
59
+ ['billion', 1_000_000_000],
60
+ ['trillion', 1_000_000_000_000],
61
+ ]
62
+
63
+ def self.numerize(string)
64
+ string = string.dup
65
+
66
+ # preprocess
67
+ string.gsub!(/ +|([^\d])-([^\d])/, '\1 \2') # will mutilate hyphenated-words but shouldn't matter for date extraction
68
+ string.gsub!(/a half/, 'haAlf') # take the 'a' out so it doesn't turn into a 1, save the half for the end
69
+
70
+ # easy/direct replacements
71
+
72
+ DIRECT_NUMS.each do |dn|
73
+ string.gsub!(/#{dn[0]}/i, '<num>' + dn[1])
74
+ end
75
+
76
+ ORDINALS.each do |on|
77
+ string.gsub!(/#{on[0]}/i, '<num>' + on[1] + on[0][-2, 2])
78
+ end
79
+
80
+ # ten, twenty, etc.
81
+
82
+ TEN_PREFIXES.each do |tp|
83
+ string.gsub!(/(?:#{tp[0]}) *<num>(\d(?=[^\d]|$))*/i) { '<num>' + (tp[1] + $1.to_i).to_s }
84
+ end
85
+
86
+ TEN_PREFIXES.each do |tp|
87
+ string.gsub!(/#{tp[0]}/i) { '<num>' + tp[1].to_s }
88
+ end
89
+
90
+ # hundreds, thousands, millions, etc.
91
+
92
+ BIG_PREFIXES.each do |bp|
93
+ string.gsub!(/(?:<num>)?(\d*) *#{bp[0]}/i) { '<num>' + (bp[1] * $1.to_i).to_s}
94
+ andition(string)
95
+ end
96
+
97
+ # fractional addition
98
+ # I'm not combining this with the previous block as using float addition complicates the strings
99
+ # (with extraneous .0's and such )
100
+ string.gsub!(/(\d+)(?: | and |-)*haAlf/i) { ($1.to_f + 0.5).to_s }
101
+
102
+ string.gsub(/<num>/, '')
103
+ end
104
+
105
+ class << self
106
+ private
107
+
108
+ def andition(string)
109
+ sc = StringScanner.new(string)
110
+
111
+ while sc.scan_until(/<num>(\d+)( | and )<num>(\d+)(?=[^\w]|$)/i)
112
+ if sc[2] =~ /and/ || sc[1].size > sc[3].size
113
+ string[(sc.pos - sc.matched_size)..(sc.pos-1)] = '<num>' + (sc[1].to_i + sc[3].to_i).to_s
114
+ sc.reset
115
+ end
116
+ end
117
+ end
118
+
119
+ end
120
+ end
121
+ end
@@ -0,0 +1,44 @@
1
+ module Chronic
2
+ class Ordinal < Tag
3
+
4
+ # Scan an Array of {Token}s and apply any necessary Ordinal tags to
5
+ # each token
6
+ #
7
+ # @param [Array<Token>] tokens Array of tokens to scan
8
+ # @param [Hash] options Options specified in {Chronic.parse}
9
+ # @return [Array] list of tokens
10
+ def self.scan(tokens, options)
11
+ tokens.each do |token|
12
+ if t = scan_for_ordinals(token) then token.tag(t) end
13
+ if t = scan_for_days(token) then token.tag(t) end
14
+ end
15
+ end
16
+
17
+ # @param [Token] token
18
+ # @return [Ordinal, nil]
19
+ def self.scan_for_ordinals(token)
20
+ Ordinal.new($1.to_i) if token.word =~ /^(\d*)(st|nd|rd|th)$/
21
+ end
22
+
23
+ # @param [Token] token
24
+ # @return [OrdinalDay, nil]
25
+ def self.scan_for_days(token)
26
+ if token.word =~ /^(\d*)(st|nd|rd|th)$/
27
+ unless $1.to_i > 31 || $1.to_i < 1
28
+ OrdinalDay.new(token.word.to_i)
29
+ end
30
+ end
31
+ end
32
+
33
+ def to_s
34
+ 'ordinal'
35
+ end
36
+ end
37
+
38
+ class OrdinalDay < Ordinal #:nodoc:
39
+ def to_s
40
+ super << '-day-' << @type.to_s
41
+ end
42
+ end
43
+
44
+ end
@@ -0,0 +1,30 @@
1
+ module Chronic
2
+ class Pointer < Tag
3
+
4
+ # Scan an Array of {Token}s and apply any necessary Pointer tags to
5
+ # each token
6
+ #
7
+ # @param [Array<Token>] tokens Array of tokens to scan
8
+ # @param [Hash] options Options specified in {Chronic.parse}
9
+ # @return [Array] list of tokens
10
+ def self.scan(tokens, options)
11
+ tokens.each do |token|
12
+ if t = scan_for_all(token) then token.tag(t) end
13
+ end
14
+ end
15
+
16
+ # @param [Token] token
17
+ # @return [Pointer, nil]
18
+ def self.scan_for_all(token)
19
+ scan_for token, self,
20
+ {
21
+ /\bpast\b/ => :past,
22
+ /\b(?:future|in)\b/ => :future,
23
+ }
24
+ end
25
+
26
+ def to_s
27
+ 'pointer-' << @type.to_s
28
+ end
29
+ end
30
+ end