chronic_2011 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (62) hide show
  1. data/.gitignore +6 -0
  2. data/HISTORY.md +4 -0
  3. data/LICENSE +21 -0
  4. data/README.md +180 -0
  5. data/Rakefile +46 -0
  6. data/chronic.gemspec +18 -0
  7. data/lib/chronic.rb +117 -0
  8. data/lib/chronic/chronic.rb +346 -0
  9. data/lib/chronic/grabber.rb +33 -0
  10. data/lib/chronic/handler.rb +88 -0
  11. data/lib/chronic/handlers.rb +553 -0
  12. data/lib/chronic/mini_date.rb +38 -0
  13. data/lib/chronic/numerizer.rb +121 -0
  14. data/lib/chronic/ordinal.rb +47 -0
  15. data/lib/chronic/pointer.rb +32 -0
  16. data/lib/chronic/repeater.rb +142 -0
  17. data/lib/chronic/repeaters/repeater_day.rb +53 -0
  18. data/lib/chronic/repeaters/repeater_day_name.rb +52 -0
  19. data/lib/chronic/repeaters/repeater_day_portion.rb +108 -0
  20. data/lib/chronic/repeaters/repeater_fortnight.rb +71 -0
  21. data/lib/chronic/repeaters/repeater_hour.rb +58 -0
  22. data/lib/chronic/repeaters/repeater_minute.rb +58 -0
  23. data/lib/chronic/repeaters/repeater_month.rb +79 -0
  24. data/lib/chronic/repeaters/repeater_month_name.rb +94 -0
  25. data/lib/chronic/repeaters/repeater_season.rb +109 -0
  26. data/lib/chronic/repeaters/repeater_season_name.rb +43 -0
  27. data/lib/chronic/repeaters/repeater_second.rb +42 -0
  28. data/lib/chronic/repeaters/repeater_time.rb +128 -0
  29. data/lib/chronic/repeaters/repeater_week.rb +74 -0
  30. data/lib/chronic/repeaters/repeater_weekday.rb +85 -0
  31. data/lib/chronic/repeaters/repeater_weekend.rb +66 -0
  32. data/lib/chronic/repeaters/repeater_year.rb +77 -0
  33. data/lib/chronic/scalar.rb +116 -0
  34. data/lib/chronic/season.rb +26 -0
  35. data/lib/chronic/separator.rb +94 -0
  36. data/lib/chronic/span.rb +31 -0
  37. data/lib/chronic/tag.rb +36 -0
  38. data/lib/chronic/time_zone.rb +32 -0
  39. data/lib/chronic/token.rb +47 -0
  40. data/test/helper.rb +12 -0
  41. data/test/test_chronic.rb +148 -0
  42. data/test/test_daylight_savings.rb +118 -0
  43. data/test/test_handler.rb +104 -0
  44. data/test/test_mini_date.rb +32 -0
  45. data/test/test_numerizer.rb +72 -0
  46. data/test/test_parsing.rb +977 -0
  47. data/test/test_repeater_day_name.rb +51 -0
  48. data/test/test_repeater_day_portion.rb +254 -0
  49. data/test/test_repeater_fortnight.rb +62 -0
  50. data/test/test_repeater_hour.rb +68 -0
  51. data/test/test_repeater_minute.rb +34 -0
  52. data/test/test_repeater_month.rb +50 -0
  53. data/test/test_repeater_month_name.rb +56 -0
  54. data/test/test_repeater_season.rb +40 -0
  55. data/test/test_repeater_time.rb +70 -0
  56. data/test/test_repeater_week.rb +62 -0
  57. data/test/test_repeater_weekday.rb +55 -0
  58. data/test/test_repeater_weekend.rb +74 -0
  59. data/test/test_repeater_year.rb +69 -0
  60. data/test/test_span.rb +23 -0
  61. data/test/test_token.rb +25 -0
  62. metadata +156 -0
@@ -0,0 +1,6 @@
1
+ pkg
2
+ *.rbc
3
+ rdoc
4
+ .yardoc
5
+ doc
6
+ tags
@@ -0,0 +1,4 @@
1
+ # 0.1.0
2
+
3
+ * Initial release
4
+ * Forked from mjombo/chronic commit 5042ae55dc
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.
@@ -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
@@ -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_2011-#{version}.gem pkg"
44
+ end
45
+
46
+ task :default => :test
@@ -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_2011'
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
@@ -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