chronic 0.9.1 → 0.10.2
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 +4 -4
- data/.gitignore +4 -4
- data/HISTORY.md +21 -0
- data/README.md +8 -0
- data/Rakefile +28 -8
- data/chronic.gemspec +8 -5
- data/lib/chronic/date.rb +82 -0
- data/lib/chronic/handler.rb +34 -25
- data/lib/chronic/handlers.rb +22 -3
- data/lib/chronic/ordinal.rb +22 -20
- data/lib/chronic/parser.rb +31 -26
- data/lib/chronic/repeater.rb +18 -18
- data/lib/chronic/repeaters/repeater_day.rb +4 -3
- data/lib/chronic/repeaters/repeater_day_name.rb +5 -4
- data/lib/chronic/repeaters/repeater_day_portion.rb +4 -3
- data/lib/chronic/repeaters/repeater_fortnight.rb +4 -3
- data/lib/chronic/repeaters/repeater_hour.rb +4 -3
- data/lib/chronic/repeaters/repeater_minute.rb +4 -3
- data/lib/chronic/repeaters/repeater_month.rb +5 -4
- data/lib/chronic/repeaters/repeater_month_name.rb +4 -3
- data/lib/chronic/repeaters/repeater_season.rb +5 -3
- data/lib/chronic/repeaters/repeater_second.rb +4 -3
- data/lib/chronic/repeaters/repeater_time.rb +35 -25
- data/lib/chronic/repeaters/repeater_week.rb +4 -3
- data/lib/chronic/repeaters/repeater_weekday.rb +4 -3
- data/lib/chronic/repeaters/repeater_weekend.rb +4 -3
- data/lib/chronic/repeaters/repeater_year.rb +5 -4
- data/lib/chronic/scalar.rb +34 -69
- data/lib/chronic/separator.rb +107 -8
- data/lib/chronic/sign.rb +49 -0
- data/lib/chronic/tag.rb +5 -4
- data/lib/chronic/time.rb +40 -0
- data/lib/chronic.rb +16 -10
- data/test/helper.rb +2 -2
- data/test/test_chronic.rb +55 -4
- data/test/test_handler.rb +20 -0
- data/test/test_parsing.rb +75 -3
- data/test/test_repeater_time.rb +18 -0
- metadata +37 -5
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA1:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: d0bcdd2460eb86f8898212a9eca5e325c9aa5e61
|
|
4
|
+
data.tar.gz: d8abd19da58e28473139b8f137991cb0ee150c49
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 04bfd4f32ce8f8ef6585042425ea64cc95d07998287fb16fa45b8d0a06d5dd48f8b85e6fecbf00e69101e560f21527ef6539a8994a9457357d25c2c832c7b87f
|
|
7
|
+
data.tar.gz: 87db3c22292ed9d5a22ecc84e6b14dafb2ed1c7a4c2f829873593b86b37707999adfaf43a5e0d801daffaaec9c7b599beade2d366d108c60bf2e8989998ed4b4
|
data/.gitignore
CHANGED
data/HISTORY.md
CHANGED
|
@@ -1,3 +1,24 @@
|
|
|
1
|
+
# 0.10.2 / 2013-09-09
|
|
2
|
+
|
|
3
|
+
* Fix 1.8.7 support (due to be dropped in 0.11.0)
|
|
4
|
+
* Bugfix for times with negative zones
|
|
5
|
+
|
|
6
|
+
# 0.10.1 / 2013-08-27
|
|
7
|
+
|
|
8
|
+
* Support `ActiveSupport::TimeZone` (#209, #208)
|
|
9
|
+
|
|
10
|
+
# 0.10.0 / 2013-08-25
|
|
11
|
+
|
|
12
|
+
* Chronic will parse subseconds correctly
|
|
13
|
+
for all supported date/time formats (#195, #198 and #200)
|
|
14
|
+
* Support for date format: dd.mm.yyyy (#197)
|
|
15
|
+
* Option `:hours24` to parse as 24 hour clock (#201 and #202)
|
|
16
|
+
* `:guess` option allows to specify which part of Span to return.
|
|
17
|
+
(accepted values `false`,`true`,`:begin`, `:middle`, `:end`)
|
|
18
|
+
* Replace `rcov` with `SimpleCov` for coverage generation
|
|
19
|
+
* Add more tests
|
|
20
|
+
* Various changes in codebase (#202 and #206)
|
|
21
|
+
|
|
1
22
|
# 0.9.1 / 2013-02-25
|
|
2
23
|
|
|
3
24
|
* Ensure Chronic strips periods from day portions (#173)
|
data/README.md
CHANGED
|
@@ -37,8 +37,15 @@ Chronic.parse('may 27th', :guess => false)
|
|
|
37
37
|
|
|
38
38
|
Chronic.parse('6/4/2012', :endian_precedence => :little)
|
|
39
39
|
#=> Fri Apr 06 00:00:00 PDT 2012
|
|
40
|
+
|
|
41
|
+
Chronic.parse('INVALID DATE')
|
|
42
|
+
#=> nil
|
|
40
43
|
```
|
|
41
44
|
|
|
45
|
+
If the parser can find a date or time, either a Time or Chronic::Span
|
|
46
|
+
will be returned (depending on the value of `:guess`). If no
|
|
47
|
+
date or time can be found, `nil` will be returned.
|
|
48
|
+
|
|
42
49
|
See `Chronic.parse` for detailed usage instructions.
|
|
43
50
|
|
|
44
51
|
## Examples
|
|
@@ -128,6 +135,7 @@ Specific Times (many of the above with an added time)
|
|
|
128
135
|
* January 5 at 7pm
|
|
129
136
|
* 22nd of june at 8am
|
|
130
137
|
* 1979-05-27 05:00:00
|
|
138
|
+
* 03/01/2012 07:25:09.234567
|
|
131
139
|
* etc
|
|
132
140
|
|
|
133
141
|
|
data/Rakefile
CHANGED
|
@@ -5,17 +5,37 @@ def version
|
|
|
5
5
|
contents[/VERSION = "([^"]+)"/, 1]
|
|
6
6
|
end
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
def do_test
|
|
9
9
|
$:.unshift './test'
|
|
10
10
|
Dir.glob('test/test_*.rb').each { |t| require File.basename(t) }
|
|
11
11
|
end
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
def open_command
|
|
14
|
+
case RUBY_PLATFORM
|
|
15
|
+
when /mswin|msys|mingw|cygwin|bccwin|wince|emc/
|
|
16
|
+
'start'
|
|
17
|
+
when /darwin|mac os/
|
|
18
|
+
'open'
|
|
19
|
+
else
|
|
20
|
+
'xdg-open'
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
task :test do
|
|
25
|
+
do_test
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
desc "Generate SimpleCov test coverage and open in your browser"
|
|
14
29
|
task :coverage do
|
|
15
|
-
require '
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
30
|
+
require 'simplecov'
|
|
31
|
+
FileUtils.rm_rf("./coverage")
|
|
32
|
+
SimpleCov.command_name 'Unit Tests'
|
|
33
|
+
SimpleCov.at_exit do
|
|
34
|
+
SimpleCov.result.format!
|
|
35
|
+
sh "#{open_command} #{SimpleCov.coverage_path}/index.html"
|
|
36
|
+
end
|
|
37
|
+
SimpleCov.start
|
|
38
|
+
do_test
|
|
19
39
|
end
|
|
20
40
|
|
|
21
41
|
desc "Open an irb session preloaded with this library"
|
|
@@ -38,9 +58,9 @@ end
|
|
|
38
58
|
|
|
39
59
|
desc "Build a gem from the gemspec"
|
|
40
60
|
task :build do
|
|
41
|
-
|
|
61
|
+
FileUtils.mkdir_p "pkg"
|
|
42
62
|
sh "gem build chronic.gemspec"
|
|
43
|
-
|
|
63
|
+
FileUtils.mv("./chronic-#{version}.gem", "pkg")
|
|
44
64
|
end
|
|
45
65
|
|
|
46
66
|
task :default => :test
|
data/chronic.gemspec
CHANGED
|
@@ -8,13 +8,16 @@ Gem::Specification.new do |s|
|
|
|
8
8
|
s.summary = 'Natural language date/time parsing.'
|
|
9
9
|
s.description = 'Chronic is a natural language date/time parser written in pure Ruby.'
|
|
10
10
|
s.authors = ['Tom Preston-Werner', 'Lee Jarvis']
|
|
11
|
-
s.email = ['tom@mojombo.com', '
|
|
11
|
+
s.email = ['tom@mojombo.com', 'ljjarvis@gmail.com']
|
|
12
12
|
s.homepage = 'http://github.com/mojombo/chronic'
|
|
13
|
+
s.license = 'MIT'
|
|
13
14
|
s.rdoc_options = ['--charset=UTF-8']
|
|
14
15
|
s.extra_rdoc_files = %w[README.md HISTORY.md LICENSE]
|
|
15
|
-
s.files = `git ls-files`.split(
|
|
16
|
-
s.test_files = `git ls-files -- test`.split(
|
|
16
|
+
s.files = `git ls-files`.split($/)
|
|
17
|
+
s.test_files = `git ls-files -- test`.split($/)
|
|
17
18
|
|
|
18
19
|
s.add_development_dependency 'rake'
|
|
19
|
-
s.add_development_dependency '
|
|
20
|
-
|
|
20
|
+
s.add_development_dependency 'simplecov'
|
|
21
|
+
s.add_development_dependency 'minitest', '~> 5.0'
|
|
22
|
+
s.add_development_dependency 'activesupport'
|
|
23
|
+
end
|
data/lib/chronic/date.rb
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
module Chronic
|
|
2
|
+
class Date
|
|
3
|
+
YEAR_MONTHS = 12
|
|
4
|
+
MONTH_DAYS = [nil, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
|
|
5
|
+
MONTH_DAYS_LEAP = [nil, 31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
|
|
6
|
+
YEAR_SECONDS = 31_536_000 # 365 * 24 * 60 * 60
|
|
7
|
+
SEASON_SECONDS = 7_862_400 # 91 * 24 * 60 * 60
|
|
8
|
+
MONTH_SECONDS = 2_592_000 # 30 * 24 * 60 * 60
|
|
9
|
+
FORTNIGHT_SECONDS = 1_209_600 # 14 * 24 * 60 * 60
|
|
10
|
+
WEEK_SECONDS = 604_800 # 7 * 24 * 60 * 60
|
|
11
|
+
WEEKEND_SECONDS = 172_800 # 2 * 24 * 60 * 60
|
|
12
|
+
DAY_SECONDS = 86_400 # 24 * 60 * 60
|
|
13
|
+
MONTHS = {
|
|
14
|
+
:january => 1,
|
|
15
|
+
:february => 2,
|
|
16
|
+
:march => 3,
|
|
17
|
+
:april => 4,
|
|
18
|
+
:may => 5,
|
|
19
|
+
:june => 6,
|
|
20
|
+
:july => 7,
|
|
21
|
+
:august => 8,
|
|
22
|
+
:september => 9,
|
|
23
|
+
:october => 10,
|
|
24
|
+
:november => 11,
|
|
25
|
+
:december => 12
|
|
26
|
+
}
|
|
27
|
+
DAYS = {
|
|
28
|
+
:sunday => 0,
|
|
29
|
+
:monday => 1,
|
|
30
|
+
:tuesday => 2,
|
|
31
|
+
:wednesday => 3,
|
|
32
|
+
:thursday => 4,
|
|
33
|
+
:friday => 5,
|
|
34
|
+
:saturday => 6
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
# Checks if given number could be day
|
|
38
|
+
def self.could_be_day?(day)
|
|
39
|
+
day >= 1 && day <= 31
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Checks if given number could be month
|
|
43
|
+
def self.could_be_month?(month)
|
|
44
|
+
month >= 1 && month <= 12
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Checks if given number could be year
|
|
48
|
+
def self.could_be_year?(year)
|
|
49
|
+
year >= 1 && year <= 9999
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Build a year from a 2 digit suffix.
|
|
53
|
+
#
|
|
54
|
+
# year - The two digit Integer year to build from.
|
|
55
|
+
# bias - The Integer amount of future years to bias.
|
|
56
|
+
#
|
|
57
|
+
# Examples:
|
|
58
|
+
#
|
|
59
|
+
# make_year(96, 50) #=> 1996
|
|
60
|
+
# make_year(79, 20) #=> 2079
|
|
61
|
+
# make_year(00, 50) #=> 2000
|
|
62
|
+
#
|
|
63
|
+
# Returns The Integer 4 digit year.
|
|
64
|
+
def self.make_year(year, bias)
|
|
65
|
+
return year if year.to_s.size > 2
|
|
66
|
+
start_year = Chronic.time_class.now.year - bias
|
|
67
|
+
century = (start_year / 100) * 100
|
|
68
|
+
full_year = century + year
|
|
69
|
+
full_year += 100 if full_year < start_year
|
|
70
|
+
full_year
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def self.month_overflow?(year, month, day)
|
|
74
|
+
if ::Date.leap?(year)
|
|
75
|
+
day > Date::MONTH_DAYS_LEAP[month]
|
|
76
|
+
else
|
|
77
|
+
day > Date::MONTH_DAYS[month]
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
end
|
|
82
|
+
end
|
data/lib/chronic/handler.rb
CHANGED
|
@@ -19,39 +19,48 @@ module Chronic
|
|
|
19
19
|
# Returns true if a match is found.
|
|
20
20
|
def match(tokens, definitions)
|
|
21
21
|
token_index = 0
|
|
22
|
+
@pattern.each do |elements|
|
|
23
|
+
was_optional = false
|
|
24
|
+
elements = [elements] unless elements.is_a?(Array)
|
|
25
|
+
|
|
26
|
+
elements.each_index do |i|
|
|
27
|
+
name = elements[i].to_s
|
|
28
|
+
optional = name[-1, 1] == '?'
|
|
29
|
+
name = name.chop if optional
|
|
30
|
+
|
|
31
|
+
case elements[i]
|
|
32
|
+
when Symbol
|
|
33
|
+
if tags_match?(name, tokens, token_index)
|
|
34
|
+
token_index += 1
|
|
35
|
+
break
|
|
36
|
+
else
|
|
37
|
+
if optional
|
|
38
|
+
was_optional = true
|
|
39
|
+
next
|
|
40
|
+
elsif i + 1 < elements.count
|
|
41
|
+
next
|
|
42
|
+
else
|
|
43
|
+
return false unless was_optional
|
|
44
|
+
end
|
|
45
|
+
end
|
|
22
46
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
optional = name[-1, 1] == '?'
|
|
26
|
-
name = name.chop if optional
|
|
47
|
+
when String
|
|
48
|
+
return true if optional && token_index == tokens.size
|
|
27
49
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
if tags_match?(name, tokens, token_index)
|
|
31
|
-
token_index += 1
|
|
32
|
-
next
|
|
33
|
-
else
|
|
34
|
-
if optional
|
|
35
|
-
next
|
|
50
|
+
if definitions.key?(name.to_sym)
|
|
51
|
+
sub_handlers = definitions[name.to_sym]
|
|
36
52
|
else
|
|
37
|
-
|
|
53
|
+
raise "Invalid subset #{name} specified"
|
|
38
54
|
end
|
|
39
|
-
end
|
|
40
|
-
when String
|
|
41
|
-
return true if optional && token_index == tokens.size
|
|
42
55
|
|
|
43
|
-
|
|
44
|
-
|
|
56
|
+
sub_handlers.each do |sub_handler|
|
|
57
|
+
return true if sub_handler.match(tokens[token_index..tokens.size], definitions)
|
|
58
|
+
end
|
|
45
59
|
else
|
|
46
|
-
raise "Invalid
|
|
47
|
-
end
|
|
48
|
-
|
|
49
|
-
sub_handlers.each do |sub_handler|
|
|
50
|
-
return true if sub_handler.match(tokens[token_index..tokens.size], definitions)
|
|
60
|
+
raise "Invalid match type: #{elements[i].class}"
|
|
51
61
|
end
|
|
52
|
-
else
|
|
53
|
-
raise "Invalid match type: #{element.class}"
|
|
54
62
|
end
|
|
63
|
+
|
|
55
64
|
end
|
|
56
65
|
|
|
57
66
|
return false if token_index != tokens.size
|
data/lib/chronic/handlers.rb
CHANGED
|
@@ -134,6 +134,8 @@ module Chronic
|
|
|
134
134
|
def handle_generic(tokens, options)
|
|
135
135
|
t = Chronic.time_class.parse(options[:text])
|
|
136
136
|
Span.new(t, t + 1)
|
|
137
|
+
rescue ArgumentError => e
|
|
138
|
+
raise e unless e.message =~ /out of range/
|
|
137
139
|
end
|
|
138
140
|
|
|
139
141
|
# Handle repeater-month-name/scalar-day/scalar-year
|
|
@@ -304,6 +306,23 @@ module Chronic
|
|
|
304
306
|
end
|
|
305
307
|
end
|
|
306
308
|
|
|
309
|
+
# Handle RepeaterDayName RepeaterMonthName OrdinalDay ScalarYear
|
|
310
|
+
def handle_rdn_rmn_od_sy(tokens, options)
|
|
311
|
+
month = tokens[1].get_tag(RepeaterMonthName)
|
|
312
|
+
day = tokens[2].get_tag(OrdinalDay).type
|
|
313
|
+
year = tokens[3].get_tag(ScalarYear).type
|
|
314
|
+
|
|
315
|
+
return if month_overflow?(year, month.index, day)
|
|
316
|
+
|
|
317
|
+
begin
|
|
318
|
+
start_time = Chronic.time_class.local(year, month.index, day)
|
|
319
|
+
end_time = time_with_rollover(year, month.index, day + 1)
|
|
320
|
+
Span.new(start_time, end_time)
|
|
321
|
+
rescue ArgumentError
|
|
322
|
+
nil
|
|
323
|
+
end
|
|
324
|
+
end
|
|
325
|
+
|
|
307
326
|
# Handle RepeaterDayName OrdinalDay
|
|
308
327
|
def handle_rdn_od(tokens, options)
|
|
309
328
|
day = tokens[1].get_tag(OrdinalDay).type
|
|
@@ -476,9 +495,9 @@ module Chronic
|
|
|
476
495
|
def day_or_time(day_start, time_tokens, options)
|
|
477
496
|
outer_span = Span.new(day_start, day_start + (24 * 60 * 60))
|
|
478
497
|
|
|
479
|
-
|
|
498
|
+
unless time_tokens.empty?
|
|
480
499
|
self.now = outer_span.begin
|
|
481
|
-
get_anchor(dealias_and_disambiguate_times(time_tokens, options), options)
|
|
500
|
+
get_anchor(dealias_and_disambiguate_times(time_tokens, options), options.merge(:context => :future))
|
|
482
501
|
else
|
|
483
502
|
outer_span
|
|
484
503
|
end
|
|
@@ -525,7 +544,7 @@ module Chronic
|
|
|
525
544
|
end
|
|
526
545
|
|
|
527
546
|
def month_overflow?(year, month, day)
|
|
528
|
-
if Date.leap?(year)
|
|
547
|
+
if ::Date.leap?(year)
|
|
529
548
|
day > RepeaterMonth::MONTH_DAYS_LEAP[month - 1]
|
|
530
549
|
else
|
|
531
550
|
day > RepeaterMonth::MONTH_DAYS[month - 1]
|
data/lib/chronic/ordinal.rb
CHANGED
|
@@ -9,26 +9,16 @@ module Chronic
|
|
|
9
9
|
#
|
|
10
10
|
# Returns an Array of tokens.
|
|
11
11
|
def self.scan(tokens, options)
|
|
12
|
-
tokens.
|
|
13
|
-
if
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
Ordinal.new($1.to_i) if token.word =~ /^(\d*)(st|nd|rd|th)$/
|
|
23
|
-
end
|
|
24
|
-
|
|
25
|
-
# token - The Token object we want to scan.
|
|
26
|
-
#
|
|
27
|
-
# Returns a new Ordinal object.
|
|
28
|
-
def self.scan_for_days(token)
|
|
29
|
-
if token.word =~ /^(\d*)(st|nd|rd|th)$/
|
|
30
|
-
unless $1.to_i > 31 || $1.to_i < 1
|
|
31
|
-
OrdinalDay.new(token.word.to_i)
|
|
12
|
+
tokens.each_index do |i|
|
|
13
|
+
if tokens[i].word =~ /^(\d+)(st|nd|rd|th|\.)$/
|
|
14
|
+
ordinal = $1.to_i
|
|
15
|
+
tokens[i].tag(Ordinal.new(ordinal))
|
|
16
|
+
tokens[i].tag(OrdinalDay.new(ordinal)) if Chronic::Date::could_be_day?(ordinal)
|
|
17
|
+
tokens[i].tag(OrdinalMonth.new(ordinal)) if Chronic::Date::could_be_month?(ordinal)
|
|
18
|
+
if Chronic::Date::could_be_year?(ordinal)
|
|
19
|
+
year = Chronic::Date::make_year(ordinal, options[:ambiguous_year_future_bias])
|
|
20
|
+
tokens[i].tag(OrdinalYear.new(year.to_i))
|
|
21
|
+
end
|
|
32
22
|
end
|
|
33
23
|
end
|
|
34
24
|
end
|
|
@@ -44,4 +34,16 @@ module Chronic
|
|
|
44
34
|
end
|
|
45
35
|
end
|
|
46
36
|
|
|
37
|
+
class OrdinalMonth < Ordinal #:nodoc:
|
|
38
|
+
def to_s
|
|
39
|
+
super << '-month-' << @type.to_s
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
class OrdinalYear < Ordinal #:nodoc:
|
|
44
|
+
def to_s
|
|
45
|
+
super << '-year-' << @type.to_s
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
47
49
|
end
|
data/lib/chronic/parser.rb
CHANGED
|
@@ -8,6 +8,7 @@ module Chronic
|
|
|
8
8
|
DEFAULT_OPTIONS = {
|
|
9
9
|
:context => :future,
|
|
10
10
|
:now => nil,
|
|
11
|
+
:hours24 => nil,
|
|
11
12
|
:guess => true,
|
|
12
13
|
:ambiguous_time_range => 6,
|
|
13
14
|
:endian_precedence => [:middle, :little],
|
|
@@ -23,10 +24,13 @@ module Chronic
|
|
|
23
24
|
# given, it will assume it is in the past.
|
|
24
25
|
# :now - Time, all computations will be based off of time
|
|
25
26
|
# instead of Time.now.
|
|
27
|
+
# :hours24 - Time will be parsed as it would be 24 hour clock.
|
|
26
28
|
# :guess - By default the parser will guess a single point in time
|
|
27
29
|
# for the given date or time. If you'd rather have the
|
|
28
30
|
# entire time span returned, set this to false
|
|
29
|
-
# and a Chronic::Span will be returned.
|
|
31
|
+
# and a Chronic::Span will be returned. Setting :guess to :end
|
|
32
|
+
# will return last time from Span, to :middle for middle (same as just true)
|
|
33
|
+
# and :begin for first time from span.
|
|
30
34
|
# :ambiguous_time_range - If an Integer is given, ambiguous times
|
|
31
35
|
# (like 5:00) will be assumed to be within the range of
|
|
32
36
|
# that time in the AM to that time in the PM. For
|
|
@@ -58,9 +62,7 @@ module Chronic
|
|
|
58
62
|
|
|
59
63
|
puts "+#{'-' * 51}\n| #{tokens}\n+#{'-' * 51}" if Chronic.debug
|
|
60
64
|
|
|
61
|
-
if span
|
|
62
|
-
options[:guess] ? guess(span) : span
|
|
63
|
-
end
|
|
65
|
+
guess(span, options[:guess]) if span
|
|
64
66
|
end
|
|
65
67
|
|
|
66
68
|
# Clean up the specified text ready for parsing.
|
|
@@ -86,14 +88,16 @@ module Chronic
|
|
|
86
88
|
# Returns a new String ready for Chronic to parse.
|
|
87
89
|
def pre_normalize(text)
|
|
88
90
|
text = text.to_s.downcase
|
|
91
|
+
text.gsub!(/\b(\d{2})\.(\d{2})\.(\d{4})\b/, '\3 / \2 / \1')
|
|
89
92
|
text.gsub!(/\b([ap])\.m\.?/, '\1m')
|
|
93
|
+
text.gsub!(/(\s+|:\d{2}|:\d{2}\.\d{3})\-(\d{2}:?\d{2})\b/, '\1tzminus\2')
|
|
90
94
|
text.gsub!(/\./, ':')
|
|
95
|
+
text.gsub!(/([ap]):m:?/, '\1m')
|
|
91
96
|
text.gsub!(/['"]/, '')
|
|
92
97
|
text.gsub!(/,/, ' ')
|
|
93
98
|
text.gsub!(/^second /, '2nd ')
|
|
94
99
|
text.gsub!(/\bsecond (of|day|month|hour|minute|second)\b/, '2nd \1')
|
|
95
100
|
text = Numerizer.numerize(text)
|
|
96
|
-
text.gsub!(/\-(\d{2}:?\d{2})\b/, 'tzminus\1')
|
|
97
101
|
text.gsub!(/([\/\-\,\@])/) { ' ' + $1 + ' ' }
|
|
98
102
|
text.gsub!(/(?:^|\s)0(\d+:\d+\s*pm?\b)/, ' \1')
|
|
99
103
|
text.gsub!(/\btoday\b/, 'this day')
|
|
@@ -126,12 +130,11 @@ module Chronic
|
|
|
126
130
|
# span - The Chronic::Span object to calcuate a guess from.
|
|
127
131
|
#
|
|
128
132
|
# Returns a new Time object.
|
|
129
|
-
def guess(span)
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
end
|
|
133
|
+
def guess(span, mode = :middle)
|
|
134
|
+
return span if not mode
|
|
135
|
+
return span.begin + span.width / 2 if span.width > 1 and (mode == true or mode == :middle)
|
|
136
|
+
return span.end if mode == :end
|
|
137
|
+
span.begin
|
|
135
138
|
end
|
|
136
139
|
|
|
137
140
|
# List of Handler definitions. See Chronic.parse for a list of options this
|
|
@@ -149,19 +152,21 @@ module Chronic
|
|
|
149
152
|
],
|
|
150
153
|
|
|
151
154
|
:date => [
|
|
152
|
-
Handler.new([:repeater_day_name, :repeater_month_name, :scalar_day, :repeater_time, :
|
|
155
|
+
Handler.new([:repeater_day_name, :repeater_month_name, :scalar_day, :repeater_time, [:separator_slash?, :separator_dash?], :time_zone, :scalar_year], :handle_generic),
|
|
153
156
|
Handler.new([:repeater_day_name, :repeater_month_name, :scalar_day], :handle_rdn_rmn_sd),
|
|
154
157
|
Handler.new([:repeater_day_name, :repeater_month_name, :scalar_day, :scalar_year], :handle_rdn_rmn_sd_sy),
|
|
155
158
|
Handler.new([:repeater_day_name, :repeater_month_name, :ordinal_day], :handle_rdn_rmn_od),
|
|
159
|
+
Handler.new([:repeater_day_name, :repeater_month_name, :ordinal_day, :scalar_year], :handle_rdn_rmn_od_sy),
|
|
156
160
|
Handler.new([:repeater_day_name, :repeater_month_name, :scalar_day, :separator_at?, 'time?'], :handle_rdn_rmn_sd),
|
|
157
161
|
Handler.new([:repeater_day_name, :repeater_month_name, :ordinal_day, :separator_at?, 'time?'], :handle_rdn_rmn_od),
|
|
158
162
|
Handler.new([:repeater_day_name, :ordinal_day, :separator_at?, 'time?'], :handle_rdn_od),
|
|
159
|
-
Handler.new([:scalar_year, :
|
|
163
|
+
Handler.new([:scalar_year, [:separator_slash, :separator_dash], :scalar_month, [:separator_slash, :separator_dash], :scalar_day, :repeater_time, :time_zone], :handle_generic),
|
|
164
|
+
Handler.new([:ordinal_day], :handle_generic),
|
|
160
165
|
Handler.new([:repeater_month_name, :scalar_day, :scalar_year], :handle_rmn_sd_sy),
|
|
161
166
|
Handler.new([:repeater_month_name, :ordinal_day, :scalar_year], :handle_rmn_od_sy),
|
|
162
167
|
Handler.new([:repeater_month_name, :scalar_day, :scalar_year, :separator_at?, 'time?'], :handle_rmn_sd_sy),
|
|
163
168
|
Handler.new([:repeater_month_name, :ordinal_day, :scalar_year, :separator_at?, 'time?'], :handle_rmn_od_sy),
|
|
164
|
-
Handler.new([:repeater_month_name, :
|
|
169
|
+
Handler.new([:repeater_month_name, [:separator_slash?, :separator_dash?], :scalar_day, :separator_at?, 'time?'], :handle_rmn_sd),
|
|
165
170
|
Handler.new([:repeater_time, :repeater_day_portion?, :separator_on?, :repeater_month_name, :scalar_day], :handle_rmn_sd_on),
|
|
166
171
|
Handler.new([:repeater_month_name, :ordinal_day, :separator_at?, 'time?'], :handle_rmn_od),
|
|
167
172
|
Handler.new([:ordinal_day, :repeater_month_name, :scalar_year, :separator_at?, 'time?'], :handle_od_rmn_sy),
|
|
@@ -171,12 +176,12 @@ module Chronic
|
|
|
171
176
|
Handler.new([:repeater_time, :repeater_day_portion?, :separator_on?, :repeater_month_name, :ordinal_day], :handle_rmn_od_on),
|
|
172
177
|
Handler.new([:repeater_month_name, :scalar_year], :handle_rmn_sy),
|
|
173
178
|
Handler.new([:scalar_day, :repeater_month_name, :scalar_year, :separator_at?, 'time?'], :handle_sd_rmn_sy),
|
|
174
|
-
Handler.new([:scalar_day, :
|
|
175
|
-
Handler.new([:scalar_year, :
|
|
176
|
-
Handler.new([:scalar_year, :
|
|
177
|
-
Handler.new([:scalar_month, :
|
|
178
|
-
Handler.new([:scalar_day, :
|
|
179
|
-
Handler.new([:scalar_year, :
|
|
179
|
+
Handler.new([:scalar_day, [:separator_slash?, :separator_dash?], :repeater_month_name, :separator_at?, 'time?'], :handle_sd_rmn),
|
|
180
|
+
Handler.new([:scalar_year, [:separator_slash, :separator_dash], :scalar_month, [:separator_slash, :separator_dash], :scalar_day, :separator_at?, 'time?'], :handle_sy_sm_sd),
|
|
181
|
+
Handler.new([:scalar_year, [:separator_slash, :separator_dash], :scalar_month], :handle_sy_sm),
|
|
182
|
+
Handler.new([:scalar_month, [:separator_slash, :separator_dash], :scalar_year], :handle_sm_sy),
|
|
183
|
+
Handler.new([:scalar_day, [:separator_slash, :separator_dash], :repeater_month_name, [:separator_slash, :separator_dash], :scalar_year, :repeater_time?], :handle_sm_rmn_sy),
|
|
184
|
+
Handler.new([:scalar_year, [:separator_slash, :separator_dash], :scalar_month, [:separator_slash, :separator_dash], :scalar?, :time_zone], :handle_generic),
|
|
180
185
|
],
|
|
181
186
|
|
|
182
187
|
:anchor => [
|
|
@@ -199,10 +204,10 @@ module Chronic
|
|
|
199
204
|
}
|
|
200
205
|
|
|
201
206
|
endians = [
|
|
202
|
-
Handler.new([:scalar_month, :
|
|
203
|
-
Handler.new([:scalar_month, :
|
|
204
|
-
Handler.new([:scalar_day, :
|
|
205
|
-
Handler.new([:scalar_day, :
|
|
207
|
+
Handler.new([:scalar_month, [:separator_slash, :separator_dash], :scalar_day, [:separator_slash, :separator_dash], :scalar_year, :separator_at?, 'time?'], :handle_sm_sd_sy),
|
|
208
|
+
Handler.new([:scalar_month, [:separator_slash, :separator_dash], :scalar_day, :separator_at?, 'time?'], :handle_sm_sd),
|
|
209
|
+
Handler.new([:scalar_day, [:separator_slash, :separator_dash], :scalar_month, :separator_at?, 'time?'], :handle_sd_sm),
|
|
210
|
+
Handler.new([:scalar_day, [:separator_slash, :separator_dash], :scalar_month, [:separator_slash, :separator_dash], :scalar_year, :separator_at?, 'time?'], :handle_sd_sm_sy)
|
|
206
211
|
]
|
|
207
212
|
|
|
208
213
|
case endian = Array(options[:endian_precedence]).first
|
|
@@ -220,7 +225,7 @@ module Chronic
|
|
|
220
225
|
def tokenize(text, options)
|
|
221
226
|
text = pre_normalize(text)
|
|
222
227
|
tokens = text.split(' ').map { |word| Token.new(word) }
|
|
223
|
-
[Repeater, Grabber, Pointer, Scalar, Ordinal, Separator, TimeZone].each do |tok|
|
|
228
|
+
[Repeater, Grabber, Pointer, Scalar, Ordinal, Separator, Sign, TimeZone].each do |tok|
|
|
224
229
|
tok.scan(tokens, options)
|
|
225
230
|
end
|
|
226
231
|
tokens.select { |token| token.tagged? }
|
|
@@ -245,7 +250,7 @@ module Chronic
|
|
|
245
250
|
|
|
246
251
|
definitions[:arrow].each do |handler|
|
|
247
252
|
if handler.match(tokens, definitions)
|
|
248
|
-
good_tokens = tokens.reject { |o| o.get_tag(SeparatorAt) || o.get_tag(
|
|
253
|
+
good_tokens = tokens.reject { |o| o.get_tag(SeparatorAt) || o.get_tag(SeparatorSlash) || o.get_tag(SeparatorDash) || o.get_tag(SeparatorComma) || o.get_tag(SeparatorAnd) }
|
|
249
254
|
return handler.invoke(:arrow, good_tokens, self, options)
|
|
250
255
|
end
|
|
251
256
|
end
|
data/lib/chronic/repeater.rb
CHANGED
|
@@ -10,32 +10,32 @@ module Chronic
|
|
|
10
10
|
# Returns an Array of tokens.
|
|
11
11
|
def self.scan(tokens, options)
|
|
12
12
|
tokens.each do |token|
|
|
13
|
-
if t = scan_for_season_names(token) then token.tag(t); next end
|
|
14
|
-
if t = scan_for_month_names(token) then token.tag(t); next end
|
|
15
|
-
if t = scan_for_day_names(token) then token.tag(t); next end
|
|
16
|
-
if t = scan_for_day_portions(token) then token.tag(t); next end
|
|
17
|
-
if t = scan_for_times(token) then token.tag(t); next end
|
|
18
|
-
if t = scan_for_units(token) then token.tag(t); next end
|
|
13
|
+
if t = scan_for_season_names(token, options) then token.tag(t); next end
|
|
14
|
+
if t = scan_for_month_names(token, options) then token.tag(t); next end
|
|
15
|
+
if t = scan_for_day_names(token, options) then token.tag(t); next end
|
|
16
|
+
if t = scan_for_day_portions(token, options) then token.tag(t); next end
|
|
17
|
+
if t = scan_for_times(token, options) then token.tag(t); next end
|
|
18
|
+
if t = scan_for_units(token, options) then token.tag(t); next end
|
|
19
19
|
end
|
|
20
20
|
end
|
|
21
21
|
|
|
22
22
|
# token - The Token object we want to scan.
|
|
23
23
|
#
|
|
24
24
|
# Returns a new Repeater object.
|
|
25
|
-
def self.scan_for_season_names(token)
|
|
25
|
+
def self.scan_for_season_names(token, options = {})
|
|
26
26
|
scan_for token, RepeaterSeasonName,
|
|
27
27
|
{
|
|
28
28
|
/^springs?$/ => :spring,
|
|
29
29
|
/^summers?$/ => :summer,
|
|
30
30
|
/^(autumn)|(fall)s?$/ => :autumn,
|
|
31
31
|
/^winters?$/ => :winter
|
|
32
|
-
}
|
|
32
|
+
}, options
|
|
33
33
|
end
|
|
34
34
|
|
|
35
35
|
# token - The Token object we want to scan.
|
|
36
36
|
#
|
|
37
37
|
# Returns a new Repeater object.
|
|
38
|
-
def self.scan_for_month_names(token)
|
|
38
|
+
def self.scan_for_month_names(token, options = {})
|
|
39
39
|
scan_for token, RepeaterMonthName,
|
|
40
40
|
{
|
|
41
41
|
/^jan[:\.]?(uary)?$/ => :january,
|
|
@@ -50,13 +50,13 @@ module Chronic
|
|
|
50
50
|
/^oct[:\.]?(ober)?$/ => :october,
|
|
51
51
|
/^nov[:\.]?(ember)?$/ => :november,
|
|
52
52
|
/^dec[:\.]?(ember)?$/ => :december
|
|
53
|
-
}
|
|
53
|
+
}, options
|
|
54
54
|
end
|
|
55
55
|
|
|
56
56
|
# token - The Token object we want to scan.
|
|
57
57
|
#
|
|
58
58
|
# Returns a new Repeater object.
|
|
59
|
-
def self.scan_for_day_names(token)
|
|
59
|
+
def self.scan_for_day_names(token, options = {})
|
|
60
60
|
scan_for token, RepeaterDayName,
|
|
61
61
|
{
|
|
62
62
|
/^m[ou]n(day)?$/ => :monday,
|
|
@@ -66,13 +66,13 @@ module Chronic
|
|
|
66
66
|
/^fr[iy](day)?$/ => :friday,
|
|
67
67
|
/^sat(t?[ue]rday)?$/ => :saturday,
|
|
68
68
|
/^su[nm](day)?$/ => :sunday
|
|
69
|
-
}
|
|
69
|
+
}, options
|
|
70
70
|
end
|
|
71
71
|
|
|
72
72
|
# token - The Token object we want to scan.
|
|
73
73
|
#
|
|
74
74
|
# Returns a new Repeater object.
|
|
75
|
-
def self.scan_for_day_portions(token)
|
|
75
|
+
def self.scan_for_day_portions(token, options = {})
|
|
76
76
|
scan_for token, RepeaterDayPortion,
|
|
77
77
|
{
|
|
78
78
|
/^ams?$/ => :am,
|
|
@@ -81,20 +81,20 @@ module Chronic
|
|
|
81
81
|
/^afternoons?$/ => :afternoon,
|
|
82
82
|
/^evenings?$/ => :evening,
|
|
83
83
|
/^(night|nite)s?$/ => :night
|
|
84
|
-
}
|
|
84
|
+
}, options
|
|
85
85
|
end
|
|
86
86
|
|
|
87
87
|
# token - The Token object we want to scan.
|
|
88
88
|
#
|
|
89
89
|
# Returns a new Repeater object.
|
|
90
|
-
def self.scan_for_times(token)
|
|
91
|
-
scan_for token, RepeaterTime, /^\d{1,2}(:?\d{1,2})?([\.:]?\d{1,2})
|
|
90
|
+
def self.scan_for_times(token, options = {})
|
|
91
|
+
scan_for token, RepeaterTime, /^\d{1,2}(:?\d{1,2})?([\.:]?\d{1,2}([\.:]\d{1,6})?)?$/, options
|
|
92
92
|
end
|
|
93
93
|
|
|
94
94
|
# token - The Token object we want to scan.
|
|
95
95
|
#
|
|
96
96
|
# Returns a new Repeater object.
|
|
97
|
-
def self.scan_for_units(token)
|
|
97
|
+
def self.scan_for_units(token, options = {})
|
|
98
98
|
{
|
|
99
99
|
/^years?$/ => :year,
|
|
100
100
|
/^seasons?$/ => :season,
|
|
@@ -114,7 +114,7 @@ module Chronic
|
|
|
114
114
|
if item =~ token.word
|
|
115
115
|
klass_name = 'Repeater' + symbol.to_s.capitalize
|
|
116
116
|
klass = Chronic.const_get(klass_name)
|
|
117
|
-
return klass.new(symbol)
|
|
117
|
+
return klass.new(symbol, options)
|
|
118
118
|
end
|
|
119
119
|
end
|
|
120
120
|
return nil
|