chronic 0.1.5 → 0.6.5

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 (68) hide show
  1. data/.gemtest +0 -0
  2. data/.gitignore +5 -0
  3. data/.yardopts +3 -0
  4. data/HISTORY.md +180 -0
  5. data/LICENSE +21 -0
  6. data/README.md +176 -0
  7. data/Rakefile +39 -32
  8. data/chronic.gemspec +17 -0
  9. data/lib/chronic/chronic.rb +294 -208
  10. data/lib/chronic/grabber.rb +22 -17
  11. data/lib/chronic/handler.rb +90 -0
  12. data/lib/chronic/handlers.rb +365 -278
  13. data/lib/chronic/mini_date.rb +38 -0
  14. data/lib/chronic/numerizer.rb +121 -0
  15. data/lib/chronic/ordinal.rb +23 -19
  16. data/lib/chronic/pointer.rb +20 -17
  17. data/lib/chronic/repeater.rb +126 -105
  18. data/lib/chronic/repeaters/repeater_day.rb +49 -43
  19. data/lib/chronic/repeaters/repeater_day_name.rb +48 -42
  20. data/lib/chronic/repeaters/repeater_day_portion.rb +81 -80
  21. data/lib/chronic/repeaters/repeater_fortnight.rb +59 -53
  22. data/lib/chronic/repeaters/repeater_hour.rb +49 -43
  23. data/lib/chronic/repeaters/repeater_minute.rb +55 -39
  24. data/lib/chronic/repeaters/repeater_month.rb +77 -55
  25. data/lib/chronic/repeaters/repeater_month_name.rb +83 -82
  26. data/lib/chronic/repeaters/repeater_season.rb +107 -21
  27. data/lib/chronic/repeaters/repeater_season_name.rb +41 -22
  28. data/lib/chronic/repeaters/repeater_second.rb +39 -33
  29. data/lib/chronic/repeaters/repeater_time.rb +117 -103
  30. data/lib/chronic/repeaters/repeater_week.rb +63 -57
  31. data/lib/chronic/repeaters/repeater_weekday.rb +85 -0
  32. data/lib/chronic/repeaters/repeater_weekend.rb +64 -9
  33. data/lib/chronic/repeaters/repeater_year.rb +70 -51
  34. data/lib/chronic/scalar.rb +64 -29
  35. data/lib/chronic/season.rb +37 -0
  36. data/lib/chronic/separator.rb +50 -38
  37. data/lib/chronic/span.rb +31 -0
  38. data/lib/chronic/tag.rb +42 -0
  39. data/lib/chronic/time_zone.rb +30 -0
  40. data/lib/chronic/token.rb +45 -0
  41. data/lib/chronic.rb +86 -22
  42. data/test/helper.rb +6 -0
  43. data/test/test_Chronic.rb +118 -20
  44. data/test/test_DaylightSavings.rb +118 -0
  45. data/test/test_Handler.rb +36 -42
  46. data/test/test_MiniDate.rb +32 -0
  47. data/test/test_Numerizer.rb +72 -0
  48. data/test/test_RepeaterDayName.rb +15 -16
  49. data/test/test_RepeaterFortnight.rb +16 -17
  50. data/test/test_RepeaterHour.rb +22 -19
  51. data/test/test_RepeaterMinute.rb +34 -0
  52. data/test/test_RepeaterMonth.rb +20 -17
  53. data/test/test_RepeaterMonthName.rb +17 -18
  54. data/test/test_RepeaterSeason.rb +40 -0
  55. data/test/test_RepeaterTime.rb +20 -22
  56. data/test/test_RepeaterWeek.rb +16 -17
  57. data/test/test_RepeaterWeekday.rb +55 -0
  58. data/test/test_RepeaterWeekend.rb +74 -0
  59. data/test/test_RepeaterYear.rb +24 -18
  60. data/test/test_Span.rb +5 -6
  61. data/test/test_Token.rb +5 -6
  62. data/test/test_parsing.rb +743 -338
  63. metadata +81 -54
  64. data/History.txt +0 -29
  65. data/Manifest.txt +0 -43
  66. data/README.txt +0 -149
  67. data/test/parse_numbers.rb +0 -50
  68. data/test/suite.rb +0 -9
@@ -1,241 +1,327 @@
1
1
  module Chronic
