chronic 0.5.0 → 0.6.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (46) hide show
  1. data/HISTORY.md +14 -1
  2. data/lib/chronic.rb +6 -42
  3. data/lib/chronic/chronic.rb +73 -48
  4. data/lib/chronic/handler.rb +90 -0
  5. data/lib/chronic/handlers.rb +131 -139
  6. data/lib/chronic/mini_date.rb +12 -6
  7. data/lib/chronic/numerizer.rb +3 -2
  8. data/lib/chronic/pointer.rb +1 -2
  9. data/lib/chronic/repeater.rb +3 -6
  10. data/lib/chronic/repeaters/repeater_day.rb +6 -6
  11. data/lib/chronic/repeaters/repeater_day_name.rb +1 -1
  12. data/lib/chronic/repeaters/repeater_day_portion.rb +8 -8
  13. data/lib/chronic/repeaters/repeater_fortnight.rb +2 -2
  14. data/lib/chronic/repeaters/repeater_hour.rb +7 -7
  15. data/lib/chronic/repeaters/repeater_minute.rb +6 -6
  16. data/lib/chronic/repeaters/repeater_month.rb +10 -10
  17. data/lib/chronic/repeaters/repeater_month_name.rb +9 -9
  18. data/lib/chronic/repeaters/repeater_season.rb +10 -10
  19. data/lib/chronic/repeaters/repeater_season_name.rb +2 -2
  20. data/lib/chronic/repeaters/repeater_weekday.rb +1 -1
  21. data/lib/chronic/repeaters/repeater_year.rb +11 -11
  22. data/lib/chronic/season.rb +3 -3
  23. data/lib/chronic/tag.rb +2 -0
  24. data/test/test_Chronic.rb +48 -4
  25. data/test/test_DaylightSavings.rb +1 -1
  26. data/test/test_Handler.rb +1 -6
  27. data/test/test_MiniDate.rb +3 -3
  28. data/test/test_Numerizer.rb +1 -1
  29. data/test/test_RepeaterDayName.rb +1 -1
  30. data/test/test_RepeaterFortnight.rb +1 -1
  31. data/test/test_RepeaterHour.rb +1 -1
  32. data/test/test_RepeaterMinute.rb +1 -1
  33. data/test/test_RepeaterMonth.rb +1 -1
  34. data/test/test_RepeaterMonthName.rb +1 -1
  35. data/test/test_RepeaterSeason.rb +1 -1
  36. data/test/test_RepeaterTime.rb +1 -1
  37. data/test/test_RepeaterWeek.rb +1 -1
  38. data/test/test_RepeaterWeekday.rb +1 -1
  39. data/test/test_RepeaterWeekend.rb +1 -1
  40. data/test/test_RepeaterYear.rb +1 -1
  41. data/test/test_Span.rb +1 -1
  42. data/test/test_Token.rb +1 -1
  43. data/test/test_parsing.rb +12 -6
  44. metadata +3 -5
  45. data/Manifest.txt +0 -63
  46. data/test/test_Time.rb +0 -49
data/HISTORY.md CHANGED
@@ -1,4 +1,17 @@
1
- # TBA
1
+ # 0.6.0 / 2011-07-19
2
+
3
+ * Attempting to parse strings with days past the last day of a month will
4
+ now return nil. ex: `Chronic.parse("30th February") #=> nil`
5
+ * All deprecated methods are marked for removal in Chronic 0.7.0
6
+ * Deprecated `Chronic.numericize_numbers` instead use
7
+ `Chronic::Numerizer.numerize`
8
+ * Deprecated `Chronic::InvalidArgumentException` and instead use
9
+ `ArgumentError`
10
+ * Deprecated `Time.construct` and use `Chronic.construct` in place of this
11
+ * Deprecated `Time#to_minidate`, instead use `Chronic::MiniDate.from_time(time)`
12
+ * Add support for handling generic timestamp for Ruby 1.9+
13
+
14
+ # 0.5.0 / 2011-07-01
2
15
 
3
16
  * Replace commas with spaces instead of removing the char (Thomas Walpole)
4
17
  * Added tests for RepeaterSeason
@@ -28,7 +28,7 @@ require 'date'
28
28
  #
29
29
  # @author Tom Preston-Werner, Lee Jarvis
30
30
  module Chronic
31
- VERSION = "0.5.0"
31
+ VERSION = "0.6.0"
32
32
 
33
33
  class << self
34
34
 
@@ -65,6 +65,7 @@ module Chronic
65
65
  end
66
66
 
67
67
  require 'chronic/chronic'
68
+ require 'chronic/handler'
68
69
  require 'chronic/handlers'
69
70
  require 'chronic/mini_date'
70
71
  require 'chronic/tag'
@@ -99,49 +100,12 @@ require 'chronic/repeaters/repeater_time'
99
100
 
100
101
  class Time
101
102
  def self.construct(year, month = 1, day = 1, hour = 0, minute = 0, second = 0)
