chronic 0.5.0 → 0.6.0
Sign up to get free protection for your applications and to get access to all the features.
- data/HISTORY.md +14 -1
- data/lib/chronic.rb +6 -42
- data/lib/chronic/chronic.rb +73 -48
- data/lib/chronic/handler.rb +90 -0
- data/lib/chronic/handlers.rb +131 -139
- data/lib/chronic/mini_date.rb +12 -6
- data/lib/chronic/numerizer.rb +3 -2
- data/lib/chronic/pointer.rb +1 -2
- data/lib/chronic/repeater.rb +3 -6
- data/lib/chronic/repeaters/repeater_day.rb +6 -6
- data/lib/chronic/repeaters/repeater_day_name.rb +1 -1
- data/lib/chronic/repeaters/repeater_day_portion.rb +8 -8
- data/lib/chronic/repeaters/repeater_fortnight.rb +2 -2
- data/lib/chronic/repeaters/repeater_hour.rb +7 -7
- data/lib/chronic/repeaters/repeater_minute.rb +6 -6
- data/lib/chronic/repeaters/repeater_month.rb +10 -10
- data/lib/chronic/repeaters/repeater_month_name.rb +9 -9
- data/lib/chronic/repeaters/repeater_season.rb +10 -10
- data/lib/chronic/repeaters/repeater_season_name.rb +2 -2
- data/lib/chronic/repeaters/repeater_weekday.rb +1 -1
- data/lib/chronic/repeaters/repeater_year.rb +11 -11
- data/lib/chronic/season.rb +3 -3
- data/lib/chronic/tag.rb +2 -0
- data/test/test_Chronic.rb +48 -4
- data/test/test_DaylightSavings.rb +1 -1
- data/test/test_Handler.rb +1 -6
- data/test/test_MiniDate.rb +3 -3
- data/test/test_Numerizer.rb +1 -1
- data/test/test_RepeaterDayName.rb +1 -1
- data/test/test_RepeaterFortnight.rb +1 -1
- data/test/test_RepeaterHour.rb +1 -1
- data/test/test_RepeaterMinute.rb +1 -1
- data/test/test_RepeaterMonth.rb +1 -1
- data/test/test_RepeaterMonthName.rb +1 -1
- data/test/test_RepeaterSeason.rb +1 -1
- data/test/test_RepeaterTime.rb +1 -1
- data/test/test_RepeaterWeek.rb +1 -1
- data/test/test_RepeaterWeekday.rb +1 -1
- data/test/test_RepeaterWeekend.rb +1 -1
- data/test/test_RepeaterYear.rb +1 -1
- data/test/test_Span.rb +1 -1
- data/test/test_Token.rb +1 -1
- data/test/test_parsing.rb +12 -6
- metadata +3 -5
- data/Manifest.txt +0 -63
- data/test/test_Time.rb +0 -49
data/HISTORY.md
CHANGED
@@ -1,4 +1,17 @@
|
|
1
|
-
#
|
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
|
data/lib/chronic.rb
CHANGED
@@ -28,7 +28,7 @@ require 'date'
|
|
28
28
|
#
|
29
29
|
# @author Tom Preston-Werner, Lee Jarvis
|
30
30
|
module Chronic
|
31
|
-
VERSION = "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
|
-
|
103
|
-
|
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.
|
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
|
data/lib/chronic/chronic.rb
CHANGED
@@ -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
|
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
|
70
|
+
raise ArgumentError, "Invalid context, :past/:future only"
|
71
71
|
end
|
72
72
|
|
73
73
|
options[:text] = text
|
74
|
-
options[: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
|
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 =
|
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
|
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
|
-
|
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
|
-
|
246
|
-
|
247
|
-
|
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
|
-
|
254
|
-
|
255
|
-
|
256
|
-
|
257
|
-
|
258
|
-
|
259
|
-
|
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
|
-
|
262
|
-
|
263
|
-
|
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
|
data/lib/chronic/handlers.rb
CHANGED
@@ -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)
|
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(
|
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)
|
17
|
-
|
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)
|
26
|
+
def handle_rmn_sd_on(tokens, options)
|
22
27
|
if tokens.size > 3
|
23
|
-
|
28
|
+
month = tokens[2].get_tag(RepeaterMonthName)
|
29
|
+
day = tokens[3].get_tag(ScalarDay).type
|
30
|
+
token_range = 0..1
|
24
31
|
else
|
25
|
-
|
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)
|
31
|
-
|
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)
|
36
|
-
|
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)
|
63
|
+
def handle_rmn_od_on(tokens, options)
|
41
64
|
if tokens.size > 3
|
42
|
-
|
65
|
+
month = tokens[2].get_tag(RepeaterMonthName)
|
66
|
+
day = tokens[3].get_tag(OrdinalDay).type
|
67
|
+
token_range = 0..1
|
43
68
|
else
|
44
|
-
|
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)
|
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
|
-
|
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)
|
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)
|
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)
|
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)
|
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)
|
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)
|
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)
|
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)
|
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)
|
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)
|
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
|
-
|
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)
|
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)
|
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)
|
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)
|
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)
|
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)
|
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)
|
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)
|
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)
|
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)
|
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.
|
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
|
-
|
284
|
-
|
285
|
-
|
286
|
-
|
287
|
-
|
288
|
-
|
289
|
-
|
290
|
-
|
291
|
-
|
292
|
-
|
293
|
-
|
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)
|
301
|
-
|
302
|
-
|
303
|
-
|
304
|
-
|
305
|
-
|
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)
|
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
|
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(
|
370
|
+
find_within(tags, h, pointer)
|
323
371
|
end
|
324
372
|
end
|
325
373
|
|
326
|
-
def dealias_and_disambiguate_times(tokens, options)
|
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
|
395
|
+
if day_portion_index && time_index
|
348
396
|
t1 = tokens[day_portion_index]
|
349
397
|
t1tag = t1.get_tag(RepeaterDayPortion)
|
350
398
|
|
351
|
-
|
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
|
-
|
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
|
-
|
365
|
-
|
366
|
-
|
367
|
-
|
368
|
-
|
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
|
-
|
423
|
+
ambiguous_tokens << distoken
|
372
424
|
end
|
373
425
|
end
|
374
|
-
|
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
|