2
+
3
+ DEFAULT_OPTIONS = {
4
+ :context => :future,
5
+ :now => nil,
6
+ :guess => true,
7
+ :ambiguous_time_range => 6,
8
+ :endian_precedence => [:middle, :little],
9
+ :ambiguous_year_future_bias => 50
10
+ }
11
+
2
12
  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.
13
+
14
+ # Parses a string containing a natural language date or time
8
15
  #
9
- # Options are:
16
+ # If the parser can find a date or time, either a Time or Chronic::Span
17
+ # will be returned (depending on the value of `:guess`). If no
18
+ # date or time can be found, `nil` will be returned
10
19
  #
11
- # [<tt>:context</tt>]
12
- # <tt>:past</tt> or <tt>:future</tt> (defaults to <tt>:future</tt>)
20
+ # @param [String] text The text to parse
13
21
  #
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.
22
+ # @option opts [Symbol] :context (:future)
23
+ # * If your string represents a birthday, you can set `:context` to
24
+ # `:past` and if an ambiguous string is given, it will assume it is
25
+ # in the past. Specify `:future` or omit to set a future context.
17
26
  #
18
- # [<tt>:now</tt>]
19
- # Time (defaults to Time.now)
27
+ # @option opts [Object] :now (Time.now)
28
+ # * By setting `:now` to a Time, all computations will be based off of
29
+ # that time instead of `Time.now`. If set to nil, Chronic will use
30
+ # `Time.now`
20
31
  #
21
- # By setting <tt>:now</tt> to a Time, all computations will be based off
22
- # of that time instead of Time.now
32
+ # @option opts [Boolean] :guess (true)
33
+ # * By default, the parser will guess a single point in time for the
34
+ # given date or time. If you'd rather have the entire time span
35
+ # returned, set `:guess` to `false` and a {Chronic::Span} will
36
+ # be returned
23
37
  #
24
- # [<tt>:guess</tt>]
25
- # +true+ or +false+ (defaults to +true+)
38
+ # @option opts [Integer] :ambiguous_time_range (6)
39
+ # * If an Integer is given, ambiguous times (like 5:00) will be
40
+ # assumed to be within the range of that time in the AM to that time
41
+ # in the PM. For example, if you set it to `7`, then the parser
42
+ # will look for the time between 7am and 7pm. In the case of 5:00, it
43
+ # would assume that means 5:00pm. If `:none` is given, no
44
+ # assumption will be made, and the first matching instance of that
45
+ # time will be used
26
46
  #
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))
47
+ # @option opts [Array] :endian_precedence ([:middle, :little])
48
+ # * By default, Chronic will parse "03/04/2011" as the fourth day
49
+ # of the third month. Alternatively you can tell Chronic to parse
50
+ # this as the third day of the fourth month by altering the
51
+ # `:endian_precedence` to `[:little, :middle]`
33
52
  #
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
-
53
+ # @option opts [Integer] :ambiguous_year_future_bias (50)
54
+ # * When parsing two digit years (ie 79) unlike Rubys Time class,
55
+ # Chronic will attempt to assume the full year using this figure.
56
+ # Chronic will look x amount of years into the future and past. If
57
+ # the two digit year is `now + x years` it's assumed to be the
58
+ # future, `now - x years` is assumed to be the past
59
+ #
60
+ # @return [Time, Chronic::Span, nil]
61
+ def parse(text, opts={})
62
+ options = DEFAULT_OPTIONS.merge opts
63
+
49
64
  # 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, :none].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
-
65
+ (opts.keys - DEFAULT_OPTIONS.keys).each do |key|
66
+ raise ArgumentError, "#{key} is not a valid option key."
67
+ end
68
+
69
+ unless [:past, :future, :none].include?(options[:context])
70
+ raise ArgumentError, "Invalid context, :past/:future only"
71
+ end
72
+
73
+ options[:text] = text
74
+ Chronic.now = options[:now] || Chronic.time_class.now
75
+
76
+ # tokenize words
77
+ tokens = tokenize(text, options)
78
+
76
79
  if Chronic.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
80
+ puts "+#{'-' * 51}\n| #{tokens}\n+#{'-' * 51}"
81
+ end
82
+
83
+ span = tokens_to_span(tokens, options)
84
+
85
+ if span
86
+ options[:guess] ? guess(span) : span
95
87
  end
96
88
  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
