chronic 0.2.3 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
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