gitlab-chronic 0.10.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (75) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +8 -0
  3. data/.gitlab-ci.yml +14 -0
  4. data/.travis.yml +10 -0
  5. data/Gemfile +3 -0
  6. data/HISTORY.md +252 -0
  7. data/LICENSE +21 -0
  8. data/README.md +188 -0
  9. data/Rakefile +68 -0
  10. data/chronic.gemspec +25 -0
  11. data/lib/chronic.rb +155 -0
  12. data/lib/chronic/date.rb +81 -0
  13. data/lib/chronic/definition.rb +128 -0
  14. data/lib/chronic/dictionary.rb +36 -0
  15. data/lib/chronic/handler.rb +97 -0
  16. data/lib/chronic/handlers.rb +672 -0
  17. data/lib/chronic/mini_date.rb +38 -0
  18. data/lib/chronic/parser.rb +222 -0
  19. data/lib/chronic/repeaters/repeater_day.rb +54 -0
  20. data/lib/chronic/repeaters/repeater_day_name.rb +53 -0
  21. data/lib/chronic/repeaters/repeater_day_portion.rb +109 -0
  22. data/lib/chronic/repeaters/repeater_fortnight.rb +72 -0
  23. data/lib/chronic/repeaters/repeater_hour.rb +59 -0
  24. data/lib/chronic/repeaters/repeater_minute.rb +59 -0
  25. data/lib/chronic/repeaters/repeater_month.rb +80 -0
  26. data/lib/chronic/repeaters/repeater_month_name.rb +95 -0
  27. data/lib/chronic/repeaters/repeater_quarter.rb +59 -0
  28. data/lib/chronic/repeaters/repeater_quarter_name.rb +40 -0
  29. data/lib/chronic/repeaters/repeater_season.rb +111 -0
  30. data/lib/chronic/repeaters/repeater_season_name.rb +43 -0
  31. data/lib/chronic/repeaters/repeater_second.rb +43 -0
  32. data/lib/chronic/repeaters/repeater_time.rb +159 -0
  33. data/lib/chronic/repeaters/repeater_week.rb +76 -0
  34. data/lib/chronic/repeaters/repeater_weekday.rb +86 -0
  35. data/lib/chronic/repeaters/repeater_weekend.rb +67 -0
  36. data/lib/chronic/repeaters/repeater_year.rb +78 -0
  37. data/lib/chronic/season.rb +26 -0
  38. data/lib/chronic/span.rb +31 -0
  39. data/lib/chronic/tag.rb +89 -0
  40. data/lib/chronic/tags/grabber.rb +29 -0
  41. data/lib/chronic/tags/ordinal.rb +52 -0
  42. data/lib/chronic/tags/pointer.rb +28 -0
  43. data/lib/chronic/tags/repeater.rb +160 -0
  44. data/lib/chronic/tags/scalar.rb +89 -0
  45. data/lib/chronic/tags/separator.rb +123 -0
  46. data/lib/chronic/tags/sign.rb +35 -0
  47. data/lib/chronic/tags/time_zone.rb +32 -0
  48. data/lib/chronic/time.rb +40 -0
  49. data/lib/chronic/token.rb +61 -0
  50. data/lib/chronic/tokenizer.rb +38 -0
  51. data/lib/chronic/version.rb +3 -0
  52. data/test/helper.rb +12 -0
  53. data/test/test_chronic.rb +203 -0
  54. data/test/test_daylight_savings.rb +122 -0
  55. data/test/test_handler.rb +128 -0
  56. data/test/test_mini_date.rb +32 -0
  57. data/test/test_parsing.rb +1537 -0
  58. data/test/test_repeater_day_name.rb +51 -0
  59. data/test/test_repeater_day_portion.rb +254 -0
  60. data/test/test_repeater_fortnight.rb +62 -0
  61. data/test/test_repeater_hour.rb +68 -0
  62. data/test/test_repeater_minute.rb +34 -0
  63. data/test/test_repeater_month.rb +50 -0
  64. data/test/test_repeater_month_name.rb +56 -0
  65. data/test/test_repeater_quarter.rb +70 -0
  66. data/test/test_repeater_quarter_name.rb +198 -0
  67. data/test/test_repeater_season.rb +40 -0
  68. data/test/test_repeater_time.rb +88 -0
  69. data/test/test_repeater_week.rb +115 -0
  70. data/test/test_repeater_weekday.rb +55 -0
  71. data/test/test_repeater_weekend.rb +74 -0
  72. data/test/test_repeater_year.rb +69 -0
  73. data/test/test_span.rb +23 -0
  74. data/test/test_token.rb +31 -0
  75. metadata +215 -0