89
+
90
+ # Clean up the specified text ready for parsing
91
+ #
92
+ # Clean up the string by stripping unwanted characters, converting
93
+ # idioms to their canonical form, converting number words to numbers
94
+ # (three => 3), and converting ordinal words to numeric
101
95
  # ordinals (third => 3rd)
102
- def pre_normalize(text) #:nodoc:
103
- normalized_text = text.to_s.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!(/\bbefore now\b/, 'past')
112
- normalized_text.gsub!(/\bnow\b/, 'this second')
113
- normalized_text.gsub!(/\b(ago|before)\b/, 'past')
114
- normalized_text.gsub!(/\bthis past\b/, 'last')
115
- normalized_text.gsub!(/\bthis last\b/, 'last')
116
- normalized_text.gsub!(/\b(?:in|during) the (morning)\b/, '\1')
117
- normalized_text.gsub!(/\b(?:in the|during the|at) (afternoon|evening|night)\b/, '\1')
118
- normalized_text.gsub!(/\btonight\b/, 'this night')
119
- normalized_text.gsub!(/(?=\w)([ap]m|oclock)\b/, ' \1')
120
- normalized_text.gsub!(/\b(hence|after|from)\b/, 'future')
121
- normalized_text.gsub!(/\ba\b/, '1')
122
- normalized_text.gsub!(/\s+/, ' ')
123
- normalized_text = numericize_numbers(normalized_text)
124
- normalized_text = numericize_ordinals(normalized_text)
125
- end
126
-
127
- # Convert number words to numbers (three => 3)
128
- def numericize_numbers(text) #:nodoc:
129
- text
130
- end
131
-
132
- # Convert ordinal words to numeric ordinals (third => 3rd)
133
- def numericize_ordinals(text) #:nodoc:
96
+ #
97
+ # @example
98
+ # Chronic.pre_normalize('first day in May')
99
+ # #=> "1st day in may"
100
+ #
101
+ # Chronic.pre_normalize('tomorrow after noon')
102
+ # #=> "next day future 12:00"
103
+ #
104
+ # Chronic.pre_normalize('one hundred and thirty six days from now')
105
+ # #=> "136 days future this second"
106
+ #
107
+ # @param [String] text The string to normalize
108
+ # @return [String] A new string ready for Chronic to parse
109
+ def pre_normalize(text)
110
+ text = text.to_s.downcase
111
+ text.gsub!(/['"\.]/, '')
112
+ text.gsub!(/,/, ' ')
113
+ text.gsub!(/\bsecond (of|day|month|hour|minute|second)\b/, '2nd \1')
114
+ text = Numerizer.numerize(text)
115
+ text.gsub!(/ \-(\d{4})\b/, ' tzminus\1')
116
+ text.gsub!(/([\/\-\,\@])/) { ' ' + $1 + ' ' }
117
+ text.gsub!(/(?:^|\s)0(\d+:\d+\s*pm?\b)/, '\1')
118
+ text.gsub!(/\btoday\b/, 'this day')
119
+ text.gsub!(/\btomm?orr?ow\b/, 'next day')
120
+ text.gsub!(/\byesterday\b/, 'last day')
121
+ text.gsub!(/\bnoon\b/, '12:00pm')
122
+ text.gsub!(/\bmidnight\b/, '24:00')
123
+ text.gsub!(/\bnow\b/, 'this second')
124
+ text.gsub!(/\b(?:ago|before(?: now)?)\b/, 'past')
125
+ text.gsub!(/\bthis (?:last|past)\b/, 'last')
126
+ text.gsub!(/\b(?:in|during) the (morning)\b/, '\1')
127
+ text.gsub!(/\b(?:in the|during the|at) (afternoon|evening|night)\b/, '\1')
128
+ text.gsub!(/\btonight\b/, 'this night')
129
+ text.gsub!(/\b\d+:?\d*[ap]\b/,'\0m')
130
+ text.gsub!(/(\d)([ap]m|oclock)\b/, '\1 \2')
131
+ text.gsub!(/\b(hence|after|from)\b/, 'future')
134
132
  text
135
133
  end
136
-
137
- # Split the text on spaces and convert each word into
138
- # a Token
139
- def base_tokenize(text) #:nodoc:
140
- text.split(' ').map { |word| Token.new(word) }
134
+
135
+ # Convert number words to numbers (three => 3, fourth => 4th)
136
+ #
137
+ # @see Numerizer.numerize
138
+ # @param [String] text The string to convert
139
+ # @return [String] A new string with words converted to numbers
140
+ def numericize_numbers(text)
141
+ warn "Chronic.numericize_numbers will be deprecated in version 0.7.0. Please use Chronic::Numerizer.numerize instead"
142
+ Numerizer.numerize(text)
141
143
  end
142
-
144
+
143
145
  # Guess a specific time within the given span
144
- def guess(span) #:nodoc:
145
- return nil if span.nil?
146
+ #
147
+ # @param [Span] span
148
+ # @return [Time]
149
+ def guess(span)
146
150
  if span.width > 1
147
151
  span.begin + (span.width / 2)
148
152
  else
149
153
  span.begin
150
154
  end
151
155
  end
152
- end
153
-
154
- class Token #:nodoc:
155
- attr_accessor :word, :tags
156
-
157
- def initialize(word)
158
- @word = word
159
- @tags = []
160
- end
161
-
162
- # Tag this token with the specified tag
163
- def tag(new_tag)
164
- @tags << new_tag
165
- end
166
-
167
- # Remove all tags of the given class
168
- def untag(tag_class)
169
- @tags = @tags.select { |m| !m.kind_of? tag_class }
170
- end
171
-
172
- # Return true if this token has any tags
173
- def tagged?
174
- @tags.size > 0
175
- end
176
-
177
- # Return the Tag that matches the given class
178
- def get_tag(tag_class)
179
- matches = @tags.select { |m| m.kind_of? tag_class }
180
- #matches.size < 2 || raise("Multiple identical tags found")
181
- return matches.first
182
- end
183
-
184
- # Print this Token in a pretty way
185
- def to_s
186
- @word << '(' << @tags.join(', ') << ') '
187
- end
188
- end
189
-
190
- # A Span represents a range of time. Since this class extends
191
- # Range, you can use #begin and #end to get the beginning and
192
- # ending times of the span (they will be of class Time)
193
- class Span < Range
194
- # Returns the width of this span in seconds
195
- def width
196
- (self.end - self.begin).to_i
197
- end
198
-
199
- # Add a number of seconds to this span, returning the
200
- # resulting Span
201
- def +(seconds)
202
- Span.new(self.begin + seconds, self.end + seconds)
203
- end
204
-
205
- # Subtract a number of seconds to this span, returning the
206
- # resulting Span
207
- def -(seconds)
208
- self + -seconds
156
+
157
+ # List of {Handler} definitions. See {parse} for a list of options this
158
+ # method accepts
159
+ #
160
+ # @see parse
161
+ # @return [Hash] A Hash of Handler definitions
162
+ def definitions(options={})
163
+ options[:endian_precedence] ||= [:middle, :little]
164
+
165
+ @definitions ||= {
166
+ :time => [
167
+ Handler.new([:repeater_time, :repeater_day_portion?], nil)
168
+ ],
169
+
170
+ :date => [
171
+ 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),
172
+ Handler.new([:repeater_day_name, :repeater_month_name, :scalar_day], :handle_rdn_rmn_sd),
173
+ Handler.new([:repeater_day_name, :repeater_month_name, :ordinal_day], :handle_rdn_rmn_od),
174
+ Handler.new([:scalar_year, :separator_slash_or_dash, :scalar_month, :separator_slash_or_dash, :scalar_day, :repeater_time, :time_zone], :handle_sy_sm_sd_t_tz),
175
+ Handler.new([:repeater_month_name, :scalar_day, :scalar_year], :handle_rmn_sd_sy),
176
+ Handler.new([:repeater_month_name, :ordinal_day, :scalar_year], :handle_rmn_od_sy),
177
+ Handler.new([:repeater_month_name, :scalar_day, :scalar_year, :separator_at?, 'time?'], :handle_rmn_sd_sy),
178
+ Handler.new([:repeater_month_name, :ordinal_day, :scalar_year, :separator_at?, 'time?'], :handle_rmn_od_sy),
179
+ Handler.new([:repeater_month_name, :scalar_day, :separator_at?, 'time?'], :handle_rmn_sd),
180
+ Handler.new([:repeater_time, :repeater_day_portion?, :separator_on?, :repeater_month_name, :scalar_day], :handle_rmn_sd_on),
181
+ Handler.new([:repeater_month_name, :ordinal_day, :separator_at?, 'time?'], :handle_rmn_od),
182
+ Handler.new([:ordinal_day, :repeater_month_name, :scalar_year, :separator_at?, 'time?'], :handle_od_rmn_sy),
183
+ Handler.new([:ordinal_day, :repeater_month_name, :separator_at?, 'time?'], :handle_od_rmn),
184
+ Handler.new([:scalar_year, :repeater_month_name, :ordinal_day], :handle_sy_rmn_od),
185
+ Handler.new([:repeater_time, :repeater_day_portion?, :separator_on?, :repeater_month_name, :ordinal_day], :handle_rmn_od_on),
186
+ Handler.new([:repeater_month_name, :scalar_year], :handle_rmn_sy),
187
+ Handler.new([:scalar_day, :repeater_month_name, :scalar_year, :separator_at?, 'time?'], :handle_sd_rmn_sy),
188
+ Handler.new([:scalar_day, :repeater_month_name, :separator_at?, 'time?'], :handle_sd_rmn),
189
+ Handler.new([:scalar_year, :separator_slash_or_dash, :scalar_month, :separator_slash_or_dash, :scalar_day, :separator_at?, 'time?'], :handle_sy_sm_sd),
190
+ Handler.new([:scalar_month, :separator_slash_or_dash, :scalar_year], :handle_sm_sy)
191
+ ],
192
+
193
+ # tonight at 7pm
194
+ :anchor => [
195
+ Handler.new([:grabber?, :repeater, :separator_at?, :repeater?, :repeater?], :handle_r),
196
+ Handler.new([:grabber?, :repeater, :repeater, :separator_at?, :repeater?, :repeater?], :handle_r),
197
+ Handler.new([:repeater, :grabber, :repeater], :handle_r_g_r)
198
+ ],
199
+
200
+ # 3 weeks from now, in 2 months
201
+ :arrow => [
202
+ Handler.new([:scalar, :repeater, :pointer], :handle_s_r_p),
203
+ Handler.new([:pointer, :scalar, :repeater], :handle_p_s_r),
204
+ Handler.new([:scalar, :repeater, :pointer, 'anchor'], :handle_s_r_p_a)
205
+ ],
206
+
207
+ # 3rd week in march
208
+ :narrow => [
209
+ Handler.new([:ordinal, :repeater, :separator_in, :repeater], :handle_o_r_s_r),
210
+ Handler.new([:ordinal, :repeater, :grabber, :repeater], :handle_o_r_g_r)
211
+ ]
212
+ }
213
+
214
+ endians = [
215
+ Handler.new([:scalar_month, :separator_slash_or_dash, :scalar_day, :separator_slash_or_dash, :scalar_year, :separator_at?, 'time?'], :handle_sm_sd_sy),
216
+ Handler.new([:scalar_day, :separator_slash_or_dash, :scalar_month, :separator_slash_or_dash, :scalar_year, :separator_at?, 'time?'], :handle_sd_sm_sy)
217
+ ]
218
+
219
+ case endian = Array(options[:endian_precedence]).first
220
+ when :little
221
+ @definitions[:endian] = endians.reverse
222
+ when :middle
223
+ @definitions[:endian] = endians
224
+ else
225
+ raise ArgumentError, "Unknown endian option '#{endian}'"
226
+ end
227
+
228
+ @definitions
209
229
  end
210
-
211
- # Prints this span in a nice fashion
212
- def to_s
213
- '(' << self.begin.to_s << '..' << self.end.to_s << ')'
230
+
231
+ # Construct a time Object
232
+ #
233
+ # @return [Time]
234
+ def construct(year, month = 1, day = 1, hour = 0, minute = 0, second = 0)
235
+ if second >= 60
236
+ minute += second / 60
237
+ second = second % 60
238
+ end
239
+
240
+ if minute >= 60
241
+ hour += minute / 60
242
+ minute = minute % 60
243
+ end
244
+
245
+ if hour >= 24
246
+ day += hour / 24
247
+ hour = hour % 24
248
+ end
249
+
250
+ # determine if there is a day overflow. this is complicated by our crappy calendar
251
+ # system (non-constant number of days per month)
252
+ day <= 56 || raise("day must be no more than 56 (makes month resolution easier)")
253
+ if day > 28
254
+ # no month ever has fewer than 28 days, so only do this if necessary
255
+ leap_year_month_days = [31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
256
+ common_year_month_days = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
257
+ days_this_month = Date.leap?(year) ? leap_year_month_days[month - 1] : common_year_month_days[month - 1]
258
+ if day > days_this_month
259
+ month += day / days_this_month
260
+ day = day % days_this_month
261
+ end
262
+ end
263
+
264
+ if month > 12
265
+ if month % 12 == 0
266
+ year += (month - 12) / 12
267
+ month = 12
268
+ else
269
+ year += month / 12
270
+ month = month % 12
271
+ end
272
+ end
273
+
274
+ Chronic.time_class.local(year, month, day, hour, minute, second)
214
275
  end
215
- end
216
276
 
217
- # Tokens are tagged with subclassed instances of this class when
218
- # they match specific criteria
219
- class Tag #:nodoc:
220
- attr_accessor :type
221
-
222
- def initialize(type)
223
- @type = type
277
+ private
278
+
279
+ def tokenize(text, options)
280
+ text = pre_normalize(text)
281
+ tokens = text.split(' ').map { |word| Token.new(word) }
282
+ [Repeater, Grabber, Pointer, Scalar, Ordinal, Separator, TimeZone].each do |tok|
283
+ tok.scan(tokens, options)
284
+ end
285
+ tokens.select { |token| token.tagged? }
224
286
  end
225
-
226
- def start=(s)
227
- @now = s
287
+
288
+ def tokens_to_span(tokens, options)
289
+ definitions = definitions(options)
290
+
291
+ (definitions[:endian] + definitions[:date]).each do |handler|
292
+ if handler.match(tokens, definitions)
293
+ good_tokens = tokens.select { |o| !o.get_tag Separator }
294
+ return handler.invoke(:date, good_tokens, options)
295
+ end
296
+ end
297
+
298
+ definitions[:anchor].each do |handler|
299
+ if handler.match(tokens, definitions)
300
+ good_tokens = tokens.select { |o| !o.get_tag Separator }
301
+ return handler.invoke(:anchor, good_tokens, options)
302
+ end
303
+ end
304
+
305
+ definitions[:arrow].each do |handler|
306
+ if handler.match(tokens, definitions)
307
+ good_tokens = tokens.reject { |o| o.get_tag(SeparatorAt) || o.get_tag(SeparatorSlashOrDash) || o.get_tag(SeparatorComma) }
308
+ return handler.invoke(:arrow, good_tokens, options)
309
+ end
310
+ end
311
+
312
+ definitions[:narrow].each do |handler|
313
+ if handler.match(tokens, definitions)
314
+ return handler.invoke(:narrow, tokens, options)
315
+ end
316
+ end
317
+
318
+ puts "-none" if Chronic.debug
319
+ return nil
228
320
  end
321
+
229
322
  end
230
-
323
+
231
324
  # Internal exception
232
- class ChronicPain < Exception #:nodoc:
233
-
234
- end
235
-
236
- # This exception is raised if an invalid argument is provided to
237
- # any of Chronic's methods
238
- class InvalidArgumentException < Exception
239
-
325
+ class ChronicPain < Exception
240
326
  end
241
- end
327
+ end
@@ -1,26 +1,31 @@
1
- #module Chronic
1
+ module Chronic
2
+ class Grabber < Tag
2
3
 
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
4
+ # Scan an Array of {Token}s and apply any necessary Grabber tags to
5
+ # each token
6
+ #
7
+ # @param [Array<Token>] tokens Array of tokens to scan
8
+ # @param [Hash] options Options specified in {Chronic.parse}
9
+ # @return [Array] list of tokens
10
+ def self.scan(tokens, options)
11
+ tokens.each do |token|
12
+ if t = scan_for_all(token) then token.tag(t); next end
7
13
  end
8
- tokens
9
14
  end
10
-
15
+
16
+ # @param [Token] token
17
+ # @return [Grabber, nil]
11
18
  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
+ scan_for token, self,
20
+ {
21
+ /last/ => :last,
22
+ /this/ => :this,
23
+ /next/ => :next
24
+ }
19
25
  end
20
-
26
+
21
27
  def to_s
22
28
  'grabber-' << @type.to_s
23
29
  end
24
30
  end
25
-
26
- #end
31
+ end