102
- if second >= 60
103
- minute += second / 60
104
- second = second % 60
105
- end
106
-
107
- if minute >= 60
108
- hour += minute / 60
109
- minute = minute % 60
110
- end
111
-
112
- if hour >= 24
113
- day += hour / 24
114
- hour = hour % 24
115
- end
116
-
117
- # determine if there is a day overflow. this is complicated by our crappy calendar
118
- # system (non-constant number of days per month)
119
- day <= 56 || raise("day must be no more than 56 (makes month resolution easier)")
120
- if day > 28
121
- # no month ever has fewer than 28 days, so only do this if necessary
122
- leap_year_month_days = [31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
123
- common_year_month_days = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
124
- days_this_month = Date.leap?(year) ? leap_year_month_days[month - 1] : common_year_month_days[month - 1]
125
- if day > days_this_month
126
- month += day / days_this_month
127
- day = day % days_this_month
128
- end
129
- end
130
-
131
- if month > 12
132
- if month % 12 == 0
133
- year += (month - 12) / 12
134
- month = 12
135
- else
136
- year += month / 12
137
- month = month % 12
138
- end
139
- end
140
-
141
- Chronic.time_class.local(year, month, day, hour, minute, second)
103
+ warn "Chronic.construct will be deprecated in version 0.7.0. Please use Chronic.construct instead"
104
+ Chronic.construct(year, month, day, hour, minute, second)
142
105
  end
143
106
 
144
107
  def to_minidate
145
- Chronic::MiniDate.new(self.month, self.day)
108
+ warn "Time.to_minidate will be deprecated in version 0.7.0. Please use Chronic::MiniDate.from_time(time) instead"
109
+ Chronic::MiniDate.from_time(self)
146
110
  end
147
111
  end
@@ -63,16 +63,15 @@ module Chronic
63
63
 
64
64
  # ensure the specified options are valid
65
65
  (opts.keys - DEFAULT_OPTIONS.keys).each do |key|
66
- raise InvalidArgumentException, "#{key} is not a valid option key."
66
+ raise ArgumentError, "#{key} is not a valid option key."
67
67
  end
68
68
 
69
69
  unless [:past, :future, :none].include?(options[:context])
70
- raise InvalidArgumentException, "Invalid context, :past/:future only"
70
+ raise ArgumentError, "Invalid context, :past/:future only"
71
71
  end
72
72
 
73
73
  options[:text] = text
74
- options[:now] ||= Chronic.time_class.now
75
- Chronic.now = options[:now]
74
+ Chronic.now = options[:now] || Chronic.time_class.now
76
75
 
77
76
  # tokenize words
78
77
  tokens = tokenize(text, options)
@@ -83,10 +82,8 @@ module Chronic
83
82
 
84
83
  span = tokens_to_span(tokens, options)
85
84
 
86
- if options[:guess]
87
- guess span
88
- else
89
- span
85
+ if span
86
+ options[:guess] ? guess(span) : span
90
87
  end
91
88
  end
92
89
 
@@ -112,9 +109,9 @@ module Chronic
112
109
  def pre_normalize(text)
113
110
  text = text.to_s.downcase
114
111
  text.gsub!(/['"\.]/, '')
115
- text.gsub!(/,/,' ')
112
+ text.gsub!(/,/, ' ')
116
113
  text.gsub!(/\bsecond (of|day|month|hour|minute|second)\b/, '2nd \1')
117
- text = numericize_numbers(text)
114
+ text = Numerizer.numerize(text)
118
115
  text.gsub!(/ \-(\d{4})\b/, ' tzminus\1')
119
116
  text.gsub!(/([\/\-\,\@])/) { ' ' + $1 + ' ' }
120
117
  text.gsub!(/\b0(\d+:\d+\s*pm?\b)/, '\1')
@@ -123,11 +120,9 @@ module Chronic
123
120
  text.gsub!(/\byesterday\b/, 'last day')
124
121
  text.gsub!(/\bnoon\b/, '12:00')
125
122
  text.gsub!(/\bmidnight\b/, '24:00')
126
- text.gsub!(/\bbefore now\b/, 'past')
127
123
  text.gsub!(/\bnow\b/, 'this second')
128
- text.gsub!(/\b(ago|before)\b/, 'past')
129
- text.gsub!(/\bthis past\b/, 'last')
130
- text.gsub!(/\bthis last\b/, 'last')
124
+ text.gsub!(/\b(?:ago|before(?: now)?)\b/, 'past')
125
+ text.gsub!(/\bthis (?:last|past)\b/, 'last')
131
126
  text.gsub!(/\b(?:in|during) the (morning)\b/, '\1')
132
127
  text.gsub!(/\b(?:in the|during the|at) (afternoon|evening|night)\b/, '\1')
133
128
  text.gsub!(/\btonight\b/, 'this night')
@@ -143,6 +138,7 @@ module Chronic
143
138
  # @param [String] text The string to convert
144
139
  # @return [String] A new string with words converted to numbers
145
140
  def numericize_numbers(text)
141
+ warn "Chronic.numericize_numbers will be deprecated in version 0.7.0. Please use Chronic::Numerizer.numerize instead"
146
142
  Numerizer.numerize(text)
147
143
  end
148
144
 
@@ -151,7 +147,6 @@ module Chronic
151
147
  # @param [Span] span
152
148
  # @return [Time]
153
149
  def guess(span)
154
- return nil if span.nil?
155
150
  if span.width > 1
156
151
  span.begin + (span.width / 2)
157
152
  else
@@ -174,6 +169,7 @@ module Chronic
174
169
 
175
170
  :date => [
176
171
  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),
172
+ Handler.new([:scalar_year, :separator_slash_or_dash, :scalar_month, :separator_slash_or_dash, :scalar_day, :repeater_time, :time_zone], :handle_sy_sm_sd_t_tz),
177
173
  Handler.new([:repeater_month_name, :scalar_day, :scalar_year], :handle_rmn_sd_sy),
178
174
  Handler.new([:repeater_month_name, :ordinal_day, :scalar_year], :handle_rmn_od_sy),
179
175
  Handler.new([:repeater_month_name, :scalar_day, :scalar_year, :separator_at?, 'time?'], :handle_rmn_sd_sy),
@@ -222,19 +218,65 @@ module Chronic
222
218
  when :middle
223
219
  @definitions[:endian] = endians
224
220
  else
225
- raise InvalidArgumentException, "Unknown endian option '#{endian}'"
221
+ raise ArgumentError, "Unknown endian option '#{endian}'"
226
222
  end
227
223
 
228
224
  @definitions
229
225
  end
230
226
 
227
+ # Construct a time Object
228
+ #
229
+ # @return [Time]
230
+ def construct(year, month = 1, day = 1, hour = 0, minute = 0, second = 0)
231
+ if second >= 60
232
+ minute += second / 60
233
+ second = second % 60
234
+ end
235
+
236
+ if minute >= 60
237
+ hour += minute / 60
238
+ minute = minute % 60
239
+ end
240
+
241
+ if hour >= 24
242
+ day += hour / 24
243
+ hour = hour % 24
244
+ end
245
+
246
+ # determine if there is a day overflow. this is complicated by our crappy calendar
247
+ # system (non-constant number of days per month)
248
+ day <= 56 || raise("day must be no more than 56 (makes month resolution easier)")
249
+ if day > 28
250
+ # no month ever has fewer than 28 days, so only do this if necessary
251
+ leap_year_month_days = [31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
252
+ common_year_month_days = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
253
+ days_this_month = Date.leap?(year) ? leap_year_month_days[month - 1] : common_year_month_days[month - 1]
254
+ if day > days_this_month
255
+ month += day / days_this_month
256
+ day = day % days_this_month
257
+ end
258
+ end
259
+
260
+ if month > 12
261
+ if month % 12 == 0
262
+ year += (month - 12) / 12
263
+ month = 12
264
+ else
265
+ year += month / 12
266
+ month = month % 12
267
+ end
268
+ end
269
+
270
+ Chronic.time_class.local(year, month, day, hour, minute, second)
271
+ end
272
+
231
273
  private
232
274
 
233
275
  def tokenize(text, options)
234
276
  text = pre_normalize(text)
235
277
  tokens = text.split(' ').map { |word| Token.new(word) }
236
278
  [Repeater, Grabber, Pointer, Scalar, Ordinal, Separator, TimeZone].each do |tok|
237
- tokens = tok.scan(tokens, options)
279
+ tok.scan(tokens, options)
238
280
  end
239
281
  tokens.select { |token| token.tagged? }
240
282
  end
@@ -242,35 +284,23 @@ module Chronic
242
284
  def tokens_to_span(tokens, options)
243
285
  definitions = definitions(options)
244
286
 
245
- (definitions[:date] + definitions[:endian]).each do |handler|
246
- if handler.match(tokens, definitions)
247
- puts "-date" if Chronic.debug
248
- good_tokens = tokens.select { |o| !o.get_tag Separator }
249
- return Handlers.send(handler.handler_method, good_tokens, options)
250
- end
251
- end
287
+ definitions.each do |type, handlers|
288
+ handlers.each do |handler|
289
+ next unless handler.match(tokens, definitions)
252
290
 
253
- definitions[:anchor].each do |handler|
254
- if handler.match(tokens, definitions)
255
- puts "-anchor" if Chronic.debug
256
- good_tokens = tokens.select { |o| !o.get_tag Separator }
257
- return Handlers.send(handler.handler_method, good_tokens, options)
258
- end
259
- end
291
+ good_tokens = case type
292
+ when :date, :endian, :anchor
293
+ tokens.reject { |o| o.get_tag Separator }
294
+ when :arrow
295
+ tokens.reject { |o| o.get_tag(SeparatorAt) || o.get_tag(SeparatorSlashOrDash) || o.get_tag(SeparatorComma) }
296
+ else
297
+ tokens
298
+ end
260
299
 
261
- definitions[:arrow].each do |handler|
262
- if handler.match(tokens, definitions)
263
- puts "-arrow" if Chronic.debug
264
- good_tokens = tokens.reject { |o| o.get_tag(SeparatorAt) || o.get_tag(SeparatorSlashOrDash) || o.get_tag(SeparatorComma) }
265
- return Handlers.send(handler.handler_method, good_tokens, options)
266
- end
267
- end
300
+ if handler.handler_method
301
+ return handler.invoke(type, good_tokens, options)
302
+ end
268
303
 
269
- definitions[:narrow].each do |handler|
270
- if handler.match(tokens, definitions)
271
- puts "-narrow" if Chronic.debug
272
- good_tokens = tokens.select { |o| !o.get_tag Separator }
273
- return Handlers.send(handler.handler_method, tokens, options)
274
304
  end
275
305
  end
276
306
 
@@ -283,9 +313,4 @@ module Chronic
283
313
  # Internal exception
284
314
  class ChronicPain < Exception
285
315
  end
286
-
287
- # This exception is raised if an invalid argument is provided to
288
- # any of Chronic's methods
289
- class InvalidArgumentException < Exception
290
- end
291
316
  end
@@ -0,0 +1,90 @@
1
+ module Chronic
2
+ class Handler
3
+
4
+ # @return [Array] A list of patterns
5
+ attr_reader :pattern
6
+
7
+ # @return [Symbol] The method which handles this list of patterns.
8
+ # This method should exist inside the {Handlers} module
9
+ attr_reader :handler_method
10
+
11
+ # @param [Array] pattern A list of patterns to match tokens against
12
+ # @param [Symbol] handler_method The method to be invoked when patterns
13
+ # are matched. This method should exist inside the {Handlers} module
14
+ def initialize(pattern, handler_method)
15
+ @pattern = pattern
16
+ @handler_method = handler_method
17
+ end
18
+
19
+ # @param [Array] tokens
20
+ # @param [Hash] definitions
21
+ # @return [Boolean]
22
+ # @see Chronic.tokens_to_span
23
+ def match(tokens, definitions)
24
+ token_index = 0
25
+
26
+ @pattern.each do |element|
27
+ name = element.to_s
28
+ optional = name[-1, 1] == '?'
29
+ name = name.chop if optional
30
+
31
+ case element
32
+ when Symbol
33
+ if tags_match?(name, tokens, token_index)
34
+ token_index += 1
35
+ next
36
+ else
37
+ if optional
38
+ next
39
+ else
40
+ return false
41
+ end
42
+ end
43
+ when String
44
+ return true if optional && token_index == tokens.size
45
+
46
+ if definitions.key?(name.to_sym)
47
+ sub_handlers = definitions[name.to_sym]
48
+ else
49
+ raise ChronicPain, "Invalid subset #{name} specified"
50
+ end
51
+
52
+ sub_handlers.each do |sub_handler|
53
+ return true if sub_handler.match(tokens[token_index..tokens.size], definitions)
54
+ end
55
+ else
56
+ raise ChronicPain, "Invalid match type: #{element.class}"
57
+ end
58
+ end
59
+
60
+ return false if token_index != tokens.size
61
+ return true
62
+ end
63
+
64
+ def invoke(type, tokens, options)
65
+ if Chronic.debug
66
+ puts "-#{type}"
67
+ puts "Handler: #{@handler_method}"
68
+ end
69
+
70
+ Handlers.send(@handler_method, tokens, options)
71
+ end
72
+
73
+ # @param [Handler] The handler to compare
74
+ # @return [Boolean] True if these handlers match
75
+ def ==(other)
76
+ @pattern == other.pattern
77
+ end
78
+
79
+ private
80
+
81
+ def tags_match?(name, tokens, token_index)
82
+ klass = Chronic.const_get(name.to_s.gsub(/(?:^|_)(.)/) { $1.upcase })
83
+
84
+ if tokens[token_index]
85
+ !tokens[token_index].tags.select { |o| o.kind_of?(klass) }.empty?
86
+ end
87
+ end
88
+
89
+ end
90
+ end
@@ -3,50 +3,81 @@ module Chronic
3
3
  module_function
4
4
 
5
5
  # Handle month/day
6
- def handle_m_d(month, day, time_tokens, options) #:nodoc:
6
+ def handle_m_d(month, day, time_tokens, options)
7
7
  month.start = Chronic.now
8
8
  span = month.this(options[:context])
9
-
10
- day_start = Chronic.time_class.local(span.begin.year, span.begin.month, day)
9
+ year, month = span.begin.year, span.begin.month
10
+ day_start = Chronic.time_class.local(year, month, day)
11
11
 
12
12
  day_or_time(day_start, time_tokens, options)
13
13
  end
14
14
 
15
15
  # Handle repeater-month-name/scalar-day
16
- def handle_rmn_sd(tokens, options) #:nodoc:
17
- handle_m_d(tokens[0].get_tag(RepeaterMonthName), tokens[1].get_tag(ScalarDay).type, tokens[2..tokens.size], options)
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)
18
23
  end
19
24
 
20
25
  # Handle repeater-month-name/scalar-day with separator-on
21
- def handle_rmn_sd_on(tokens, options) #:nodoc:
26
+ def handle_rmn_sd_on(tokens, options)
22
27
  if tokens.size > 3
23
- handle_m_d(tokens[2].get_tag(RepeaterMonthName), tokens[3].get_tag(ScalarDay).type, tokens[0..1], options)
28
+ month = tokens[2].get_tag(RepeaterMonthName)
29
+ day = tokens[3].get_tag(ScalarDay).type
30
+ token_range = 0..1
24
31
  else
25
- handle_m_d(tokens[1].get_tag(RepeaterMonthName), tokens[2].get_tag(ScalarDay).type, tokens[0..0], options)
32
+ month = tokens[1].get_tag(RepeaterMonthName)
33
+ day = tokens[2].get_tag(ScalarDay).type
34
+ token_range = 0..0
26
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)
27
40
  end
28
41
 
29
42
  # Handle repeater-month-name/ordinal-day
30
- def handle_rmn_od(tokens, options) #:nodoc:
31
- handle_m_d(tokens[0].get_tag(RepeaterMonthName), tokens[1].get_tag(OrdinalDay).type, tokens[2..tokens.size], options)
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)
32
50
  end
33
51
 
34
52
  # Handle ordinal-day/repeater-month-name
35
- def handle_od_rmn(tokens, options) #:nodoc:
36
- handle_m_d(tokens[1].get_tag(RepeaterMonthName), tokens[0].get_tag(OrdinalDay).type, tokens[2..tokens.size], options)
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)
37
60
  end
