chronic-mmlac 0.6.4.2 → 0.10.2.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (67) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +4 -3
  3. data/.travis.yml +8 -0
  4. data/HISTORY.md +69 -0
  5. data/README.md +47 -42
  6. data/Rakefile +28 -8
  7. data/chronic.gemspec +9 -3
  8. data/lib/chronic.rb +113 -74
  9. data/lib/chronic/date.rb +82 -0
  10. data/lib/chronic/grabber.rb +9 -7
  11. data/lib/chronic/handler.rb +47 -40
  12. data/lib/chronic/handlers.rb +210 -28
  13. data/lib/chronic/numerizer.rb +11 -2
  14. data/lib/chronic/ordinal.rb +28 -23
  15. data/lib/chronic/parser.rb +268 -0
  16. data/lib/chronic/pointer.rb +9 -7
  17. data/lib/chronic/repeater.rb +58 -48
  18. data/lib/chronic/repeaters/repeater_day.rb +4 -3
  19. data/lib/chronic/repeaters/repeater_day_name.rb +5 -4
  20. data/lib/chronic/repeaters/repeater_day_portion.rb +29 -14
  21. data/lib/chronic/repeaters/repeater_fortnight.rb +4 -3
  22. data/lib/chronic/repeaters/repeater_hour.rb +4 -3
  23. data/lib/chronic/repeaters/repeater_minute.rb +4 -3
  24. data/lib/chronic/repeaters/repeater_month.rb +5 -4
  25. data/lib/chronic/repeaters/repeater_month_name.rb +4 -3
  26. data/lib/chronic/repeaters/repeater_season.rb +5 -3
  27. data/lib/chronic/repeaters/repeater_second.rb +4 -3
  28. data/lib/chronic/repeaters/repeater_time.rb +35 -25
  29. data/lib/chronic/repeaters/repeater_week.rb +4 -3
  30. data/lib/chronic/repeaters/repeater_weekday.rb +4 -3
  31. data/lib/chronic/repeaters/repeater_weekend.rb +4 -3
  32. data/lib/chronic/repeaters/repeater_year.rb +5 -4
  33. data/lib/chronic/scalar.rb +40 -68
  34. data/lib/chronic/season.rb +1 -12
  35. data/lib/chronic/separator.rb +142 -23
  36. data/lib/chronic/sign.rb +49 -0
  37. data/lib/chronic/span.rb +2 -2
  38. data/lib/chronic/tag.rb +10 -15
  39. data/lib/chronic/time.rb +40 -0
  40. data/lib/chronic/time_zone.rb +9 -7
  41. data/lib/chronic/token.rb +16 -10
  42. data/test/helper.rb +7 -1
  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 +367 -18
  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 +107 -46
  65. data/.gemtest +0 -0
  66. data/.yardopts +0 -3
  67. data/lib/chronic/chronic.rb +0 -325
data/.gemtest DELETED
File without changes
data/.yardopts DELETED
@@ -1,3 +0,0 @@
1
- -m markdown
2
- --readme README.md
3
- --title "Chronic - Natural language date/time parsing"
@@ -1,325 +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: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