chronic 0.6.3 → 0.10.2

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 (67) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +4 -3
  3. data/.travis.yml +8 -0
  4. data/HISTORY.md +76 -0
  5. data/README.md +47 -42
  6. data/Rakefile +30 -12
  7. data/chronic.gemspec +10 -4
  8. data/lib/chronic/date.rb +82 -0
  9. data/lib/chronic/grabber.rb +9 -7
  10. data/lib/chronic/handler.rb +47 -40
  11. data/lib/chronic/handlers.rb +240 -28
  12. data/lib/chronic/numerizer.rb +11 -2
  13. data/lib/chronic/ordinal.rb +28 -23
  14. data/lib/chronic/parser.rb +268 -0
  15. data/lib/chronic/pointer.rb +9 -7
  16. data/lib/chronic/repeater.rb +58 -48
  17. data/lib/chronic/repeaters/repeater_day.rb +4 -3
  18. data/lib/chronic/repeaters/repeater_day_name.rb +5 -4
  19. data/lib/chronic/repeaters/repeater_day_portion.rb +33 -18
  20. data/lib/chronic/repeaters/repeater_fortnight.rb +4 -3
  21. data/lib/chronic/repeaters/repeater_hour.rb +4 -3
  22. data/lib/chronic/repeaters/repeater_minute.rb +4 -3
  23. data/lib/chronic/repeaters/repeater_month.rb +5 -4
  24. data/lib/chronic/repeaters/repeater_month_name.rb +4 -3
  25. data/lib/chronic/repeaters/repeater_season.rb +5 -3
  26. data/lib/chronic/repeaters/repeater_second.rb +4 -3
  27. data/lib/chronic/repeaters/repeater_time.rb +35 -25
  28. data/lib/chronic/repeaters/repeater_week.rb +4 -3
  29. data/lib/chronic/repeaters/repeater_weekday.rb +4 -3
  30. data/lib/chronic/repeaters/repeater_weekend.rb +4 -3
  31. data/lib/chronic/repeaters/repeater_year.rb +6 -5
  32. data/lib/chronic/scalar.rb +40 -68
  33. data/lib/chronic/season.rb +1 -12
  34. data/lib/chronic/separator.rb +142 -23
  35. data/lib/chronic/sign.rb +49 -0
  36. data/lib/chronic/span.rb +2 -2
  37. data/lib/chronic/tag.rb +10 -15
  38. data/lib/chronic/time.rb +40 -0
  39. data/lib/chronic/time_zone.rb +9 -7
  40. data/lib/chronic/token.rb +16 -10
  41. data/lib/chronic.rb +109 -70
  42. data/test/helper.rb +8 -2
  43. data/test/{test_Chronic.rb → test_chronic.rb} +69 -34
  44. data/test/{test_DaylightSavings.rb → test_daylight_savings.rb} +1 -1
  45. data/test/{test_Handler.rb → test_handler.rb} +38 -14
  46. data/test/{test_MiniDate.rb → test_mini_date.rb} +9 -9
  47. data/test/{test_Numerizer.rb → test_numerizer.rb} +16 -2
  48. data/test/test_parsing.rb +392 -22
  49. data/test/{test_RepeaterDayName.rb → test_repeater_day_name.rb} +1 -1
  50. data/test/test_repeater_day_portion.rb +254 -0
  51. data/test/{test_RepeaterFortnight.rb → test_repeater_fortnight.rb} +1 -1
  52. data/test/{test_RepeaterHour.rb → test_repeater_hour.rb} +1 -1
  53. data/test/{test_RepeaterMinute.rb → test_repeater_minute.rb} +1 -1
  54. data/test/{test_RepeaterMonth.rb → test_repeater_month.rb} +1 -1
  55. data/test/{test_RepeaterMonthName.rb → test_repeater_month_name.rb} +1 -1
  56. data/test/{test_RepeaterSeason.rb → test_repeater_season.rb} +1 -1
  57. data/test/{test_RepeaterTime.rb → test_repeater_time.rb} +19 -1
  58. data/test/{test_RepeaterWeek.rb → test_repeater_week.rb} +1 -1
  59. data/test/{test_RepeaterWeekday.rb → test_repeater_weekday.rb} +1 -1
  60. data/test/{test_RepeaterWeekend.rb → test_repeater_weekend.rb} +1 -1
  61. data/test/{test_RepeaterYear.rb → test_repeater_year.rb} +1 -1
  62. data/test/{test_Span.rb → test_span.rb} +2 -2
  63. data/test/{test_Token.rb → test_token.rb} +1 -1
  64. metadata +129 -87
  65. data/.gemtest +0 -0
  66. data/.yardopts +0 -3
  67. data/lib/chronic/chronic.rb +0 -323
