chronic 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (41) hide show
  1. data/README +119 -0
  2. data/lib/chronic.rb +30 -0
  3. data/lib/chronic/chronic.rb +242 -0
  4. data/lib/chronic/grabber.rb +26 -0
  5. data/lib/chronic/handlers.rb +405 -0
  6. data/lib/chronic/ordinal.rb +40 -0
  7. data/lib/chronic/pointer.rb +27 -0
  8. data/lib/chronic/repeater.rb +114 -0
  9. data/lib/chronic/repeaters/repeater_day.rb +40 -0
  10. data/lib/chronic/repeaters/repeater_day_name.rb +41 -0
  11. data/lib/chronic/repeaters/repeater_day_portion.rb +93 -0
  12. data/lib/chronic/repeaters/repeater_fortnight.rb +64 -0
  13. data/lib/chronic/repeaters/repeater_hour.rb +52 -0
  14. data/lib/chronic/repeaters/repeater_minute.rb +21 -0
  15. data/lib/chronic/repeaters/repeater_month.rb +54 -0
  16. data/lib/chronic/repeaters/repeater_month_name.rb +82 -0
  17. data/lib/chronic/repeaters/repeater_season.rb +23 -0
  18. data/lib/chronic/repeaters/repeater_season_name.rb +24 -0
  19. data/lib/chronic/repeaters/repeater_second.rb +34 -0
  20. data/lib/chronic/repeaters/repeater_time.rb +106 -0
  21. data/lib/chronic/repeaters/repeater_week.rb +62 -0
  22. data/lib/chronic/repeaters/repeater_weekend.rb +11 -0
  23. data/lib/chronic/repeaters/repeater_year.rb +55 -0
  24. data/lib/chronic/scalar.rb +74 -0
  25. data/lib/chronic/separator.rb +76 -0
  26. data/test/parse_numbers.rb +50 -0
  27. data/test/suite.rb +9 -0
  28. data/test/test_Chronic.rb +50 -0
  29. data/test/test_Handler.rb +110 -0
  30. data/test/test_RepeaterDayName.rb +52 -0
  31. data/test/test_RepeaterFortnight.rb +63 -0
  32. data/test/test_RepeaterHour.rb +65 -0
  33. data/test/test_RepeaterMonth.rb +47 -0
  34. data/test/test_RepeaterMonthName.rb +57 -0
  35. data/test/test_RepeaterTime.rb +72 -0
  36. data/test/test_RepeaterWeek.rb +63 -0
  37. data/test/test_RepeaterYear.rb +63 -0
  38. data/test/test_Span.rb +24 -0
  39. data/test/test_Token.rb +26 -0
  40. data/test/test_parsing.rb +472 -0
  41. metadata +87 -0
