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