evaryont-chronic 0.3.0.2

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.rdoc +186 -0
  2. data/lib/chronic.rb +57 -0
  3. data/lib/chronic/chronic.rb +341 -0
  4. data/lib/chronic/grabber.rb +26 -0
  5. data/lib/chronic/handlers.rb +545 -0
  6. data/lib/chronic/ordinal.rb +40 -0
  7. data/lib/chronic/pointer.rb +29 -0
  8. data/lib/chronic/repeater.rb +149 -0
  9. data/lib/chronic/repeaters/repeater_day.rb +52 -0
  10. data/lib/chronic/repeaters/repeater_day_name.rb +53 -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 +58 -0
  14. data/lib/chronic/repeaters/repeater_minute.rb +57 -0
  15. data/lib/chronic/repeaters/repeater_month.rb +66 -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 +124 -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 +76 -0
  26. data/lib/chronic/separator.rb +91 -0
  27. data/lib/chronic/time_zone.rb +23 -0
  28. data/lib/numerizer/numerizer.rb +98 -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 +54 -0
  33. data/test/test_RepeaterDayName.rb +52 -0
  34. data/test/test_RepeaterFortnight.rb +63 -0
  35. data/test/test_RepeaterHour.rb +68 -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 +33 -0
  44. data/test/test_Time.rb +50 -0
  45. data/test/test_Token.rb +26 -0
  46. data/test/test_parsing.rb +797 -0
  47. metadata +102 -0