38
61
 
39
62
  # Handle repeater-month-name/ordinal-day with separator-on
40
- def handle_rmn_od_on(tokens, options) #:nodoc:
63
+ def handle_rmn_od_on(tokens, options)
41
64
  if tokens.size > 3
42
- handle_m_d(tokens[2].get_tag(RepeaterMonthName), tokens[3].get_tag(OrdinalDay).type, tokens[0..1], options)
65
+ month = tokens[2].get_tag(RepeaterMonthName)
66
+ day = tokens[3].get_tag(OrdinalDay).type
67
+ token_range = 0..1
43
68
  else
44
- handle_m_d(tokens[1].get_tag(RepeaterMonthName), tokens[2].get_tag(OrdinalDay).type, tokens[0..0], options)
69
+ month = tokens[1].get_tag(RepeaterMonthName)
70
+ day = tokens[2].get_tag(OrdinalDay).type
71
+ token_range = 0..0
45
72
  end
73
+
74
+ return if month_overflow?(Chronic.now.year, month.index, day)
75
+
76
+ handle_m_d(month, day, tokens[token_range], options)
46
77
  end
47
78
 
48
79
  # Handle repeater-month-name/scalar-year
49
- def handle_rmn_sy(tokens, options) #:nodoc:
80
+ def handle_rmn_sy(tokens, options)
50
81
  month = tokens[0].get_tag(RepeaterMonthName).index
