chronic-mmlac 0.6.4.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (64) hide show
  1. checksums.yaml +7 -0
  2. data/.gemtest +0 -0
  3. data/.gitignore +5 -0
  4. data/.yardopts +3 -0
  5. data/HISTORY.md +174 -0
  6. data/LICENSE +21 -0
  7. data/README.md +177 -0
  8. data/Rakefile +46 -0
  9. data/chronic.gemspec +17 -0
  10. data/lib/chronic/chronic.rb +325 -0
  11. data/lib/chronic/grabber.rb +31 -0
  12. data/lib/chronic/handler.rb +90 -0
  13. data/lib/chronic/handlers.rb +465 -0
  14. data/lib/chronic/mini_date.rb +38 -0
  15. data/lib/chronic/numerizer.rb +121 -0
  16. data/lib/chronic/ordinal.rb +44 -0
  17. data/lib/chronic/pointer.rb +30 -0
  18. data/lib/chronic/repeater.rb +135 -0
  19. data/lib/chronic/repeaters/repeater_day.rb +53 -0
  20. data/lib/chronic/repeaters/repeater_day_name.rb +52 -0
  21. data/lib/chronic/repeaters/repeater_day_portion.rb +94 -0
  22. data/lib/chronic/repeaters/repeater_fortnight.rb +71 -0
  23. data/lib/chronic/repeaters/repeater_hour.rb +58 -0
  24. data/lib/chronic/repeaters/repeater_minute.rb +58 -0
  25. data/lib/chronic/repeaters/repeater_month.rb +79 -0
  26. data/lib/chronic/repeaters/repeater_month_name.rb +94 -0
  27. data/lib/chronic/repeaters/repeater_season.rb +109 -0
  28. data/lib/chronic/repeaters/repeater_season_name.rb +43 -0
  29. data/lib/chronic/repeaters/repeater_second.rb +42 -0
  30. data/lib/chronic/repeaters/repeater_time.rb +128 -0
  31. data/lib/chronic/repeaters/repeater_week.rb +74 -0
  32. data/lib/chronic/repeaters/repeater_weekday.rb +85 -0
  33. data/lib/chronic/repeaters/repeater_weekend.rb +66 -0
  34. data/lib/chronic/repeaters/repeater_year.rb +77 -0
  35. data/lib/chronic/scalar.rb +109 -0
  36. data/lib/chronic/season.rb +37 -0
  37. data/lib/chronic/separator.rb +88 -0
  38. data/lib/chronic/span.rb +31 -0
  39. data/lib/chronic/tag.rb +42 -0
  40. data/lib/chronic/time_zone.rb +30 -0
  41. data/lib/chronic/token.rb +45 -0
  42. data/lib/chronic.rb +118 -0
  43. data/test/helper.rb +6 -0
  44. data/test/test_Chronic.rb +148 -0
  45. data/test/test_DaylightSavings.rb +118 -0
  46. data/test/test_Handler.rb +104 -0
  47. data/test/test_MiniDate.rb +32 -0
  48. data/test/test_Numerizer.rb +72 -0
  49. data/test/test_RepeaterDayName.rb +51 -0
  50. data/test/test_RepeaterFortnight.rb +62 -0
  51. data/test/test_RepeaterHour.rb +68 -0
  52. data/test/test_RepeaterMinute.rb +34 -0
  53. data/test/test_RepeaterMonth.rb +50 -0
  54. data/test/test_RepeaterMonthName.rb +56 -0
  55. data/test/test_RepeaterSeason.rb +40 -0
  56. data/test/test_RepeaterTime.rb +70 -0
  57. data/test/test_RepeaterWeek.rb +62 -0
  58. data/test/test_RepeaterWeekday.rb +55 -0
  59. data/test/test_RepeaterWeekend.rb +74 -0
  60. data/test/test_RepeaterYear.rb +69 -0
  61. data/test/test_Span.rb +23 -0
  62. data/test/test_Token.rb +25 -0
  63. data/test/test_parsing.rb +886 -0
  64. metadata +132 -0
