chronic 0.2.3 → 0.3.0

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