51
82
  year = tokens[1].get_tag(ScalarYear).type
52
83
 
@@ -59,26 +90,34 @@ module Chronic
59
90
  end
60
91
 
61
92
  begin
62
- Span.new(Chronic.time_class.local(year, month), Chronic.time_class.local(next_month_year, next_month_month))
93
+ end_time = Chronic.time_class.local(next_month_year, next_month_month)
94
+ Span.new(Chronic.time_class.local(year, month), end_time)
63
95
  rescue ArgumentError
64
96
  nil
65
97
  end
66
98
  end
67
99
 
68
- # Handle generic timestamp
69
- def handle_rdn_rmn_sd_t_tz_sy(tokens, options) #:nodoc:
100
+ # Handle generic timestamp (ruby 1.8)
101
+ def handle_rdn_rmn_sd_t_tz_sy(tokens, options)
102
+ t = Chronic.time_class.parse(options[:text])
103
+ Span.new(t, t + 1)
104
+ end
105
+
106
+ # Handle generic timestamp (ruby 1.9)
107
+ def handle_sy_sm_sd_t_tz(tokens, options)
70
108
  t = Chronic.time_class.parse(options[:text])
71
109
  Span.new(t, t + 1)
72
110
  end
73
111
 
74
112
  # Handle repeater-month-name/scalar-day/scalar-year
