chronic 0.5.0 → 0.6.0

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