@@ -0,0 +1,325 @@
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
+
12
+ class << self
13
+
14
+ # Parses a string containing a natural language date or time
15
+ #
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
19
+ #
20
+ # @param [String] text The text to parse
21
+ #
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.
26
+ #
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`
31
+ #
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
37
+ #
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
46
+ #
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]`
52
+ #
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
+
64
+ # ensure the specified options are valid
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
+
79
+ if Chronic.debug
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
87
+ end
88
+ end
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
95
+ # ordinals (third => 3rd)
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!(/\b0(\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')
132
+ text
133
+ end
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)
143
+ end
144
+
145
+ # Guess a specific time within the given span
146
+ #
147
+ # @param [Span] span
148
+ # @return [Time]
149
+ def guess(span)
150
+ if span.width > 1
151
+ span.begin + (span.width / 2)
152
+ else
153
+ span.begin
154
+ end
155
+ end
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([:scalar_year, :separator_slash_or_dash, :scalar_month, :separator_slash_or_dash, :scalar_day, :repeater_time, :time_zone], :handle_sy_sm_sd_t_tz),
173
+ Handler.new([:repeater_month_name, :scalar_day, :scalar_year], :handle_rmn_sd_sy),
174
+ Handler.new([:repeater_month_name, :ordinal_day, :scalar_year], :handle_rmn_od_sy),
175
+ Handler.new([:repeater_month_name, :scalar_day, :scalar_year, :separator_at?, 'time?'], :handle_rmn_sd_sy),
176
+ Handler.new([:repeater_month_name, :ordinal_day, :scalar_year, :separator_at?, 'time?'], :handle_rmn_od_sy),
177
+ Handler.new([:repeater_month_name, :scalar_day, :separator_at?, 'time?'], :handle_rmn_sd),
178
+ Handler.new([:repeater_time, :repeater_day_portion?, :separator_on?, :repeater_month_name, :scalar_day], :handle_rmn_sd_on),
179
+ Handler.new([:repeater_month_name, :ordinal_day, :separator_at?, 'time?'], :handle_rmn_od),
180
+ Handler.new([:ordinal_day, :repeater_month_name, :scalar_year, :separator_at?, 'time?'], :handle_od_rmn_sy),
181
+ Handler.new([:ordinal_day, :repeater_month_name, :separator_at?, 'time?'], :handle_od_rmn),
182
+ Handler.new([:scalar_year, :repeater_month_name, :ordinal_day], :handle_sy_rmn_od),
183
+ Handler.new([:repeater_time, :repeater_day_portion?, :separator_on?, :repeater_month_name, :ordinal_day], :handle_rmn_od_on),
184
+ Handler.new([:repeater_month_name, :scalar_year], :handle_rmn_sy),
185
+ Handler.new([:scalar_day, :repeater_month_name, :scalar_year, :separator_at?, 'time?'], :handle_sd_rmn_sy),
186
+ Handler.new([:scalar_day, :repeater_month_name, :separator_at?, 'time?'], :handle_sd_rmn),
187
+ Handler.new([:scalar_year, :separator_slash_or_dash, :scalar_month, :separator_slash_or_dash, :scalar_day, :separator_at?, 'time?'], :handle_sy_sm_sd),
188
+ Handler.new([:scalar_month, :separator_slash_or_dash, :scalar_year], :handle_sm_sy)
189
+ ],
190
+
191
+ # tonight at 7pm
192
+ :anchor => [
193
+ Handler.new([:grabber?, :repeater, :separator_at?, :repeater?, :repeater?], :handle_r),
194
+ Handler.new([:grabber?, :repeater, :repeater, :separator_at?, :repeater?, :repeater?], :handle_r),
195
+ Handler.new([:repeater, :grabber, :repeater], :handle_r_g_r)
196
+ ],
197
+
198
+ # 3 weeks from now, in 2 months
199
+ :arrow => [
200
+ Handler.new([:scalar, :repeater, :pointer], :handle_s_r_p),
201
+ Handler.new([:pointer, :scalar, :repeater], :handle_p_s_r),
202
+ Handler.new([:scalar, :repeater, :pointer, 'anchor'], :handle_s_r_p_a)
203
+ ],
204
+
205
+ # 3rd week in march
206
+ :narrow => [
207
+ Handler.new([:ordinal, :repeater, :separator_in, :repeater], :handle_o_r_s_r),
208
+ Handler.new([:ordinal, :repeater, :grabber, :repeater], :handle_o_r_g_r)
209
+ ]
210
+ }
211
+
212
+ endians = [
213
+ Handler.new([:scalar_month, :separator_slash_or_dash, :scalar_day, :separator_slash_or_dash, :scalar_year, :separator_at?, 'time?'], :handle_sm_sd_sy),
214
+ Handler.new([:scalar_day, :separator_slash_or_dash, :scalar_month, :separator_slash_or_dash, :scalar_year, :separator_at?, 'time?'], :handle_sd_sm_sy)
215
+ ]
216
+
217
+ case endian = Array(options[:endian_precedence]).first
218
+ when :little
219
+ @definitions[:endian] = endians.reverse
220
+ when :middle
221
+ @definitions[:endian] = endians
222
+ else
223
+ raise ArgumentError, "Unknown endian option '#{endian}'"
224
+ end
225
+
226
+ @definitions
227
+ end
228
+
229
+ # Construct a time Object
230
+ #
231
+ # @return [Time]
232
+ def construct(year, month = 1, day = 1, hour = 0, minute = 0, second = 0)
233
+ if second >= 60
234
+ minute += second / 60
235
+ second = second % 60
236
+ end
237
+
238
+ if minute >= 60
239
+ hour += minute / 60
240
+ minute = minute % 60
241
+ end
242
+
243
+ if hour >= 24
244
+ day += hour / 24
245
+ hour = hour % 24
246
+ end
247
+
248
+ # determine if there is a day overflow. this is complicated by our crappy calendar
249
+ # system (non-constant number of days per month)
250
+ day <= 56 || raise("day must be no more than 56 (makes month resolution easier)")
251
+ if day > 28
252
+ # no month ever has fewer than 28 days, so only do this if necessary
253
+ leap_year_month_days = [31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
254
+ common_year_month_days = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
255
+ days_this_month = Date.leap?(year) ? leap_year_month_days[month - 1] : common_year_month_days[month - 1]
256
+ if day > days_this_month
257
+ month += day / days_this_month
258
+ day = day % days_this_month
259
+ end
260
+ end
261
+
262
+ if month > 12
263
+ if month % 12 == 0
264
+ year += (month - 12) / 12
265
+ month = 12
266
+ else
267
+ year += month / 12
268
+ month = month % 12
269
+ end
270
+ end
271
+
272
+ Chronic.time_class.local(year, month, day, hour, minute, second)
273
+ end
274
+
275
+ private
276
+
277
+ def tokenize(text, options)
278
+ text = pre_normalize(text)
279
+ tokens = text.split(' ').map { |word| Token.new(word) }
280
+ [Repeater, Grabber, Pointer, Scalar, Ordinal, Separator, TimeZone].each do |tok|
281
+ tok.scan(tokens, options)
282
+ end
283
+ tokens.select { |token| token.tagged? }
284
+ end
285
+
286
+ def tokens_to_span(tokens, options)
287
+ definitions = definitions(options)
288
+
289
+ (definitions[:endian] + definitions[:date]).each do |handler|
290
+ if handler.match(tokens, definitions)
291
+ good_tokens = tokens.select { |o| !o.get_tag Separator }
292
+ return handler.invoke(:date, good_tokens, options)
293
+ end
294
+ end
295
+
296
+ definitions[:anchor].each do |handler|
297
+ if handler.match(tokens, definitions)
298
+ good_tokens = tokens.select { |o| !o.get_tag Separator }
299
+ return handler.invoke(:anchor, good_tokens, options)
300
+ end
301
+ end
302
+
303
+ definitions[:arrow].each do |handler|
304
+ if handler.match(tokens, definitions)
305
+ good_tokens = tokens.reject { |o| o.get_tag(SeparatorAt) || o.get_tag(SeparatorSlashOrDash) || o.get_tag(SeparatorComma) }
306
+ return handler.invoke(:arrow, good_tokens, options)
307
+ end
308
+ end
309
+
310
+ definitions[:narrow].each do |handler|
311
+ if handler.match(tokens, definitions)
312
+ return handler.invoke(:narrow, tokens, options)
313
+ end
314
+ end
315
+
316
+ puts "-none" if Chronic.debug
317
+ return nil
318
+ end
319
+
320
+ end
321
+
322
+ # Internal exception
323
+ class ChronicPain < Exception
324
+ end
325
+ end
@@ -0,0 +1,31 @@
1
+ module Chronic
2
+ class Grabber < Tag
3
+
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
13
+ end
14
+ end
15
+
16
+ # @param [Token] token
17
+ # @return [Grabber, nil]
18
+ def self.scan_for_all(token)
19
+ scan_for token, self,
20
+ {
21
+ /last/ => :last,
22
+ /this/ => :this,
23
+ /next/ => :next
24
+ }
25
+ end
26
+
27
+ def to_s
28
+ 'grabber-' << @type.to_s
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,90 @@
1
+ module Chronic
2
+ class Handler
3
+
4
+ # @return [Array] A list of patterns
5
+ attr_reader :pattern
6
+
7
+ # @return [Symbol] The method which handles this list of patterns.
8
+ # This method should exist inside the {Handlers} module
9
+ attr_reader :handler_method
10
+
11
+ # @param [Array] pattern A list of patterns to match tokens against
12
+ # @param [Symbol] handler_method The method to be invoked when patterns
13
+ # are matched. This method should exist inside the {Handlers} module
14
+ def initialize(pattern, handler_method)
15
+ @pattern = pattern
16
+ @handler_method = handler_method
17
+ end
18
+
19
+ # @param [Array] tokens
20
+ # @param [Hash] definitions
21
+ # @return [Boolean]
22
+ # @see Chronic.tokens_to_span
23
+ def match(tokens, definitions)
24
+ token_index = 0
25
+
26
+ @pattern.each do |element|
27
+ name = element.to_s
28
+ optional = name[-1, 1] == '?'
29
+ name = name.chop if optional
30
+
31
+ case element
32
+ when Symbol
33
+ if tags_match?(name, tokens, token_index)
34
+ token_index += 1
35
+ next
36
+ else
37
+ if optional
38
+ next
39
+ else
40
+ return false
41
+ end
42
+ end
43
+ when String
44
+ return true if optional && token_index == tokens.size
45
+
46
+ if definitions.key?(name.to_sym)
47
+ sub_handlers = definitions[name.to_sym]
48
+ else
49
+ raise ChronicPain, "Invalid subset #{name} specified"
50
+ end
51
+
52
+ sub_handlers.each do |sub_handler|
53
+ return true if sub_handler.match(tokens[token_index..tokens.size], definitions)
54
+ end
55
+ else
56
+ raise ChronicPain, "Invalid match type: #{element.class}"
57
+ end
58
+ end
59
+
60
+ return false if token_index != tokens.size
61
+ return true
62
+ end
63
+
64
+ def invoke(type, tokens, options)
65
+ if Chronic.debug
66
+ puts "-#{type}"
67
+ puts "Handler: #{@handler_method}"
68
+ end
69
+
70
+ Handlers.send(@handler_method, tokens, options)
71
+ end
72
+
73
+ # @param [Handler] The handler to compare
74
+ # @return [Boolean] True if these handlers match
75
+ def ==(other)
76
+ @pattern == other.pattern
77
+ end
78
+
79
+ private
80
+
81
+ def tags_match?(name, tokens, token_index)
82
+ klass = Chronic.const_get(name.to_s.gsub(/(?:^|_)(.)/) { $1.upcase })
83
+
84
+ if tokens[token_index]
85
+ !tokens[token_index].tags.select { |o| o.kind_of?(klass) }.empty?
86
+ end
87
+ end
88
+
89
+ end
90
+ end