75
- def handle_rmn_sd_sy(tokens, options) #:nodoc:
113
+ def handle_rmn_sd_sy(tokens, options)
76
114
  month = tokens[0].get_tag(RepeaterMonthName).index
77
115
  day = tokens[1].get_tag(ScalarDay).type
78
116
  year = tokens[2].get_tag(ScalarYear).type
79
-
80
117
  time_tokens = tokens.last(tokens.size - 3)
81
118
 
119
+ return if month_overflow?(year, month, day)
120
+
82
121
  begin
83
122
  day_start = Chronic.time_class.local(year, month, day)
84
123
  day_or_time(day_start, time_tokens, options)
@@ -88,13 +127,14 @@ module Chronic
88
127
  end
89
128
 
90
129
  # Handle repeater-month-name/ordinal-day/scalar-year
91
- def handle_rmn_od_sy(tokens, options) #:nodoc:
130
+ def handle_rmn_od_sy(tokens, options)
92
131
  month = tokens[0].get_tag(RepeaterMonthName).index
93
132
  day = tokens[1].get_tag(OrdinalDay).type
94
133
  year = tokens[2].get_tag(ScalarYear).type
95
-
96
134
  time_tokens = tokens.last(tokens.size - 3)
97
135
 
136
+ return if month_overflow?(year, month, day)
137
+
98
138
  begin
99
139
  day_start = Chronic.time_class.local(year, month, day)
100
140
  day_or_time(day_start, time_tokens, options)
@@ -104,13 +144,14 @@ module Chronic
104
144
  end
105
145
 
106
146
  # Handle oridinal-day/repeater-month-name/scalar-year
107
- def handle_od_rmn_sy(tokens, options) #:nodoc:
147
+ def handle_od_rmn_sy(tokens, options)
108
148
  day = tokens[0].get_tag(OrdinalDay).type
109
149
  month = tokens[1].get_tag(RepeaterMonthName).index