@@ -0,0 +1,36 @@
1
+ require 'chronic/definition'
2
+
3
+ module Chronic
4
+ # A collection of definitions
5
+ class Dictionary
6
+ attr_reader :defined_items, :options
7
+
8
+ def initialize(options = {})
9
+ @options = options
10
+ @defined_items = []
11
+ end
12
+
13
+ # returns a hash of each word's Definitions
14
+ def definitions
15
+ defined_items.each_with_object({}) do |word, defs|
16
+ word_type = "#{word.capitalize.to_s + 'Definitions'}"
17
+ defs[word] = Chronic.const_get(word_type).new(options).definitions
18
+ end
19
+ end
20
+ end
21
+
22
+ class SpanDictionary < Dictionary
23
+ # Collection of SpanDefinitions
24
+ def initialize(options = {})
25
+ super
26
+ @defined_items = [:time,:date,:anchor,:arrow,:narrow,:endian]
27
+ end
28
+
29
+ # returns the definitions of a specific subclass of SpanDefinitions
30
+ # SpanDefinition#definitions returns an Hash of Handler instances
31
+ # arguments should come in as symbols
32
+ def [](handler_type=:symbol)
33
+ definitions[handler_type]
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,97 @@
1
+ module Chronic
2
+ class Handler
3
+
4
+ attr_reader :pattern
5
+
6
+ attr_reader :handler_method
7
+
8
+ # pattern - An Array of patterns to match tokens against.
9
+ # handler_method - A Symbol representing the method to be invoked
10
+ # when a pattern matches.
11
+ def initialize(pattern, handler_method)
12
+ @pattern = pattern
13
+ @handler_method = handler_method
14
+ end
15
+
16
+ # tokens - An Array of tokens to process.
17
+ # definitions - A Hash of definitions to check against.
18
+ #
19
+ # Returns true if a match is found.
20
+ def match(tokens, definitions)
21
+ token_index = 0
22
+ @pattern.each do |elements|
23
+ was_optional = false
24
+ elements = [elements] unless elements.is_a?(Array)
25
+
26
+ elements.each_index do |i|
27
+ name = elements[i].to_s
28
+ optional = name[-1, 1] == '?'
29
+ name = name.chop if optional
30
+
31
+ case elements[i]
32
+ when Symbol
33
+ if tags_match?(name, tokens, token_index)
34
+ token_index += 1
35
+ break
36
+ else
37
+ if optional
38
+ was_optional = true
39
+ next
40
+ elsif i + 1 < elements.count
41
+ next
42
+ else
43
+ return false unless was_optional
44
+ end
45
+ end
46
+
47
+ when String
48
+ return true if optional && token_index == tokens.size
49
+
50
+ if definitions.key?(name.to_sym)
51
+ sub_handlers = definitions[name.to_sym]
52
+ else
53
+ raise "Invalid subset #{name} specified"
54
+ end
55
+
56
+ sub_handlers.each do |sub_handler|
57
+ return true if sub_handler.match(tokens[token_index..tokens.size], definitions)
58
+ end
59
+ else
60
+ raise "Invalid match type: #{elements[i].class}"
61
+ end
62
+ end
63
+
64
+ end
65
+
66
+ return false if token_index != tokens.size
67
+ return true
68
+ end
69
+
70
+ def invoke(type, tokens, parser, options)
71
+ if Chronic.debug
72
+ puts "-#{type}"
73
+ puts "Handler: #{@handler_method}"
74
+ end
75
+
76
+ parser.send(@handler_method, tokens, options)
77
+ end
78
+
79
+ # other - The other Handler object to compare.
80
+ #
81
+ # Returns true if these Handlers match.
82
+ def ==(other)
83
+ @pattern == other.pattern
84
+ end
85
+
86
+ private
87
+
88
+ def tags_match?(name, tokens, token_index)
89
+ klass = Chronic.const_get(name.to_s.gsub(/(?:^|_)(.)/) { $1.upcase })
90
+
91
+ if tokens[token_index]
92
+ !tokens[token_index].tags.select { |o| o.kind_of?(klass) }.empty?
93
+ end
94
+ end
95
+
96
+ end
97
+ end
@@ -0,0 +1,672 @@
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 = self.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
+ day_start = Chronic.time_class.local(year + 1, month, day) if options[:context] == :future && day_start < now
12
+
13
+ day_or_time(day_start, time_tokens, options)
14
+ end
15
+
16
+ # Handle repeater-month-name/scalar-day
17
+ def handle_rmn_sd(tokens, options)
18
+ month = tokens[0].get_tag(RepeaterMonthName)
19
+ day = tokens[1].get_tag(ScalarDay).type
20
+
21
+ return if month_overflow?(self.now.year, month.index, day)
22
+
23
+ handle_m_d(month, day, tokens[2..tokens.size], options)
24
+ end
25
+
26
+ # Handle repeater-month-name/scalar-day with separator-on
27
+ def handle_rmn_sd_on(tokens, options)
28
+ if tokens.size > 3
29
+ month = tokens[2].get_tag(RepeaterMonthName)
30
+ day = tokens[3].get_tag(ScalarDay).type
31
+ token_range = 0..1
32
+ else
33
+ month = tokens[1].get_tag(RepeaterMonthName)
34
+ day = tokens[2].get_tag(ScalarDay).type
35
+ token_range = 0..0
36
+ end
37
+
38
+ return if month_overflow?(self.now.year, month.index, day)
39
+
40
+ handle_m_d(month, day, tokens[token_range], options)
41
+ end
42
+
43
+ # Handle repeater-month-name/ordinal-day
44
+ def handle_rmn_od(tokens, options)
45
+ month = tokens[0].get_tag(RepeaterMonthName)
46
+ day = tokens[1].get_tag(OrdinalDay).type
47
+
48
+ return if month_overflow?(self.now.year, month.index, day)
49
+
50
+ handle_m_d(month, day, tokens[2..tokens.size], options)
51
+ end
52
+
53
+ # Handle ordinal this month
54
+ def handle_od_rm(tokens, options)
55
+ day = tokens[0].get_tag(OrdinalDay).type
56
+ month = tokens[2].get_tag(RepeaterMonth)
57
+ handle_m_d(month, day, tokens[3..tokens.size], options)
58
+ end
59
+
60
+ # Handle ordinal-day/repeater-month-name
61
+ def handle_od_rmn(tokens, options)
62
+ month = tokens[1].get_tag(RepeaterMonthName)
63
+ day = tokens[0].get_tag(OrdinalDay).type
64
+
65
+ return if month_overflow?(self.now.year, month.index, day)
66
+
67
+ handle_m_d(month, day, tokens[2..tokens.size], options)
68
+ end
69
+
70
+ def handle_sy_rmn_od(tokens, options)
71
+ year = tokens[0].get_tag(ScalarYear).type
72
+ month = tokens[1].get_tag(RepeaterMonthName).index
73
+ day = tokens[2].get_tag(OrdinalDay).type
74
+ time_tokens = tokens.last(tokens.size - 3)
75
+
76
+ return if month_overflow?(year, month, day)
77
+
78
+ begin
79
+ day_start = Chronic.time_class.local(year, month, day)
80
+ day_or_time(day_start, time_tokens, options)
81
+ rescue ArgumentError
82
+ nil
83
+ end
84
+ end
85
+
86
+ # Handle scalar-day/repeater-month-name
87
+ def handle_sd_rmn(tokens, options)
88
+ month = tokens[1].get_tag(RepeaterMonthName)
89
+ day = tokens[0].get_tag(ScalarDay).type
90
+
91
+ return if month_overflow?(self.now.year, month.index, day)
92
+
93
+ handle_m_d(month, day, tokens[2..tokens.size], options)
94
+ end
95
+
96
+ # Handle repeater-month-name/ordinal-day with separator-on
97
+ def handle_rmn_od_on(tokens, options)
98
+ if tokens.size > 3
99
+ month = tokens[2].get_tag(RepeaterMonthName)
100
+ day = tokens[3].get_tag(OrdinalDay).type
101
+ token_range = 0..1
102
+ else
103
+ month = tokens[1].get_tag(RepeaterMonthName)
104
+ day = tokens[2].get_tag(OrdinalDay).type
105
+ token_range = 0..0
106
+ end
107
+
108
+ return if month_overflow?(self.now.year, month.index, day)
109
+
110
+ handle_m_d(month, day, tokens[token_range], options)
111
+ end
112
+
113
+ # Handle scalar-year/repeater-quarter-name
114
+ def handle_sy_rqn(tokens, options)
115
+ handle_rqn_sy(tokens[0..1].reverse, options)
116
+ end
117
+
118
+ # Handle repeater-quarter-name/scalar-year
119
+ def handle_rqn_sy(tokens, options)
120
+ year = tokens[1].get_tag(ScalarYear).type
121
+ quarter_tag = tokens[0].get_tag(RepeaterQuarterName)
122
+ quarter_tag.start = Chronic.construct(year)
123
+ quarter_tag.this(:none)
124
+ end
125
+
126
+ # Handle repeater-month-name/scalar-year
127
+ def handle_rmn_sy(tokens, options)
128
+ month = tokens[0].get_tag(RepeaterMonthName).index
129
+ year = tokens[1].get_tag(ScalarYear).type
130
+
131
+ if month == 12
132
+ next_month_year = year + 1
133
+ next_month_month = 1
134
+ else
135
+ next_month_year = year
136
+ next_month_month = month + 1
137
+ end
138
+
139
+ begin
140
+ end_time = Chronic.time_class.local(next_month_year, next_month_month)
141
+ Span.new(Chronic.time_class.local(year, month), end_time)
142
+ rescue ArgumentError
143
+ nil
144
+ end
145
+ end
146
+
147
+ # Handle generic timestamp (ruby 1.8)
148
+ def handle_generic(tokens, options)
149
+ t = Chronic.time_class.parse(options[:text])
150
+ Span.new(t, t + 1)
151
+ rescue ArgumentError => e
152
+ raise e unless e.message =~ /out of range/
153
+ end
154
+
155
+ # Handle repeater-month-name/scalar-day/scalar-year
156
+ def handle_rmn_sd_sy(tokens, options)
157
+ month = tokens[0].get_tag(RepeaterMonthName).index
158
+ day = tokens[1].get_tag(ScalarDay).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 repeater-month-name/ordinal-day/scalar-year
173
+ def handle_rmn_od_sy(tokens, options)
174
+ month = tokens[0].get_tag(RepeaterMonthName).index
175
+ day = tokens[1].get_tag(OrdinalDay).type
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 oridinal-day/repeater-month-name/scalar-year
190
+ def handle_od_rmn_sy(tokens, options)
191
+ day = tokens[0].get_tag(OrdinalDay).type
192
+ month = tokens[1].get_tag(RepeaterMonthName).index
193
+ year = tokens[2].get_tag(ScalarYear).type
194
+ time_tokens = tokens.last(tokens.size - 3)
195
+
196
+ return if month_overflow?(year, month, day)
197
+
198
+ begin
199
+ day_start = Chronic.time_class.local(year, month, day)
200
+ day_or_time(day_start, time_tokens, options)
201
+ rescue ArgumentError
202
+ nil
203
+ end
204
+ end
205
+
206
+ # Handle scalar-day/repeater-month-name/scalar-year
207
+ def handle_sd_rmn_sy(tokens, options)
208
+ new_tokens = [tokens[1], tokens[0], tokens[2]]
209
+ time_tokens = tokens.last(tokens.size - 3)
210
+ handle_rmn_sd_sy(new_tokens + time_tokens, options)
211
+ end
212
+
213
+ # Handle scalar-month/scalar-day/scalar-year (endian middle)
214
+ def handle_sm_sd_sy(tokens, options)
215
+ month = tokens[0].get_tag(ScalarMonth).type
216
+ day = tokens[1].get_tag(ScalarDay).type
217
+ year = tokens[2].get_tag(ScalarYear).type
218
+ time_tokens = tokens.last(tokens.size - 3)
219
+
220
+ return if month_overflow?(year, month, day)
221
+
222
+ begin
223
+ day_start = Chronic.time_class.local(year, month, day)
224
+ day_or_time(day_start, time_tokens, options)
225
+ rescue ArgumentError
226
+ nil
227
+ end
228
+ end
229
+
230
+ # Handle scalar-day/scalar-month/scalar-year (endian little)
231
+ def handle_sd_sm_sy(tokens, options)
232
+ new_tokens = [tokens[1], tokens[0], tokens[2]]
233
+ time_tokens = tokens.last(tokens.size - 3)
234
+ handle_sm_sd_sy(new_tokens + time_tokens, options)
235
+ end
236
+
237
+ # Handle scalar-year/scalar-month/scalar-day
238
+ def handle_sy_sm_sd(tokens, options)
239
+ new_tokens = [tokens[1], tokens[2], tokens[0]]
240
+ time_tokens = tokens.last(tokens.size - 3)
241
+ handle_sm_sd_sy(new_tokens + time_tokens, options)
242
+ end
243
+
244
+ # Handle scalar-month/scalar-day
245
+ def handle_sm_sd(tokens, options)
246
+ month = tokens[0].get_tag(ScalarMonth).type
247
+ day = tokens[1].get_tag(ScalarDay).type
248
+ year = self.now.year
249
+ time_tokens = tokens.last(tokens.size - 2)
250
+
251
+ return if month_overflow?(year, month, day)
252
+
253
+ begin
254
+ day_start = Chronic.time_class.local(year, month, day)
255
+
256
+ if options[:context] == :future && day_start < now
257
+ day_start = Chronic.time_class.local(year + 1, month, day)
258
+ elsif options[:context] == :past && day_start > now
259
+ day_start = Chronic.time_class.local(year - 1, month, day)
260
+ end
261
+
262
+ day_or_time(day_start, time_tokens, options)
263
+ rescue ArgumentError
264
+ nil
265
+ end
266
+ end
267
+
268
+ # Handle scalar-day/scalar-month
269
+ def handle_sd_sm(tokens, options)
270
+ new_tokens = [tokens[1], tokens[0]]
271
+ time_tokens = tokens.last(tokens.size - 2)
272
+ handle_sm_sd(new_tokens + time_tokens, options)
273
+ end
274
+
275
+ def handle_year_and_month(year, month)
276
+ if month == 12
277
+ next_month_year = year + 1
278
+ next_month_month = 1
279
+ else
280
+ next_month_year = year
281
+ next_month_month = month + 1
282
+ end
283
+
284
+ begin
285
+ end_time = Chronic.time_class.local(next_month_year, next_month_month)
286
+ Span.new(Chronic.time_class.local(year, month), end_time)
287
+ rescue ArgumentError
288
+ nil
289
+ end
290
+ end
291
+
292
+ # Handle scalar-month/scalar-year
293
+ def handle_sm_sy(tokens, options)
294
+ month = tokens[0].get_tag(ScalarMonth).type
295
+ year = tokens[1].get_tag(ScalarYear).type
296
+ handle_year_and_month(year, month)
297
+ end
298
+
299
+ # Handle scalar-year/scalar-month
300
+ def handle_sy_sm(tokens, options)
301
+ year = tokens[0].get_tag(ScalarYear).type
302
+ month = tokens[1].get_tag(ScalarMonth).type
303
+ handle_year_and_month(year, month)
304
+ end
305
+
306
+ # Handle RepeaterDayName RepeaterMonthName OrdinalDay
307
+ def handle_rdn_rmn_od(tokens, options)
308
+ month = tokens[1].get_tag(RepeaterMonthName)
309
+ day = tokens[2].get_tag(OrdinalDay).type
310
+ time_tokens = tokens.last(tokens.size - 3)
311
+ year = self.now.year
312
+
313
+ return if month_overflow?(year, month.index, day)
314
+
315
+ begin
316
+ if time_tokens.empty?
317
+ start_time = Chronic.time_class.local(year, month.index, day)
318
+ end_time = time_with_rollover(year, month.index, day + 1)
319
+ Span.new(start_time, end_time)
320
+ else
321
+ day_start = Chronic.time_class.local(year, month.index, day)
322
+ day_or_time(day_start, time_tokens, options)
323
+ end
324
+ rescue ArgumentError
325
+ nil
326
+ end
327
+ end
328
+
329
+ # Handle RepeaterDayName RepeaterMonthName OrdinalDay ScalarYear
330
+ def handle_rdn_rmn_od_sy(tokens, options)
331
+ month = tokens[1].get_tag(RepeaterMonthName)
332
+ day = tokens[2].get_tag(OrdinalDay).type
333
+ year = tokens[3].get_tag(ScalarYear).type
334
+
335
+ return if month_overflow?(year, month.index, day)
336
+
337
+ begin
338
+ start_time = Chronic.time_class.local(year, month.index, day)
339
+ end_time = time_with_rollover(year, month.index, day + 1)
340
+ Span.new(start_time, end_time)
341
+ rescue ArgumentError
342
+ nil
343
+ end
344
+ end
345
+
346
+ # Handle RepeaterDayName OrdinalDay
347
+ def handle_rdn_od(tokens, options)
348
+ day = tokens[1].get_tag(OrdinalDay).type
349
+ time_tokens = tokens.last(tokens.size - 2)
350
+ year = self.now.year
351
+ month = self.now.month
352
+ if options[:context] == :future
353
+ self.now.day > day ? month += 1 : month
354
+ end
355
+
356
+ return if month_overflow?(year, month, day)
357
+
358
+ begin
359
+ if time_tokens.empty?
360
+ start_time = Chronic.time_class.local(year, month, day)
361
+ end_time = time_with_rollover(year, month, day + 1)
362
+ Span.new(start_time, end_time)
363
+ else
364
+ day_start = Chronic.time_class.local(year, month, day)
365
+ day_or_time(day_start, time_tokens, options)
366
+ end
367
+ rescue ArgumentError
368
+ nil
369
+ end
370
+ end
371
+
372
+ # Handle RepeaterDayName RepeaterMonthName ScalarDay
373
+ def handle_rdn_rmn_sd(tokens, options)
374
+ month = tokens[1].get_tag(RepeaterMonthName)
375
+ day = tokens[2].get_tag(ScalarDay).type
376
+ time_tokens = tokens.last(tokens.size - 3)
377
+ year = self.now.year
378
+
379
+ return if month_overflow?(year, month.index, day)
380
+
381
+ begin
382
+ if time_tokens.empty?
383
+ start_time = Chronic.time_class.local(year, month.index, day)
384
+ end_time = time_with_rollover(year, month.index, day + 1)
385
+ Span.new(start_time, end_time)
386
+ else
387
+ day_start = Chronic.time_class.local(year, month.index, day)
388
+ day_or_time(day_start, time_tokens, options)
389
+ end
390
+ rescue ArgumentError
391
+ nil
392
+ end
393
+ end
394
+
395
+ # Handle RepeaterDayName RepeaterMonthName ScalarDay ScalarYear
396
+ def handle_rdn_rmn_sd_sy(tokens, options)
397
+ month = tokens[1].get_tag(RepeaterMonthName)
398
+ day = tokens[2].get_tag(ScalarDay).type
399
+ year = tokens[3].get_tag(ScalarYear).type
400
+
401
+ return if month_overflow?(year, month.index, day)
402
+
403
+ begin
404
+ start_time = Chronic.time_class.local(year, month.index, day)
405
+ end_time = time_with_rollover(year, month.index, day + 1)
406
+ Span.new(start_time, end_time)
407
+ rescue ArgumentError
408
+ nil
409
+ end
410
+ end
411
+
412
+ def handle_sm_rmn_sy(tokens, options)
413
+ day = tokens[0].get_tag(ScalarDay).type
414
+ month = tokens[1].get_tag(RepeaterMonthName).index
415
+ year = tokens[2].get_tag(ScalarYear).type
416
+ if tokens.size > 3
417
+ time = get_anchor([tokens.last], options).begin
418
+ h, m, s = time.hour, time.min, time.sec
419
+ time = Chronic.time_class.local(year, month, day, h, m, s)
420
+ end_time = Chronic.time_class.local(year, month, day + 1, h, m, s)
421
+ else
422
+ time = Chronic.time_class.local(year, month, day)
423
+ day += 1 unless day >= 31
424
+ end_time = Chronic.time_class.local(year, month, day)
425
+ end
426
+ Span.new(time, end_time)
427
+ end
428
+
429
+ # anchors
430
+
431
+ # Handle repeaters
432
+ def handle_r(tokens, options)
433
+ dd_tokens = dealias_and_disambiguate_times(tokens, options)
434
+ get_anchor(dd_tokens, options)
435
+ end
436
+
437
+ # Handle repeater/grabber/repeater
438
+ def handle_r_g_r(tokens, options)
439
+ new_tokens = [tokens[1], tokens[0], tokens[2]]
440
+ handle_r(new_tokens, options)
441
+ end
442
+
443
+ # arrows
444
+
445
+ # Handle scalar/repeater/pointer helper
446
+ def handle_srp(tokens, span, options)
447
+ distance = tokens[0].get_tag(Scalar).type
448
+ repeater = tokens[1].get_tag(Repeater)
449
+ pointer = tokens[2].get_tag(Pointer).type
450
+
451
+ repeater.offset(span, distance, pointer) if repeater.respond_to?(:offset)
452
+ end
453
+
454
+ # Handle scalar/repeater/pointer
455
+ def handle_s_r_p(tokens, options)
456
+ span = Span.new(self.now, self.now + 1)
457
+
458
+ handle_srp(tokens, span, options)
459
+ end
460
+
461
+ # Handle pointer/scalar/repeater
462
+ def handle_p_s_r(tokens, options)
463
+ new_tokens = [tokens[1], tokens[2], tokens[0]]
464
+ handle_s_r_p(new_tokens, options)
465
+ end
466
+
467
+ # Handle scalar/repeater/pointer/anchor
468
+ def handle_s_r_p_a(tokens, options)
469
+ anchor_span = get_anchor(tokens[3..tokens.size - 1], options)
470
+ handle_srp(tokens, anchor_span, options)
471
+ end
472
+
473
+ # Handle repeater/scalar/repeater/pointer
474
+ def handle_rmn_s_r_p(tokens, options)
475
+ handle_s_r_p_a(tokens[1..3] + tokens[0..0], options)
476
+ end
477
+
478
+ def handle_s_r_a_s_r_p_a(tokens, options)
479
+ anchor_span = get_anchor(tokens[4..tokens.size - 1], options)
480
+
481
+ span = handle_srp(tokens[0..1]+tokens[4..6], anchor_span, options)
482
+ handle_srp(tokens[2..3]+tokens[4..6], span, options)
483
+ end
484
+
485
+ # narrows
486
+
487
+ # Handle oridinal repeaters
488
+ def handle_orr(tokens, outer_span, options)
489
+ repeater = tokens[1].get_tag(Repeater)
490
+ repeater.start = outer_span.begin - 1
491
+ ordinal = tokens[0].get_tag(Ordinal).type
492
+ span = nil
493
+
494
+ ordinal.times do
495
+ span = repeater.next(:future)
496
+
497
+ if span.begin >= outer_span.end
498
+ span = nil
499
+ break
500
+ end
501
+ end
502
+
503
+ span
504
+ end
505
+
506
+ # Handle ordinal/repeater/separator/repeater
507
+ def handle_o_r_s_r(tokens, options)
508
+ outer_span = get_anchor([tokens[3]], options)
509
+ handle_orr(tokens[0..1], outer_span, options)
510
+ end
511
+
512
+ # Handle ordinal/repeater/grabber/repeater
513
+ def handle_o_r_g_r(tokens, options)
514
+ outer_span = get_anchor(tokens[2..3], options)
515
+ handle_orr(tokens[0..1], outer_span, options)
516
+ end
517
+
518
+ # support methods
519
+
520
+ def day_or_time(day_start, time_tokens, options)
521
+ outer_span = Span.new(day_start, day_start + (24 * 60 * 60))
522
+
523
+ unless time_tokens.empty?
524
+ self.now = outer_span.begin
525
+ get_anchor(dealias_and_disambiguate_times(time_tokens, options), options.merge(:context => :future))
526
+ else
527
+ outer_span
528
+ end
529
+ end
530
+
531
+ def get_anchor(tokens, options)
532
+ grabber = Grabber.new(:this)
533
+ pointer = :future
534
+ repeaters = get_repeaters(tokens)
535
+ repeaters.size.times { tokens.pop }
536
+
537
+ if tokens.first && tokens.first.get_tag(Grabber)
538
+ grabber = tokens.shift.get_tag(Grabber)
539
+ end
540
+
541
+ head = repeaters.shift
542
+ head.start = self.now
543
+
544
+ case grabber.type
545
+ when :last
546
+ outer_span = head.next(:past)
547
+ when :this
548
+ if options[:context] != :past and repeaters.size > 0
549
+ outer_span = head.this(:none)
550
+ else
551
+ outer_span = head.this(options[:context])
552
+ end
553
+ when :next
554
+ outer_span = head.next(:future)
555
+ else
556
+ raise 'Invalid grabber'
557
+ end
558
+
559
+ if Chronic.debug
560
+ puts "Handler-class: #{head.class}"
561
+ puts "--#{outer_span}"
562
+ end
563
+
564
+ find_within(repeaters, outer_span, pointer)
565
+ end
566
+
567
+ def get_repeaters(tokens)
568
+ tokens.map { |token| token.get_tag(Repeater) }.compact.sort.reverse
569
+ end
570
+
571
+ def month_overflow?(year, month, day)
572
+ if ::Date.leap?(year)
573
+ day > RepeaterMonth::MONTH_DAYS_LEAP[month - 1]
574
+ else
575
+ day > RepeaterMonth::MONTH_DAYS[month - 1]
576
+ end
577
+ rescue ArgumentError
578
+ false
579
+ end
580
+
581
+ # Recursively finds repeaters within other repeaters.
582
+ # Returns a Span representing the innermost time span
583
+ # or nil if no repeater union could be found
584
+ def find_within(tags, span, pointer)
585
+ puts "--#{span}" if Chronic.debug
586
+ return span if tags.empty?
587
+
588
+ head = tags.shift
589
+ head.start = (pointer == :future ? span.begin : span.end)
590
+ h = head.this(:none)
591
+
592
+ if span.cover?(h.begin) || span.cover?(h.end)
593
+ find_within(tags, h, pointer)
594
+ end
595
+ end
596
+
597
+ def time_with_rollover(year, month, day)
598
+ date_parts =
599
+ if month_overflow?(year, month, day)
600
+ if month == 12
601
+ [year + 1, 1, 1]
602
+ else
603
+ [year, month + 1, 1]
604
+ end
605
+ else
606
+ [year, month, day]
607
+ end
608
+ Chronic.time_class.local(*date_parts)
609
+ end
610
+
611
+ def dealias_and_disambiguate_times(tokens, options)
612
+ # handle aliases of am/pm
613
+ # 5:00 in the morning -> 5:00 am
614
+ # 7:00 in the evening -> 7:00 pm
615
+
616
+ day_portion_index = nil
617
+ tokens.each_with_index do |t, i|
618
+ if t.get_tag(RepeaterDayPortion)
619
+ day_portion_index = i
620
+ break
621
+ end
622
+ end
623
+
624
+ time_index = nil
625
+ tokens.each_with_index do |t, i|
626
+ if t.get_tag(RepeaterTime)
627
+ time_index = i
628
+ break
629
+ end
630
+ end
631
+
632
+ if day_portion_index && time_index
633
+ t1 = tokens[day_portion_index]
634
+ t1tag = t1.get_tag(RepeaterDayPortion)
635
+
636
+ case t1tag.type
637
+ when :morning
638
+ puts '--morning->am' if Chronic.debug
639
+ t1.untag(RepeaterDayPortion)
640
+ t1.tag(RepeaterDayPortion.new(:am))
641
+ when :afternoon, :evening, :night
642
+ puts "--#{t1tag.type}->pm" if Chronic.debug
643
+ t1.untag(RepeaterDayPortion)
644
+ t1.tag(RepeaterDayPortion.new(:pm))
645
+ end
646
+ end
647
+
648
+ # handle ambiguous times if :ambiguous_time_range is specified
649
+ if options[:ambiguous_time_range] != :none
650
+ ambiguous_tokens = []
651
+
652
+ tokens.each_with_index do |token, i|
653
+ ambiguous_tokens << token
654
+ next_token = tokens[i + 1]
655
+
656
+ if token.get_tag(RepeaterTime) && token.get_tag(RepeaterTime).type.ambiguous? && (!next_token || !next_token.get_tag(RepeaterDayPortion))
657
+ distoken = Token.new('disambiguator')
658
+
659
+ distoken.tag(RepeaterDayPortion.new(options[:ambiguous_time_range]))
660
+ ambiguous_tokens << distoken
661
+ end
662
+ end
663
+
664
+ tokens = ambiguous_tokens
665
+ end
666
+
667
+ tokens
668
+ end
669
+
670
+ end
671
+
672
+ end