rizwanreza-chronic 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (54) hide show
  1. data/README.rdoc +188 -0
  2. data/lib/chronic.rb +57 -0
  3. data/lib/chronic/blunt.rb +234 -0
  4. data/lib/chronic/chronic.rb +326 -0
  5. data/lib/chronic/grabber.rb +26 -0
  6. data/lib/chronic/handlers.rb +549 -0
  7. data/lib/chronic/ordinal.rb +39 -0
  8. data/lib/chronic/pointer.rb +29 -0
  9. data/lib/chronic/repeater.rb +139 -0
  10. data/lib/chronic/repeaters/repeater_day.rb +52 -0
  11. data/lib/chronic/repeaters/repeater_day_name.rb +53 -0
  12. data/lib/chronic/repeaters/repeater_day_portion.rb +94 -0
  13. data/lib/chronic/repeaters/repeater_decade.rb +23 -0
  14. data/lib/chronic/repeaters/repeater_fortnight.rb +70 -0
  15. data/lib/chronic/repeaters/repeater_hour.rb +58 -0
  16. data/lib/chronic/repeaters/repeater_minute.rb +57 -0
  17. data/lib/chronic/repeaters/repeater_month.rb +66 -0
  18. data/lib/chronic/repeaters/repeater_month_name.rb +98 -0
  19. data/lib/chronic/repeaters/repeater_season.rb +150 -0
  20. data/lib/chronic/repeaters/repeater_season_name.rb +45 -0
  21. data/lib/chronic/repeaters/repeater_second.rb +41 -0
  22. data/lib/chronic/repeaters/repeater_time.rb +124 -0
  23. data/lib/chronic/repeaters/repeater_week.rb +73 -0
  24. data/lib/chronic/repeaters/repeater_weekday.rb +77 -0
  25. data/lib/chronic/repeaters/repeater_weekend.rb +65 -0
  26. data/lib/chronic/repeaters/repeater_year.rb +64 -0
  27. data/lib/chronic/scalar.rb +76 -0
  28. data/lib/chronic/separator.rb +91 -0
  29. data/lib/chronic/time_zone.rb +26 -0
  30. data/lib/core_ext/object.rb +7 -0
  31. data/lib/core_ext/time.rb +74 -0
  32. data/lib/numerizer/numerizer.rb +98 -0
  33. data/test/test_Chronic.rb +75 -0
  34. data/test/test_DaylightSavings.rb +119 -0
  35. data/test/test_Handler.rb +110 -0
  36. data/test/test_Numerizer.rb +54 -0
  37. data/test/test_RepeaterDayName.rb +52 -0
  38. data/test/test_RepeaterDecade.rb +46 -0
  39. data/test/test_RepeaterFortnight.rb +63 -0
  40. data/test/test_RepeaterHour.rb +68 -0
  41. data/test/test_RepeaterMinute.rb +35 -0
  42. data/test/test_RepeaterMonth.rb +47 -0
  43. data/test/test_RepeaterMonthName.rb +57 -0
  44. data/test/test_RepeaterSeason.rb +43 -0
  45. data/test/test_RepeaterTime.rb +72 -0
  46. data/test/test_RepeaterWeek.rb +63 -0
  47. data/test/test_RepeaterWeekday.rb +56 -0
  48. data/test/test_RepeaterWeekend.rb +75 -0
  49. data/test/test_RepeaterYear.rb +63 -0
  50. data/test/test_Span.rb +33 -0
  51. data/test/test_Time.rb +50 -0
  52. data/test/test_Token.rb +26 -0
  53. data/test/test_parsing.rb +809 -0
  54. metadata +118 -0