110
150
  year = tokens[2].get_tag(ScalarYear).type
111
-
112
151
  time_tokens = tokens.last(tokens.size - 3)
113
152
 
153
+ return if month_overflow?(year, month, day)
154
+
114
155
  begin
115
156
  day_start = Chronic.time_class.local(year, month, day)
116
157
  day_or_time(day_start, time_tokens, options)
@@ -120,22 +161,23 @@ module Chronic
120
161
  end
121
162
 
122
163
  # Handle scalar-day/repeater-month-name/scalar-year
123
- def handle_sd_rmn_sy(tokens, options) #:nodoc:
164
+ def handle_sd_rmn_sy(tokens, options)
124
165
  new_tokens = [tokens[1], tokens[0], tokens[2]]
125
166
  time_tokens = tokens.last(tokens.size - 3)
126
167
  handle_rmn_sd_sy(new_tokens + time_tokens, options)
127
168
  end
128
169
 
129
170
  # Handle scalar-month/scalar-day/scalar-year (endian middle)
130
- def handle_sm_sd_sy(tokens, options) #:nodoc:
171
+ def handle_sm_sd_sy(tokens, options)
131
172
  month = tokens[0].get_tag(ScalarMonth).type
132
173
  day = tokens[1].get_tag(ScalarDay).type
133
174
  year = tokens[2].get_tag(ScalarYear).type
134
-
135
175
  time_tokens = tokens.last(tokens.size - 3)
136
176
 
177
+ return if month_overflow?(year, month, day)
178
+
137
179
  begin
138
- day_start = Chronic.time_class.local(year, month, day) #:nodoc:
180
+ day_start = Chronic.time_class.local(year, month, day)
139
181
  day_or_time(day_start, time_tokens, options)
140
182
  rescue ArgumentError
141
183
  nil
@@ -143,21 +185,21 @@ module Chronic
143
185
  end
144
186
 
145
187
  # Handle scalar-day/scalar-month/scalar-year (endian little)
146
- def handle_sd_sm_sy(tokens, options) #:nodoc:
188
+ def handle_sd_sm_sy(tokens, options)
147
189
  new_tokens = [tokens[1], tokens[0], tokens[2]]
148
190
  time_tokens = tokens.last(tokens.size - 3)
149
191
  handle_sm_sd_sy(new_tokens + time_tokens, options)
150
192
  end
151
193
 
152
194
  # Handle scalar-year/scalar-month/scalar-day
153
- def handle_sy_sm_sd(tokens, options) #:nodoc:
195
+ def handle_sy_sm_sd(tokens, options)
154
196
  new_tokens = [tokens[1], tokens[2], tokens[0]]
155
197
  time_tokens = tokens.last(tokens.size - 3)
156
198
  handle_sm_sd_sy(new_tokens + time_tokens, options)
157
199
  end
158
200
 
159
201
  # Handle scalar-month/scalar-year
160
- def handle_sm_sy(tokens, options) #:nodoc:
202
+ def handle_sm_sy(tokens, options)
161
203
  month = tokens[0].get_tag(ScalarMonth).type
162
204
  year = tokens[1].get_tag(ScalarYear).type
163
205
 
@@ -170,7 +212,8 @@ module Chronic
170
212
  end
171
213
 
172
214
  begin
173
- Span.new(Chronic.time_class.local(year, month), Chronic.time_class.local(next_month_year, next_month_month))
215
+ end_time = Chronic.time_class.local(next_month_year, next_month_month)
216
+ Span.new(Chronic.time_class.local(year, month), end_time)
174
217
  rescue ArgumentError
175
218
  nil
176
219
  end
@@ -179,13 +222,13 @@ module Chronic
179
222
  # anchors
180
223
 
181
224
  # Handle repeaters
182
- def handle_r(tokens, options) #:nodoc:
225
+ def handle_r(tokens, options)
183
226
  dd_tokens = dealias_and_disambiguate_times(tokens, options)
184
227
  get_anchor(dd_tokens, options)
185
228
  end
186
229
 
187
230
  # Handle repeater/grabber/repeater
188
- def handle_r_g_r(tokens, options) #:nodoc:
231
+ def handle_r_g_r(tokens, options)
189
232
  new_tokens = [tokens[1], tokens[0], tokens[2]]
190
233
  handle_r(new_tokens, options)
191
234
  end
@@ -193,7 +236,7 @@ module Chronic
193
236
  # arrows
194
237
 
195
238
  # Handle scalar/repeater/pointer helper
196
- def handle_srp(tokens, span, options) #:nodoc:
239
+ def handle_srp(tokens, span, options)
197
240
  distance = tokens[0].get_tag(Scalar).type
198
241
  repeater = tokens[1].get_tag(Repeater)
199
242
  pointer = tokens[2].get_tag(Pointer).type
@@ -202,7 +245,7 @@ module Chronic
202
245
  end
203
246
 
204
247
  # Handle scalar/repeater/pointer
205
- def handle_s_r_p(tokens, options) #:nodoc:
248
+ def handle_s_r_p(tokens, options)
206
249
  repeater = tokens[1].get_tag(Repeater)
207
250
  span = Span.new(Chronic.now, Chronic.now + 1)
208
251
 
@@ -210,13 +253,13 @@ module Chronic
210
253
  end
