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.
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
data/.gitignore ADDED
@@ -0,0 +1,6 @@
1
+ pkg
2
+ *.rbc
3
+ rdoc
4
+ .yardoc
5
+ doc
6
+ tags
data/HISTORY.md ADDED
@@ -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.
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