@@ -1,323 +0,0 @@
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:00')
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([:repeater_time, :repeater_day_portion?, :separator_on?, :repeater_month_name, :ordinal_day], :handle_rmn_od_on),
183
- Handler.new([:repeater_month_name, :scalar_year], :handle_rmn_sy),
184
- Handler.new([:scalar_day, :repeater_month_name, :scalar_year, :separator_at?, 'time?'], :handle_sd_rmn_sy),
185
- Handler.new([:scalar_year, :separator_slash_or_dash, :scalar_month, :separator_slash_or_dash, :scalar_day, :separator_at?, 'time?'], :handle_sy_sm_sd),
186
- Handler.new([:scalar_month, :separator_slash_or_dash, :scalar_year], :handle_sm_sy)
187
- ],
188
-
189
- # tonight at 7pm
190
- :anchor => [
191
- Handler.new([:grabber?, :repeater, :separator_at?, :repeater?, :repeater?], :handle_r),
192
- Handler.new([:grabber?, :repeater, :repeater, :separator_at?, :repeater?, :repeater?], :handle_r),
193
- Handler.new([:repeater, :grabber, :repeater], :handle_r_g_r)
194
- ],
195
-
196
- # 3 weeks from now, in 2 months
197
- :arrow => [
198
- Handler.new([:scalar, :repeater, :pointer], :handle_s_r_p),
199
- Handler.new([:pointer, :scalar, :repeater], :handle_p_s_r),
200
- Handler.new([:scalar, :repeater, :pointer, 'anchor'], :handle_s_r_p_a)
201
- ],
202
-
203
- # 3rd week in march
204
- :narrow => [
205
- Handler.new([:ordinal, :repeater, :separator_in, :repeater], :handle_o_r_s_r),
206
- Handler.new([:ordinal, :repeater, :grabber, :repeater], :handle_o_r_g_r)
207
- ]
208
- }
209
-
210
- endians = [
211
- Handler.new([:scalar_month, :separator_slash_or_dash, :scalar_day, :separator_slash_or_dash, :scalar_year, :separator_at?, 'time?'], :handle_sm_sd_sy),
212
- Handler.new([:scalar_day, :separator_slash_or_dash, :scalar_month, :separator_slash_or_dash, :scalar_year, :separator_at?, 'time?'], :handle_sd_sm_sy)
213
- ]
214
-
215
- case endian = Array(options[:endian_precedence]).first
216
- when :little
217
- @definitions[:endian] = endians.reverse
218
- when :middle
219
- @definitions[:endian] = endians
220
- else
221
- raise ArgumentError, "Unknown endian option '#{endian}'"
222
- end
223
-
224
- @definitions
225
- end
226
-
227
- # Construct a time Object
228
- #
229
- # @return [Time]
230
- def construct(year, month = 1, day = 1, hour = 0, minute = 0, second = 0)
231
- if second >= 60
232
- minute += second / 60
233
- second = second % 60
234
- end
235
-
236
- if minute >= 60
237
- hour += minute / 60
238
- minute = minute % 60
239
- end
240
-
241
- if hour >= 24
242
- day += hour / 24
243
- hour = hour % 24
244
- end
245
-
246
- # determine if there is a day overflow. this is complicated by our crappy calendar
247
- # system (non-constant number of days per month)
248
- day <= 56 || raise("day must be no more than 56 (makes month resolution easier)")
249
- if day > 28
250
- # no month ever has fewer than 28 days, so only do this if necessary
251
- leap_year_month_days = [31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
252
- common_year_month_days = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
253
- days_this_month = Date.leap?(year) ? leap_year_month_days[month - 1] : common_year_month_days[month - 1]
254
- if day > days_this_month
255
- month += day / days_this_month
256
- day = day % days_this_month
257
- end
258
- end
259
-
260
- if month > 12
261
- if month % 12 == 0
262
- year += (month - 12) / 12
263
- month = 12
264
- else
265
- year += month / 12
266
- month = month % 12
267
- end
268
- end
269
-
270
- Chronic.time_class.local(year, month, day, hour, minute, second)
271
- end
272
-
273
- private
274
-
275
- def tokenize(text, options)
276
- text = pre_normalize(text)
277
- tokens = text.split(' ').map { |word| Token.new(word) }
278
- [Repeater, Grabber, Pointer, Scalar, Ordinal, Separator, TimeZone].each do |tok|
279
- tok.scan(tokens, options)
280
- end
281
- tokens.select { |token| token.tagged? }
282
- end
283
-
284
- def tokens_to_span(tokens, options)
285
- definitions = definitions(options)
286
-
287
- (definitions[:endian] + definitions[:date]).each do |handler|
288
- if handler.match(tokens, definitions)
289
- good_tokens = tokens.select { |o| !o.get_tag Separator }
290
- return handler.invoke(:date, good_tokens, options)
291
- end
292
- end
293
-
294
- definitions[:anchor].each do |handler|
295
- if handler.match(tokens, definitions)
296
- good_tokens = tokens.select { |o| !o.get_tag Separator }
297
- return handler.invoke(:anchor, good_tokens, options)
298
- end
299
- end
300
-
301
- definitions[:arrow].each do |handler|
302
- if handler.match(tokens, definitions)
303
- good_tokens = tokens.reject { |o| o.get_tag(SeparatorAt) || o.get_tag(SeparatorSlashOrDash) || o.get_tag(SeparatorComma) }
304
- return handler.invoke(:arrow, good_tokens, options)
305
- end
306
- end
307
-
308
- definitions[:narrow].each do |handler|
309
- if handler.match(tokens, definitions)
310
- return handler.invoke(:narrow, tokens, options)
311
- end
312
- end
313
-
314
- puts "-none" if Chronic.debug
315
- return nil
316
- end
317
-
318
- end
319
-
320
- # Internal exception
321
- class ChronicPain < Exception
322
- end
323
- end