slaxor-chronic 0.3.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 (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