rizwanreza-chronic 0.0.1

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 (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