chronic 0.2.2 → 0.3.0

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 (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/chronic.rb +59 -49
  8. data/lib/chronic/grabber.rb +2 -2
  9. data/lib/chronic/handlers.rb +167 -112
  10. data/lib/{numerizer → chronic/numerizer}/numerizer.rb +17 -23
  11. data/lib/chronic/ordinal.rb +6 -6
  12. data/lib/chronic/pointer.rb +3 -3
  13. data/lib/chronic/repeater.rb +26 -12
  14. data/lib/chronic/repeaters/repeater_day.rb +17 -12
  15. data/lib/chronic/repeaters/repeater_day_name.rb +17 -12
  16. data/lib/chronic/repeaters/repeater_day_portion.rb +15 -14
  17. data/lib/chronic/repeaters/repeater_fortnight.rb +14 -9
  18. data/lib/chronic/repeaters/repeater_hour.rb +15 -10
  19. data/lib/chronic/repeaters/repeater_minute.rb +15 -10
  20. data/lib/chronic/repeaters/repeater_month.rb +20 -15
  21. data/lib/chronic/repeaters/repeater_month_name.rb +21 -16
  22. data/lib/chronic/repeaters/repeater_season.rb +136 -9
  23. data/lib/chronic/repeaters/repeater_season_name.rb +38 -17
  24. data/lib/chronic/repeaters/repeater_second.rb +15 -10
  25. data/lib/chronic/repeaters/repeater_time.rb +53 -43
  26. data/lib/chronic/repeaters/repeater_week.rb +16 -11
  27. data/lib/chronic/repeaters/repeater_weekday.rb +77 -0
  28. data/lib/chronic/repeaters/repeater_weekend.rb +14 -9
  29. data/lib/chronic/repeaters/repeater_year.rb +19 -13
  30. data/lib/chronic/scalar.rb +16 -14
  31. data/lib/chronic/separator.rb +25 -10
  32. data/lib/chronic/time_zone.rb +4 -3
  33. data/lib/chronic.rb +21 -19
  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 +314 -196
  54. metadata +74 -49
  55. data/History.txt +0 -49
  56. data/README.txt +0 -149
  57. data/test/suite.rb +0 -9
@@ -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