hank-chronic 0.3.12

Sign up to get free protection for your applications and to get access to all the features.
Files changed (47) hide show
  1. data/README.txt +167 -0
  2. data/lib/chronic.rb +134 -0
  3. data/lib/chronic/chronic.rb +340 -0
  4. data/lib/chronic/grabber.rb +26 -0
  5. data/lib/chronic/handlers.rb +549 -0
  6. data/lib/chronic/ordinal.rb +40 -0
  7. data/lib/chronic/pointer.rb +27 -0
  8. data/lib/chronic/repeater.rb +129 -0
  9. data/lib/chronic/repeaters/repeater_day.rb +52 -0
  10. data/lib/chronic/repeaters/repeater_day_name.rb +51 -0
  11. data/lib/chronic/repeaters/repeater_day_portion.rb +94 -0
  12. data/lib/chronic/repeaters/repeater_fortnight.rb +70 -0
  13. data/lib/chronic/repeaters/repeater_hour.rb +57 -0
  14. data/lib/chronic/repeaters/repeater_minute.rb +57 -0
  15. data/lib/chronic/repeaters/repeater_month.rb +75 -0
  16. data/lib/chronic/repeaters/repeater_month_name.rb +98 -0
  17. data/lib/chronic/repeaters/repeater_season.rb +150 -0
  18. data/lib/chronic/repeaters/repeater_season_name.rb +45 -0
  19. data/lib/chronic/repeaters/repeater_second.rb +41 -0
  20. data/lib/chronic/repeaters/repeater_time.rb +125 -0
  21. data/lib/chronic/repeaters/repeater_week.rb +73 -0
  22. data/lib/chronic/repeaters/repeater_weekday.rb +77 -0
  23. data/lib/chronic/repeaters/repeater_weekend.rb +65 -0
  24. data/lib/chronic/repeaters/repeater_year.rb +64 -0
  25. data/lib/chronic/scalar.rb +95 -0
  26. data/lib/chronic/separator.rb +91 -0
  27. data/lib/chronic/time_zone.rb +26 -0
  28. data/lib/numerizer/numerizer.rb +97 -0
  29. data/test/suite.rb +9 -0
  30. data/test/test_Chronic.rb +50 -0
  31. data/test/test_Handler.rb +110 -0
  32. data/test/test_Numerizer.rb +52 -0
  33. data/test/test_RepeaterDayName.rb +52 -0
  34. data/test/test_RepeaterFortnight.rb +63 -0
  35. data/test/test_RepeaterHour.rb +65 -0
  36. data/test/test_RepeaterMonth.rb +47 -0
  37. data/test/test_RepeaterMonthName.rb +57 -0
  38. data/test/test_RepeaterTime.rb +72 -0
  39. data/test/test_RepeaterWeek.rb +63 -0
  40. data/test/test_RepeaterWeekday.rb +56 -0
  41. data/test/test_RepeaterWeekend.rb +75 -0
  42. data/test/test_RepeaterYear.rb +63 -0
  43. data/test/test_Span.rb +24 -0
  44. data/test/test_Time.rb +50 -0
  45. data/test/test_Token.rb +26 -0
  46. data/test/test_parsing.rb +784 -0
  47. metadata +110 -0