data/README.rdoc ADDED
@@ -0,0 +1,186 @@
1
+ Chronic
2
+
3
+ http://chronic.rubyforge.org/
4
+ by Tom Preston-Werner
5
+
6
+ ==== Please read the warning at the bottom of this document
7
+
8
+ == DESCRIPTION:
9
+
10
+ Adding some support for Russian.
11
+
12
+ Chronic is a natural language date/time parser written in pure Ruby. See below for the wide variety of formats Chronic will parse.
13
+
14
+ == INSTALLATION:
15
+
16
+ Chronic can be installed via RubyGems:
17
+
18
+ $ sudo gem install evaryont-chronic
19
+
20
+ == CODE:
21
+
22
+ Browse the code and get an RSS feed of the commit log at:
23
+
24
+ http://github.com/mojombo/chronic.git
25
+
26
+ You can grab the code (and help with development) via git:
27
+
28
+ $ git clone git://github.com/mojombo/chronic.git
29
+
30
+ == USAGE:
31
+
32
+ You can parse strings containing a natural language date using the Chronic.parse method.
33
+
34
+ require 'rubygems'
35
+ require 'chronic'
36
+
37
+ Time.now #=> Sun Aug 27 23:18:25 PDT 2006
38
+
39
+ #---
40
+
41
+ Chronic.parse('tomorrow')
42
+ #=> Mon Aug 28 12:00:00 PDT 2006
43
+
44
+ Chronic.parse('monday', :context => :past)
45
+ #=> Mon Aug 21 12:00:00 PDT 2006
46
+
47
+ Chronic.parse('this tuesday 5:00')
48
+ #=> Tue Aug 29 17:00:00 PDT 2006
49
+
50
+ Chronic.parse('this tuesday 5:00', :ambiguous_time_range => :none)
51
+ #=> Tue Aug 29 05:00:00 PDT 2006
52
+
53
+ Chronic.parse('may 27th', :now => Time.local(2000, 1, 1))
54
+ #=> Sat May 27 12:00:00 PDT 2000
55
+
56
+ Chronic.parse('may 27th', :guess => false)
57
+ #=> Sun May 27 00:00:00 PDT 2007..Mon May 28 00:00:00 PDT 2007
58
+
59
+ See Chronic.parse for detailed usage instructions.
60
+
61
+ == EXAMPLES:
62
+
63
+ 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.
64
+
65
+ Simple
66
+
67
+ thursday
68
+ november
69
+ summer
70
+ friday 13:00
71
+ mon 2:35
72
+ 4pm
73
+ 6 in the morning
74
+ friday 1pm
75
+ sat 7 in the evening
76
+ yesterday
77
+ today
78
+ tomorrow
79
+ this tuesday
80
+ next month
81
+ last winter
82
+ this morning
83
+ last night
84
+ this second
85
+ yesterday at 4:00
86
+ last friday at 20:00
87
+ last week tuesday
88
+ tomorrow at 6:45pm
89
+ afternoon yesterday
90
+ thursday last week
91
+
92
+ Complex
93
+
94
+ 3 years ago
95
+ 5 months before now
96
+ 7 hours ago
97
+ 7 days from now
98
+ 1 week hence
99
+ in 3 hours
100
+ 1 year ago tomorrow
101
+ 3 months ago saturday at 5:00 pm
102
+ 7 hours before tomorrow at noon
103
+ 3rd wednesday in november
104
+ 3rd month next year
105
+ 3rd thursday this september
106
+ 4th day last week
107
+
108
+ Specific Dates
109
+
110
+ January 5
111
+ dec 25
112
+ may 27th
113
+ October 2006
114
+ oct 06
115
+ jan 3 2010
116
+ february 14, 2004
117
+ 3 jan 2000
118
+ 17 april 85
119
+ 5/27/1979
120
+ 27/5/1979
121
+ 05/06
122
+ 1979-05-27
123
+ Friday
124
+ 5
125
+ 4:00
126
+ 17:00
127
+ 0800
128
+
129
+ Specific Times (many of the above with an added time)
130
+
131
+ January 5 at 7pm
132
+ 1979-05-27 05:00:00
133
+ etc
134
+
135
+ == TIME ZONES:
136
+
137
+ 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
138
+ local time zone. You can set this to something like ActiveSupport's TimeZone class to get full time zone support.
139
+
140
+ >> Time.zone = "UTC"
141
+ >> Chronic.time_class = Time.zone
142
+ >> Chronic.parse("June 15 2006 at 5:45 AM")
143
+ => Thu, 15 Jun 2006 05:45:00 UTC +00:00
144
+
145
+ == LIMITATIONS:
146
+
147
+ 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.
148
+
149
+ == LICENSE:
150
+
151
+ (The MIT License)
152
+
153
+ Copyright (c) 2008 Tom Preston-Werner
154
+
155
+ Permission is hereby granted, free of charge, to any person obtaining
156
+ a copy of this software and associated documentation files (the
157
+ "Software"), to deal in the Software without restriction, including
158
+ without limitation the rights to use, copy, modify, merge, publish,
159
+ distribute, sublicense, and/or sell copies of the Software, and to
160
+ permit persons to whom the Software is furnished to do so, subject to
161
+ the following conditions:
162
+
163
+ The above copyright notice and this permission notice shall be
164
+ included in all copies or substantial portions of the Software.
165
+
166
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
167
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
168
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
169
+ IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
170
+ CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
171
+ TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
172
+ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
173
+
174
+ = WARNING:
175
+
176
+ If you haven't noticed already, this is a fork of mojombo's 'true' chronic. I
177
+ decided on my own volition that the 40-some (as reported by Github) network
178
+ should be merged together. I made this so, and quite haphazardly. There are a
179
+ lot of new features (mostly undocumented except the git logs) so be a little
180
+ flexible in your language passed to Chronic.
181
+
182
+ Given that, if there is a bug, more than likely it's my own fault, not
183
+ mojombo's and therefore bug reports should be sent to my fork, not his. That's
184
+ all.
185
+
186
+ Enjoy Chronic!
data/lib/chronic.rb ADDED
@@ -0,0 +1,57 @@
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 'core_ext/object'
13
+ require 'core_ext/time'
14
+
15
+ require 'chronic/chronic'
16
+ require 'chronic/handlers'
17
+
18
+ require 'chronic/repeater'
19
+ require 'chronic/repeaters/repeater_year'
20
+ require 'chronic/repeaters/repeater_season'
21
+ require 'chronic/repeaters/repeater_season_name'
22
+ require 'chronic/repeaters/repeater_month'
23
+ require 'chronic/repeaters/repeater_month_name'
24
+ require 'chronic/repeaters/repeater_fortnight'
25
+ require 'chronic/repeaters/repeater_week'
26
+ require 'chronic/repeaters/repeater_weekend'
27
+ require 'chronic/repeaters/repeater_weekday'
28
+ require 'chronic/repeaters/repeater_day'
29
+ require 'chronic/repeaters/repeater_day_name'
30
+ require 'chronic/repeaters/repeater_day_portion'
31
+ require 'chronic/repeaters/repeater_decade'
32
+ require 'chronic/repeaters/repeater_hour'
33
+ require 'chronic/repeaters/repeater_minute'
34
+ require 'chronic/repeaters/repeater_second'
35
+ require 'chronic/repeaters/repeater_time'
36
+
37
+ require 'chronic/grabber'
38
+ require 'chronic/pointer'
39
+ require 'chronic/scalar'
40
+ require 'chronic/ordinal'
41
+ require 'chronic/separator'
42
+ require 'chronic/time_zone'
43
+ require 'chronic/blunt.rb'
44
+
45
+ require 'numerizer/numerizer'
46
+
47
+ module Chronic
48
+ VERSION = "0.3.0.2"
49
+
50
+ class << self
51
+ attr_accessor :debug
52
+ attr_accessor :time_class
53
+ end
54
+
55
+ self.debug = false
56
+ self.time_class = Time
57
+ end
@@ -0,0 +1,341 @@
1
+ module Chronic
2
+ class << self
3
+
4
+ # Parses a string containing a natural language date or time. If the parser
5
+ # can find a date or time, either a Time or Chronic::Span will be returned
6
+ # (depending on the value of <tt>:guess</tt>). If no date or time can be found,
7
+ # +nil+ will be returned.
8
+ #
9
+ # Options are:
10
+ #
11
+ # [<tt>:context</tt>]
12
+ # <tt>:past</tt> or <tt>:future</tt> (defaults to <tt>:future</tt>)
13
+ #
14
+ # If your string represents a birthday, you can set <tt>:context</tt> to <tt>:past</tt>
15
+ # and if an ambiguous string is given, it will assume it is in the
16
+ # past. Specify <tt>:future</tt> or omit to set a future context.
17
+ #
18
+ # [<tt>:now</tt>]
19
+ # Time (defaults to Time.now)
20
+ #
21
+ # By setting <tt>:now</tt> to a Time, all computations will be based off
22
+ # of that time instead of Time.now. If set to nil, Chronic will use Time.now.
23
+ #
24
+ # [<tt>:guess</tt>]
25
+ # +true+, +false+, +"start"+, +"middle"+, and +"end"+ (defaults to +true+)
26
+ #
27
+ # By default, the parser will guess a single point in time for the
28
+ # given date or time. +:guess+ => +true+ or +"middle"+ will return the middle
29
+ # value of the range. If +"start"+ is specified, Chronic::Span will return the
30
+ # beginning of the range. If +"end"+ is specified, the last value in
31
+ # Chronic::Span will be returned. If you'd rather have the entire time span returned,
32
+ # set <tt>:guess</tt> to +false+ and a Chronic::Span will be returned.
33
+ #
34
+ # [<tt>:ambiguous_time_range</tt>]
35
+ # Integer or <tt>:none</tt> (defaults to <tt>6</tt> (6am-6pm))
36
+ #
37
+ # If an Integer is given, ambiguous times (like 5:00) will be
38
+ # assumed to be within the range of that time in the AM to that time
39
+ # in the PM. For example, if you set it to <tt>7</tt>, then the parser will
40
+ # look for the time between 7am and 7pm. In the case of 5:00, it would
41
+ # assume that means 5:00pm. If <tt>:none</tt> is given, no assumption
42
+ # will be made, and the first matching instance of that time will
43
+ # be used.
44
+ def parse(text, specified_options = {})
45
+ @text = text
46
+
47
+ # get options and set defaults if necessary
48
+ default_options = {:context => :future,
49
+ :now => Chronic.time_class.now,
50
+ :guess => true,
51
+ :guess_how => :middle,
52
+ :ambiguous_time_range => 6,
53
+ :endian_precedence => nil}
54
+ options = default_options.merge specified_options
55
+
56
+ # handle options that were set to nil
57
+ options[:context] = :future unless options[:context]
58
+ options[:now] = Chronic.time_class.now unless options[:context]
59
+ options[:ambiguous_time_range] = 6 unless options[:ambiguous_time_range]
60
+
61
+ # ensure the specified options are valid
62
+ specified_options.keys.each do |key|
63
+ default_options.keys.include?(key) || raise(InvalidArgumentException, "#{key} is not a valid option key.")
64
+ end
65
+ [:past, :future, :none].include?(options[:context]) || raise(InvalidArgumentException, "Invalid value ':#{options[:context]}' for :context specified. Valid values are :past and :future.")
66
+ ["start", "middle", "end", true, false].include?(options[:guess]) || validate_percentness_of(options[:guess]) || raise(InvalidArgumentException, "Invalid value ':#{options[:guess]}' for :guess how specified. Valid values are true, false, \"start\", \"middle\", and \"end\". true will default to \"middle\". :guess can also be a percent(0.60)")
67
+
68
+ # store now for later =)
69
+ @now = options[:now]
70
+
71
+ # put the text into a normal format to ease scanning
72
+ puts "+++ text = #{text}" if Chronic.debug
73
+ text = self.pre_normalize(text)
74
+ puts "--- text = #{text}" if Chronic.debug
75
+
76
+ # get base tokens for each word
77
+ @tokens = self.base_tokenize(text)
78
+ puts @tokens if Chronic.debug
79
+
80
+ # scan the tokens with each token scanner
81
+ [Repeater].each do |tokenizer|
82
+ @tokens = tokenizer.scan(@tokens, options)
83
+ end
84
+
85
+ [Grabber, Pointer, Scalar, Ordinal, Separator, TimeZone].each do |tokenizer|
86
+ @tokens = tokenizer.scan(@tokens)
87
+ end
88
+
89
+ # strip any non-tagged tokens
90
+ @tokens = @tokens.select { |token| token.tagged? }
91
+
92
+ if Chronic.debug
93
+ puts "+---------------------------------------------------"
94
+ puts "| " + @tokens.to_s
95
+ puts "+---------------------------------------------------"
96
+ end
97
+
98
+ # do the heavy lifting
99
+ begin
100
+ span = self.tokens_to_span(@tokens, options)
101
+ rescue
102
+ raise
103
+ return nil
104
+ end
105
+
106
+ # guess a time within a span if required
107
+ if options[:guess]
108
+ return self.guess(span, options[:guess])
109
+ else
110
+ return span
111
+ end
112
+ end
113
+
114
+ def strip_tokens(text, specified_options = {})
115
+ @text = text
116
+
117
+ # get options and set defaults if necessary
118
+ default_options = {:context => :future,
119
+ :now => Chronic.time_class.now,
120
+ :guess => true,
121
+ :ambiguous_time_range => 6,
122
+ :endian_precedence => nil}
123
+ options = default_options.merge specified_options
124
+
125
+ # handle options that were set to nil
126
+ options[:context] = :future unless options[:context]
127
+ options[:now] = Chronic.time_class.now unless options[:context]
128
+ options[:ambiguous_time_range] = 6 unless options[:ambiguous_time_range]
129
+
130
+ # ensure the specified options are valid
131
+ specified_options.keys.each do |key|
132
+ default_options.keys.include?(key) || raise(InvalidArgumentException, "#{key} is not a valid option key.")
133
+ end
134
+ [:past, :future, :none].include?(options[:context]) || raise(InvalidArgumentException, "Invalid value ':#{options[:context]}' for :context specified. Valid values are :past and :future.")
135
+
136
+ # store now for later =)
137
+ @now = options[:now]
138
+
139
+ # put the text into a normal format to ease scanning
140
+ text = self.pre_normalize(text)
141
+
142
+ # get base tokens for each word
143
+ @tokens = self.base_tokenize(text)
144
+
145
+ # scan the tokens with each token scanner
146
+ [Repeater].each do |tokenizer|
147
+ @tokens = tokenizer.scan(@tokens, options)
148
+ end
149
+
150
+ [Grabber, Pointer, Scalar, Ordinal, Separator, TimeZone].each do |tokenizer|
151
+ @tokens = tokenizer.scan(@tokens)
152
+ end
153
+
154
+ # strip any tagged tokens
155
+ return @tokens.select { |token| !token.tagged? }.map { |token| token.word }.join(' ')
156
+ end
157
+
158
+ # Clean up the specified input text by stripping unwanted characters,
159
+ # converting idioms to their canonical form, converting number words
160
+ # to numbers (three => 3), and converting ordinal words to numeric
161
+ # ordinals (third => 3rd)
162
+ def pre_normalize(text) #:nodoc:
163
+ normalized_text = text.to_s
164
+ normalized_text = numericize_numbers(normalized_text)
165
+ normalized_text.gsub!(/['",]/, '')
166
+ normalized_text.gsub!(/(\d+\:\d+)\.(\d+)/, '\1\2')
167
+ normalized_text.gsub!(/ \-(\d{4})\b/, ' tzminus\1')
168
+ normalized_text.gsub!(/([\/\-\,\@])/) { ' ' + $1 + ' ' }
169
+ normalized_text.gsub!(/\btoday\b/i, 'this day')
170
+ normalized_text.gsub!(/\btomm?orr?ow\b/i, 'next day')
171
+ normalized_text.gsub!(/\byesterday\b/i, 'last day')
172
+ normalized_text.gsub!(/\bnoon\b/i, '12:00')
173
+ normalized_text.gsub!(/\bmidnight\b/i, '24:00')
174
+ normalized_text.gsub!(/\bbefore now\b/i, 'past')
175
+ normalized_text.gsub!(/\bnow\b/i, 'this second')
176
+ normalized_text.gsub!(/\b(ago|before)\b/i, 'past')
177
+ normalized_text.gsub!(/\bthis past\b/i, 'last')
178
+ normalized_text.gsub!(/\bthis last\b/i, 'last')
179
+ normalized_text.gsub!(/\b(?:in|during) the (morning)\b/i, '\1')
180
+ normalized_text.gsub!(/\b(?:in the|during the|at) (afternoon|evening|night)\b/i, '\1')
181
+ normalized_text.gsub!(/\btonight\b/i, 'this night')
182
+ normalized_text.gsub!(/\b\d+:?\d*[ap]\b/i,'\0m')
183
+ normalized_text.gsub!(/(\d)([ap]m|oclock)\b/i, '\1 \2')
184
+ normalized_text.gsub!(/\b(hence|after|from)\b/i, 'future')
185
+ normalized_text.gsub!(/\bh[ou]{0,2}rs?\b/i, 'hour')
186
+ #not needed - see test_parse_before_now (test_parsing.rb +726)
187
+ #normalized_text.gsub!(/\bbefore now\b/, 'past')
188
+
189
+ normalized_text.gsub!(/\bсегодня\b/i, 'this day')
190
+ normalized_text.gsub!(/\bзавтра\b/i, 'next day')
191
+ normalized_text.gsub!(/\bвчера\b/i, 'last day')
192
+ normalized_text.gsub!(/\bвечер(?:ом|а)\b/i, '12:00')
193
+ normalized_text.gsub!(/\b(день|дня|днём|днем)\b/i, '24:00')
194
+ normalized_text.gsub!(/\b(до|перед|назад|предыдущ(ие|ая|ий)|прошл(ая|ые|ое|ый))\b/i, 'past')
195
+ normalized_text.gsub!(/\bсейчас\b/i, 'this second')
196
+ normalized_text.gsub!(/\b(утро(?:м))\b/i, '\1')
197
+ normalized_text.gsub!(/\b(вечер(?:ом)|(днем|днем|день)|ночь(?:ю))\b/i, '\1')
198
+ normalized_text.gsub!(/\b(ночью|вечером)\b/i, 'this night')
199
+
200
+ normalized_text = numericize_ordinals(normalized_text)
201
+ end
202
+
203
+ # Convert number words to numbers (three => 3)
204
+ def numericize_numbers(text) #:nodoc:
205
+ Numerizer.numerize(text)
206
+ end
207
+
208
+ # Convert ordinal words to numeric ordinals (third => 3rd)
209
+ def numericize_ordinals(text) #:nodoc:
210
+ text
211
+ end
212
+
213
+ # Split the text on spaces and convert each word into
214
+ # a Token
215
+ def base_tokenize(text) #:nodoc:
216
+ text.split(' ').map { |word| Token.new(word) }
217
+ end
218
+
219
+ # Guess a specific time within the given span
220
+ def guess(span, guess=true) #:nodoc:
221
+ return nil if span.nil?
222
+ if span.width > 1
223
+ # Account for a timezone difference between the start and end of the range.
224
+ # This most likely will happen when dealing with a Daylight Saving Time start
225
+ # or end day.
226
+ gmt_offset_diff = span.begin.gmt_offset - span.end.gmt_offset
227
+ case guess
228
+ when "start"
229
+ span.begin
230
+ when true, "middle"
231
+ span.begin + ((span.width - gmt_offset_diff) / 2)
232
+ when "end"
233
+ span.begin + (span.width - gmt_offset_diff)
234
+ else
235
+ span.begin + ((span.width - gmt_offset_diff) * guess)
236
+ end
237
+ else
238
+ span.begin
239
+ end
240
+ end
241
+
242
+ # Validates numericality of something
243
+ def validate_percentness_of(number) #:nodoc:
244
+ number.to_s.to_f == number && number >= 0 && number <= 1
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
+
289
+ def initialize(range_begin, range_end)
290
+ # Use exclusive range.
291
+ super(range_begin, range_end, true)
292
+ end
293
+
294
+ # Returns the width of this span in seconds
295
+ def width
296
+ (self.end - self.begin).to_i
297
+ end
298
+
299
+ # Add a number of seconds to this span, returning the
300
+ # resulting Span
301
+ def +(seconds)
302
+ Span.new(self.begin + seconds, self.end + seconds)
303
+ end
304
+
305
+ # Subtract a number of seconds to this span, returning the
306
+ # resulting Span
307
+ def -(seconds)
308
+ self + -seconds
309
+ end
310
+
311
+ # Prints this span in a nice fashion
312
+ def to_s
313
+ '(' << self.begin.to_s << '...' << self.end.to_s << ')'
314
+ end
315
+ end
316
+
317
+ # Tokens are tagged with subclassed instances of this class when
318
+ # they match specific criteria
319
+ class Tag #:nodoc:
320
+ attr_accessor :type
321
+
322
+ def initialize(type)
323
+ @type = type
324
+ end
325
+
326
+ def start=(s)
327
+ @now = s
328
+ end
329
+ end
330
+
331
+ # Internal exception
332
+ class ChronicPain < Exception #:nodoc:
333
+
334
+ end
335
+
336
+ # This exception is raised if an invalid argument is provided to
337
+ # any of Chronic's methods
338
+ class InvalidArgumentException < Exception
339
+
340
+ end
341
+ end