slaxor-chronic 0.3.1

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