211
254
 
212
255
  # Handle pointer/scalar/repeater
213
- def handle_p_s_r(tokens, options) #:nodoc:
256
+ def handle_p_s_r(tokens, options)
214
257
  new_tokens = [tokens[1], tokens[2], tokens[0]]
215
258
  handle_s_r_p(new_tokens, options)
216
259
  end
217
260
 
218
261
  # Handle scalar/repeater/pointer/anchor
219
- def handle_s_r_p_a(tokens, options) #:nodoc:
262
+ def handle_s_r_p_a(tokens, options)
220
263
  anchor_span = get_anchor(tokens[3..tokens.size - 1], options)
221
264
  handle_srp(tokens, anchor_span, options)
222
265
  end
@@ -224,29 +267,32 @@ module Chronic
224
267
  # narrows
225
268
 
226
269
  # Handle oridinal repeaters
227
- def handle_orr(tokens, outer_span, options) #:nodoc:
270
+ def handle_orr(tokens, outer_span, options)
228
271
  repeater = tokens[1].get_tag(Repeater)
229
272
  repeater.start = outer_span.begin - 1
230
273
  ordinal = tokens[0].get_tag(Ordinal).type
231
274
  span = nil
275
+
232
276
  ordinal.times do
233
277
  span = repeater.next(:future)
278
+
234
279
  if span.begin > outer_span.end
235
280
  span = nil
236
281
  break
237
282
  end
238
283
  end
284
+
239
285
  span
240
286
  end
241
287
 
242
288
  # Handle ordinal/repeater/separator/repeater
243
- def handle_o_r_s_r(tokens, options) #:nodoc:
289
+ def handle_o_r_s_r(tokens, options)
244
290
  outer_span = get_anchor([tokens[3]], options)
245
291
  handle_orr(tokens[0..1], outer_span, options)
246
292
  end
247
293
 
248
294
  # Handle ordinal/repeater/grabber/repeater
249
- def handle_o_r_g_r(tokens, options) #:nodoc:
295
+ def handle_o_r_g_r(tokens, options)
250
296
  outer_span = get_anchor(tokens[2..3], options)
251
297
  handle_orr(tokens[0..1], outer_span, options)
252
298
  end
@@ -264,7 +310,7 @@ module Chronic
264
310
  end
265
311
  end
266
312
 
267
- def get_anchor(tokens, options) #:nodoc:
313
+ def get_anchor(tokens, options)
268
314
  grabber = Grabber.new(:this)
269
315
  pointer = :future
270
316
 
@@ -272,58 +318,60 @@ module Chronic
272
318
  repeaters.size.times { tokens.pop }
273
319
 
274
320
  if tokens.first && tokens.first.get_tag(Grabber)
275
- grabber = tokens.first.get_tag(Grabber)
276
- tokens.pop
321
+ grabber = tokens.shift.get_tag(Grabber)
277
322
  end
278
323
 
279
324
  head = repeaters.shift
280
325
  head.start = Chronic.now
281
326
 
282
327
  case grabber.type
283
- when :last
284
- outer_span = head.next(:past)
285
- when :this
286
- if options[:context] != :past and repeaters.size > 0
287
- outer_span = head.this(:none)
288
- else
289
- outer_span = head.this(options[:context])
290
- end
291
- when :next
292
- outer_span = head.next(:future)
293
- else raise(ChronicPain, "Invalid grabber")
328
+ when :last
329
+ outer_span = head.next(:past)
330
+ when :this
331
+ if options[:context] != :past and repeaters.size > 0
332
+ outer_span = head.this(:none)
333
+ else
334
+ outer_span = head.this(options[:context])
335
+ end
336
+ when :next
337
+ outer_span = head.next(:future)
338
+ else
339
+ raise ChronicPain, "Invalid grabber"
294
340
  end
295
341
 
296
342
  puts "--#{outer_span}" if Chronic.debug
297
343
  find_within(repeaters, outer_span, pointer)
298
344
  end
299
345
 
300
- def get_repeaters(tokens) #:nodoc:
301
- repeaters = []
302
- tokens.each do |token|
303
- if t = token.get_tag(Repeater)
304
- repeaters << t
305
- end
346
+ def get_repeaters(tokens)
347
+ tokens.map { |token| token.get_tag(Repeater) }.compact.sort.reverse
348
+ end
349
+
350
+ def month_overflow?(year, month, day)
351
+ if Date.leap?(year)
352
+ day > RepeaterMonth::MONTH_DAYS_LEAP[month - 1]
353
+ else
354
+ day > RepeaterMonth::MONTH_DAYS[month - 1]
306
355
  end
307
- repeaters.sort.reverse
308
356
  end
309
357
 
310
358
  # Recursively finds repeaters within other repeaters.
311
359
  # Returns a Span representing the innermost time span
312
360
  # or nil if no repeater union could be found
313
- def find_within(tags, span, pointer) #:nodoc:
361
+ def find_within(tags, span, pointer)
314
362
  puts "--#{span}" if Chronic.debug
315
363
  return span if tags.empty?
316
364
 
317
- head, *rest = tags
318
- head.start = pointer == :future ? span.begin : span.end
365
+ head = tags.shift
366
+ head.start = (pointer == :future ? span.begin : span.end)
319
367
  h = head.this(:none)