data/README ADDED
@@ -0,0 +1,119 @@
1
+ =Chronic
2
+
3
+ Chronic is a natural language date/time parser written in pure Ruby. See below for the wide variety of formats Chronic will parse.
4
+
5
+ ==Installation
6
+
7
+ Chronic can be installed via RubyGems:
8
+
9
+ $ sudo gem install chronic
10
+
11
+ ==Usage
12
+
13
+ You can parse strings containing a natural language date using the Chronic.parse method.
14
+
15
+ require 'chronic'
16
+
17
+ Time.now #=> Sun Aug 27 23:18:25 PDT 2006
18
+
19
+ #---
20
+
21
+ Chronic.parse('tomorrow')
22
+ #=> Mon Aug 28 12:00:00 PDT 2006
23
+
24
+ Chronic.parse('monday', :context => :past)
25
+ #=> Mon Aug 21 12:00:00 PDT 2006
26
+
27
+ Chronic.parse('this tuesday 5:00')
28
+ #=> Tue Aug 29 17:00:00 PDT 2006
29
+
30
+ Chronic.parse('this tuesday 5:00', :ambiguous_time_range => :none)
31
+ #=> Tue Aug 29 05:00:00 PDT 2006
32
+
33
+ Chronic.parse('may 27th', :now => Time.local(2000, 1, 1))
34
+ #=> Sat May 27 12:00:00 PDT 2000
35
+
36
+ Chronic.parse('may 27th', :guess => false)
37
+ #=> Sun May 27 00:00:00 PDT 2007..Mon May 28 00:00:00 PDT 2007
38
+
39
+ See Chronic.parse for detailed usage instructions.
40
+
41
+ ==Examples
42
+
43
+ Chronic can parse a huge variety of date and time formats. Following is a small sample of strings that will be properly parsed. Parsing is case insensitive and will handle common abbreviations and misspellings.
44
+
45
+ Simple
46
+
47
+ thursday
48
+ november
49
+ summer
50
+ friday 13:00
51
+ mon 2:35
52
+ 4pm
53
+ 6 in the morning
54
+ friday 1pm
55
+ sat 7 in the evening
56
+ yesterday
57
+ today
58
+ tomorrow
59
+ this tuesday
60
+ next month
61
+ last winter
62
+ this morning
63
+ last night
64
+ this second
65
+ yesterday at 4:00
66
+ last friday at 20:00
67
+ last week tuesday
68
+ tomorrow at 6:45pm
69
+ afternoon yesterday
70
+ thursday last week
71
+
72
+ Complex
73
+
74
+ 3 years ago
75
+ 5 months before now
76
+ 7 hours ago
77
+ 7 days from now
78
+ 1 week hence
79
+ in 3 hours
80
+ 1 year ago tomorrow
81
+ 3 months ago saturday at 5:00 pm
82
+ 7 hours before tomorrow at noon
83
+ 3rd wednesday in november
84
+ 3rd month next year
85
+ 3rd thursday this september
86
+ 4th day last week
87
+
88
+ Specific Dates
89
+
90
+ January 5
91
+ dec 25
92
+ may 27th
93
+ October 2006
94
+ oct 06
95
+ jan 3 2010
96
+ february 14, 2004
97
+ 3 jan 2000
98
+ 17 april 85
99
+ 5/27/1979
100
+ 27/5/1979
101
+ 05/06
102
+ 1979-05-27
103
+ Friday
104
+ 5
105
+ 4:00
106
+ 17:00
107
+ 0800
108
+
109
+ Specific Times (many of the above with an added time)
110
+
111
+ January 5 at 7pm
112
+ 1979-05-27 05:00
113
+ etc
114
+
115
+ ==Limitations
116
+
117
+ Chronic uses Ruby's built in Time class for all time storage and computation. Because of this, only times that the Time class can handle will be properly parsed. Parsing for times outside of this range will simply return nil. Support for a wider range of times is planned for a future release.
118
+
119
+ Time zones other than the local one are not currently supported. Support for other time zones is planned for a future release.
data/lib/chronic.rb ADDED
@@ -0,0 +1,30 @@
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
+ require 'date'
11
+
12
+ require 'chronic/chronic'
13
+ require 'chronic/handlers'
14
+ require 'chronic/grabber'
15
+ require 'chronic/ordinal'
16
+ require 'chronic/pointer'
17
+ require 'chronic/scalar'
18
+ require 'chronic/separator'
19
+
20
+ require 'chronic/repeater'
21
+ Dir["#{File.dirname(__FILE__)}/chronic/repeaters/*.rb"].each do |file|
22
+ require file
23
+ end
24
+
25
+ module Chronic
26
+ def self.debug=(val); @debug = val; end
27
+ end
28
+
29
+ Chronic.debug = false
30
+
@@ -0,0 +1,242 @@
1
+ module Chronic
2
+ class << self
3
+
4
+ # Parses a string containing a natural language date or time. If the parser
5
+ # can find a date or time, either a Time or Chronic::Span will be returned
6
+ # (depending on the value of <tt>:guess</tt>). If no date or time can be found,
7
+ # +nil+ will be returned.
8
+ #
9
+ # Options are:
10
+ #
11
+ # [<tt>:context</tt>]
12
+ # <tt>:past</tt> or <tt>:future</tt> (defaults to <tt>:future</tt>)
13
+ #
14
+ # If your string represents a birthday, you can set <tt>:context</tt> to <tt>:past</tt>
15
+ # and if an ambiguous string is given, it will assume it is in the
16
+ # past. Specify <tt>:future</tt> or omit to set a future context.
17
+ #
18
+ # [<tt>:now</tt>]
19
+ # Time (defaults to Time.now)
20
+ #
21
+ # By setting <tt>:now</tt> to a Time, all computations will be based off
22
+ # of that time instead of Time.now
23
+ #
24
+ # [<tt>:guess</tt>]
25
+ # +true+ or +false+ (defaults to +true+)
26
+ #
27
+ # By default, the parser will guess a single point in time for the
28
+ # given date or time. If you'd rather have the entire time span returned,
29
+ # set <tt>:guess</tt> to +false+ and a Chronic::Span will be returned.
30
+ #
31
+ # [<tt>:ambiguous_time_range</tt>]
32
+ # Integer or <tt>:none</tt> (defaults to <tt>6</tt> (6am-6pm))
33
+ #
34
+ # If an Integer is given, ambiguous times (like 5:00) will be
35
+ # assumed to be within the range of that time in the AM to that time
36
+ # in the PM. For example, if you set it to <tt>7</tt>, then the parser will
37
+ # look for the time between 7am and 7pm. In the case of 5:00, it would
38
+ # assume that means 5:00pm. If <tt>:none</tt> is given, no assumption
39
+ # will be made, and the first matching instance of that time will
40
+ # be used.
41
+ def parse(text, specified_options = {})
42
+ # get options and set defaults if necessary
43
+ default_options = {:context => :future,
44
+ :now => Time.now,
45
+ :guess => true,
46
+ :ambiguous_time_range => 6}
47
+ options = default_options.merge specified_options
48
+
49
+ # ensure the specified options are valid
50
+ specified_options.keys.each do |key|
51
+ default_options.keys.include?(key) || raise(InvalidArgumentException, "#{key} is not a valid option key.")
52
+ end
53
+ [:past, :future].include?(options[:context]) || raise(InvalidArgumentException, "Invalid value '#{options[:context]}' for :context specified. Valid values are :past and :future.")
54
+
55
+ # store now for later =)
56
+ @now = options[:now]
57
+
58
+ # put the text into a normal format to ease scanning
59
+ text = self.pre_normalize(text)
60
+
61
+ # get base tokens for each word
62
+ @tokens = self.base_tokenize(text)
63
+
64
+ # scan the tokens with each token scanner
65
+ [Repeater].each do |tokenizer|
66
+ @tokens = tokenizer.scan(@tokens, options)
67
+ end
68
+
69
+ [Grabber, Pointer, Scalar, Ordinal, Separator].each do |tokenizer|
70
+ @tokens = tokenizer.scan(@tokens)
71
+ end
72
+
73
+ # strip any non-tagged tokens
74
+ @tokens = @tokens.select { |token| token.tagged? }
75
+
76
+ if @debug
77
+ puts "+---------------------------------------------------"
78
+ puts "| " + @tokens.to_s
79
+ puts "+---------------------------------------------------"
80
+ end
81
+
82
+ # do the heavy lifting
83
+ begin
84
+ span = self.tokens_to_span(@tokens, options)
85
+ rescue
86
+ raise
87
+ return nil
88
+ end
89
+
90
+ # guess a time within a span if required
91
+ if options[:guess]
92
+ return self.guess(span)
93
+ else
94
+ return span
95
+ end
96
+ end
97
+
98
+ # Clean up the specified input text by stripping unwanted characters,
99
+ # converting idioms to their canonical form, converting number words
100
+ # to numbers (three => 3), and converting ordinal words to numeric
101
+ # ordinals (third => 3rd)
102
+ def pre_normalize(text) #:nodoc:
103
+ normalized_text = text.downcase
104
+ normalized_text.gsub!(/['"\.]/, '')
105
+ normalized_text.gsub!(/([\/\-\,\@])/) { ' ' + $1 + ' ' }
106
+ normalized_text.gsub!(/\btoday\b/, 'this day')
107
+ normalized_text.gsub!(/\btomm?orr?ow\b/, 'next day')
108
+ normalized_text.gsub!(/\byesterday\b/, 'last day')
109
+ normalized_text.gsub!(/\bnoon\b/, '12:00')
110
+ normalized_text.gsub!(/\bmidnight\b/, '24:00')
111
+ normalized_text.gsub!(/\bfrom now\b/, 'future')
112
+ normalized_text.gsub!(/\bbefore now\b/, 'past')
113
+ normalized_text.gsub!(/\bnow\b/, 'this second')
114
+ normalized_text.gsub!(/\b(ago|before)\b/, 'past')
115
+ normalized_text.gsub!(/\bthis past\b/, 'last')
116
+ normalized_text.gsub!(/\bthis last\b/, 'last')
117
+ normalized_text.gsub!(/\b(?:in|during) the (morning)\b/, '\1')
118
+ normalized_text.gsub!(/\b(?:in the|during the|at) (afternoon|evening|night)\b/, '\1')
119
+ normalized_text.gsub!(/\btonight\b/, 'this night')
120
+ normalized_text.gsub!(/(?=\w)([ap]m|oclock)\b/, ' \1')
121
+ normalized_text.gsub!(/\b(hence|after|from)\b/, 'future')
122
+ normalized_text.gsub!(/\ba\b/, '1')
123
+ normalized_text.gsub!(/\s+/, ' ')
124
+ normalized_text = numericize_numbers(normalized_text)
125
+ normalized_text = numericize_ordinals(normalized_text)
126
+ end
127
+
128
+ # Convert number words to numbers (three => 3)
129
+ def numericize_numbers(text) #:nodoc:
130
+ text
131
+ end
132
+
133
+ # Convert ordinal words to numeric ordinals (third => 3rd)
134
+ def numericize_ordinals(text) #:nodoc:
135
+ text
136
+ end
137
+
138
+ # Split the text on spaces and convert each word into
139
+ # a Token
140
+ def base_tokenize(text) #:nodoc:
141
+ text.split(' ').map { |word| Token.new(word) }
142
+ end
143
+
144
+ # Guess a specific time within the given span
145
+ def guess(span) #:nodoc:
146
+ return nil if span.nil?
147
+ if span.width > 1
148
+ span.begin + (span.width / 2)
149
+ else
150
+ span.begin
151
+ end
152
+ end
153
+ end
154
+
155
+ class Token #:nodoc:
156
+ attr_accessor :word, :tags
157
+
158
+ def initialize(word)
159
+ @word = word
160
+ @tags = []
161
+ end
162
+
163
+ # Tag this token with the specified tag
164
+ def tag(new_tag)
165
+ @tags << new_tag
166
+ end
167
+
168
+ # Remove all tags of the given class
169
+ def untag(tag_class)
170
+ @tags = @tags.select { |m| !m.kind_of? tag_class }
171
+ end
172
+
173
+ # Return true if this token has any tags
174
+ def tagged?
175
+ @tags.size > 0
176
+ end
177
+
178
+ # Return the Tag that matches the given class
179
+ def get_tag(tag_class)
180
+ matches = @tags.select { |m| m.kind_of? tag_class }
181
+ #matches.size < 2 || raise("Multiple identical tags found")
182
+ return matches.first
183
+ end
184
+
185
+ # Print this Token in a pretty way
186
+ def to_s
187
+ @word << '(' << @tags.join(', ') << ') '
188
+ end
189
+ end
190
+
191
+ # A Span represents a range of time. Since this class extends
192
+ # Range, you can use #begin and #end to get the beginning and
193
+ # ending times of the span (they will be of class Time)
194
+ class Span < Range
195
+ # Returns the width of this span in seconds
196
+ def width
197
+ (self.end - self.begin).to_i
198
+ end
199
+
200
+ # Add a number of seconds to this span, returning the
201
+ # resulting Span
202
+ def +(seconds)
203
+ Span.new(self.begin + seconds, self.end + seconds)
204
+ end
205
+
206
+ # Subtract a number of seconds to this span, returning the
207
+ # resulting Span
208
+ def -(seconds)
209
+ self + -seconds
210
+ end
211
+
212
+ # Prints this span in a nice fashion
213
+ def to_s
214
+ '(' << self.begin.to_s << '..' << self.end.to_s << ')'
215
+ end
216
+ end
217
+
218
+ # Tokens are tagged with subclassed instances of this class when
219
+ # they match specific criteria
220
+ class Tag #:nodoc:
221
+ attr_accessor :type
222
+
223
+ def initialize(type)
224
+ @type = type
225
+ end
226
+
227
+ def start=(s)
228
+ @now = s
229
+ end
230
+ end
231
+
232
+ # Internal exception
233
+ class ChronicPain < Exception #:nodoc:
234
+
235
+ end
236
+
237
+ # This exception is raised if an invalid argument is provided to
238
+ # any of Chronic's methods
239
+ class InvalidArgumentException < Exception
240
+
241
+ end
242
+ 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,405 @@
1
+ module Chronic
2
+
3
+ class << self
4
+
5
+ def definitions #:nodoc:
6
+ @definitions ||=
7
+ {:time => [Handler.new([:repeater_time, :repeater_day_portion?], nil)],
8
+
9
+ :date => [Handler.new([:repeater_month_name, :scalar_day, :scalar_year], :handle_rmn_sd_sy),
10
+ Handler.new([:repeater_month_name, :scalar_day, :scalar_year, :separator_at?, 'time?'], :handle_rmn_sd_sy),
11
+ Handler.new([:repeater_month_name, :scalar_day, :separator_at?, 'time?'], :handle_rmn_sd),
12
+ Handler.new([:repeater_month_name, :ordinal_day, :separator_at?, 'time?'], :handle_rmn_od),
13
+ Handler.new([:repeater_month_name, :scalar_year], :handle_rmn_sy),
14
+ Handler.new([:scalar_day, :repeater_month_name, :scalar_year, :separator_at?, 'time?'], :handle_sd_rmn_sy),
15
+ Handler.new([:scalar_month, :separator_slash_or_dash, :scalar_day, :separator_slash_or_dash, :scalar_year, :separator_at?, 'time?'], :handle_sm_sd_sy),
16
+ Handler.new([:scalar_day, :separator_slash_or_dash, :scalar_month, :separator_slash_or_dash, :scalar_year, :separator_at?, 'time?'], :handle_sd_sm_sy),
17
+ Handler.new([:scalar_year, :separator_slash_or_dash, :scalar_month, :separator_slash_or_dash, :scalar_day, :separator_at?, 'time?'], :handle_sy_sm_sd),
18
+ Handler.new([:scalar_month, :separator_slash_or_dash, :scalar_year], :handle_sm_sy)],
19
+
20
+ :anchor => [Handler.new([:grabber?, :repeater, :separator_at?, :repeater?, :repeater?], :handle_r),
21
+ Handler.new([:repeater, :grabber, :repeater], :handle_r_g_r)],
22
+
23
+ :arrow => [Handler.new([:scalar, :repeater, :pointer], :handle_s_r_p),
24
+ Handler.new([:pointer, :scalar, :repeater], :handle_p_s_r),
25
+ Handler.new([:scalar, :repeater, :pointer, 'anchor'], :handle_s_r_p_a)],
26
+
27
+ :narrow => [Handler.new([:ordinal, :repeater, :separator_in, :repeater], :handle_o_r_s_r),
28
+ Handler.new([:ordinal, :repeater, :grabber, :repeater], :handle_o_r_g_r)]
29
+ }
30
+ end
31
+
32
+ def tokens_to_span(tokens, options) #:nodoc:
33
+ # maybe it's a specific date
34
+
35
+ self.definitions[:date].each do |handler|
36
+ if handler.match(tokens, self.definitions)
37
+ good_tokens = tokens.select { |o| !o.get_tag Separator }
38
+ return self.send(handler.handler_method, good_tokens, options)
39
+ end
40
+ end
41
+
42
+ # I guess it's not a specific date, maybe it's just an anchor
43
+
44
+ self.definitions[:anchor].each do |handler|
45
+ if handler.match(tokens, self.definitions)
46
+ good_tokens = tokens.select { |o| !o.get_tag Separator }
47
+ return self.send(handler.handler_method, good_tokens, options)
48
+ end
49
+ end
50
+
51
+ # not an anchor, perhaps it's an arrow
52
+
53
+ self.definitions[:arrow].each do |handler|
54
+ if handler.match(tokens, self.definitions)
55
+ good_tokens = tokens.reject { |o| o.get_tag(SeparatorAt) || o.get_tag(SeparatorSlashOrDash) || o.get_tag(SeparatorComma) }
56
+ return self.send(handler.handler_method, good_tokens, options)
57
+ end
58
+ end
59
+
60
+ # not an arrow, let's hope it's an narrow
61
+
62
+ self.definitions[:narrow].each do |handler|
63
+ if handler.match(tokens, self.definitions)
64
+ #good_tokens = tokens.select { |o| !o.get_tag Separator }
65
+ return self.send(handler.handler_method, tokens, options)
66
+ end
67
+ end
68
+
69
+ # I guess you're out of luck!
70
+ return nil
71
+ end
72
+
73
+ #--------------
74
+
75
+ def day_or_time(day_start, time_tokens, options)
76
+ outer_span = Span.new(day_start, day_start + (24 * 60 * 60))
77
+
78
+ if !time_tokens.empty?
79
+ @now = outer_span.begin
80
+ time = get_anchor(dealias_and_disambiguate_times(time_tokens, options), options)
81
+ return time
82
+ else
83
+ return outer_span
84
+ end
85
+ end
86
+
87
+ #--------------
88
+
89
+ def handle_m_d(month, day, time_tokens, options) #:nodoc:
90
+ month.start = @now
91
+ span = month.next(options[:context])
92
+
93
+ day_start = Time.local(span.begin.year, span.begin.month, day)
94
+
95
+ day_or_time(day_start, time_tokens, options)
96
+ end
97
+
98
+ def handle_rmn_sd(tokens, options) #:nodoc:
99
+ handle_m_d(tokens[0].get_tag(RepeaterMonthName), tokens[1].get_tag(ScalarDay).type, tokens[2..tokens.size], options)
100
+ end
101
+
102
+ def handle_rmn_od(tokens, options) #:nodoc:
103
+ handle_m_d(tokens[0].get_tag(RepeaterMonthName), tokens[1].get_tag(OrdinalDay).type, tokens[2..tokens.size], options)
104
+ end
105
+
106
+ def handle_rmn_sy(tokens, options) #:nodoc:
107
+ month = tokens[0].get_tag(RepeaterMonthName).index
108
+ year = tokens[1].get_tag(ScalarYear).type
109
+
110
+ if month == 12
111
+ next_month_year = year + 1
112
+ next_month_month = 1
113
+ else
114
+ next_month_year = year
115
+ next_month_month = month + 1
116
+ end
117
+
118
+ begin
119
+ Span.new(Time.local(year, month), Time.local(next_month_year, next_month_month))
120
+ rescue ArgumentError
121
+ nil
122
+ end
123
+ end
124
+
125
+ def handle_rmn_sd_sy(tokens, options) #:nodoc:
126
+ month = tokens[0].get_tag(RepeaterMonthName).index
127
+ day = tokens[1].get_tag(ScalarDay).type
128
+ year = tokens[2].get_tag(ScalarYear).type
129
+
130
+ time_tokens = tokens.last(tokens.size - 3)
131
+
132
+ begin
133
+ day_start = Time.local(year, month, day)
134
+ day_or_time(day_start, time_tokens, options)
135
+ rescue ArgumentError
136
+ nil
137
+ end
138
+ end
139
+
140
+ def handle_sd_rmn_sy(tokens, options) #:nodoc:
141
+ new_tokens = [tokens[1], tokens[0], tokens[2]]
142
+ time_tokens = tokens.last(tokens.size - 3)
143
+ self.handle_rmn_sd_sy(new_tokens + time_tokens, options)
144
+ end
145
+
146
+ def handle_sm_sd_sy(tokens, options) #:nodoc:
147
+ month = tokens[0].get_tag(ScalarMonth).type
148
+ day = tokens[1].get_tag(ScalarDay).type
149
+ year = tokens[2].get_tag(ScalarYear).type
150
+
151
+ time_tokens = tokens.last(tokens.size - 3)
152
+
153
+ begin
154
+ day_start = Time.local(year, month, day) #:nodoc:
155
+ day_or_time(day_start, time_tokens, options)
156
+ rescue ArgumentError
157
+ nil
158
+ end
159
+ end
160
+
161
+ def handle_sd_sm_sy(tokens, options) #:nodoc:
162
+ new_tokens = [tokens[1], tokens[0], tokens[2]]
163
+ time_tokens = tokens.last(tokens.size - 3)
164
+ self.handle_sm_sd_sy(new_tokens + time_tokens, options)
165
+ end
166
+
167
+ def handle_sy_sm_sd(tokens, options) #:nodoc:
168
+ new_tokens = [tokens[1], tokens[2], tokens[0]]
169
+ time_tokens = tokens.last(tokens.size - 3)
170
+ self.handle_sm_sd_sy(new_tokens + time_tokens, options)
171
+ end
172
+
173
+ def handle_sm_sy(tokens, options) #:nodoc:
174
+ month = tokens[0].get_tag(ScalarMonth).type
175
+ year = tokens[1].get_tag(ScalarYear).type
176
+
177
+ if month == 12
178
+ next_month_year = year + 1
179
+ next_month_month = 1
180
+ else
181
+ next_month_year = year
182
+ next_month_month = month + 1
183
+ end
184
+
185
+ begin
186
+ Span.new(Time.local(year, month), Time.local(next_month_year, next_month_month))
187
+ rescue ArgumentError
188
+ nil
189
+ end
190
+ end
191
+
192
+ # anchors
193
+
194
+ def handle_r(tokens, options) #:nodoc:
195
+ dd_tokens = dealias_and_disambiguate_times(tokens, options)
196
+ self.get_anchor(dd_tokens, options)
197
+ end
198
+
199
+ def handle_r_g_r(tokens, options) #:nodoc:
200
+ new_tokens = [tokens[1], tokens[0], tokens[2]]
201
+ self.handle_r(new_tokens, options)
202
+ end
203
+
204
+ # arrows
205
+
206
+ def handle_srp(tokens, span, options) #:nodoc:
207
+ distance = tokens[0].get_tag(Scalar).type
208
+ repeater = tokens[1].get_tag(Repeater)
209
+ pointer = tokens[2].get_tag(Pointer).type
210
+
211
+ repeater.offset(span, distance, pointer)
212
+ end
213
+
214
+ def handle_s_r_p(tokens, options) #:nodoc:
215
+ repeater = tokens[1].get_tag(Repeater)
216
+
217
+ span =
218
+ case true
219
+ when [RepeaterYear, RepeaterSeason, RepeaterSeasonName, RepeaterMonth, RepeaterMonthName, RepeaterFortnight, RepeaterWeek].include?(repeater.class)
220
+ self.parse("this hour", :guess => false, :now => @now)
221
+ when [RepeaterWeekend, RepeaterDay, RepeaterDayName, RepeaterDayPortion, RepeaterHour].include?(repeater.class)
222
+ self.parse("this minute", :guess => false, :now => @now)
223
+ when [RepeaterMinute, RepeaterSecond].include?(repeater.class)
224
+ self.parse("this second", :guess => false, :now => @now)
225
+ else
226
+ raise(ChronicPain, "Invalid repeater: #{repeater.class}")
227
+ end
228
+
229
+ self.handle_srp(tokens, span, options)
230
+ end
231
+
232
+ def handle_p_s_r(tokens, options) #:nodoc:
233
+ new_tokens = [tokens[1], tokens[2], tokens[0]]
234
+ self.handle_s_r_p(new_tokens, options)
235
+ end
236
+
237
+ def handle_s_r_p_a(tokens, options) #:nodoc:
238
+ anchor_span = get_anchor(tokens[3..tokens.size - 1], options)
239
+ self.handle_srp(tokens, anchor_span, options)
240
+ end
241
+
242
+ # narrows
243
+
244
+ def handle_orr(tokens, outer_span, options) #:nodoc:
245
+ repeater = tokens[1].get_tag(Repeater)
246
+ repeater.start = outer_span.begin - 1
247
+ ordinal = tokens[0].get_tag(Ordinal).type
248
+ span = nil
249
+ ordinal.times do
250
+ span = repeater.next(:future)
251
+ if span.begin > outer_span.end
252
+ span = nil
253
+ break
254
+ end
255
+ end
256
+ span
257
+ end
258
+
259
+ def handle_o_r_s_r(tokens, options) #:nodoc:
260
+ outer_span = get_anchor([tokens[3]], options)
261
+ handle_orr(tokens[0..1], outer_span, options)
262
+ end
263
+
264
+ def handle_o_r_g_r(tokens, options) #:nodoc:
265
+ outer_span = get_anchor(tokens[2..3], options)
266
+ handle_orr(tokens[0..1], outer_span, options)
267
+ end
268
+
269
+ # support methods
270
+
271
+ def get_anchor(tokens, options) #:nodoc:
272
+ grabber = Grabber.new(:this)
273
+ pointer = :future
274
+
275
+ repeaters = self.get_repeaters(tokens)
276
+ repeaters.size.times { tokens.pop }
277
+
278
+ if tokens.last && tokens.last.get_tag(Grabber)
279
+ grabber = tokens.last.get_tag(Grabber)
280
+ tokens.pop
281
+ end
282
+
283
+ head = repeaters.shift
284
+ head.start = @now
285
+
286
+ case grabber.type
287
+ when :last: outer_span = head.next(:past)
288
+ when :this: outer_span = head.this(options[:context])
289
+ when :next: outer_span = head.next(:future)
290
+ else raise(ChronicPain, "Invalid grabber")
291
+ end
292
+
293
+ anchor = find_within(repeaters, outer_span, pointer)
294
+ end
295
+
296
+ def get_repeaters(tokens) #:nodoc:
297
+ repeaters = []
298
+ tokens.reverse.each do |token|
299
+ if t = token.get_tag(Repeater)
300
+ repeaters << t
301
+ else
302
+ break
303
+ end
304
+ end
305
+ repeaters.sort.reverse
306
+ end
307
+
308
+ # Recursively finds repeaters within other repeaters.
309
+ # Returns a Span representing the innermost time span
310
+ # or nil if no repeater union could be found
311
+ def find_within(tags, span, pointer) #:nodoc:
312
+ return span if tags.empty?
313
+
314
+ head, *rest = tags
315
+ head.start = pointer == :future ? span.begin : span.end
316
+ h = head.next(pointer)
317
+
318
+ if span.include?(h.begin) || span.include?(h.end)
319
+ return find_within(rest, h, pointer)
320
+ else
321
+ return nil
322
+ end
323
+ end
324
+
325
+ def dealias_and_disambiguate_times(tokens, options) #:nodoc:
326
+ # handle aliases of am/pm
327
+ # 5:00 in the morning => 5:00 am
328
+ # 7:00 in the evening => 7:00 pm
329
+ #ttokens = []
330
+ tokens.each_with_index do |t0, i|
331
+ t1 = tokens[i + 1]
332
+ if t1 && (t1tag = t1.get_tag(RepeaterDayPortion)) && t0.get_tag(RepeaterTime)
333
+ if [:morning].include?(t1tag.type)
334
+ t1.untag(RepeaterDayPortion)
335
+ t1.tag(RepeaterDayPortion.new(:am))
336
+ elsif [:afternoon, :evening, :night].include?(t1tag.type)
337
+ t1.untag(RepeaterDayPortion)
338
+ t1.tag(RepeaterDayPortion.new(:pm))
339
+ end
340
+ end
341
+ end
342
+ #tokens = ttokens
343
+
344
+ # handle ambiguous times if :ambiguous_time_range is specified
345
+ if options[:ambiguous_time_range] != :none
346
+ ttokens = []
347
+ tokens.each_with_index do |t0, i|
348
+ ttokens << t0
349
+ t1 = tokens[i + 1]
350
+ if t0.get_tag(RepeaterTime) && t0.get_tag(RepeaterTime).type.ambiguous? && (!t1 || !t1.get_tag(RepeaterDayPortion))
351
+ distoken = Token.new('disambiguator')
352
+ distoken.tag(RepeaterDayPortion.new(options[:ambiguous_time_range]))
353
+ ttokens << distoken
354
+ end
355
+ end
356
+ tokens = ttokens
357
+ end
358
+
359
+ tokens
360
+ end
361
+
362
+ end
363
+
364
+ class Handler #:nodoc:
365
+ attr_accessor :pattern, :handler_method
366
+
367
+ def initialize(pattern, handler_method)
368
+ @pattern = pattern
369
+ @handler_method = handler_method
370
+ end
371
+
372
+ def constantize(name)
373
+ camel = name.to_s.gsub(/(^|_)(.)/) { $2.upcase }
374
+ ::Chronic.module_eval(camel, __FILE__, __LINE__)
375
+ end
376
+
377
+ def match(tokens, definitions)
378
+ token_index = 0
379
+ @pattern.each do |element|
380
+ name = element.to_s
381
+ optional = name.reverse[0..0] == '?'
382
+ name = name.chop if optional
383
+ if element.instance_of? Symbol
384
+ klass = constantize(name)
385
+ match = tokens[token_index] && !tokens[token_index].tags.select { |o| o.kind_of?(klass) }.empty?
386
+ return false if !match && !optional
387
+ (token_index += 1; next) if match
388
+ next if !match && optional
389
+ elsif element.instance_of? String
390
+ return true if optional && token_index == tokens.size
391
+ sub_handlers = definitions[name.intern] || raise(ChronicPain, "Invalid subset #{name} specified")
392
+ sub_handlers.each do |sub_handler|
393
+ return true if sub_handler.match(tokens[token_index..tokens.size], definitions)
394
+ end
395
+ return false
396
+ else
397
+ raise(ChronicPain, "Invalid match type: #{element.class}")
398
+ end
399
+ end
400
+ return false if token_index != tokens.size
401
+ return true
402
+ end
403
+ end
404
+
405
+ end