chronic_2001 0.1.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/.gitignore +6 -0
- data/HISTORY.md +4 -0
- data/LICENSE +21 -0
- data/README.md +180 -0
- data/Rakefile +46 -0
- data/chronic.gemspec +18 -0
- data/lib/chronic.rb +117 -0
- data/lib/chronic/chronic.rb +346 -0
- data/lib/chronic/grabber.rb +33 -0
- data/lib/chronic/handler.rb +88 -0
- data/lib/chronic/handlers.rb +553 -0
- data/lib/chronic/mini_date.rb +38 -0
- data/lib/chronic/numerizer.rb +121 -0
- data/lib/chronic/ordinal.rb +47 -0
- data/lib/chronic/pointer.rb +32 -0
- data/lib/chronic/repeater.rb +142 -0
- data/lib/chronic/repeaters/repeater_day.rb +53 -0
- data/lib/chronic/repeaters/repeater_day_name.rb +52 -0
- data/lib/chronic/repeaters/repeater_day_portion.rb +108 -0
- data/lib/chronic/repeaters/repeater_fortnight.rb +71 -0
- data/lib/chronic/repeaters/repeater_hour.rb +58 -0
- data/lib/chronic/repeaters/repeater_minute.rb +58 -0
- data/lib/chronic/repeaters/repeater_month.rb +79 -0
- data/lib/chronic/repeaters/repeater_month_name.rb +94 -0
- data/lib/chronic/repeaters/repeater_season.rb +109 -0
- data/lib/chronic/repeaters/repeater_season_name.rb +43 -0
- data/lib/chronic/repeaters/repeater_second.rb +42 -0
- data/lib/chronic/repeaters/repeater_time.rb +128 -0
- data/lib/chronic/repeaters/repeater_week.rb +74 -0
- data/lib/chronic/repeaters/repeater_weekday.rb +85 -0
- data/lib/chronic/repeaters/repeater_weekend.rb +66 -0
- data/lib/chronic/repeaters/repeater_year.rb +77 -0
- data/lib/chronic/scalar.rb +116 -0
- data/lib/chronic/season.rb +26 -0
- data/lib/chronic/separator.rb +94 -0
- data/lib/chronic/span.rb +31 -0
- data/lib/chronic/tag.rb +36 -0
- data/lib/chronic/time_zone.rb +32 -0
- data/lib/chronic/token.rb +47 -0
- data/test/helper.rb +12 -0
- data/test/test_chronic.rb +148 -0
- data/test/test_daylight_savings.rb +118 -0
- data/test/test_handler.rb +104 -0
- data/test/test_mini_date.rb +32 -0
- data/test/test_numerizer.rb +72 -0
- data/test/test_parsing.rb +977 -0
- data/test/test_repeater_day_name.rb +51 -0
- data/test/test_repeater_day_portion.rb +254 -0
- data/test/test_repeater_fortnight.rb +62 -0
- data/test/test_repeater_hour.rb +68 -0
- data/test/test_repeater_minute.rb +34 -0
- data/test/test_repeater_month.rb +50 -0
- data/test/test_repeater_month_name.rb +56 -0
- data/test/test_repeater_season.rb +40 -0
- data/test/test_repeater_time.rb +70 -0
- data/test/test_repeater_week.rb +62 -0
- data/test/test_repeater_weekday.rb +55 -0
- data/test/test_repeater_weekend.rb +74 -0
- data/test/test_repeater_year.rb +69 -0
- data/test/test_span.rb +23 -0
- data/test/test_token.rb +25 -0
- metadata +156 -0
data/HISTORY.md
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License
|
2
|
+
|
3
|
+
Copyright (c) Tom Preston-Werner
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
13
|
+
all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,180 @@
|
|
1
|
+
Chronic
|
2
|
+
=======
|
3
|
+
|
4
|
+
## DESCRIPTION
|
5
|
+
|
6
|
+
Chronic is a natural language date/time parser written in pure Ruby. See below
|
7
|
+
for the wide variety of formats Chronic will parse.
|
8
|
+
|
9
|
+
|
10
|
+
## INSTALLATION
|
11
|
+
|
12
|
+
### RubyGems
|
13
|
+
|
14
|
+
$ [sudo] gem install chronic
|
15
|
+
|
16
|
+
### GitHub
|
17
|
+
|
18
|
+
$ git clone git://github.com/mojombo/chronic.git
|
19
|
+
$ cd chronic && gem build chronic.gemspec
|
20
|
+
$ gem install chronic-<version>.gem
|
21
|
+
|
22
|
+
|
23
|
+
## USAGE
|
24
|
+
|
25
|
+
You can parse strings containing a natural language date using the
|
26
|
+
`Chronic.parse` method.
|
27
|
+
|
28
|
+
require 'chronic'
|
29
|
+
|
30
|
+
Time.now #=> Sun Aug 27 23:18:25 PDT 2006
|
31
|
+
|
32
|
+
Chronic.parse('tomorrow')
|
33
|
+
#=> Mon Aug 28 12:00:00 PDT 2006
|
34
|
+
|
35
|
+
Chronic.parse('monday', :context => :past)
|
36
|
+
#=> Mon Aug 21 12:00:00 PDT 2006
|
37
|
+
|
38
|
+
Chronic.parse('this tuesday 5:00')
|
39
|
+
#=> Tue Aug 29 17:00:00 PDT 2006
|
40
|
+
|
41
|
+
Chronic.parse('this tuesday 5:00', :ambiguous_time_range => :none)
|
42
|
+
#=> Tue Aug 29 05:00:00 PDT 2006
|
43
|
+
|
44
|
+
Chronic.parse('may 27th', :now => Time.local(2000, 1, 1))
|
45
|
+
#=> Sat May 27 12:00:00 PDT 2000
|
46
|
+
|
47
|
+
Chronic.parse('may 27th', :guess => false)
|
48
|
+
#=> Sun May 27 00:00:00 PDT 2007..Mon May 28 00:00:00 PDT 2007
|
49
|
+
|
50
|
+
Chronic.parse('6/4/2012', :endian_precedence => :little)
|
51
|
+
#=> Fri Apr 06 00:00:00 PDT 2012
|
52
|
+
|
53
|
+
|
54
|
+
See `Chronic.parse` for detailed usage instructions.
|
55
|
+
|
56
|
+
|
57
|
+
## EXAMPLES
|
58
|
+
|
59
|
+
Chronic can parse a huge variety of date and time formats. Following is a
|
60
|
+
small sample of strings that will be properly parsed. Parsing is case
|
61
|
+
insensitive and will handle common abbreviations and misspellings.
|
62
|
+
|
63
|
+
Simple
|
64
|
+
|
65
|
+
* thursday
|
66
|
+
* november
|
67
|
+
* summer
|
68
|
+
* friday 13:00
|
69
|
+
* mon 2:35
|
70
|
+
* 4pm
|
71
|
+
* 6 in the morning
|
72
|
+
* friday 1pm
|
73
|
+
* sat 7 in the evening
|
74
|
+
* yesterday
|
75
|
+
* today
|
76
|
+
* tomorrow
|
77
|
+
* this tuesday
|
78
|
+
* next month
|
79
|
+
* last winter
|
80
|
+
* this morning
|
81
|
+
* last night
|
82
|
+
* this second
|
83
|
+
* yesterday at 4:00
|
84
|
+
* last friday at 20:00
|
85
|
+
* last week tuesday
|
86
|
+
* tomorrow at 6:45pm
|
87
|
+
* afternoon yesterday
|
88
|
+
* thursday last week
|
89
|
+
|
90
|
+
Complex
|
91
|
+
|
92
|
+
* 3 years ago
|
93
|
+
* 5 months before now
|
94
|
+
* 7 hours ago
|
95
|
+
* 7 days from now
|
96
|
+
* 1 week hence
|
97
|
+
* in 3 hours
|
98
|
+
* 1 year ago tomorrow
|
99
|
+
* 3 months ago saturday at 5:00 pm
|
100
|
+
* 7 hours before tomorrow at noon
|
101
|
+
* 3rd wednesday in november
|
102
|
+
* 3rd month next year
|
103
|
+
* 3rd thursday this september
|
104
|
+
* 4th day last week
|
105
|
+
* fourteenth of june 2010 at eleven o'clock in the evening
|
106
|
+
* may seventh '97 at three in the morning
|
107
|
+
|
108
|
+
Specific Dates
|
109
|
+
|
110
|
+
* January 5
|
111
|
+
* 22nd of june
|
112
|
+
* 5th may 2017
|
113
|
+
* February twenty first
|
114
|
+
* dec 25
|
115
|
+
* may 27th
|
116
|
+
* October 2006
|
117
|
+
* oct 06
|
118
|
+
* jan 3 2010
|
119
|
+
* february 14, 2004
|
120
|
+
* february 14th, 2004
|
121
|
+
* 3 jan 2000
|
122
|
+
* 17 april 85
|
123
|
+
* 5/27/1979
|
124
|
+
* 27/5/1979
|
125
|
+
* 05/06
|
126
|
+
* 1979-05-27
|
127
|
+
* Friday
|
128
|
+
* 5
|
129
|
+
* 4:00
|
130
|
+
* 17:00
|
131
|
+
* 0800
|
132
|
+
|
133
|
+
Specific Times (many of the above with an added time)
|
134
|
+
|
135
|
+
* January 5 at 7pm
|
136
|
+
* 22nd of june at 8am
|
137
|
+
* 1979-05-27 05:00:00
|
138
|
+
* etc
|
139
|
+
|
140
|
+
|
141
|
+
## TIME ZONES
|
142
|
+
|
143
|
+
Chronic allows you to set which Time class to use when constructing times. By
|
144
|
+
default, the built in Ruby time class creates times in your system's local
|
145
|
+
time zone. You can set this to something like ActiveSupport's
|
146
|
+
[TimeZone](http://api.rubyonrails.org/classes/ActiveSupport/TimeZone.html)
|
147
|
+
class to get full time zone support.
|
148
|
+
|
149
|
+
>> Time.zone = "UTC"
|
150
|
+
>> Chronic.time_class = Time.zone
|
151
|
+
>> Chronic.parse("June 15 2006 at 5:45 AM")
|
152
|
+
=> Thu, 15 Jun 2006 05:45:00 UTC +00:00
|
153
|
+
|
154
|
+
|
155
|
+
## LIMITATIONS
|
156
|
+
|
157
|
+
Chronic uses Ruby's built in Time class for all time storage and computation.
|
158
|
+
Because of this, only times that the Time class can handle will be properly
|
159
|
+
parsed. Parsing for times outside of this range will simply return nil.
|
160
|
+
Support for a wider range of times is planned for a future release.
|
161
|
+
|
162
|
+
|
163
|
+
## CONTRIBUTE
|
164
|
+
|
165
|
+
If you'd like to hack on Chronic, start by forking the repo on GitHub:
|
166
|
+
|
167
|
+
https://github.com/mojombo/chronic
|
168
|
+
|
169
|
+
The best way to get your changes merged back into core is as follows:
|
170
|
+
|
171
|
+
1. Clone down your fork
|
172
|
+
1. Create a thoughtfully named topic branch to contain your change
|
173
|
+
1. Hack away
|
174
|
+
1. Add tests and make sure everything still passes by running `rake`
|
175
|
+
1. Ensure your tests pass in multiple timezones. ie `TZ=utc rake` `TZ=BST rake`
|
176
|
+
1. If you are adding new functionality, document it in the README
|
177
|
+
1. Do not change the version number, we will do that on our end
|
178
|
+
1. If necessary, rebase your commits into logical chunks, without errors
|
179
|
+
1. Push the branch up to GitHub
|
180
|
+
1. Send a pull request for your branch
|
data/Rakefile
ADDED
@@ -0,0 +1,46 @@
|
|
1
|
+
require 'date'
|
2
|
+
|
3
|
+
def version
|
4
|
+
contents = File.read File.expand_path('../lib/chronic.rb', __FILE__)
|
5
|
+
contents[/VERSION = "([^"]+)"/, 1]
|
6
|
+
end
|
7
|
+
|
8
|
+
task :test do
|
9
|
+
$:.unshift './test'
|
10
|
+
Dir.glob('test/test_*.rb').each { |t| require File.basename(t) }
|
11
|
+
end
|
12
|
+
|
13
|
+
desc "Generate RCov test coverage and open in your browser"
|
14
|
+
task :coverage do
|
15
|
+
require 'rcov'
|
16
|
+
sh "rm -fr coverage"
|
17
|
+
sh "rcov test/test_*.rb"
|
18
|
+
sh "open coverage/index.html"
|
19
|
+
end
|
20
|
+
|
21
|
+
desc "Open an irb session preloaded with this library"
|
22
|
+
task :console do
|
23
|
+
sh "irb -Ilib -rchronic"
|
24
|
+
end
|
25
|
+
|
26
|
+
desc "Release Chronic version #{version}"
|
27
|
+
task :release => :build do
|
28
|
+
unless `git branch` =~ /^\* master$/
|
29
|
+
puts "You must be on the master branch to release!"
|
30
|
+
exit!
|
31
|
+
end
|
32
|
+
sh "git commit --allow-empty -a -m 'Release #{version}'"
|
33
|
+
sh "git tag v#{version}"
|
34
|
+
sh "git push origin master"
|
35
|
+
sh "git push origin v#{version}"
|
36
|
+
sh "gem push pkg/chronic-#{version}.gem"
|
37
|
+
end
|
38
|
+
|
39
|
+
desc "Build a gem from the gemspec"
|
40
|
+
task :build do
|
41
|
+
sh "mkdir -p pkg"
|
42
|
+
sh "gem build chronic.gemspec"
|
43
|
+
sh "mv chronic_2001-#{version}.gem pkg"
|
44
|
+
end
|
45
|
+
|
46
|
+
task :default => :test
|
data/chronic.gemspec
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
$:.unshift File.expand_path('../lib', __FILE__)
|
2
|
+
require 'chronic'
|
3
|
+
|
4
|
+
Gem::Specification.new do |s|
|
5
|
+
s.name = 'chronic_2001'
|
6
|
+
s.version = Chronic::VERSION
|
7
|
+
s.date = '2012-05-16'
|
8
|
+
s.summary = 'Natural language date/time parsing'
|
9
|
+
s.description = 'The original Chronic gem is a natural language date/time parser written in pure Ruby. This gem, Chronic 2011, returns a Date or Time depending on the user input.'
|
10
|
+
s.authors = ['Jason Lew']
|
11
|
+
s.rdoc_options = ['--charset=UTF-8']
|
12
|
+
s.extra_rdoc_files = %w[README.md HISTORY.md LICENSE]
|
13
|
+
s.files = `git ls-files`.split("\n")
|
14
|
+
s.test_files = `git ls-files -- test`.split("\n")
|
15
|
+
|
16
|
+
s.add_development_dependency 'rake'
|
17
|
+
s.add_development_dependency 'minitest'
|
18
|
+
end
|
data/lib/chronic.rb
ADDED
@@ -0,0 +1,117 @@
|
|
1
|
+
require 'time'
|
2
|
+
require 'date'
|
3
|
+
|
4
|
+
# Parse natural language dates and times into Time or Chronic::Span objects.
|
5
|
+
#
|
6
|
+
# Examples:
|
7
|
+
#
|
8
|
+
# require 'chronic'
|
9
|
+
#
|
10
|
+
# Time.now #=> Sun Aug 27 23:18:25 PDT 2006
|
11
|
+
#
|
12
|
+
# Chronic.parse('tomorrow')
|
13
|
+
# #=> Mon Aug 28 12:00:00 PDT 2006
|
14
|
+
#
|
15
|
+
# Chronic.parse('monday', :context => :past)
|
16
|
+
# #=> 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
|
+
module Chronic
|
30
|
+
VERSION = "0.1.0"
|
31
|
+
|
32
|
+
class << self
|
33
|
+
|
34
|
+
# Returns true when debug mode is enabled.
|
35
|
+
attr_accessor :debug
|
36
|
+
|
37
|
+
# Examples:
|
38
|
+
#
|
39
|
+
# require 'chronic'
|
40
|
+
# require 'active_support/time'
|
41
|
+
#
|
42
|
+
# Time.zone = 'UTC'
|
43
|
+
# Chronic.time_class = Time.zone
|
44
|
+
# Chronic.parse('June 15 2006 at 5:54 AM')
|
45
|
+
# # => Thu, 15 Jun 2006 05:45:00 UTC +00:00
|
46
|
+
#
|
47
|
+
# Returns The Time class Chronic uses internally.
|
48
|
+
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
|
+
end
|
63
|
+
|
64
|
+
self.debug = true #jlew
|
65
|
+
self.time_class = Time
|
66
|
+
|
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
|
+
|
101
|
+
end
|
102
|
+
|
103
|
+
require 'chronic/chronic'
|
104
|
+
|
105
|
+
class Time
|
106
|
+
|
107
|
+
def self.construct(year, month = 1, day = 1, hour = 0, minute = 0, second = 0)
|
108
|
+
warn "Time.construct will be deprecated in version 0.7.0. Please use Chronic.construct instead"
|
109
|
+
Chronic.construct(year, month, day, hour, minute, second)
|
110
|
+
end
|
111
|
+
|
112
|
+
def to_minidate
|
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)
|
115
|
+
end
|
116
|
+
|
117
|
+
end
|
@@ -0,0 +1,346 @@
|
|
1
|
+
module Chronic
|
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
|
+
}
|
12
|
+
|
13
|
+
SEC_PER_DAY = 86400
|
14
|
+
|
15
|
+
class << self
|
16
|
+
|
17
|
+
# Parses a string containing a natural language date or time.
|
18
|
+
#
|
19
|
+
# If the parser can find a date or time, either a Time or Chronic::Span
|
20
|
+
# will be returned (depending on the value of `:guess`). If no
|
21
|
+
# date or time can be found, `nil` will be returned.
|
22
|
+
#
|
23
|
+
# text - The String text to parse.
|
24
|
+
# opts - An optional Hash of configuration options:
|
25
|
+
# :context - If your string represents a birthday, you can set
|
26
|
+
# this value to :past and if an ambiguous string is
|
27
|
+
# given, it will assume it is in the past.
|
28
|
+
# :now - Time, all computations will be based off of time
|
29
|
+
# instead of Time.now.
|
30
|
+
# :guess - By default the parser will guess a single point in time
|
31
|
+
# for the given date or time. If you'd rather have the
|
32
|
+
# entire time span returned, set this to false
|
33
|
+
# and a Chronic::Span will be returned.
|
34
|
+
# :ambiguous_time_range - If an Integer is given, ambiguous times
|
35
|
+
# (like 5:00) will be assumed to be within the range of
|
36
|
+
# that time in the AM to that time in the PM. For
|
37
|
+
# example, if you set it to `7`, then the parser will
|
38
|
+
# look for the time between 7am and 7pm. In the case of
|
39
|
+
# 5:00, it would assume that means 5:00pm. If `:none`
|
40
|
+
# is given, no assumption will be made, and the first
|
41
|
+
# matching instance of that time will be used.
|
42
|
+
# :endian_precedence - By default, Chronic will parse "03/04/2011"
|
43
|
+
# as the fourth day of the third month. Alternatively you
|
44
|
+
# can tell Chronic to parse this as the third day of the
|
45
|
+
# fourth month by setting this to [:little, :middle].
|
46
|
+
# :ambiguous_year_future_bias - When parsing two digit years
|
47
|
+
# (ie 79) unlike Rubys Time class, Chronic will attempt
|
48
|
+
# to assume the full year using this figure. Chronic will
|
49
|
+
# look x amount of years into the future and past. If the
|
50
|
+
# two digit year is `now + x years` it's assumed to be the
|
51
|
+
# future, `now - x years` is assumed to be the past.
|
52
|
+
#
|
53
|
+
# Returns a new Time object, or Chronic::Span if :guess option is false.
|
54
|
+
def parse(text, opts={})
|
55
|
+
puts "THE CHRONIC!!!"
|
56
|
+
|
57
|
+
options = DEFAULT_OPTIONS.merge opts
|
58
|
+
|
59
|
+
# ensure the specified options are valid
|
60
|
+
(opts.keys - DEFAULT_OPTIONS.keys).each do |key|
|
61
|
+
raise ArgumentError, "#{key} is not a valid option key."
|
62
|
+
end
|
63
|
+
|
64
|
+
unless [:past, :future, :none].include?(options[:context])
|
65
|
+
raise ArgumentError, "Invalid context, :past/:future only"
|
66
|
+
end
|
67
|
+
|
68
|
+
options[:text] = text
|
69
|
+
Chronic.now = options[:now] || Chronic.time_class.now
|
70
|
+
|
71
|
+
# tokenize words
|
72
|
+
tokens = tokenize(text, options)
|
73
|
+
|
74
|
+
puts "tokens: #{tokens}" #jlew
|
75
|
+
|
76
|
+
if Chronic.debug
|
77
|
+
puts "+#{'-' * 51}\n| #{tokens}\n+#{'-' * 51}"
|
78
|
+
end
|
79
|
+
|
80
|
+
span = tokens_to_span(tokens, options)
|
81
|
+
|
82
|
+
puts "span: #{span}"
|
83
|
+
|
84
|
+
if span
|
85
|
+
options[:guess] ? guess(span) : span
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
# Clean up the specified text ready for parsing.
|
90
|
+
#
|
91
|
+
# Clean up the string by stripping unwanted characters, converting
|
92
|
+
# idioms to their canonical form, converting number words to numbers
|
93
|
+
# (three => 3), and converting ordinal words to numeric
|
94
|
+
# ordinals (third => 3rd)
|
95
|
+
#
|
96
|
+
# text - The String text to normalize.
|
97
|
+
#
|
98
|
+
# Examples:
|
99
|
+
#
|
100
|
+
# Chronic.pre_normalize('first day in May')
|
101
|
+
# #=> "1st day in may"
|
102
|
+
#
|
103
|
+
# Chronic.pre_normalize('tomorrow after noon')
|
104
|
+
# #=> "next day future 12:00"
|
105
|
+
#
|
106
|
+
# Chronic.pre_normalize('one hundred and thirty six days from now')
|
107
|
+
# #=> "136 days future this second"
|
108
|
+
#
|
109
|
+
# Returns a new String ready for Chronic to parse.
|
110
|
+
def pre_normalize(text)
|
111
|
+
text = text.to_s.downcase
|
112
|
+
text.gsub!(/\./, ':')
|
113
|
+
text.gsub!(/['"]/, '')
|
114
|
+
text.gsub!(/,/, ' ')
|
115
|
+
text.gsub!(/\bsecond (of|day|month|hour|minute|second)\b/, '2nd \1')
|
116
|
+
text = Numerizer.numerize(text)
|
117
|
+
text.gsub!(/ \-(\d{4})\b/, ' tzminus\1')
|
118
|
+
text.gsub!(/([\/\-\,\@])/) { ' ' + $1 + ' ' }
|
119
|
+
text.gsub!(/(?:^|\s)0(\d+:\d+\s*pm?\b)/, '\1')
|
120
|
+
text.gsub!(/\btoday\b/, 'this day')
|
121
|
+
text.gsub!(/\btomm?orr?ow\b/, 'next day')
|
122
|
+
text.gsub!(/\byesterday\b/, 'last day')
|
123
|
+
text.gsub!(/\bnoon\b/, '12:00pm')
|
124
|
+
text.gsub!(/\bmidnight\b/, '24:00')
|
125
|
+
text.gsub!(/\bnow\b/, 'this second')
|
126
|
+
text.gsub!(/\b(?:ago|before(?: now)?)\b/, 'past')
|
127
|
+
text.gsub!(/\bthis (?:last|past)\b/, 'last')
|
128
|
+
text.gsub!(/\b(?:in|during) the (morning)\b/, '\1')
|
129
|
+
text.gsub!(/\b(?:in the|during the|at) (afternoon|evening|night)\b/, '\1')
|
130
|
+
text.gsub!(/\btonight\b/, 'this night')
|
131
|
+
text.gsub!(/\b\d+:?\d*[ap]\b/,'\0m')
|
132
|
+
text.gsub!(/(\d)([ap]m|oclock)\b/, '\1 \2')
|
133
|
+
text.gsub!(/\b(hence|after|from)\b/, 'future')
|
134
|
+
text
|
135
|
+
end
|
136
|
+
|
137
|
+
# Convert number words to numbers (three => 3, fourth => 4th).
|
138
|
+
#
|
139
|
+
# text - The String to convert.
|
140
|
+
#
|
141
|
+
# Returns a new String with words converted to numbers.
|
142
|
+
def numericize_numbers(text)
|
143
|
+
warn "Chronic.numericize_numbers will be deprecated in version 0.7.0. Please use Chronic::Numerizer.numerize instead"
|
144
|
+
Numerizer.numerize(text)
|
145
|
+
end
|
146
|
+
|
147
|
+
# Guess a specific time within the given span.
|
148
|
+
#
|
149
|
+
# span - The Chronic::Span object to calcuate a guess from.
|
150
|
+
#
|
151
|
+
# Returns a new Time object or a new Date object
|
152
|
+
def guess(span)
|
153
|
+
if span.width > 1
|
154
|
+
if span.width == SEC_PER_DAY
|
155
|
+
(span.begin + (span.width / 2)).to_date
|
156
|
+
else
|
157
|
+
span.begin + (span.width / 2)
|
158
|
+
end
|
159
|
+
else
|
160
|
+
span.begin
|
161
|
+
end
|
162
|
+
end
|
163
|
+
|
164
|
+
# List of Handler definitions. See #parse for a list of options this
|
165
|
+
# method accepts.
|
166
|
+
#
|
167
|
+
# options - An optional Hash of configuration options:
|
168
|
+
# :endian_precedence -
|
169
|
+
#
|
170
|
+
# Returns A Hash of Handler definitions.
|
171
|
+
def definitions(options={})
|
172
|
+
options[:endian_precedence] ||= [:middle, :little]
|
173
|
+
|
174
|
+
@definitions ||= {
|
175
|
+
:time => [
|
176
|
+
Handler.new([:repeater_time, :repeater_day_portion?], nil)
|
177
|
+
],
|
178
|
+
|
179
|
+
:date => [
|
180
|
+
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),
|
181
|
+
Handler.new([:repeater_day_name, :repeater_month_name, :scalar_day], :handle_rdn_rmn_sd),
|
182
|
+
Handler.new([:repeater_day_name, :repeater_month_name, :scalar_day, :scalar_year], :handle_rdn_rmn_sd_sy),
|
183
|
+
Handler.new([:repeater_day_name, :repeater_month_name, :ordinal_day], :handle_rdn_rmn_od),
|
184
|
+
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),
|
185
|
+
Handler.new([:repeater_month_name, :scalar_day, :scalar_year], :handle_rmn_sd_sy),
|
186
|
+
Handler.new([:repeater_month_name, :ordinal_day, :scalar_year], :handle_rmn_od_sy),
|
187
|
+
Handler.new([:repeater_month_name, :scalar_day, :scalar_year, :separator_at?, 'time?'], :handle_rmn_sd_sy),
|
188
|
+
Handler.new([:repeater_month_name, :ordinal_day, :scalar_year, :separator_at?, 'time?'], :handle_rmn_od_sy),
|
189
|
+
Handler.new([:repeater_month_name, :scalar_day, :separator_at?, 'time?'], :handle_rmn_sd),
|
190
|
+
Handler.new([:repeater_time, :repeater_day_portion?, :separator_on?, :repeater_month_name, :scalar_day], :handle_rmn_sd_on),
|
191
|
+
Handler.new([:repeater_month_name, :ordinal_day, :separator_at?, 'time?'], :handle_rmn_od),
|
192
|
+
Handler.new([:ordinal_day, :repeater_month_name, :scalar_year, :separator_at?, 'time?'], :handle_od_rmn_sy),
|
193
|
+
Handler.new([:ordinal_day, :repeater_month_name, :separator_at?, 'time?'], :handle_od_rmn),
|
194
|
+
Handler.new([:scalar_year, :repeater_month_name, :ordinal_day], :handle_sy_rmn_od),
|
195
|
+
Handler.new([:repeater_time, :repeater_day_portion?, :separator_on?, :repeater_month_name, :ordinal_day], :handle_rmn_od_on),
|
196
|
+
Handler.new([:repeater_month_name, :scalar_year], :handle_rmn_sy),
|
197
|
+
Handler.new([:scalar_day, :repeater_month_name, :scalar_year, :separator_at?, 'time?'], :handle_sd_rmn_sy),
|
198
|
+
Handler.new([:scalar_day, :repeater_month_name, :separator_at?, 'time?'], :handle_sd_rmn),
|
199
|
+
Handler.new([:scalar_year, :separator_slash_or_dash, :scalar_month, :separator_slash_or_dash, :scalar_day, :separator_at?, 'time?'], :handle_sy_sm_sd),
|
200
|
+
Handler.new([:scalar_month, :separator_slash_or_dash, :scalar_day], :handle_sm_sd),
|
201
|
+
Handler.new([:scalar_month, :separator_slash_or_dash, :scalar_year], :handle_sm_sy)
|
202
|
+
],
|
203
|
+
|
204
|
+
# tonight at 7pm
|
205
|
+
:anchor => [
|
206
|
+
Handler.new([:grabber?, :repeater, :separator_at?, :repeater?, :repeater?], :handle_r),
|
207
|
+
Handler.new([:grabber?, :repeater, :repeater, :separator_at?, :repeater?, :repeater?], :handle_r),
|
208
|
+
Handler.new([:repeater, :grabber, :repeater], :handle_r_g_r)
|
209
|
+
],
|
210
|
+
|
211
|
+
# 3 weeks from now, in 2 months
|
212
|
+
:arrow => [
|
213
|
+
Handler.new([:scalar, :repeater, :pointer], :handle_s_r_p),
|
214
|
+
Handler.new([:pointer, :scalar, :repeater], :handle_p_s_r),
|
215
|
+
Handler.new([:scalar, :repeater, :pointer, 'anchor'], :handle_s_r_p_a)
|
216
|
+
],
|
217
|
+
|
218
|
+
# 3rd week in march
|
219
|
+
:narrow => [
|
220
|
+
Handler.new([:ordinal, :repeater, :separator_in, :repeater], :handle_o_r_s_r),
|
221
|
+
Handler.new([:ordinal, :repeater, :grabber, :repeater], :handle_o_r_g_r)
|
222
|
+
]
|
223
|
+
}
|
224
|
+
|
225
|
+
endians = [
|
226
|
+
Handler.new([:scalar_month, :separator_slash_or_dash, :scalar_day, :separator_slash_or_dash, :scalar_year, :separator_at?, 'time?'], :handle_sm_sd_sy),
|
227
|
+
Handler.new([:scalar_day, :separator_slash_or_dash, :scalar_month, :separator_slash_or_dash, :scalar_year, :separator_at?, 'time?'], :handle_sd_sm_sy)
|
228
|
+
]
|
229
|
+
|
230
|
+
case endian = Array(options[:endian_precedence]).first
|
231
|
+
when :little
|
232
|
+
@definitions[:endian] = endians.reverse
|
233
|
+
when :middle
|
234
|
+
@definitions[:endian] = endians
|
235
|
+
else
|
236
|
+
raise ArgumentError, "Unknown endian option '#{endian}'"
|
237
|
+
end
|
238
|
+
|
239
|
+
@definitions
|
240
|
+
end
|
241
|
+
|
242
|
+
# Construct a new time object determining possible month overflows
|
243
|
+
# and leap years.
|
244
|
+
#
|
245
|
+
# year - Integer year.
|
246
|
+
# month - Integer month.
|
247
|
+
# day - Integer day.
|
248
|
+
# hour - Integer hour.
|
249
|
+
# minute - Integer minute.
|
250
|
+
# second - Integer second.
|
251
|
+
#
|
252
|
+
# Returns a new Time object constructed from these params.
|
253
|
+
def construct(year, month = 1, day = 1, hour = 0, minute = 0, second = 0)
|
254
|
+
if second >= 60
|
255
|
+
minute += second / 60
|
256
|
+
second = second % 60
|
257
|
+
end
|
258
|
+
|
259
|
+
if minute >= 60
|
260
|
+
hour += minute / 60
|
261
|
+
minute = minute % 60
|
262
|
+
end
|
263
|
+
|
264
|
+
if hour >= 24
|
265
|
+
day += hour / 24
|
266
|
+
hour = hour % 24
|
267
|
+
end
|
268
|
+
|
269
|
+
# determine if there is a day overflow. this is complicated by our crappy calendar
|
270
|
+
# system (non-constant number of days per month)
|
271
|
+
day <= 56 || raise("day must be no more than 56 (makes month resolution easier)")
|
272
|
+
if day > 28
|
273
|
+
# no month ever has fewer than 28 days, so only do this if necessary
|
274
|
+
leap_year_month_days = [31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
|
275
|
+
common_year_month_days = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
|
276
|
+
days_this_month = Date.leap?(year) ? leap_year_month_days[month - 1] : common_year_month_days[month - 1]
|
277
|
+
if day > days_this_month
|
278
|
+
month += day / days_this_month
|
279
|
+
day = day % days_this_month
|
280
|
+
end
|
281
|
+
end
|
282
|
+
|
283
|
+
if month > 12
|
284
|
+
if month % 12 == 0
|
285
|
+
year += (month - 12) / 12
|
286
|
+
month = 12
|
287
|
+
else
|
288
|
+
year += month / 12
|
289
|
+
month = month % 12
|
290
|
+
end
|
291
|
+
end
|
292
|
+
|
293
|
+
Chronic.time_class.local(year, month, day, hour, minute, second)
|
294
|
+
end
|
295
|
+
|
296
|
+
private
|
297
|
+
|
298
|
+
def tokenize(text, options)
|
299
|
+
text = pre_normalize(text)
|
300
|
+
tokens = text.split(' ').map { |word| Token.new(word) }
|
301
|
+
[Repeater, Grabber, Pointer, Scalar, Ordinal, Separator, TimeZone].each do |tok|
|
302
|
+
tok.scan(tokens, options)
|
303
|
+
end
|
304
|
+
tokens.select { |token| token.tagged? }
|
305
|
+
end
|
306
|
+
|
307
|
+
def tokens_to_span(tokens, options)
|
308
|
+
definitions = definitions(options)
|
309
|
+
|
310
|
+
(definitions[:endian] + definitions[:date]).each do |handler|
|
311
|
+
if handler.match(tokens, definitions)
|
312
|
+
good_tokens = tokens.select { |o| !o.get_tag Separator }
|
313
|
+
return handler.invoke(:date, good_tokens, options)
|
314
|
+
end
|
315
|
+
end
|
316
|
+
|
317
|
+
definitions[:anchor].each do |handler|
|
318
|
+
if handler.match(tokens, definitions)
|
319
|
+
good_tokens = tokens.select { |o| !o.get_tag Separator }
|
320
|
+
return handler.invoke(:anchor, good_tokens, options)
|
321
|
+
end
|
322
|
+
end
|
323
|
+
|
324
|
+
definitions[:arrow].each do |handler|
|
325
|
+
if handler.match(tokens, definitions)
|
326
|
+
good_tokens = tokens.reject { |o| o.get_tag(SeparatorAt) || o.get_tag(SeparatorSlashOrDash) || o.get_tag(SeparatorComma) }
|
327
|
+
return handler.invoke(:arrow, good_tokens, options)
|
328
|
+
end
|
329
|
+
end
|
330
|
+
|
331
|
+
definitions[:narrow].each do |handler|
|
332
|
+
if handler.match(tokens, definitions)
|
333
|
+
return handler.invoke(:narrow, tokens, options)
|
334
|
+
end
|
335
|
+
end
|
336
|
+
|
337
|
+
puts "-none" if Chronic.debug
|
338
|
+
return nil
|
339
|
+
end
|
340
|
+
|
341
|
+
end
|
342
|
+
|
343
|
+
# Internal exception
|
344
|
+
class ChronicPain < Exception
|
345
|
+
end
|
346
|
+
end
|