320
368
 
321
369
  if span.cover?(h.begin) || span.cover?(h.end)
322
- find_within(rest, h, pointer)
370
+ find_within(tags, h, pointer)
323
371
  end
324
372
  end
325
373
 
326
- def dealias_and_disambiguate_times(tokens, options) #:nodoc:
374
+ def dealias_and_disambiguate_times(tokens, options)
327
375
  # handle aliases of am/pm
328
376
  # 5:00 in the morning -> 5:00 am
329
377
  # 7:00 in the evening -> 7:00 pm
@@ -344,15 +392,16 @@ module Chronic
344
392
  end
345
393
  end
346
394
 
347
- if (day_portion_index && time_index)
395
+ if day_portion_index && time_index
348
396
  t1 = tokens[day_portion_index]
349
397
  t1tag = t1.get_tag(RepeaterDayPortion)
350
398
 
351
- if [:morning].include?(t1tag.type)
399
+ case t1tag.type
400
+ when :morning
352
401
  puts '--morning->am' if Chronic.debug
353
402
  t1.untag(RepeaterDayPortion)
354
403
  t1.tag(RepeaterDayPortion.new(:am))
355
- elsif [:afternoon, :evening, :night].include?(t1tag.type)
404
+ when :afternoon, :evening, :night
356
405
  puts "--#{t1tag.type}->pm" if Chronic.debug
357
406
  t1.untag(RepeaterDayPortion)
358
407
  t1.tag(RepeaterDayPortion.new(:pm))
@@ -361,17 +410,21 @@ module Chronic
361
410
 
362
411
  # handle ambiguous times if :ambiguous_time_range is specified
363
412
  if options[:ambiguous_time_range] != :none
364
- ttokens = []
365
- tokens.each_with_index do |t0, i|
366
- ttokens << t0
367
- t1 = tokens[i + 1]
368
- if t0.get_tag(RepeaterTime) && t0.get_tag(RepeaterTime).type.ambiguous? && (!t1 || !t1.get_tag(RepeaterDayPortion))
413
+ ambiguous_tokens = []
414
+
415
+ tokens.each_with_index do |token, i|
416
+ ambiguous_tokens << token
417
+ next_token = tokens[i + 1]
418
+
419
+ if token.get_tag(RepeaterTime) && token.get_tag(RepeaterTime).type.ambiguous? && (!next_token || !next_token.get_tag(RepeaterDayPortion))
369
420
  distoken = Token.new('disambiguator')
421
+
370
422
  distoken.tag(RepeaterDayPortion.new(options[:ambiguous_time_range]))
371
- ttokens << distoken
423
+ ambiguous_tokens << distoken
372
424
  end
373
425
  end
374
- tokens = ttokens
426
+
427
+ tokens = ambiguous_tokens
375
428
  end
376
429
 
377
430
  tokens
@@ -379,65 +432,4 @@ module Chronic
379
432
 
380
433
  end
381
434
 
382
- class Handler
383
- # @return [Array] A list of patterns
384
- attr_accessor :pattern
385
-
386
- # @return [Symbol] The method which handles this list of patterns.
387
- # This method should exist inside the {Handlers} module
388
- attr_accessor :handler_method
389
-
390
- # @param [Array] pattern A list of patterns to match tokens against
391
- # @param [Symbol] handler_method The method to be invoked when patterns
392
- # are matched. This method should exist inside the {Handlers} module
393
- def initialize(pattern, handler_method)
394
- @pattern = pattern
395
- @handler_method = handler_method
396
- end
397
-
398
- # @param [#to_s] The snake_case name representing a Chronic constant
399
- # @return [Class] The class represented by `name`
400
- # @raise [NameError] Raises if this constant could not be found
401
- def constantize(name)
402
- Chronic.const_get name.to_s.gsub(/(^|_)(.)/) { $2.upcase }
403
- end
404
-
405
- # @param [Array] tokens
406
- # @param [Hash] definitions
407
- # @return [Boolean]
408
- # @see Chronic.tokens_to_span
409
- def match(tokens, definitions)
410
- token_index = 0
411
- @pattern.each do |element|
412
- name = element.to_s
413
- optional = name[-1, 1] == '?'
414
- name = name.chop if optional
415
- if element.instance_of? Symbol
416
- klass = constantize(name)
417
- match = tokens[token_index] && !tokens[token_index].tags.select { |o| o.kind_of?(klass) }.empty?
418
- return false if !match && !optional
419
- (token_index += 1; next) if match
420
- next if !match && optional
421
- elsif element.instance_of? String
422
- return true if optional && token_index == tokens.size
423
- sub_handlers = definitions[name.intern] || raise(ChronicPain, "Invalid subset #{name} specified")
424
- sub_handlers.each do |sub_handler|
425
- return true if sub_handler.match(tokens[token_index..tokens.size], definitions)
426
- end
427
- return false
428
- else
429
- raise(ChronicPain, "Invalid match type: #{element.class}")
430
- end
431
- end
432
- return false if token_index != tokens.size
433
- return true
434
- end
435
-
436
- # @param [Handler] The handler to compare
437
- # @return [Boolean] True if these handlers match
438
- def ==(other)
439
- @pattern == other.pattern
440
- end
441
- end
442
-
443
435
  end