chronic 0.8.0 → 0.9.1
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.
- checksums.yaml +7 -0
- data/.travis.yml +8 -0
- data/HISTORY.md +17 -0
- data/README.md +38 -45
- data/lib/chronic/handler.rb +7 -7
- data/lib/chronic/handlers.rb +83 -27
- data/lib/chronic/numerizer.rb +11 -2
- data/lib/chronic/{chronic.rb → parser.rb} +55 -134
- data/lib/chronic/repeater.rb +11 -11
- data/lib/chronic/separator.rb +15 -1
- data/lib/chronic/span.rb +2 -2
- data/lib/chronic/token.rb +4 -0
- data/lib/chronic.rb +96 -69
- data/test/test_chronic.rb +9 -27
- data/test/test_handler.rb +17 -13
- data/test/test_numerizer.rb +15 -1
- data/test/test_parsing.rb +130 -18
- data/test/test_span.rb +1 -1
- metadata +13 -18
|
@@ -1,25 +1,23 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
# Returns a Hash of default configuration options.
|
|
4
|
-
DEFAULT_OPTIONS = {
|
|
5
|
-
:context => :future,
|
|
6
|
-
:now => nil,
|
|
7
|
-
:guess => true,
|
|
8
|
-
:ambiguous_time_range => 6,
|
|
9
|
-
:endian_precedence => [:middle, :little],
|
|
10
|
-
:ambiguous_year_future_bias => 50
|
|
11
|
-
}
|
|
1
|
+
require 'chronic/handlers'
|
|
12
2
|
|
|
13
|
-
|
|
3
|
+
module Chronic
|
|
4
|
+
class Parser
|
|
5
|
+
include Handlers
|
|
6
|
+
|
|
7
|
+
# Hash of default configuration options.
|
|
8
|
+
DEFAULT_OPTIONS = {
|
|
9
|
+
:context => :future,
|
|
10
|
+
:now => nil,
|
|
11
|
+
:guess => true,
|
|
12
|
+
:ambiguous_time_range => 6,
|
|
13
|
+
:endian_precedence => [:middle, :little],
|
|
14
|
+
:ambiguous_year_future_bias => 50
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
attr_accessor :now
|
|
18
|
+
attr_reader :options
|
|
14
19
|
|
|
15
|
-
#
|
|
16
|
-
#
|
|
17
|
-
# If the parser can find a date or time, either a Time or Chronic::Span
|
|
18
|
-
# will be returned (depending on the value of `:guess`). If no
|
|
19
|
-
# date or time can be found, `nil` will be returned.
|
|
20
|
-
#
|
|
21
|
-
# text - The String text to parse.
|
|
22
|
-
# opts - An optional Hash of configuration options:
|
|
20
|
+
# options - An optional Hash of configuration options:
|
|
23
21
|
# :context - If your string represents a birthday, you can set
|
|
24
22
|
# this value to :past and if an ambiguous string is
|
|
25
23
|
# given, it will assume it is in the past.
|
|
@@ -47,31 +45,18 @@ module Chronic
|
|
|
47
45
|
# look x amount of years into the future and past. If the
|
|
48
46
|
# two digit year is `now + x years` it's assumed to be the
|
|
49
47
|
# future, `now - x years` is assumed to be the past.
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
# ensure the specified options are valid
|
|
56
|
-
(opts.keys - DEFAULT_OPTIONS.keys).each do |key|
|
|
57
|
-
raise ArgumentError, "#{key} is not a valid option key."
|
|
58
|
-
end
|
|
59
|
-
|
|
60
|
-
unless [:past, :future, :none].include?(options[:context])
|
|
61
|
-
raise ArgumentError, "Invalid context, :past/:future only"
|
|
62
|
-
end
|
|
63
|
-
|
|
64
|
-
options[:text] = text
|
|
65
|
-
Chronic.now = options[:now] || Chronic.time_class.now
|
|
48
|
+
def initialize(options = {})
|
|
49
|
+
@options = DEFAULT_OPTIONS.merge(options)
|
|
50
|
+
@now = options.delete(:now) || Chronic.time_class.now
|
|
51
|
+
end
|
|
66
52
|
|
|
67
|
-
|
|
53
|
+
# Parse "text" with the given options
|
|
54
|
+
# Returns either a Time or Chronic::Span, depending on the value of options[:guess]
|
|
55
|
+
def parse(text)
|
|
68
56
|
tokens = tokenize(text, options)
|
|
57
|
+
span = tokens_to_span(tokens, options.merge(:text => text))
|
|
69
58
|
|
|
70
|
-
if Chronic.debug
|
|
71
|
-
puts "+#{'-' * 51}\n| #{tokens}\n+#{'-' * 51}"
|
|
72
|
-
end
|
|
73
|
-
|
|
74
|
-
span = tokens_to_span(tokens, options)
|
|
59
|
+
puts "+#{'-' * 51}\n| #{tokens}\n+#{'-' * 51}" if Chronic.debug
|
|
75
60
|
|
|
76
61
|
if span
|
|
77
62
|
options[:guess] ? guess(span) : span
|
|
@@ -101,13 +86,14 @@ module Chronic
|
|
|
101
86
|
# Returns a new String ready for Chronic to parse.
|
|
102
87
|
def pre_normalize(text)
|
|
103
88
|
text = text.to_s.downcase
|
|
89
|
+
text.gsub!(/\b([ap])\.m\.?/, '\1m')
|
|
104
90
|
text.gsub!(/\./, ':')
|
|
105
91
|
text.gsub!(/['"]/, '')
|
|
106
92
|
text.gsub!(/,/, ' ')
|
|
107
93
|
text.gsub!(/^second /, '2nd ')
|
|
108
94
|
text.gsub!(/\bsecond (of|day|month|hour|minute|second)\b/, '2nd \1')
|
|
109
95
|
text = Numerizer.numerize(text)
|
|
110
|
-
text.gsub!(
|
|
96
|
+
text.gsub!(/\-(\d{2}:?\d{2})\b/, 'tzminus\1')
|
|
111
97
|
text.gsub!(/([\/\-\,\@])/) { ' ' + $1 + ' ' }
|
|
112
98
|
text.gsub!(/(?:^|\s)0(\d+:\d+\s*pm?\b)/, ' \1')
|
|
113
99
|
text.gsub!(/\btoday\b/, 'this day')
|
|
@@ -116,29 +102,25 @@ module Chronic
|
|
|
116
102
|
text.gsub!(/\bnoon\b/, '12:00pm')
|
|
117
103
|
text.gsub!(/\bmidnight\b/, '24:00')
|
|
118
104
|
text.gsub!(/\bnow\b/, 'this second')
|
|
105
|
+
text.gsub!('quarter', '15')
|
|
106
|
+
text.gsub!('half', '30')
|
|
107
|
+
text.gsub!(/(\d{1,2}) (to|till|prior to|before)\b/, '\1 minutes past')
|
|
108
|
+
text.gsub!(/(\d{1,2}) (after|past)\b/, '\1 minutes future')
|
|
119
109
|
text.gsub!(/\b(?:ago|before(?: now)?)\b/, 'past')
|
|
120
110
|
text.gsub!(/\bthis (?:last|past)\b/, 'last')
|
|
121
111
|
text.gsub!(/\b(?:in|during) the (morning)\b/, '\1')
|
|
122
112
|
text.gsub!(/\b(?:in the|during the|at) (afternoon|evening|night)\b/, '\1')
|
|
123
113
|
text.gsub!(/\btonight\b/, 'this night')
|
|
124
114
|
text.gsub!(/\b\d+:?\d*[ap]\b/,'\0m')
|
|
115
|
+
text.gsub!(/\b(\d{2})(\d{2})(am|pm)\b/, '\1:\2\3')
|
|
125
116
|
text.gsub!(/(\d)([ap]m|oclock)\b/, '\1 \2')
|
|
126
117
|
text.gsub!(/\b(hence|after|from)\b/, 'future')
|
|
127
118
|
text.gsub!(/^\s?an? /i, '1 ')
|
|
128
119
|
text.gsub!(/\b(\d{4}):(\d{2}):(\d{2})\b/, '\1 / \2 / \3') # DTOriginal
|
|
120
|
+
text.gsub!(/\b0(\d+):(\d{2}):(\d{2}) ([ap]m)\b/, '\1:\2:\3 \4')
|
|
129
121
|
text
|
|
130
122
|
end
|
|
131
123
|
|
|
132
|
-
# Convert number words to numbers (three => 3, fourth => 4th).
|
|
133
|
-
#
|
|
134
|
-
# text - The String to convert.
|
|
135
|
-
#
|
|
136
|
-
# Returns a new String with words converted to numbers.
|
|
137
|
-
def numericize_numbers(text)
|
|
138
|
-
warn "Chronic.numericize_numbers will be deprecated in version 0.7.0. Please use Chronic::Numerizer.numerize instead"
|
|
139
|
-
Numerizer.numerize(text)
|
|
140
|
-
end
|
|
141
|
-
|
|
142
124
|
# Guess a specific time within the given span.
|
|
143
125
|
#
|
|
144
126
|
# span - The Chronic::Span object to calcuate a guess from.
|
|
@@ -152,17 +134,16 @@ module Chronic
|
|
|
152
134
|
end
|
|
153
135
|
end
|
|
154
136
|
|
|
155
|
-
# List of Handler definitions. See
|
|
137
|
+
# List of Handler definitions. See Chronic.parse for a list of options this
|
|
156
138
|
# method accepts.
|
|
157
139
|
#
|
|
158
|
-
# options - An optional Hash of configuration options
|
|
159
|
-
# :endian_precedence -
|
|
140
|
+
# options - An optional Hash of configuration options.
|
|
160
141
|
#
|
|
161
|
-
# Returns
|
|
162
|
-
def definitions(options={})
|
|
142
|
+
# Returns a Hash of Handler definitions.
|
|
143
|
+
def definitions(options = {})
|
|
163
144
|
options[:endian_precedence] ||= [:middle, :little]
|
|
164
145
|
|
|
165
|
-
|
|
146
|
+
@@definitions ||= {
|
|
166
147
|
:time => [
|
|
167
148
|
Handler.new([:repeater_time, :repeater_day_portion?], nil)
|
|
168
149
|
],
|
|
@@ -172,12 +153,15 @@ module Chronic
|
|
|
172
153
|
Handler.new([:repeater_day_name, :repeater_month_name, :scalar_day], :handle_rdn_rmn_sd),
|
|
173
154
|
Handler.new([:repeater_day_name, :repeater_month_name, :scalar_day, :scalar_year], :handle_rdn_rmn_sd_sy),
|
|
174
155
|
Handler.new([:repeater_day_name, :repeater_month_name, :ordinal_day], :handle_rdn_rmn_od),
|
|
156
|
+
Handler.new([:repeater_day_name, :repeater_month_name, :scalar_day, :separator_at?, 'time?'], :handle_rdn_rmn_sd),
|
|
157
|
+
Handler.new([:repeater_day_name, :repeater_month_name, :ordinal_day, :separator_at?, 'time?'], :handle_rdn_rmn_od),
|
|
158
|
+
Handler.new([:repeater_day_name, :ordinal_day, :separator_at?, 'time?'], :handle_rdn_od),
|
|
175
159
|
Handler.new([:scalar_year, :separator_slash_or_dash, :scalar_month, :separator_slash_or_dash, :scalar_day, :repeater_time, :time_zone], :handle_generic),
|
|
176
160
|
Handler.new([:repeater_month_name, :scalar_day, :scalar_year], :handle_rmn_sd_sy),
|
|
177
161
|
Handler.new([:repeater_month_name, :ordinal_day, :scalar_year], :handle_rmn_od_sy),
|
|
178
162
|
Handler.new([:repeater_month_name, :scalar_day, :scalar_year, :separator_at?, 'time?'], :handle_rmn_sd_sy),
|
|
179
163
|
Handler.new([:repeater_month_name, :ordinal_day, :scalar_year, :separator_at?, 'time?'], :handle_rmn_od_sy),
|
|
180
|
-
Handler.new([:repeater_month_name, :scalar_day, :separator_at?, 'time?'], :handle_rmn_sd),
|
|
164
|
+
Handler.new([:repeater_month_name, :separator_slash_or_dash?, :scalar_day, :separator_at?, 'time?'], :handle_rmn_sd),
|
|
181
165
|
Handler.new([:repeater_time, :repeater_day_portion?, :separator_on?, :repeater_month_name, :scalar_day], :handle_rmn_sd_on),
|
|
182
166
|
Handler.new([:repeater_month_name, :ordinal_day, :separator_at?, 'time?'], :handle_rmn_od),
|
|
183
167
|
Handler.new([:ordinal_day, :repeater_month_name, :scalar_year, :separator_at?, 'time?'], :handle_od_rmn_sy),
|
|
@@ -187,29 +171,27 @@ module Chronic
|
|
|
187
171
|
Handler.new([:repeater_time, :repeater_day_portion?, :separator_on?, :repeater_month_name, :ordinal_day], :handle_rmn_od_on),
|
|
188
172
|
Handler.new([:repeater_month_name, :scalar_year], :handle_rmn_sy),
|
|
189
173
|
Handler.new([:scalar_day, :repeater_month_name, :scalar_year, :separator_at?, 'time?'], :handle_sd_rmn_sy),
|
|
190
|
-
Handler.new([:scalar_day, :repeater_month_name, :separator_at?, 'time?'], :handle_sd_rmn),
|
|
174
|
+
Handler.new([:scalar_day, :separator_slash_or_dash?, :repeater_month_name, :separator_at?, 'time?'], :handle_sd_rmn),
|
|
191
175
|
Handler.new([:scalar_year, :separator_slash_or_dash, :scalar_month, :separator_slash_or_dash, :scalar_day, :separator_at?, 'time?'], :handle_sy_sm_sd),
|
|
176
|
+
Handler.new([:scalar_year, :separator_slash_or_dash, :scalar_month], :handle_sy_sm),
|
|
192
177
|
Handler.new([:scalar_month, :separator_slash_or_dash, :scalar_year], :handle_sm_sy),
|
|
193
178
|
Handler.new([:scalar_day, :separator_slash_or_dash, :repeater_month_name, :separator_slash_or_dash, :scalar_year, :repeater_time?], :handle_sm_rmn_sy),
|
|
194
|
-
Handler.new([:scalar_year, :separator_slash_or_dash, :scalar_month, :separator_slash_or_dash, :time_zone], :handle_generic)
|
|
195
|
-
|
|
179
|
+
Handler.new([:scalar_year, :separator_slash_or_dash, :scalar_month, :separator_slash_or_dash, :scalar?, :time_zone], :handle_generic),
|
|
196
180
|
],
|
|
197
181
|
|
|
198
|
-
# tonight at 7pm
|
|
199
182
|
:anchor => [
|
|
200
183
|
Handler.new([:separator_on?, :grabber?, :repeater, :separator_at?, :repeater?, :repeater?], :handle_r),
|
|
201
184
|
Handler.new([:grabber?, :repeater, :repeater, :separator?, :repeater?, :repeater?], :handle_r),
|
|
202
185
|
Handler.new([:repeater, :grabber, :repeater], :handle_r_g_r)
|
|
203
186
|
],
|
|
204
187
|
|
|
205
|
-
# 3 weeks from now, in 2 months
|
|
206
188
|
:arrow => [
|
|
207
189
|
Handler.new([:scalar, :repeater, :pointer], :handle_s_r_p),
|
|
190
|
+
Handler.new([:scalar, :repeater, :separator_and?, :scalar, :repeater, :pointer, :separator_at?, 'anchor'], :handle_s_r_a_s_r_p_a),
|
|
208
191
|
Handler.new([:pointer, :scalar, :repeater], :handle_p_s_r),
|
|
209
192
|
Handler.new([:scalar, :repeater, :pointer, :separator_at?, 'anchor'], :handle_s_r_p_a)
|
|
210
193
|
],
|
|
211
194
|
|
|
212
|
-
# 3rd week in march
|
|
213
195
|
:narrow => [
|
|
214
196
|
Handler.new([:ordinal, :repeater, :separator_in, :repeater], :handle_o_r_s_r),
|
|
215
197
|
Handler.new([:ordinal, :repeater, :grabber, :repeater], :handle_o_r_g_r)
|
|
@@ -225,68 +207,12 @@ module Chronic
|
|
|
225
207
|
|
|
226
208
|
case endian = Array(options[:endian_precedence]).first
|
|
227
209
|
when :little
|
|
228
|
-
|
|
210
|
+
@@definitions.merge(:endian => endians.reverse)
|
|
229
211
|
when :middle
|
|
230
|
-
|
|
212
|
+
@@definitions.merge(:endian => endians)
|
|
231
213
|
else
|
|
232
214
|
raise ArgumentError, "Unknown endian option '#{endian}'"
|
|
233
215
|
end
|
|
234
|
-
|
|
235
|
-
@definitions
|
|
236
|
-
end
|
|
237
|
-
|
|
238
|
-
# Construct a new time object determining possible month overflows
|
|
239
|
-
# and leap years.
|
|
240
|
-
#
|
|
241
|
-
# year - Integer year.
|
|
242
|
-
# month - Integer month.
|
|
243
|
-
# day - Integer day.
|
|
244
|
-
# hour - Integer hour.
|
|
245
|
-
# minute - Integer minute.
|
|
246
|
-
# second - Integer second.
|
|
247
|
-
#
|
|
248
|
-
# Returns a new Time object constructed from these params.
|
|
249
|
-
def construct(year, month = 1, day = 1, hour = 0, minute = 0, second = 0)
|
|
250
|
-
if second >= 60
|
|
251
|
-
minute += second / 60
|
|
252
|
-
second = second % 60
|
|
253
|
-
end
|
|
254
|
-
|
|
255
|
-
if minute >= 60
|
|
256
|
-
hour += minute / 60
|
|
257
|
-
minute = minute % 60
|
|
258
|
-
end
|
|
259
|
-
|
|
260
|
-
if hour >= 24
|
|
261
|
-
day += hour / 24
|
|
262
|
-
hour = hour % 24
|
|
263
|
-
end
|
|
264
|
-
|
|
265
|
-
# determine if there is a day overflow. this is complicated by our crappy calendar
|
|
266
|
-
# system (non-constant number of days per month)
|
|
267
|
-
day <= 56 || raise("day must be no more than 56 (makes month resolution easier)")
|
|
268
|
-
if day > 28
|
|
269
|
-
# no month ever has fewer than 28 days, so only do this if necessary
|
|
270
|
-
leap_year_month_days = [31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
|
|
271
|
-
common_year_month_days = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
|
|
272
|
-
days_this_month = Date.leap?(year) ? leap_year_month_days[month - 1] : common_year_month_days[month - 1]
|
|
273
|
-
if day > days_this_month
|
|
274
|
-
month += day / days_this_month
|
|
275
|
-
day = day % days_this_month
|
|
276
|
-
end
|
|
277
|
-
end
|
|
278
|
-
|
|
279
|
-
if month > 12
|
|
280
|
-
if month % 12 == 0
|
|
281
|
-
year += (month - 12) / 12
|
|
282
|
-
month = 12
|
|
283
|
-
else
|
|
284
|
-
year += month / 12
|
|
285
|
-
month = month % 12
|
|
286
|
-
end
|
|
287
|
-
end
|
|
288
|
-
|
|
289
|
-
Chronic.time_class.local(year, month, day, hour, minute, second)
|
|
290
216
|
end
|
|
291
217
|
|
|
292
218
|
private
|
|
@@ -306,37 +232,32 @@ module Chronic
|
|
|
306
232
|
(definitions[:endian] + definitions[:date]).each do |handler|
|
|
307
233
|
if handler.match(tokens, definitions)
|
|
308
234
|
good_tokens = tokens.select { |o| !o.get_tag Separator }
|
|
309
|
-
return handler.invoke(:date, good_tokens, options)
|
|
235
|
+
return handler.invoke(:date, good_tokens, self, options)
|
|
310
236
|
end
|
|
311
237
|
end
|
|
312
238
|
|
|
313
239
|
definitions[:anchor].each do |handler|
|
|
314
240
|
if handler.match(tokens, definitions)
|
|
315
241
|
good_tokens = tokens.select { |o| !o.get_tag Separator }
|
|
316
|
-
return handler.invoke(:anchor, good_tokens, options)
|
|
242
|
+
return handler.invoke(:anchor, good_tokens, self, options)
|
|
317
243
|
end
|
|
318
244
|
end
|
|
319
245
|
|
|
320
246
|
definitions[:arrow].each do |handler|
|
|
321
247
|
if handler.match(tokens, definitions)
|
|
322
|
-
good_tokens = tokens.reject { |o| o.get_tag(SeparatorAt) || o.get_tag(SeparatorSlashOrDash) || o.get_tag(SeparatorComma) }
|
|
323
|
-
return handler.invoke(:arrow, good_tokens, options)
|
|
248
|
+
good_tokens = tokens.reject { |o| o.get_tag(SeparatorAt) || o.get_tag(SeparatorSlashOrDash) || o.get_tag(SeparatorComma) || o.get_tag(SeparatorAnd) }
|
|
249
|
+
return handler.invoke(:arrow, good_tokens, self, options)
|
|
324
250
|
end
|
|
325
251
|
end
|
|
326
252
|
|
|
327
253
|
definitions[:narrow].each do |handler|
|
|
328
254
|
if handler.match(tokens, definitions)
|
|
329
|
-
return handler.invoke(:narrow, tokens, options)
|
|
255
|
+
return handler.invoke(:narrow, tokens, self, options)
|
|
330
256
|
end
|
|
331
257
|
end
|
|
332
258
|
|
|
333
259
|
puts "-none" if Chronic.debug
|
|
334
260
|
return nil
|
|
335
261
|
end
|
|
336
|
-
|
|
337
|
-
end
|
|
338
|
-
|
|
339
|
-
# Internal exception
|
|
340
|
-
class ChronicPain < Exception
|
|
341
262
|
end
|
|
342
263
|
end
|
data/lib/chronic/repeater.rb
CHANGED
|
@@ -38,18 +38,18 @@ module Chronic
|
|
|
38
38
|
def self.scan_for_month_names(token)
|
|
39
39
|
scan_for token, RepeaterMonthName,
|
|
40
40
|
{
|
|
41
|
-
/^jan
|
|
42
|
-
/^feb
|
|
43
|
-
/^mar
|
|
44
|
-
/^apr
|
|
41
|
+
/^jan[:\.]?(uary)?$/ => :january,
|
|
42
|
+
/^feb[:\.]?(ruary)?$/ => :february,
|
|
43
|
+
/^mar[:\.]?(ch)?$/ => :march,
|
|
44
|
+
/^apr[:\.]?(il)?$/ => :april,
|
|
45
45
|
/^may$/ => :may,
|
|
46
|
-
/^jun
|
|
47
|
-
/^jul
|
|
48
|
-
/^aug
|
|
49
|
-
/^sep
|
|
50
|
-
/^oct
|
|
51
|
-
/^nov
|
|
52
|
-
/^dec
|
|
46
|
+
/^jun[:\.]?e?$/ => :june,
|
|
47
|
+
/^jul[:\.]?y?$/ => :july,
|
|
48
|
+
/^aug[:\.]?(ust)?$/ => :august,
|
|
49
|
+
/^sep[:\.]?(t[:\.]?|tember)?$/ => :september,
|
|
50
|
+
/^oct[:\.]?(ober)?$/ => :october,
|
|
51
|
+
/^nov[:\.]?(ember)?$/ => :november,
|
|
52
|
+
/^dec[:\.]?(ember)?$/ => :december
|
|
53
53
|
}
|
|
54
54
|
end
|
|
55
55
|
|
data/lib/chronic/separator.rb
CHANGED
|
@@ -15,6 +15,7 @@ module Chronic
|
|
|
15
15
|
if t = scan_for_at(token) then token.tag(t); next end
|
|
16
16
|
if t = scan_for_in(token) then token.tag(t); next end
|
|
17
17
|
if t = scan_for_on(token) then token.tag(t); next end
|
|
18
|
+
if t = scan_for_and(token) then token.tag(t); next end
|
|
18
19
|
end
|
|
19
20
|
end
|
|
20
21
|
|
|
@@ -57,6 +58,13 @@ module Chronic
|
|
|
57
58
|
scan_for token, SeparatorOn, { /^on$/ => :on }
|
|
58
59
|
end
|
|
59
60
|
|
|
61
|
+
# token - The Token object we want to scan.
|
|
62
|
+
#
|
|
63
|
+
# Returns a new SeperatorAnd Object object.
|
|
64
|
+
def self.scan_for_and(token)
|
|
65
|
+
scan_for token, SeparatorAnd, { /^and$/ => :and }
|
|
66
|
+
end
|
|
67
|
+
|
|
60
68
|
def to_s
|
|
61
69
|
'separator'
|
|
62
70
|
end
|
|
@@ -91,4 +99,10 @@ module Chronic
|
|
|
91
99
|
super << '-on'
|
|
92
100
|
end
|
|
93
101
|
end
|
|
94
|
-
|
|
102
|
+
|
|
103
|
+
class SeparatorAnd < Separator #:nodoc:
|
|
104
|
+
def to_s
|
|
105
|
+
super << '-and'
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
end
|
data/lib/chronic/span.rb
CHANGED
data/lib/chronic/token.rb
CHANGED
data/lib/chronic.rb
CHANGED
|
@@ -1,6 +1,41 @@
|
|
|
1
1
|
require 'time'
|
|
2
2
|
require 'date'
|
|
3
3
|
|
|
4
|
+
require 'chronic/parser'
|
|
5
|
+
|
|
6
|
+
require 'chronic/handler'
|
|
7
|
+
require 'chronic/handlers'
|
|
8
|
+
require 'chronic/mini_date'
|
|
9
|
+
require 'chronic/tag'
|
|
10
|
+
require 'chronic/span'
|
|
11
|
+
require 'chronic/token'
|
|
12
|
+
require 'chronic/grabber'
|
|
13
|
+
require 'chronic/pointer'
|
|
14
|
+
require 'chronic/scalar'
|
|
15
|
+
require 'chronic/ordinal'
|
|
16
|
+
require 'chronic/separator'
|
|
17
|
+
require 'chronic/time_zone'
|
|
18
|
+
require 'chronic/numerizer'
|
|
19
|
+
require 'chronic/season'
|
|
20
|
+
|
|
21
|
+
require 'chronic/repeater'
|
|
22
|
+
require 'chronic/repeaters/repeater_year'
|
|
23
|
+
require 'chronic/repeaters/repeater_season'
|
|
24
|
+
require 'chronic/repeaters/repeater_season_name'
|
|
25
|
+
require 'chronic/repeaters/repeater_month'
|
|
26
|
+
require 'chronic/repeaters/repeater_month_name'
|
|
27
|
+
require 'chronic/repeaters/repeater_fortnight'
|
|
28
|
+
require 'chronic/repeaters/repeater_week'
|
|
29
|
+
require 'chronic/repeaters/repeater_weekend'
|
|
30
|
+
require 'chronic/repeaters/repeater_weekday'
|
|
31
|
+
require 'chronic/repeaters/repeater_day'
|
|
32
|
+
require 'chronic/repeaters/repeater_day_name'
|
|
33
|
+
require 'chronic/repeaters/repeater_day_portion'
|
|
34
|
+
require 'chronic/repeaters/repeater_hour'
|
|
35
|
+
require 'chronic/repeaters/repeater_minute'
|
|
36
|
+
require 'chronic/repeaters/repeater_second'
|
|
37
|
+
require 'chronic/repeaters/repeater_time'
|
|
38
|
+
|
|
4
39
|
# Parse natural language dates and times into Time or Chronic::Span objects.
|
|
5
40
|
#
|
|
6
41
|
# Examples:
|
|
@@ -14,20 +49,8 @@ require 'date'
|
|
|
14
49
|
#
|
|
15
50
|
# Chronic.parse('monday', :context => :past)
|
|
16
51
|
# #=> Mon Aug 21 12:00:00 PDT 2006
|
|
17
|
-
#
|
|
18
|
-
# Chronic.parse('this tuesday 5:00')
|
|
19
|
-
# #=> Tue Aug 29 17:00:00 PDT 2006
|
|
20
|
-
#
|
|
21
|
-
# Chronic.parse('this tuesday 5:00', :ambiguous_time_range => :none)
|
|
22
|
-
# #=> Tue Aug 29 05:00:00 PDT 2006
|
|
23
|
-
#
|
|
24
|
-
# Chronic.parse('may 27th', :now => Time.local(2000, 1, 1))
|
|
25
|
-
# #=> Sat May 27 12:00:00 PDT 2000
|
|
26
|
-
#
|
|
27
|
-
# Chronic.parse('may 27th', :guess => false)
|
|
28
|
-
# #=> Sun May 27 00:00:00 PDT 2007..Mon May 28 00:00:00 PDT 2007
|
|
29
52
|
module Chronic
|
|
30
|
-
VERSION = "0.
|
|
53
|
+
VERSION = "0.9.1"
|
|
31
54
|
|
|
32
55
|
class << self
|
|
33
56
|
|
|
@@ -46,72 +69,76 @@ module Chronic
|
|
|
46
69
|
#
|
|
47
70
|
# Returns The Time class Chronic uses internally.
|
|
48
71
|
attr_accessor :time_class
|
|
49
|
-
|
|
50
|
-
# The current Time Chronic is using to base from.
|
|
51
|
-
#
|
|
52
|
-
# Examples:
|
|
53
|
-
#
|
|
54
|
-
# Time.now #=> 2011-06-06 14:13:43 +0100
|
|
55
|
-
# Chronic.parse('yesterday') #=> 2011-06-05 12:00:00 +0100
|
|
56
|
-
#
|
|
57
|
-
# now = Time.local(2025, 12, 24)
|
|
58
|
-
# Chronic.parse('tomorrow', :now => now) #=> 2025-12-25 12:00:00 +0000
|
|
59
|
-
#
|
|
60
|
-
# Returns a Time object.
|
|
61
|
-
attr_accessor :now
|
|
62
72
|
end
|
|
63
73
|
|
|
64
74
|
self.debug = false
|
|
65
75
|
self.time_class = Time
|
|
66
76
|
|
|
67
|
-
autoload :Handler, 'chronic/handler'
|
|
68
|
-
autoload :Handlers, 'chronic/handlers'
|
|
69
|
-
autoload :MiniDate, 'chronic/mini_date'
|
|
70
|
-
autoload :Tag, 'chronic/tag'
|
|
71
|
-
autoload :Span, 'chronic/span'
|
|
72
|
-
autoload :Token, 'chronic/token'
|
|
73
|
-
autoload :Grabber, 'chronic/grabber'
|
|
74
|
-
autoload :Pointer, 'chronic/pointer'
|
|
75
|
-
autoload :Scalar, 'chronic/scalar'
|
|
76
|
-
autoload :Ordinal, 'chronic/ordinal'
|
|
77
|
-
autoload :OrdinalDay, 'chronic/ordinal'
|
|
78
|
-
autoload :Separator, 'chronic/separator'
|
|
79
|
-
autoload :TimeZone, 'chronic/time_zone'
|
|
80
|
-
autoload :Numerizer, 'chronic/numerizer'
|
|
81
|
-
autoload :Season, 'chronic/season'
|
|
82
|
-
|
|
83
|
-
autoload :Repeater, 'chronic/repeater'
|
|
84
|
-
autoload :RepeaterYear, 'chronic/repeaters/repeater_year'
|
|
85
|
-
autoload :RepeaterSeason, 'chronic/repeaters/repeater_season'
|
|
86
|
-
autoload :RepeaterSeasonName, 'chronic/repeaters/repeater_season_name'
|
|
87
|
-
autoload :RepeaterMonth, 'chronic/repeaters/repeater_month'
|
|
88
|
-
autoload :RepeaterMonthName, 'chronic/repeaters/repeater_month_name'
|
|
89
|
-
autoload :RepeaterFortnight, 'chronic/repeaters/repeater_fortnight'
|
|
90
|
-
autoload :RepeaterWeek, 'chronic/repeaters/repeater_week'
|
|
91
|
-
autoload :RepeaterWeekend, 'chronic/repeaters/repeater_weekend'
|
|
92
|
-
autoload :RepeaterWeekday, 'chronic/repeaters/repeater_weekday'
|
|
93
|
-
autoload :RepeaterDay, 'chronic/repeaters/repeater_day'
|
|
94
|
-
autoload :RepeaterDayName, 'chronic/repeaters/repeater_day_name'
|
|
95
|
-
autoload :RepeaterDayPortion, 'chronic/repeaters/repeater_day_portion'
|
|
96
|
-
autoload :RepeaterHour, 'chronic/repeaters/repeater_hour'
|
|
97
|
-
autoload :RepeaterMinute, 'chronic/repeaters/repeater_minute'
|
|
98
|
-
autoload :RepeaterSecond, 'chronic/repeaters/repeater_second'
|
|
99
|
-
autoload :RepeaterTime, 'chronic/repeaters/repeater_time'
|
|
100
77
|
|
|
101
|
-
|
|
78
|
+
# Parses a string containing a natural language date or time.
|
|
79
|
+
#
|
|
80
|
+
# If the parser can find a date or time, either a Time or Chronic::Span
|
|
81
|
+
# will be returned (depending on the value of `:guess`). If no
|
|
82
|
+
# date or time can be found, `nil` will be returned.
|
|
83
|
+
#
|
|
84
|
+
# text - The String text to parse.
|
|
85
|
+
# opts - An optional Hash of configuration options passed to Parser::new.
|
|
86
|
+
def self.parse(text, options = {})
|
|
87
|
+
Parser.new(options).parse(text)
|
|
88
|
+
end
|
|
102
89
|
|
|
103
|
-
|
|
90
|
+
# Construct a new time object determining possible month overflows
|
|
91
|
+
# and leap years.
|
|
92
|
+
#
|
|
93
|
+
# year - Integer year.
|
|
94
|
+
# month - Integer month.
|
|
95
|
+
# day - Integer day.
|
|
96
|
+
# hour - Integer hour.
|
|
97
|
+
# minute - Integer minute.
|
|
98
|
+
# second - Integer second.
|
|
99
|
+
#
|
|
100
|
+
# Returns a new Time object constructed from these params.
|
|
101
|
+
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
|
|
104
106
|
|
|
105
|
-
|
|
107
|
+
if minute >= 60
|
|
108
|
+
hour += minute / 60
|
|
109
|
+
minute = minute % 60
|
|
110
|
+
end
|
|
106
111
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
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
|
|
111
140
|
|
|
112
|
-
|
|
113
|
-
warn "Time.to_minidate will be deprecated in version 0.7.0. Please use Chronic::MiniDate.from_time(time) instead"
|
|
114
|
-
Chronic::MiniDate.from_time(self)
|
|
141
|
+
Chronic.time_class.local(year, month, day, hour, minute, second)
|
|
115
142
|
end
|
|
116
143
|
|
|
117
144
|
end
|
data/test/test_chronic.rb
CHANGED
|
@@ -8,12 +8,12 @@ class TestChronic < TestCase
|
|
|
8
8
|
end
|
|
9
9
|
|
|
10
10
|
def test_pre_normalize
|
|
11
|
-
assert_equal Chronic.pre_normalize('12:55 pm'), Chronic.pre_normalize('12.55 pm')
|
|
11
|
+
assert_equal Chronic::Parser.new.pre_normalize('12:55 pm'), Chronic::Parser.new.pre_normalize('12.55 pm')
|
|
12
12
|
end
|
|
13
13
|
|
|
14
14
|
def test_pre_normalize_numerized_string
|
|
15
15
|
string = 'two and a half years'
|
|
16
|
-
assert_equal Chronic::Numerizer.numerize(string), Chronic.pre_normalize(string)
|
|
16
|
+
assert_equal Chronic::Numerizer.numerize(string), Chronic::Parser.new.pre_normalize(string)
|
|
17
17
|
end
|
|
18
18
|
|
|
19
19
|
def test_post_normalize_am_pm_aliases
|
|
@@ -46,21 +46,13 @@ class TestChronic < TestCase
|
|
|
46
46
|
|
|
47
47
|
def test_guess
|
|
48
48
|
span = Chronic::Span.new(Time.local(2006, 8, 16, 0), Time.local(2006, 8, 17, 0))
|
|
49
|
-
assert_equal Time.local(2006, 8, 16, 12), Chronic.guess(span)
|
|
49
|
+
assert_equal Time.local(2006, 8, 16, 12), Chronic::Parser.new.guess(span)
|
|
50
50
|
|
|
51
51
|
span = Chronic::Span.new(Time.local(2006, 8, 16, 0), Time.local(2006, 8, 17, 0, 0, 1))
|
|
52
|
-
assert_equal Time.local(2006, 8, 16, 12), Chronic.guess(span)
|
|
52
|
+
assert_equal Time.local(2006, 8, 16, 12), Chronic::Parser.new.guess(span)
|
|
53
53
|
|
|
54
54
|
span = Chronic::Span.new(Time.local(2006, 11), Time.local(2006, 12))
|
|
55
|
-
assert_equal Time.local(2006, 11, 16), Chronic.guess(span)
|
|
56
|
-
end
|
|
57
|
-
|
|
58
|
-
def test_now
|
|
59
|
-
Chronic.parse('now', :now => Time.local(2006, 01))
|
|
60
|
-
assert_equal Time.local(2006, 01), Chronic.now
|
|
61
|
-
|
|
62
|
-
Chronic.parse('now', :now => Time.local(2007, 01))
|
|
63
|
-
assert_equal Time.local(2007, 01), Chronic.now
|
|
55
|
+
assert_equal Time.local(2006, 11, 16), Chronic::Parser.new.guess(span)
|
|
64
56
|
end
|
|
65
57
|
|
|
66
58
|
def test_endian_definitions
|
|
@@ -72,26 +64,16 @@ class TestChronic < TestCase
|
|
|
72
64
|
Chronic::Handler.new([:scalar_day, :separator_slash_or_dash, :scalar_month, :separator_slash_or_dash, :scalar_year, :separator_at?, 'time?'], :handle_sd_sm_sy)
|
|
73
65
|
]
|
|
74
66
|
|
|
75
|
-
assert_equal endians, Chronic.definitions[:endian]
|
|
67
|
+
assert_equal endians, Chronic::Parser.new.definitions[:endian]
|
|
76
68
|
|
|
77
|
-
defs = Chronic.definitions(:endian_precedence => :little)
|
|
69
|
+
defs = Chronic::Parser.new.definitions(:endian_precedence => :little)
|
|
78
70
|
assert_equal endians.reverse, defs[:endian]
|
|
79
71
|
|
|
80
|
-
defs = Chronic.definitions(:endian_precedence => [:little, :middle])
|
|
72
|
+
defs = Chronic::Parser.new.definitions(:endian_precedence => [:little, :middle])
|
|
81
73
|
assert_equal endians.reverse, defs[:endian]
|
|
82
74
|
|
|
83
75
|
assert_raises(ArgumentError) do
|
|
84
|
-
Chronic.definitions(:endian_precedence => :invalid)
|
|
85
|
-
end
|
|
86
|
-
end
|
|
87
|
-
|
|
88
|
-
def test_passing_options
|
|
89
|
-
assert_raises(ArgumentError) do
|
|
90
|
-
Chronic.parse('now', :invalid => :option)
|
|
91
|
-
end
|
|
92
|
-
|
|
93
|
-
assert_raises(ArgumentError) do
|
|
94
|
-
Chronic.parse('now', :context => :invalid_context)
|
|
76
|
+
Chronic::Parser.new.definitions(:endian_precedence => :invalid)
|
|
95
77
|
end
|
|
96
78
|
end
|
|
97
79
|
|