Merge branch 'gem'
[fuzed.git] / helloworld / vendor / gems / chronic-0.2.2 / lib / chronic / chronic.rb
blob5e7779f634148a5c4a42205ba575044fbe2e8775
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
23     #
24     # [<tt>:guess</tt>]
25     #     +true+ or +false+ (defaults to +true+)
26     #
27     #     By default, the parser will guess a single point in time for the
28     #     given date or time. If you'd rather have the entire time span returned,
29     #     set <tt>:guess</tt> to +false+ and a Chronic::Span will be returned.
30     #     
31     # [<tt>:ambiguous_time_range</tt>]
32     #     Integer or <tt>:none</tt> (defaults to <tt>6</tt> (6am-6pm))
33     #
34     #     If an Integer is given, ambiguous times (like 5:00) will be 
35     #     assumed to be within the range of that time in the AM to that time
36     #     in the PM. For example, if you set it to <tt>7</tt>, then the parser will
37     #     look for the time between 7am and 7pm. In the case of 5:00, it would
38     #     assume that means 5:00pm. If <tt>:none</tt> is given, no assumption
39     #     will be made, and the first matching instance of that time will 
40     #     be used.
41     def parse(text, specified_options = {})
42       # get options and set defaults if necessary
43       default_options = {:context => :future,
44                          :now => Time.now,
45                          :guess => true,
46                          :ambiguous_time_range => 6}
47       options = default_options.merge specified_options
48             
49       # ensure the specified options are valid
50       specified_options.keys.each do |key|
51         default_options.keys.include?(key) || raise(InvalidArgumentException, "#{key} is not a valid option key.")
52       end
53       [:past, :future, :none].include?(options[:context]) || raise(InvalidArgumentException, "Invalid value ':#{options[:context]}' for :context specified. Valid values are :past and :future.")
54       
55       # store now for later =)
56       @now = options[:now]
57       
58       # put the text into a normal format to ease scanning
59       text = self.pre_normalize(text)
60           
61       # get base tokens for each word
62       @tokens = self.base_tokenize(text)
63     
64       # scan the tokens with each token scanner
65       [Repeater].each do |tokenizer|
66         @tokens = tokenizer.scan(@tokens, options)
67       end
68       
69       [Grabber, Pointer, Scalar, Ordinal, Separator, TimeZone].each do |tokenizer|
70         @tokens = tokenizer.scan(@tokens)
71       end
72       
73       # strip any non-tagged tokens
74       @tokens = @tokens.select { |token| token.tagged? }
75       
76       if Chronic.debug
77         puts "+---------------------------------------------------"
78         puts "| " + @tokens.to_s
79         puts "+---------------------------------------------------"
80       end
81       
82       # do the heavy lifting
83       begin
84         span = self.tokens_to_span(@tokens, options)
85       rescue
86         raise
87         return nil
88       end
89       
90       # guess a time within a span if required
91       if options[:guess]
92         return self.guess(span)
93       else
94         return span
95       end
96     end
97     
98     # Clean up the specified input text by stripping unwanted characters,
99     # converting idioms to their canonical form, converting number words
100     # to numbers (three => 3), and converting ordinal words to numeric
101     # ordinals (third => 3rd)
102     def pre_normalize(text) #:nodoc:
103       normalized_text = text.to_s.downcase
104       normalized_text = numericize_numbers(normalized_text)
105       normalized_text.gsub!(/['"\.]/, '')
106       normalized_text.gsub!(/([\/\-\,\@])/) { ' ' + $1 + ' ' }
107       normalized_text.gsub!(/\btoday\b/, 'this day')
108       normalized_text.gsub!(/\btomm?orr?ow\b/, 'next day')
109       normalized_text.gsub!(/\byesterday\b/, 'last day')
110       normalized_text.gsub!(/\bnoon\b/, '12:00')
111       normalized_text.gsub!(/\bmidnight\b/, '24:00')
112       normalized_text.gsub!(/\bbefore now\b/, 'past')
113       normalized_text.gsub!(/\bnow\b/, 'this second')
114       normalized_text.gsub!(/\b(ago|before)\b/, 'past')
115       normalized_text.gsub!(/\bthis past\b/, 'last')
116       normalized_text.gsub!(/\bthis last\b/, 'last')
117       normalized_text.gsub!(/\b(?:in|during) the (morning)\b/, '\1')
118       normalized_text.gsub!(/\b(?:in the|during the|at) (afternoon|evening|night)\b/, '\1')
119       normalized_text.gsub!(/\btonight\b/, 'this night')
120       normalized_text.gsub!(/(?=\w)([ap]m|oclock)\b/, ' \1')
121       normalized_text.gsub!(/\b(hence|after|from)\b/, 'future')
122       normalized_text = numericize_ordinals(normalized_text)
123     end
124   
125     # Convert number words to numbers (three => 3)
126     def numericize_numbers(text) #:nodoc:
127       Numerizer.numerize(text)
128     end
129   
130     # Convert ordinal words to numeric ordinals (third => 3rd)
131     def numericize_ordinals(text) #:nodoc:
132       text
133     end
134   
135     # Split the text on spaces and convert each word into
136     # a Token
137     def base_tokenize(text) #:nodoc:
138       text.split(' ').map { |word| Token.new(word) }
139     end
140     
141     # Guess a specific time within the given span
142     def guess(span) #:nodoc:
143       return nil if span.nil?
144       if span.width > 1
145         span.begin + (span.width / 2)
146       else
147         span.begin
148       end
149     end
150   end
151   
152   class Token #:nodoc:
153     attr_accessor :word, :tags
154     
155     def initialize(word)
156       @word = word
157       @tags = []
158     end
159     
160     # Tag this token with the specified tag
161     def tag(new_tag)
162       @tags << new_tag
163     end
164     
165     # Remove all tags of the given class
166     def untag(tag_class)
167       @tags = @tags.select { |m| !m.kind_of? tag_class }
168     end
169     
170     # Return true if this token has any tags
171     def tagged?
172       @tags.size > 0
173     end
174     
175     # Return the Tag that matches the given class
176     def get_tag(tag_class)
177       matches = @tags.select { |m| m.kind_of? tag_class }
178       #matches.size < 2 || raise("Multiple identical tags found")
179       return matches.first
180     end
181     
182     # Print this Token in a pretty way
183     def to_s
184       @word << '(' << @tags.join(', ') << ') '
185     end
186   end
187   
188   # A Span represents a range of time. Since this class extends
189   # Range, you can use #begin and #end to get the beginning and
190   # ending times of the span (they will be of class Time)
191   class Span < Range   
192     # Returns the width of this span in seconds   
193     def width
194       (self.end - self.begin).to_i
195     end
196     
197     # Add a number of seconds to this span, returning the 
198     # resulting Span
199     def +(seconds)
200       Span.new(self.begin + seconds, self.end + seconds)
201     end
202     
203     # Subtract a number of seconds to this span, returning the 
204     # resulting Span
205     def -(seconds)
206       self + -seconds
207     end
208     
209     # Prints this span in a nice fashion
210     def to_s
211       '(' << self.begin.to_s << '..' << self.end.to_s << ')'
212     end
213   end
215   # Tokens are tagged with subclassed instances of this class when
216   # they match specific criteria
217   class Tag #:nodoc:
218     attr_accessor :type
219     
220     def initialize(type)
221       @type = type
222     end
223     
224     def start=(s)
225       @now = s
226     end
227   end
228   
229   # Internal exception
230   class ChronicPain < Exception #:nodoc:
231     
232   end
233   
234   # This exception is raised if an invalid argument is provided to
235   # any of Chronic's methods
236   class InvalidArgumentException < Exception
237     
238   end