gitlab-chronic 0.10.3

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 (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