@@ -0,0 +1,167 @@
1
+ == Chronic
2
+ http://chronic.rubyforge.org/
3
+ by Tom Preston-Werner
4
+
5
+ == DESCRIPTION:
6
+
7
+ Chronic is a natural language date/time parser written in pure Ruby. See below for the wide variety of formats Chronic will parse.
8
+
9
+ == INSTALLATION:
10
+
11
+ Chronic can be installed via RubyGems:
12
+
13
+ $ sudo gem install chronic
14
+
15
+ == CODE:
16
+
17
+ Browse the code and get an RSS feed of the commit log at:
18
+
19
+ http://github.com/mojombo/chronic.git
20
+
21
+ You can grab the code (and help with development) via git:
22
+
23
+ $ git clone git://github.com/mojombo/chronic.git
24
+
25
+ == USAGE:
26
+
27
+ You can parse strings containing a natural language date using the Chronic.parse method.
28
+
29
+ require 'rubygems'
30
+ require 'chronic'
31
+
32
+ Time.now #=> Sun Aug 27 23:18:25 PDT 2006
33
+
34
+ #---
35
+
36
+ Chronic.parse('tomorrow')
37
+ #=> Mon Aug 28 12:00:00 PDT 2006
38
+
39
+ Chronic.parse('monday', :context => :past)
40
+ #=> Mon Aug 21 12:00:00 PDT 2006
41
+
42
+ Chronic.parse('this tuesday 5:00')
43
+ #=> Tue Aug 29 17:00:00 PDT 2006
44
+
45
+ Chronic.parse('this tuesday 5:00', :ambiguous_time_range => :none)
46
+ #=> Tue Aug 29 05:00:00 PDT 2006
47
+
48
+ Chronic.parse('may 27th', :now => Time.local(2000, 1, 1))
49
+ #=> Sat May 27 12:00:00 PDT 2000
50
+
51
+ Chronic.parse('may 27th', :guess => false)
52
+ #=> Sun May 27 00:00:00 PDT 2007..Mon May 28 00:00:00 PDT 2007
53
+
54
+ See Chronic.parse for detailed usage instructions.
55
+
56
+ == EXAMPLES:
57
+
58
+ Chronic can parse a huge variety of date and time formats. Following is a small sample of strings that will be properly parsed. Parsing is case insensitive and will handle common abbreviations and misspellings.
59
+
60
+ Simple
61
+
62
+ thursday
63
+ november
64
+ summer
65
+ friday 13:00
66
+ mon 2:35
67
+ 4pm
68
+ 6 in the morning
69
+ friday 1pm
70
+ sat 7 in the evening
71
+ yesterday
72
+ today
73
+ tomorrow
74
+ this tuesday
75
+ next month
76
+ last winter
77
+ this morning
78
+ last night
79
+ this second
80
+ yesterday at 4:00
81
+ last friday at 20:00
82
+ last week tuesday
83
+ tomorrow at 6:45pm
84
+ afternoon yesterday
85
+ thursday last week
86
+
87
+ Complex
88
+
89
+ 3 years ago
90
+ 5 months before now
91
+ 7 hours ago
92
+ 7 days from now
93
+ 1 week hence
94
+ in 3 hours
95
+ 1 year ago tomorrow
96
+ 3 months ago saturday at 5:00 pm
97
+ 7 hours before tomorrow at noon
98
+ 3rd wednesday in november
99
+ 3rd month next year
100
+ 3rd thursday this september
101
+ 4th day last week
102
+
103
+ Specific Dates
104
+
105
+ January 5
106
+ dec 25
107
+ may 27th
108
+ October 2006
109
+ oct 06
110
+ jan 3 2010
111
+ february 14, 2004
112
+ 3 jan 2000
113
+ 17 april 85
114
+ 5/27/1979
115
+ 27/5/1979
116
+ 05/06
117
+ 1979-05-27
118
+ Friday
119
+ 5
120
+ 4:00
121
+ 17:00
122
+ 0800
123
+
124
+ Specific Times (many of the above with an added time)
125
+
126
+ January 5 at 7pm
127
+ 1979-05-27 05:00:00
128
+ etc
129
+
130
+ == TIME ZONES:
131
+
132
+ Chronic allows you to set which Time class to use when constructing times. By default, the built in Ruby time class creates times in your system's
133
+ local time zone. You can set this to something like ActiveSupport's TimeZone class to get full time zone support.
134
+
135
+ >> Time.zone = "UTC"
136
+ >> Chronic.time_class = Time.zone
137
+ >> Chronic.parse("June 15 2006 at 5:45 AM")
138
+ => Thu, 15 Jun 2006 05:45:00 UTC +00:00
139
+
140
+ == LIMITATIONS:
141
+
142
+ Chronic uses Ruby's built in Time class for all time storage and computation. Because of this, only times that the Time class can handle will be properly parsed. Parsing for times outside of this range will simply return nil. Support for a wider range of times is planned for a future release.
143
+
144
+ == LICENSE:
145
+
146
+ (The MIT License)
147
+
148
+ Copyright (c) 2008 Tom Preston-Werner
149
+
150
+ Permission is hereby granted, free of charge, to any person obtaining
151
+ a copy of this software and associated documentation files (the
152
+ "Software"), to deal in the Software without restriction, including
153
+ without limitation the rights to use, copy, modify, merge, publish,
154
+ distribute, sublicense, and/or sell copies of the Software, and to
155
+ permit persons to whom the Software is furnished to do so, subject to
156
+ the following conditions:
157
+
158
+ The above copyright notice and this permission notice shall be
159
+ included in all copies or substantial portions of the Software.
160
+
161
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
162
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
163
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
164
+ IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
165
+ CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
166
+ TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
167
+ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,134 @@
1
+ #=============================================================================
2
+ #
3
+ # Name: Chronic
4
+ # Author: Tom Preston-Werner
5
+ # Purpose: Parse natural language dates and times into Time or
6
+ # Chronic::Span objects
7
+ #
8
+ #=============================================================================
9
+
10
+ $:.unshift File.dirname(__FILE__) # For use/testing when no gem is installed
11
+
12
+ require 'time'
13
+
14
+ require 'chronic/chronic'
15
+ require 'chronic/handlers'
16
+
17
+ require 'chronic/repeater'
18
+ require 'chronic/repeaters/repeater_year'
19
+ require 'chronic/repeaters/repeater_season'
20
+ require 'chronic/repeaters/repeater_season_name'
21
+ require 'chronic/repeaters/repeater_month'
22
+ require 'chronic/repeaters/repeater_month_name'
23
+ require 'chronic/repeaters/repeater_fortnight'
24
+ require 'chronic/repeaters/repeater_week'
25
+ require 'chronic/repeaters/repeater_weekend'
26
+ require 'chronic/repeaters/repeater_weekday'
27
+ require 'chronic/repeaters/repeater_day'
28
+ require 'chronic/repeaters/repeater_day_name'
29
+ require 'chronic/repeaters/repeater_day_portion'
30
+ require 'chronic/repeaters/repeater_hour'
31
+ require 'chronic/repeaters/repeater_minute'
32
+ require 'chronic/repeaters/repeater_second'
33
+ require 'chronic/repeaters/repeater_time'
34
+
35
+ require 'chronic/grabber'
36
+ require 'chronic/pointer'
37
+ require 'chronic/scalar'
38
+ require 'chronic/ordinal'
39
+ require 'chronic/separator'
40
+ require 'chronic/time_zone'
41
+
42
+ require 'numerizer/numerizer'
43
+
44
+ module Chronic
45
+ VERSION = "0.3.9"
46
+
47
+ class << self
48
+ attr_accessor :debug
49
+
50
+ def time_class
51
+ Thread.current[:chronic_time_class] ||= Time
52
+ end
53
+
54
+ def time_class=(klass)
55
+ Thread.current[:chronic_time_class] = klass
56
+ end
57
+ end
58
+
59
+ self.debug = false
60
+ end
61
+
62
+ # class Time
63
+ # def self.construct(year, month = 1, day = 1, hour = 0, minute = 0, second = 0)
64
+ # # extra_seconds = second > 60 ? second - 60 : 0
65
+ # # extra_minutes = minute > 59 ? minute - 59 : 0
66
+ # # extra_hours = hour > 23 ? hour - 23 : 0
67
+ # # extra_days = day >
68
+ #
69
+ # if month > 12
70
+ # if month % 12 == 0
71
+ # year += (month - 12) / 12
72
+ # month = 12
73
+ # else
74
+ # year += month / 12
75
+ # month = month % 12
76
+ # end
77
+ # end
78
+ #
79
+ # base = Time.local(year, month)
80
+ # puts base
81
+ # offset = ((day - 1) * 24 * 60 * 60) + (hour * 60 * 60) + (minute * 60) + second
82
+ # puts offset.to_s
83
+ # date = base + offset
84
+ # puts date
85
+ # date
86
+ # end
87
+ # end
88
+
89
+ class Time
90
+ def self.construct(year, month = 1, day = 1, hour = 0, minute = 0, second = 0)
91
+
92
+ if second >= 60
93
+ minute += second / 60
94
+ second = second % 60
95
+ end
96
+
97
+ if minute >= 60
98
+ hour += minute / 60
99
+ minute = minute % 60
100
+ end
101
+
102
+ if hour >= 24
103
+ day += hour / 24
104
+ hour = hour % 24
105
+ end
106
+
107
+ # determine if there is a day overflow. this is complicated by our crappy calendar
108
+ # system (non-constant number of days per month)
109
+ day <= 56 || raise("day must be no more than 56 (makes month resolution easier)")
110
+ if day > 28
111
+ # no month ever has fewer than 28 days, so only do this if necessary
112
+ leap_year = (year % 4 == 0) && !(year % 100 == 0)
113
+ leap_year_month_days = [31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
114
+ common_year_month_days = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
115
+ days_this_month = leap_year ? leap_year_month_days[month - 1] : common_year_month_days[month - 1]
116
+ if day > days_this_month
117
+ month += day / days_this_month
118
+ day = day % days_this_month
119
+ end
120
+ end
121
+
122
+ if month > 12
123
+ if month % 12 == 0
124
+ year += (month - 12) / 12
125
+ month = 12
126
+ else
127
+ year += month / 12
128
+ month = month % 12
129
+ end
130
+ end
131
+
132
+ Chronic.time_class.local(year, month, day, hour, minute, second)
133
+ end
134
+ end
@@ -0,0 +1,340 @@
1
+ require 'time'
2
+
3
+ module Chronic
4
+ class << self
5
+
6
+ # Parses a string containing a natural language date or time. If the parser
7
+ # can find a date or time, either a Time or Chronic::Span will be returned
8
+ # (depending on the value of <tt>:guess</tt>). If no date or time can be found,
9
+ # +nil+ will be returned.
10
+ #
11
+ # Options are:
12
+ #
13
+ # [<tt>:context</tt>]
14
+ # <tt>:past</tt> or <tt>:future</tt> (defaults to <tt>:future</tt>)
15
+ #
16
+ # If your string represents a birthday, you can set <tt>:context</tt> to <tt>:past</tt>
17
+ # and if an ambiguous string is given, it will assume it is in the
18
+ # past. Specify <tt>:future</tt> or omit to set a future context.
19
+ #
20
+ # [<tt>:now</tt>]
21
+ # Time (defaults to Time.now)
22
+ #
23
+ # By setting <tt>:now</tt> to a Time, all computations will be based off
24
+ # of that time instead of Time.now. If set to nil, Chronic will use Time.now.
25
+ #
26
+ # [<tt>:guess</tt>]
27
+ # +true+ or +false+ (defaults to +true+)
28
+ #
29
+ # By default, the parser will guess a single point in time for the
30
+ # given date or time. If you'd rather have the entire time span returned,
31
+ # set <tt>:guess</tt> to +false+ and a Chronic::Span will be returned.
32
+ #
33
+ # [<tt>:ambiguous_time_range</tt>]
34
+ # Integer or <tt>:none</tt> (defaults to <tt>6</tt> (6am-6pm))
35
+ #
36
+ # If an Integer is given, ambiguous times (like 5:00) will be
37
+ # assumed to be within the range of that time in the AM to that time
38
+ # in the PM. For example, if you set it to <tt>7</tt>, then the parser will
39
+ # look for the time between 7am and 7pm. In the case of 5:00, it would
40
+ # assume that means 5:00pm. If <tt>:none</tt> is given, no assumption
41
+ # will be made, and the first matching instance of that time will
42
+ # be used.
43
+ def parse(text, specified_options = {})
44
+ @text = text
45
+
46
+ # get options and set defaults if necessary
47
+ default_options = {:context => :future,
48
+ :now => Chronic.time_class.now,
49
+ :guess => true,
50
+ :ambiguous_time_range => 6,
51
+ :endian_precedence => nil}
52
+ options = default_options.merge specified_options
53
+
54
+ # handle options that were set to nil
55
+ options[:context] = :future unless options[:context]
56
+ options[:now] = Chronic.time_class.now unless options[:context]
57
+ options[:ambiguous_time_range] = 6 unless options[:ambiguous_time_range]
58
+
59
+ # ensure the specified options are valid
60
+ specified_options.keys.each do |key|
61
+ default_options.keys.include?(key) || raise(InvalidArgumentException, "#{key} is not a valid option key.")
62
+ end
63
+ [:past, :future, :none].include?(options[:context]) || raise(InvalidArgumentException, "Invalid value ':#{options[:context]}' for :context specified. Valid values are :past and :future.")
64
+
65
+ # store now for later =)
66
+ @now = options[:now]
67
+
68
+ # put the text into a normal format to ease scanning
69
+ text = self.pre_normalize(text)
70
+
71
+ # get base tokens for each word
72
+ @tokens = self.base_tokenize(text)
73
+
74
+ # scan the tokens with each token scanner
75
+ [Repeater].each do |tokenizer|
76
+ @tokens = tokenizer.scan(@tokens, options)
77
+ end
78
+
79
+ [Grabber, Pointer, Scalar, Ordinal, Separator, TimeZone].each do |tokenizer|
80
+ @tokens = tokenizer.scan(@tokens)
81
+ end
82
+
83
+ # strip any non-tagged tokens
84
+ @tokens = @tokens.select { |token| token.tagged? }
85
+
86
+ if Chronic.debug
87
+ puts "+---------------------------------------------------"
88
+ puts "| " + @tokens.to_s
89
+ puts "+---------------------------------------------------"
90
+ end
91
+
92
+ # do the heavy lifting
93
+ begin
94
+ span = self.tokens_to_span(@tokens, options)
95
+ rescue
96
+ raise
97
+ return nil
98
+ end
99
+
100
+ # guess a time within a span if required
101
+ if options[:guess]
102
+ return self.guess(span)
103
+ else
104
+ return span
105
+ end
106
+ end
107
+ # Find the n-th day of the given month
108
+ # Day is integer, distance from Sunday (Sunday = 0, Saturday = 6)
109
+ # Month is string, needs to be parsed by Time class
110
+ # n is integer, can be 'last' or -1 - 4, -1 means last
111
+ # Returns String of month and day number (eg. "January 3")
112
+ def day_in_month(day, n, month)
113
+ t = Time.parse("#{month} 1")
114
+ # Move to the first day
115
+ t += (7 - (day - t.wday).abs) * 86400 if t.wday != day
116
+ return nil if n < -1 or n > 4
117
+ mon = t.mon
118
+ if n == -1
119
+ while t.mon == mon
120
+ day = t
121
+ t += 86400 * 7 # Add a week
122
+ end
123
+ else
124
+ day = t
125
+ (n - 1).times do
126
+ day = t
127
+ t += 86400 * 7 # Add a week
128
+ break if t.mon != mon
129
+ end
130
+ end
131
+ "#{month} #{day.mday}"
132
+ end
133
+
134
+ # Convert US holidays to their respective dates in the calendar year
135
+ # TODO: Add your country's holidays! Must implement a way to filter.
136
+ # Religious holidays mostly omitted due to movability. Please add.
137
+ def parse_holidays(text)
138
+ text.gsub!(/\bNew Year'?s Day\b/i, 'january 1')
139
+ # (3rd Monday of January, traditionally 15 Jan.)
140
+ text.gsub!(/\b(Martin Luther King.*?Day|MLK day)\b/i,
141
+ "3rd monday in january")
142
+ text.gsub!(/\bGroundhog Day\b/i, 'february 2')
143
+ text.gsub!(/\bValentine'?s Day\b/i, 'february 14')
144
+ # (officially George Washington's Birthday
145
+ # 3rd Monday of February, traditionally 22 Feb.)
146
+ text.gsub!(/\bPresident'?s Day\b/i, "3rd monday in february")
147
+ text.gsub!(/\b(St.?|Saint) Patrick'?s Day\b/, 'march 17')
148
+ text.gsub!(/\bApril Fool'?s'? Day\b/i, 'april 1')
149
+ text.gsub!(/\bEarth Day\b/i, 'april 22')
150
+ # (last Friday of April)
151
+ text.gsub!(/\bArbor Day\b/i, 'last friday in april')
152
+ text.gsub!(/\bCinco De Mayo\b/i, 'may 5')
153
+ # (2nd Sunday of May)
154
+ text.gsub!(/\bMother'?s Day\b/i, '2nd sunday in may')
155
+ # (last Monday of May, traditionally 30 May)
156
+ text.gsub!(/\bMemorial Day\b/i, 'last monday in may')
157
+ # (3rd Sunday of June)
158
+ text.gsub!(/\bFather'?s Day\b/i, '3rd sunday in june')
159
+ text.gsub!(/\bIndependence Day\b/i, 'july 4')
160
+ # (first Monday of September)
161
+ text.gsub!(/\bLabor Day\b/, '1st monday in september')
162
+ text.gsub!(/\bPatriot Day\b/, 'september 11')
163
+ # (Sunday after Labor Day)
164
+ #text.gsub!(/\bGrandparent'?s Day\b/i, Chronic.parse("1st monday in september") + 6 * 86400)
165
+ text.gsub!(/\bConstitution Day\b/i, 'september 17')
166
+ text.gsub!(/\bLeif Erikson Day\b/, 'october 9')
167
+ # (2nd Monday of October, traditionally 12 Oct.)
168
+ text.gsub!(/\bColumbus Day\b/, "2nd monday in october")
169
+ text.gsub!(/\bHalloween\b/i, 'october 31')
170
+ text.gsub!(/\bVeterans Day\b/i, 'november 11')
171
+ # (4th Thursday of November)
172
+ text.gsub!(/\bThanksgiving\b/i, "4th thursday in november")
173
+ # (Friday after Thanksgiving Day)
174
+ #text.gsub!(/\bBlack Friday\b/i, Chronic.parse("4th thursday in november") + 86400)
175
+ text.gsub!(/\bChristmas Eve\b/i, 'december 24')
176
+ text.gsub!(/\b(Christmas Day|Christmas.*?(?!Eve))\b/i, 'december 25')
177
+ text.gsub!(/\bKwanzaa\b/i, 'december 26')
178
+ text.gsub!(/\bNew Year'?s Eve\b/i, 'december 31')
179
+ end
180
+
181
+ # Clean up the specified input text by stripping unwanted characters,
182
+ # converting idioms to their canonical form, converting number words
183
+ # to numbers (three => 3), and converting ordinal words to numeric
184
+ # ordinals (third => 3rd)
185
+ def pre_normalize(text) #:nodoc:
186
+ normalized_text = text.to_s.downcase
187
+ normalized_text = numericize_numbers(normalized_text)
188
+ # Tests indicate a period should really act as a : in time
189
+ # and a - in the date.
190
+ # If the date is formatted as a 1-2.1-2.4 digit, change to -
191
+ # eg. 1.4.2010 becomes 1-4-2010, 05.23.2011 becomes 05-23-2011
192
+ normalized_text.gsub!(/(\d{1,2})\.(\d{1,2})\.(\d{4})/, '\1-\2-\3')
193
+ normalized_text.gsub!(/(\d{4})\.(\d{1,2})\.(\d{1,2})/, '\1-\2-\3')
194
+ # If between numbers now, assume time and make it a colon.
195
+ normalized_text.gsub!(/([0-9])\.([0-9])/, '\1:\2')
196
+
197
+ # probably not time now, so let's make the rest a space
198
+ normalized_text.gsub!(/['"\.,]/, ' ')
199
+ normalized_text.gsub!(/ \-(\d{4})\b/, ' tzminus\1')
200
+ normalized_text.gsub!(/([\/\-\,\@])/) { ' ' + $1 + ' ' }
201
+ parse_holidays(normalized_text)
202
+ normalized_text.gsub!(/\btoday\b/, 'this day')
203
+ normalized_text.gsub!(/\btomm?orr?ow\b/, 'next day')
204
+ normalized_text.gsub!(/\byesterday\b/, 'last day')
205
+ normalized_text.gsub!(/\bnoon\b/, '12:00')
206
+ normalized_text.gsub!(/\bmidnight\b/, '24:00')
207
+ normalized_text.gsub!(/\bbefore now\b/, 'past')
208
+ normalized_text.gsub!(/\bnow\b/, 'this second')
209
+ normalized_text.gsub!(/\b(ago|before)\b/, 'past')
210
+ normalized_text.gsub!(/\bthis past\b/, 'last')
211
+ normalized_text.gsub!(/\bthis last\b/, 'last')
212
+ normalized_text.gsub!(/\b(?:in|during) the (morning)\b/, '\1')
213
+ normalized_text.gsub!(/\b(?:in the|during the|at) (afternoon|evening|night)\b/, '\1')
214
+ normalized_text.gsub!(/\btonight\b/, 'this night')
215
+ normalized_text.gsub!(/\b\d+:?\d*[ap]\b/,'\0m')
216
+ normalized_text.gsub!(/(\d)([ap]m|oclock)\b/, '\1 \2')
217
+ normalized_text.gsub!(/\b(hence|after|from)\b/, 'future')
218
+ normalized_text = numericize_ordinals(normalized_text)
219
+ end
220
+
221
+ # Convert number words to numbers (three => 3)
222
+ def numericize_numbers(text) #:nodoc:
223
+ Numerizer.numerize(text)
224
+ end
225
+
226
+ # Convert ordinal words to numeric ordinals (third => 3rd)
227
+ def numericize_ordinals(text) #:nodoc:
228
+ text
229
+ end
230
+
231
+ # Split the text on spaces and convert each word into
232
+ # a Token
233
+ def base_tokenize(text) #:nodoc:
234
+ text.split(' ').map { |word| Token.new(word) }
235
+ end
236
+
237
+ # Guess a specific time within the given span
238
+ def guess(span) #:nodoc:
239
+ return nil if span.nil?
240
+ if span.width > 1
241
+ span.begin + (span.width / 2)
242
+ else
243
+ span.begin
244
+ end
245
+ end
246
+ end
247
+
248
+ class Token #:nodoc:
249
+ attr_accessor :word, :tags
250
+
251
+ def initialize(word)
252
+ @word = word
253
+ @tags = []
254
+ end
255
+
256
+ # Tag this token with the specified tag
257
+ def tag(new_tag)
258
+ @tags << new_tag
259
+ end
260
+
261
+ # Remove all tags of the given class
262
+ def untag(tag_class)
263
+ @tags = @tags.select { |m| !m.kind_of? tag_class }
264
+ end
265
+
266
+ # Return true if this token has any tags
267
+ def tagged?
268
+ @tags.size > 0
269
+ end
270
+
271
+ # Return the Tag that matches the given class
272
+ def get_tag(tag_class)
273
+ matches = @tags.select { |m| m.kind_of? tag_class }
274
+ #matches.size < 2 || raise("Multiple identical tags found")
275
+ return matches.first
276
+ end
277
+
278
+ # Print this Token in a pretty way
279
+ def to_s
280
+ @word << '(' << @tags.join(', ') << ') '
281
+ end
282
+ end
283
+
284
+ # A Span represents a range of time. Since this class extends
285
+ # Range, you can use #begin and #end to get the beginning and
286
+ # ending times of the span (they will be of class Time)
287
+ class Span < Range
288
+ # Returns the width of this span in seconds
289
+ def width
290
+ (self.end - self.begin).to_i
291
+ end
292
+
293
+ # Add a number of seconds to this span, returning the
294
+ # resulting Span
295
+ def +(seconds)
296
+ Span.new(self.begin + seconds, self.end + seconds)
297
+ end
298
+
299
+ # Subtract a number of seconds to this span, returning the
300
+ # resulting Span
301
+ def -(seconds)
302
+ self + -seconds
303
+ end
304
+
305
+ # Prints this span in a nice fashion
306
+ def to_s
307
+ '(' << self.begin.to_s << '..' << self.end.to_s << ')'
308
+ end
309
+
310
+ unless RUBY_VERSION =~ /1\.9\./
311
+ alias :cover? :include?
312
+ end
313
+
314
+ end
315
+
316
+ # Tokens are tagged with subclassed instances of this class when
317
+ # they match specific criteria
318
+ class Tag #:nodoc:
319
+ attr_accessor :type
320
+
321
+ def initialize(type)
322
+ @type = type
323
+ end
324
+
325
+ def start=(s)
326
+ @now = s
327
+ end
328
+ end
329
+
330
+ # Internal exception
331
+ class ChronicPain < Exception #:nodoc:
332
+
333
+ end
334
+
335
+ # This exception is raised if an invalid argument is provided to
336
+ # any of Chronic's methods
337
+ class InvalidArgumentException < Exception
338
+
339
+ end
340
+ end