@@ -0,0 +1,326 @@
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
+ # strip any non-tagged tokens
46
+ @tokens = tokenize(text, specified_options).select { |token| token.tagged? }
47
+
48
+ if Chronic.debug
49
+ puts "+---------------------------------------------------"
50
+ puts "| " + @tokens.to_s
51
+ puts "+---------------------------------------------------"
52
+ end
53
+
54
+ options = default_options(specified_options)
55
+
56
+ # do the heavy lifting
57
+ begin
58
+ span = self.tokens_to_span(@tokens, options)
59
+ rescue
60
+ raise
61
+ return nil
62
+ end
63
+
64
+ # guess a time within a span if required
65
+ if options[:guess]
66
+ return self.guess(span, options[:guess])
67
+ else
68
+ return span
69
+ end
70
+ end
71
+
72
+ def strip_tokens(text, specified_options = {})
73
+ # strip any tagged tokens
74
+ return tokenize(text, specified_options).select { |token| !token.tagged? }.map { |token| token.word }.join(' ')
75
+ end
76
+
77
+ def date_string(text, specified_options = {})
78
+ text = pre_normalize(text)
79
+ date_tokens = tokenize(text).map { |token| token.word if token.tagged? }
80
+ return date_tokens.join(' ').strip
81
+ end
82
+
83
+ # Returns an array with text tokenized by the respective classes
84
+ def tokenize(text, specified_options = {})
85
+ @text = text
86
+
87
+ options = default_options(specified_options)
88
+ # store now for later =)
89
+ @now = options[:now]
90
+
91
+ # put the text into a normal format to ease scanning
92
+ puts "+++ text = #{text}" if Chronic.debug
93
+ text = self.pre_normalize(text)
94
+ puts "--- text = #{text}" if Chronic.debug
95
+
96
+ # get base tokens for each word
97
+ @tokens = self.base_tokenize(text)
98
+ puts @tokens if Chronic.debug
99
+
100
+ # scan the tokens with each token scanner
101
+ [Repeater].each do |tokenizer|
102
+ @tokens = tokenizer.scan(@tokens, options)
103
+ end
104
+
105
+ [Grabber, Pointer, Scalar, Ordinal, Separator, TimeZone].each do |tokenizer|
106
+ @tokens = tokenizer.scan(@tokens)
107
+ end
108
+
109
+ # remove trailing separator tokens
110
+ @tokens.length.times do |ix|
111
+ # if this is a separator token, there needs to be another tagged token next
112
+ if @tokens[ix].get_tag(Chronic::Separator)
113
+ next_token = ix + 1
114
+ next_token_exists_and_is_not_tagged = (@tokens[next_token] and not @tokens[next_token].tagged?)
115
+ next_token_does_not_exist = (not @tokens[next_token])
116
+ if (next_token_does_not_exist) or (next_token_exists_and_is_not_tagged)
117
+ @tokens[ix].tags = []
118
+ end
119
+ end
120
+ end
121
+
122
+ return @tokens
123
+ end
124
+
125
+ def default_options(specified_options)
126
+ # get options and set defaults if necessary
127
+ default_options = {:context => :future,
128
+ :now => Chronic.time_class.now,
129
+ :guess => true,
130
+ :guess_how => :middle,
131
+ :ambiguous_time_range => 6,
132
+ :endian_precedence => nil}
133
+ options = default_options.merge specified_options
134
+
135
+ # handle options that were set to nil
136
+ options[:context] = :future unless options[:context]
137
+ options[:now] = Chronic.time_class.now unless options[:context]
138
+ options[:ambiguous_time_range] = 6 unless options[:ambiguous_time_range]
139
+
140
+ # ensure the specified options are valid
141
+ specified_options.keys.each do |key|
142
+ default_options.keys.include?(key) || raise(InvalidArgumentException, "#{key} is not a valid option key.")
143
+ end
144
+ [:past, :future, :none].include?(options[:context]) || raise(InvalidArgumentException, "Invalid value ':#{options[:context]}' for :context specified. Valid values are :past and :future.")
145
+ ["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)")
146
+
147
+ return options
148
+ end
149
+
150
+ # Clean up the specified input text by stripping unwanted characters,
151
+ # converting idioms to their canonical form, converting number words
152
+ # to numbers (three => 3), and converting ordinal words to numeric
153
+ # ordinals (third => 3rd)
154
+ def pre_normalize(text) #:nodoc:
155
+ normalized_text = text.to_s
156
+ normalized_text = numericize_numbers(normalized_text)
157
+ normalized_text.gsub!(/['",]/, '')
158
+ normalized_text.gsub!(/(\d+\:\d+)\.(\d+)/, '\1\2')
159
+ normalized_text.gsub!(/ \-(\d{4})\b/, ' tzminus\1')
160
+ normalized_text.gsub!(/([\/\-\,\@])/) { ' ' + $1 + ' ' }
161
+ normalized_text.gsub!(/\btoday\b/i, 'this day')
162
+ normalized_text.gsub!(/\btomm?orr?ow\b/i, 'next day')
163
+ normalized_text.gsub!(/\byesterday\b/i, 'last day')
164
+ normalized_text.gsub!(/\bnoon\b/i, '12:00')
165
+ normalized_text.gsub!(/\bmidnight\b/i, '24:00')
166
+ normalized_text.gsub!(/\bbefore now\b/i, 'past')
167
+ normalized_text.gsub!(/\bnow\b/i, 'this second')
168
+ normalized_text.gsub!(/\b(ago|before)\b/i, 'past')
169
+ normalized_text.gsub!(/\bthis past\b/i, 'last')
170
+ normalized_text.gsub!(/\bthis last\b/i, 'last')
171
+ normalized_text.gsub!(/\b(?:in|during) the (morning)\b/i, '\1')
172
+ normalized_text.gsub!(/\b(?:in the|during the|at) (afternoon|evening|night)\b/i, '\1')
173
+ normalized_text.gsub!(/\btonight\b/i, 'this night')
174
+ normalized_text.gsub!(/\b\d+:?\d*[ap]\b/i,'\0m')
175
+ normalized_text.gsub!(/(\d)([ap]m|oclock)\b/i, '\1 \2')
176
+ normalized_text.gsub!(/\b(hence|after|from)\b/i, 'future')
177
+ normalized_text.gsub!(/\bh[ou]{0,2}rs?\b/i, 'hour')
178
+ #not needed - see test_parse_before_now (test_parsing.rb +726)
179
+ #normalized_text.gsub!(/\bbefore now\b/, 'past')
180
+
181
+ normalized_text = numericize_ordinals(normalized_text)
182
+ end
183
+
184
+ # Convert number words to numbers (three => 3)
185
+ def numericize_numbers(text) #:nodoc:
186
+ Numerizer.numerize(text)
187
+ end
188
+
189
+ # Convert ordinal words to numeric ordinals (third => 3rd)
190
+ def numericize_ordinals(text) #:nodoc:
191
+ text
192
+ end
193
+
194
+ # Split the text on spaces and convert each word into
195
+ # a Token
196
+ def base_tokenize(text) #:nodoc:
197
+ text.split(' ').map { |word| Token.new(word) }
198
+ end
199
+
200
+ # Guess a specific time within the given span
201
+ def guess(span, guess=true) #:nodoc:
202
+ return nil if span.nil?
203
+ if span.width > 1
204
+ # Account for a timezone difference between the start and end of the range.
205
+ # This most likely will happen when dealing with a Daylight Saving Time start
206
+ # or end day.
207
+ gmt_offset_diff = span.begin.gmt_offset - span.end.gmt_offset
208
+ case guess
209
+ when "start"
210
+ span.begin
211
+ when true, "middle"
212
+ span.begin + ((span.width - gmt_offset_diff) / 2)
213
+ when "end"
214
+ span.begin + (span.width - gmt_offset_diff)
215
+ else
216
+ span.begin + ((span.width - gmt_offset_diff) * guess)
217
+ end
218
+ else
219
+ span.begin
220
+ end
221
+ end
222
+
223
+ # Validates numericality of something
224
+ def validate_percentness_of(number) #:nodoc:
225
+ number.to_s.to_f == number && number >= 0 && number <= 1
226
+ end
227
+ end
228
+
229
+ class Token #:nodoc:
230
+ attr_accessor :word, :tags
231
+
232
+ def initialize(word)
233
+ @word = word
234
+ @tags = []
235
+ end
236
+
237
+ # Tag this token with the specified tag
238
+ def tag(new_tag)
239
+ @tags << new_tag
240
+ end
241
+
242
+ # Remove all tags of the given class
243
+ def untag(tag_class)
244
+ @tags = @tags.select { |m| !m.kind_of? tag_class }
245
+ end
246
+
247
+ # Return true if this token has any tags
248
+ def tagged?
249
+ @tags.size > 0
250
+ end
251
+
252
+ # Return the Tag that matches the given class
253
+ def get_tag(tag_class)
254
+ matches = @tags.select { |m| m.kind_of? tag_class }
255
+ #matches.size < 2 || raise("Multiple identical tags found")
256
+ return matches.first
257
+ end
258
+
259
+ # Print this Token in a pretty way
260
+ def to_s
261
+ "#{@word}(#{@tags.join(', ')})"
262
+ end
263
+
264
+ unless RUBY_VERSION =~ /1\.9\./
265
+ alias :cover? :include?
266
+ end
267
+ end
268
+
269
+ # A Span represents a range of time. Since this class extends
270
+ # Range, you can use #begin and #end to get the beginning and
271
+ # ending times of the span (they will be of class Time)
272
+ class Span < Range
273
+
274
+ def initialize(range_begin, range_end)
275
+ # Use exclusive range.
276
+ super(range_begin, range_end, true)
277
+ end
278
+
279
+ # Returns the width of this span in seconds
280
+ def width
281
+ (self.end - self.begin).to_i
282
+ end
283
+
284
+ # Add a number of seconds to this span, returning the
285
+ # resulting Span
286
+ def +(seconds)
287
+ Span.new(self.begin + seconds, self.end + seconds)
288
+ end
289
+
290
+ # Subtract a number of seconds to this span, returning the
291
+ # resulting Span
292
+ def -(seconds)
293
+ self + -seconds
294
+ end
295
+
296
+ # Prints this span in a nice fashion
297
+ def to_s
298
+ '(' << self.begin.to_s << '...' << self.end.to_s << ')'
299
+ end
300
+ end
301
+
302
+ # Tokens are tagged with subclassed instances of this class when
303
+ # they match specific criteria
304
+ class Tag #:nodoc:
305
+ attr_accessor :type
306
+
307
+ def initialize(type)
308
+ @type = type
309
+ end
310
+
311
+ def start=(s)
312
+ @now = s
313
+ end
314
+ end
315
+
316
+ # Internal exception
317
+ class ChronicPain < Exception #:nodoc:
318
+
319
+ end
320
+
321
+ # This exception is raised if an invalid argument is provided to
322
+ # any of Chronic's methods
323
+ class InvalidArgumentException < Exception
324
+
325
+ end
326
+ end
@@ -0,0 +1,26 @@
1
+ #module Chronic
2
+
3
+ class Chronic::Grabber < Chronic::Tag #:nodoc:
4
+ def self.scan(tokens)
5
+ tokens.each_index do |i|
6
+ if t = self.scan_for_all(tokens[i]) then tokens[i].tag(t); next end
7
+ end
8
+ tokens
9
+ end
10
+
11
+ def self.scan_for_all(token)
12
+ scanner = {/last/i => :last,
13
+ /this/i => :this,
14
+ /next/i => :next}
15
+ scanner.keys.each do |scanner_item|
16
+ return self.new(scanner[scanner_item]) if scanner_item =~ token.word
17
+ end
18
+ return nil
19
+ end
20
+
21
+ def to_s
22
+ 'grabber-' << @type.to_s
23
+ end
24
+ end
25
+
26
+ #end
@@ -0,0 +1,549 @@
1
+ module Chronic
2
+
3
+ class << self
4
+
5
+ def definitions(options={}) #:nodoc:
6
+ options[:endian_precedence] = [:middle, :little] if options[:endian_precedence].nil?
7
+
8
+ # ensure the endian precedence is exactly two elements long
9
+ raise ChronicPain, "More than two elements specified for endian precedence array" unless options[:endian_precedence].length == 2
10
+
11
+ # handler for dd/mm/yyyy
12
+ @little_endian_handler ||= Handler.new([:scalar_day, :separator_slash_or_dash, :scalar_month, :separator_slash_or_dash, :scalar_year, :separator_at?, 'time?'], :handle_sd_sm_sy)
13
+
14
+ # handler for mm/dd/yyyy
15
+ @middle_endian_handler ||= Handler.new([:scalar_month, :separator_slash_or_dash, :scalar_day, :separator_slash_or_dash, :scalar_year, :separator_at?, 'time?'], :handle_sm_sd_sy)
16
+
17
+ # ensure we have valid endian values
18
+ options[:endian_precedence].each do |e|
19
+ raise ChronicPain, "Unknown endian type: #{e.to_s}" unless instance_variable_defined?(endian_variable_name_for(e))
20
+ end
21
+
22
+ @definitions ||=
23
+ {:time => [Handler.new([:repeater_time, :repeater_day_portion?], nil)],
24
+
25
+ :date => [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),
26
+ Handler.new([:repeater_month_name, :scalar_day, :scalar_year], :handle_rmn_sd_sy),
27
+ Handler.new([:repeater_month_name, :scalar_day, :scalar_year, :separator_at?, 'time?'], :handle_rmn_sd_sy),
28
+ Handler.new([:repeater_month_name, :scalar_day, :separator_at?, 'time?'], :handle_rmn_sd),
29
+ Handler.new([:repeater_month_name, :ordinal_day, :scalar_year], :handle_rmn_od_sy),
30
+ Handler.new([:repeater_time, :repeater_day_portion?, :separator_on?, :repeater_month_name, :scalar_day], :handle_rmn_sd_on),
31
+ Handler.new([:repeater_month_name, :ordinal_day, :separator_at?, 'time?'], :handle_rmn_od),
32
+ Handler.new([:repeater_time, :repeater_day_portion?, :separator_on?, :repeater_month_name, :ordinal_day], :handle_rmn_od_on),
33
+ Handler.new([:repeater_month_name, :scalar_year], :handle_rmn_sy),
34
+ Handler.new([:scalar_day, :repeater_month_name, :scalar_year, :separator_at?, 'time?'], :handle_sd_rmn_sy),
35
+ @middle_endian_handler,
36
+ @little_endian_handler,
37
+ Handler.new([:scalar_year, :separator_slash_or_dash, :scalar_month, :separator_slash_or_dash, :scalar_day, :separator_at?, 'time?'], :handle_sy_sm_sd),
38
+ Handler.new([:scalar_month, :separator_slash_or_dash, :scalar_year], :handle_sm_sy)],
39
+
40
+ # tonight at 7pm
41
+ :anchor => [Handler.new([:grabber?, :repeater, :separator_at?, :repeater?, :repeater?], :handle_r),
42
+ Handler.new([:grabber?, :repeater, :repeater, :separator_at?, :repeater?, :repeater?], :handle_r),
43
+ Handler.new([:repeater, :repeater, :grabber, :repeater], :handle_r_r_g_r),
44
+ Handler.new([:repeater, :grabber, :repeater], :handle_r_g_r)],
45
+
46
+ # 3 weeks from now, in 2 months
47
+ :arrow => [Handler.new([:scalar, :repeater, :pointer], :handle_s_r_p),
48
+ Handler.new([:pointer, :scalar, :repeater], :handle_p_s_r),
49
+ Handler.new([:scalar, :repeater, :pointer, 'anchor'], :handle_s_r_p_a)],
50
+
51
+ # 3rd week in march
52
+ :narrow => [Handler.new([:ordinal, :repeater, :separator_in, :repeater], :handle_o_r_s_r),
53
+ Handler.new([:ordinal, :repeater, :grabber, :repeater], :handle_o_r_g_r)]
54
+ }
55
+
56
+ apply_endian_precedences(options[:endian_precedence])
57
+
58
+ @definitions
59
+ end
60
+
61
+ def tokens_to_span(tokens, options) #:nodoc:
62
+ # maybe it's a specific date
63
+
64
+ definitions = self.definitions(options)
65
+ definitions[:date].each do |handler|
66
+ if handler.match(tokens, definitions)
67
+ puts "-date" if Chronic.debug
68
+ good_tokens = tokens.select { |o| !o.get_tag Separator }
69
+ return self.send(handler.handler_method, good_tokens, options)
70
+ end
71
+ end
72
+
73
+ # I guess it's not a specific date, maybe it's just an anchor
74
+
75
+ definitions[:anchor].each do |handler|
76
+ if handler.match(tokens, definitions)
77
+ puts "-anchor" if Chronic.debug
78
+ good_tokens = tokens.select { |o| !o.get_tag Separator }
79
+ return self.send(handler.handler_method, good_tokens, options)
80
+ end
81
+ end
82
+
83
+ # not an anchor, perhaps it's an arrow
84
+
85
+ definitions[:arrow].each do |handler|
86
+ if handler.match(tokens, definitions)
87
+ puts "-arrow" if Chronic.debug
88
+ good_tokens = tokens.reject { |o| o.get_tag(SeparatorAt) || o.get_tag(SeparatorSlashOrDash) || o.get_tag(SeparatorComma) }
89
+ return self.send(handler.handler_method, good_tokens, options)
90
+ end
91
+ end
92
+
93
+ # not an arrow, let's hope it's a narrow
94
+
95
+ definitions[:narrow].each do |handler|
96
+ if handler.match(tokens, definitions)
97
+ puts "-narrow" if Chronic.debug
98
+ #good_tokens = tokens.select { |o| !o.get_tag Separator }
99
+ return self.send(handler.handler_method, tokens, options)
100
+ end
101
+ end
102
+
103
+ # I guess you're out of luck!
104
+ puts "-none" if Chronic.debug
105
+ return nil
106
+ end
107
+
108
+ #--------------
109
+
110
+ def apply_endian_precedences(precedences)
111
+ date_defs = @definitions[:date]
112
+
113
+ # map the precedence array to indices on @definitions[:date]
114
+ indices = precedences.map { |e|
115
+ handler = instance_variable_get(endian_variable_name_for(e))
116
+ date_defs.index(handler)
117
+ }
118
+
119
+ # swap the handlers if we discover they are at odds with the desired preferences
120
+ swap(date_defs, indices.first, indices.last) if indices.first > indices.last
121
+ end
122
+
123
+ def endian_variable_name_for(e)
124
+ "@#{e.to_s}_endian_handler".to_sym
125
+ end
126
+
127
+ # exchange two elements in an array
128
+ def swap(arr, a, b); arr[a], arr[b] = arr[b], arr[a]; end
129
+
130
+ def day_or_time(day_start, time_tokens, options)
131
+ outer_span = Span.new(day_start, day_start + (24 * 60 * 60))
132
+
133
+ if !time_tokens.empty?
134
+ @now = outer_span.begin
135
+ time = get_anchor(dealias_and_disambiguate_times(time_tokens, options), options)
136
+ return time
137
+ else
138
+ return outer_span
139
+ end
140
+ end
141
+
142
+ #--------------
143
+
144
+ def handle_m_d(month, day, time_tokens, options) #:nodoc:
145
+ month.start = @now
146
+ span = month.this(options[:context])
147
+
148
+ day_start = Chronic.time_class.local(span.begin.year, span.begin.month, day)
149
+
150
+ day_or_time(day_start, time_tokens, options)
151
+ end
152
+
153
+ def handle_rmn_sd(tokens, options) #:nodoc:
154
+ handle_m_d(tokens[0].get_tag(RepeaterMonthName), tokens[1].get_tag(ScalarDay).type, tokens[2..tokens.size], options)
155
+ end
156
+
157
+ def handle_rmn_sd_on(tokens, options) #:nodoc:
158
+ if tokens.size > 3
159
+ handle_m_d(tokens[2].get_tag(RepeaterMonthName), tokens[3].get_tag(ScalarDay).type, tokens[0..1], options)
160
+ else
161
+ handle_m_d(tokens[1].get_tag(RepeaterMonthName), tokens[2].get_tag(ScalarDay).type, tokens[0..0], options)
162
+ end
163
+ end
164
+
165
+ def handle_rmn_od(tokens, options) #:nodoc:
166
+ handle_m_d(tokens[0].get_tag(RepeaterMonthName), tokens[1].get_tag(OrdinalDay).type, tokens[2..tokens.size], options)
167
+ end
168
+
169
+ def handle_rmn_od_sy(tokens, options) #:nodoc:
170
+ month = tokens[0].get_tag(RepeaterMonthName).index
171
+ day = tokens[1].get_tag(OrdinalDay).type
172
+ year = tokens[2].get_tag(ScalarYear).type
173
+
174
+ time_tokens = tokens.last(tokens.size - 3)
175
+
176
+ begin
177
+ day_start = Chronic.time_class.local(year, month, day)
178
+ day_or_time(day_start, time_tokens, options)
179
+ rescue ArgumentError
180
+ nil
181
+ end
182
+ end
183
+
184
+
185
+ def handle_rmn_od_on(tokens, options) #:nodoc:
186
+ if tokens.size > 3
187
+ handle_m_d(tokens[2].get_tag(RepeaterMonthName), tokens[3].get_tag(OrdinalDay).type, tokens[0..1], options)
188
+ else
189
+ handle_m_d(tokens[1].get_tag(RepeaterMonthName), tokens[2].get_tag(OrdinalDay).type, tokens[0..0], options)
190
+ end
191
+ end
192
+
193
+ def handle_rmn_sy(tokens, options) #:nodoc:
194
+ month = tokens[0].get_tag(RepeaterMonthName).index
195
+ year = tokens[1].get_tag(ScalarYear).type
196
+
197
+ if month == 12
198
+ next_month_year = year + 1
199
+ next_month_month = 1
200
+ else
201
+ next_month_year = year
202
+ next_month_month = month + 1
203
+ end
204
+
205
+ begin
206
+ Span.new(Chronic.time_class.local(year, month), Chronic.time_class.local(next_month_year, next_month_month))
207
+ rescue ArgumentError
208
+ nil
209
+ end
210
+ end
211
+
212
+ def handle_rdn_rmn_sd_t_tz_sy(tokens, options) #:nodoc:
213
+ t = Chronic.time_class.parse(@text)
214
+ Span.new(t, t + 1)
215
+ end
216
+
217
+ def handle_rmn_sd_sy(tokens, options) #:nodoc:
218
+ month = tokens[0].get_tag(RepeaterMonthName).index
219
+ day = tokens[1].get_tag(ScalarDay).type
220
+ year = tokens[2].get_tag(ScalarYear).type
221
+
222
+ time_tokens = tokens.last(tokens.size - 3)
223
+
224
+ begin
225
+ day_start = Chronic.time_class.local(year, month, day)
226
+ day_or_time(day_start, time_tokens, options)
227
+ rescue ArgumentError
228
+ nil
229
+ end
230
+ end
231
+
232
+ def handle_sd_rmn_sy(tokens, options) #:nodoc:
233
+ new_tokens = [tokens[1], tokens[0], tokens[2]]
234
+ time_tokens = tokens.last(tokens.size - 3)
235
+ self.handle_rmn_sd_sy(new_tokens + time_tokens, options)
236
+ end
237
+
238
+ def handle_sm_sd_sy(tokens, options) #:nodoc:
239
+ month = tokens[0].get_tag(ScalarMonth).type
240
+ day = tokens[1].get_tag(ScalarDay).type
241
+ year = tokens[2].get_tag(ScalarYear).type
242
+
243
+ time_tokens = tokens.last(tokens.size - 3)
244
+
245
+ begin
246
+ day_start = Chronic.time_class.local(year, month, day) #:nodoc:
247
+ day_or_time(day_start, time_tokens, options)
248
+ rescue ArgumentError
249
+ nil
250
+ end
251
+ end
252
+
253
+ def handle_sd_sm_sy(tokens, options) #:nodoc:
254
+ new_tokens = [tokens[1], tokens[0], tokens[2]]
255
+ time_tokens = tokens.last(tokens.size - 3)
256
+ self.handle_sm_sd_sy(new_tokens + time_tokens, options)
257
+ end
258
+
259
+ def handle_sy_sm_sd(tokens, options) #:nodoc:
260
+ new_tokens = [tokens[1], tokens[2], tokens[0]]
261
+ time_tokens = tokens.last(tokens.size - 3)
262
+ self.handle_sm_sd_sy(new_tokens + time_tokens, options)
263
+ end
264
+
265
+ def handle_sm_sy(tokens, options) #:nodoc:
266
+ month = tokens[0].get_tag(ScalarMonth).type
267
+ year = tokens[1].get_tag(ScalarYear).type
268
+
269
+ if month == 12
270
+ next_month_year = year + 1
271
+ next_month_month = 1
272
+ else
273
+ next_month_year = year
274
+ next_month_month = month + 1
275
+ end
276
+
277
+ begin
278
+ Span.new(Chronic.time_class.local(year, month), Chronic.time_class.local(next_month_year, next_month_month))
279
+ rescue ArgumentError
280
+ nil
281
+ end
282
+ end
283
+
284
+ # anchors
285
+
286
+ def handle_r(tokens, options) #:nodoc:
287
+ dd_tokens = dealias_and_disambiguate_times(tokens, options)
288
+ self.get_anchor(dd_tokens, options)
289
+ end
290
+
291
+ def handle_r_g_r(tokens, options) #:nodoc:
292
+ self.handle_r(tokens.values_at(1,0,2), options)
293
+ end
294
+
295
+ def handle_r_r_g_r(tokens, options) #:nodoc:
296
+ self.handle_r(tokens.values_at(2,3,0,1), options)
297
+ end
298
+
299
+ # arrows
300
+
301
+ def handle_srp(tokens, span, options) #:nodoc:
302
+ distance = tokens[0].get_tag(Scalar).type
303
+ repeater = tokens[1].get_tag(Repeater)
304
+ pointer = tokens[2].get_tag(Pointer).type
305
+
306
+ repeater.offset(span, distance, pointer)
307
+ end
308
+
309
+ def handle_s_r_p(tokens, options) #:nodoc:
310
+ repeater = tokens[1].get_tag(Repeater)
311
+
312
+ # span =
313
+ # case true
314
+ # when [RepeaterYear, RepeaterSeason, RepeaterSeasonName, RepeaterMonth, RepeaterMonthName, RepeaterFortnight, RepeaterWeek].include?(repeater.class)
315
+ # self.parse("this hour", :guess => false, :now => @now)
316
+ # when [RepeaterWeekend, RepeaterDay, RepeaterDayName, RepeaterDayPortion, RepeaterHour].include?(repeater.class)
317
+ # self.parse("this minute", :guess => false, :now => @now)
318
+ # when [RepeaterMinute, RepeaterSecond].include?(repeater.class)
319
+ # self.parse("this second", :guess => false, :now => @now)
320
+ # else
321
+ # raise(ChronicPain, "Invalid repeater: #{repeater.class}")
322
+ # end
323
+
324
+ span = self.parse("this second", :guess => false, :now => @now)
325
+
326
+ self.handle_srp(tokens, span, options)
327
+ end
328
+
329
+ def handle_p_s_r(tokens, options) #:nodoc:
330
+ new_tokens = [tokens[1], tokens[2], tokens[0]]
331
+ self.handle_s_r_p(new_tokens, options)
332
+ end
333
+
334
+ def handle_s_r_p_a(tokens, options) #:nodoc:
335
+ anchor_span = get_anchor(tokens[3..tokens.size - 1], options)
336
+ self.handle_srp(tokens, anchor_span, options)
337
+ end
338
+
339
+ # narrows
340
+
341
+ def handle_orr(tokens, outer_span, options) #:nodoc:
342
+ repeater = tokens[1].get_tag(Repeater)
343
+ repeater.start = outer_span.begin - 1
344
+ ordinal = tokens[0].get_tag(Ordinal).type
345
+ span = nil
346
+ ordinal.times do
347
+ span = repeater.next(:future)
348
+ if span.begin > outer_span.end
349
+ span = nil
350
+ break
351
+ end
352
+ end
353
+ span
354
+ end
355
+
356
+ def handle_o_r_s_r(tokens, options) #:nodoc:
357
+ outer_span = get_anchor([tokens[3]], options)
358
+ handle_orr(tokens[0..1], outer_span, options)
359
+ end
360
+
361
+ def handle_o_r_g_r(tokens, options) #:nodoc:
362
+ outer_span = get_anchor(tokens[2..3], options)
363
+ handle_orr(tokens[0..1], outer_span, options)
364
+ end
365
+
366
+ # support methods
367
+
368
+ def get_anchor(tokens, options) #:nodoc:
369
+ grabber = Grabber.new(:this)
370
+ pointer = :future
371
+
372
+ repeaters = self.get_repeaters(tokens)
373
+ repeaters.size.times { tokens.pop }
374
+
375
+ if tokens.first && tokens.first.get_tag(Grabber)
376
+ grabber = tokens.first.get_tag(Grabber)
377
+ tokens.pop
378
+ end
379
+
380
+ head = repeaters.shift
381
+ head.start = @now
382
+
383
+ case grabber.type
384
+ when :last
385
+ outer_span = head.next(:past)
386
+ when :this
387
+ if repeaters.size > 0
388
+ outer_span = head.this(:none)
389
+ else
390
+ outer_span = head.this(options[:context])
391
+ end
392
+ when :next
393
+ outer_span = head.next(:future)
394
+ else raise(ChronicPain, "Invalid grabber")
395
+ end
396
+
397
+ puts "--#{outer_span}" if Chronic.debug
398
+ anchor = find_within(repeaters, outer_span, pointer)
399
+ end
400
+
401
+ def get_repeaters(tokens) #:nodoc:
402
+ repeaters = []
403
+ tokens.each do |token|
404
+ if t = token.get_tag(Repeater)
405
+ repeaters << t
406
+ end
407
+ end
408
+ repeaters.sort.reverse
409
+ end
410
+
411
+ def in_span?(span, t)
412
+ t && span && t >= span.begin && t <= span.end
413
+ end
414
+
415
+ # Recursively finds repeaters within other repeaters.
416
+ # Returns a Span representing the innermost time span
417
+ # or nil if no repeater union could be found
418
+ def find_within(tags, span, pointer) #:nodoc:
419
+ puts "--#{span}" if Chronic.debug
420
+ return span if tags.empty?
421
+
422
+ head, *rest = tags
423
+ head.start = pointer == :future ? span.begin : span.end
424
+ h = head.this(:none)
425
+
426
+ if in_span?(span, h.begin) || in_span(span, h.end)
427
+ return find_within(rest, h, pointer)
428
+ else
429
+ return nil
430
+ end
431
+ end
432
+
433
+ def dealias_and_disambiguate_times(tokens, options) #:nodoc:
434
+ # handle aliases of am/pm
435
+ # 5:00 in the morning -> 5:00 am
436
+ # 7:00 in the evening -> 7:00 pm
437
+
438
+ day_portion_index = nil
439
+ tokens.each_with_index do |t, i|
440
+ if t.get_tag(RepeaterDayPortion)
441
+ day_portion_index = i
442
+ break
443
+ end
444
+ end
445
+
446
+ time_index = nil
447
+ tokens.each_with_index do |t, i|
448
+ if t.get_tag(RepeaterTime)
449
+ time_index = i
450
+ break
451
+ end
452
+ end
453
+
454
+ if (day_portion_index && time_index)
455
+ t1 = tokens[day_portion_index]
456
+ t1tag = t1.get_tag(RepeaterDayPortion)
457
+
458
+ if [:morning].include?(t1tag.type)
459
+ puts '--morning->am' if Chronic.debug
460
+ t1.untag(RepeaterDayPortion)
461
+ t1.tag(RepeaterDayPortion.new(:am))
462
+ elsif [:afternoon, :evening, :night].include?(t1tag.type)
463
+ puts "--#{t1tag.type}->pm" if Chronic.debug
464
+ t1.untag(RepeaterDayPortion)
465
+ t1.tag(RepeaterDayPortion.new(:pm))
466
+ end
467
+ end
468
+
469
+ # tokens.each_with_index do |t0, i|
470
+ # t1 = tokens[i + 1]
471
+ # if t1 && (t1tag = t1.get_tag(RepeaterDayPortion)) && t0.get_tag(RepeaterTime)
472
+ # if [:morning].include?(t1tag.type)
473
+ # puts '--morning->am' if Chronic.debug
474
+ # t1.untag(RepeaterDayPortion)
475
+ # t1.tag(RepeaterDayPortion.new(:am))
476
+ # elsif [:afternoon, :evening, :night].include?(t1tag.type)
477
+ # puts "--#{t1tag.type}->pm" if Chronic.debug
478
+ # t1.untag(RepeaterDayPortion)
479
+ # t1.tag(RepeaterDayPortion.new(:pm))
480
+ # end
481
+ # end
482
+ # end
483
+
484
+ # handle ambiguous times if :ambiguous_time_range is specified
485
+ if options[:ambiguous_time_range] != :none
486
+ ttokens = []
487
+ tokens.each_with_index do |t0, i|
488
+ ttokens << t0
489
+ t1 = tokens[i + 1]
490
+ if t0.get_tag(RepeaterTime) && t0.get_tag(RepeaterTime).type.ambiguous? && (!t1 || !t1.get_tag(RepeaterDayPortion))
491
+ distoken = Token.new('disambiguator')
492
+ distoken.tag(RepeaterDayPortion.new(options[:ambiguous_time_range]))
493
+ ttokens << distoken
494
+ end
495
+ end
496
+ tokens = ttokens
497
+ end
498
+
499
+ tokens
500
+ end
501
+
502
+ end
503
+
504
+ class Handler #:nodoc:
505
+ attr_accessor :pattern, :handler_method
506
+
507
+ def initialize(pattern, handler_method)
508
+ @pattern = pattern
509
+ @handler_method = handler_method
510
+ end
511
+
512
+ def constantize(name)
513
+ camel = name.to_s.gsub(/(^|_)(.)/) { $2.upcase }
514
+ ::Chronic.module_eval(camel, __FILE__, __LINE__)
515
+ end
516
+
517
+ def match(tokens, definitions)
518
+ token_index = 0
519
+ @pattern.each do |element|
520
+ name = element.to_s
521
+ optional = name.reverse[0..0] == '?'
522
+ name = name.chop if optional
523
+ if element.instance_of? Symbol
524
+ klass = constantize(name)
525
+ match = tokens[token_index] && !tokens[token_index].tags.select { |o| o.kind_of?(klass) }.empty?
526
+ return false if !match && !optional
527
+ (token_index += 1; next) if match
528
+ next if !match && optional
529
+ elsif element.instance_of? String
530
+ return true if optional && token_index == tokens.size
531
+ sub_handlers = definitions[name.intern] || raise(ChronicPain, "Invalid subset #{name} specified")
532
+ sub_handlers.each do |sub_handler|
533
+ return true if sub_handler.match(tokens[token_index..tokens.size], definitions)
534
+ end
535
+ return false
536
+ else
537
+ raise(ChronicPain, "Invalid match type: #{element.class}")
538
+ end
539
+ end
540
+ return false if token_index != tokens.size
541
+ return true
542
+ end
543
+
544
+ def ==(other)
545
+ self.pattern == other.pattern
546
+ end
547
+ end
548
+
549
+ end