chronic 0.2.3 → 0.4.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 (63) hide show
  1. data/HISTORY.md +103 -0
  2. data/LICENSE +21 -0
  3. data/Manifest.txt +16 -5
  4. data/README.md +171 -0
  5. data/Rakefile +136 -15
  6. data/benchmark/benchmark.rb +13 -0
  7. data/chronic.gemspec +82 -0
  8. data/lib/chronic/chronic.rb +65 -147
  9. data/lib/chronic/grabber.rb +13 -17
  10. data/lib/chronic/handlers.rb +216 -138
  11. data/lib/chronic/mini_date.rb +27 -0
  12. data/lib/chronic/numerizer.rb +120 -0
  13. data/lib/chronic/ordinal.rb +11 -16
  14. data/lib/chronic/pointer.rb +11 -13
  15. data/lib/chronic/repeater.rb +117 -106
  16. data/lib/chronic/repeaters/repeater_day.rb +50 -43
  17. data/lib/chronic/repeaters/repeater_day_name.rb +49 -42
  18. data/lib/chronic/repeaters/repeater_day_portion.rb +82 -80
  19. data/lib/chronic/repeaters/repeater_fortnight.rb +60 -53
  20. data/lib/chronic/repeaters/repeater_hour.rb +50 -43
  21. data/lib/chronic/repeaters/repeater_minute.rb +50 -43
  22. data/lib/chronic/repeaters/repeater_month.rb +63 -56
  23. data/lib/chronic/repeaters/repeater_month_name.rb +91 -82
  24. data/lib/chronic/repeaters/repeater_season.rb +125 -20
  25. data/lib/chronic/repeaters/repeater_season_name.rb +43 -22
  26. data/lib/chronic/repeaters/repeater_second.rb +40 -33
  27. data/lib/chronic/repeaters/repeater_time.rb +118 -106
  28. data/lib/chronic/repeaters/repeater_week.rb +64 -57
  29. data/lib/chronic/repeaters/repeater_weekday.rb +86 -0
  30. data/lib/chronic/repeaters/repeater_weekend.rb +58 -51
  31. data/lib/chronic/repeaters/repeater_year.rb +58 -50
  32. data/lib/chronic/scalar.rb +37 -27
  33. data/lib/chronic/separator.rb +34 -37
  34. data/lib/chronic/span.rb +31 -0
  35. data/lib/chronic/tag.rb +26 -0
  36. data/lib/chronic/time_zone.rb +9 -10
  37. data/lib/chronic/token.rb +35 -0
  38. data/lib/chronic.rb +34 -25
  39. data/test/helper.rb +6 -0
  40. data/test/test_Chronic.rb +22 -18
  41. data/test/test_DaylightSavings.rb +118 -0
  42. data/test/test_Handler.rb +37 -38
  43. data/test/test_Numerizer.rb +66 -42
  44. data/test/test_RepeaterDayName.rb +15 -16
  45. data/test/test_RepeaterFortnight.rb +16 -17
  46. data/test/test_RepeaterHour.rb +22 -19
  47. data/test/test_RepeaterMinute.rb +34 -0
  48. data/test/test_RepeaterMonth.rb +16 -17
  49. data/test/test_RepeaterMonthName.rb +17 -18
  50. data/test/test_RepeaterTime.rb +20 -22
  51. data/test/test_RepeaterWeek.rb +16 -17
  52. data/test/test_RepeaterWeekday.rb +55 -0
  53. data/test/test_RepeaterWeekend.rb +21 -22
  54. data/test/test_RepeaterYear.rb +17 -18
  55. data/test/test_Span.rb +5 -6
  56. data/test/test_Time.rb +11 -12
  57. data/test/test_Token.rb +5 -6
  58. data/test/test_parsing.rb +395 -208
  59. metadata +68 -52
  60. data/History.txt +0 -53
  61. data/README.txt +0 -149
  62. data/lib/numerizer/numerizer.rb +0 -103
  63. data/test/suite.rb +0 -9
@@ -1,8 +1,18 @@
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
-
13
+
4
14
  # 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
15
+ # can find a date or time, either a Time or Chronic::Span will be returned
6
16
  # (depending on the value of <tt>:guess</tt>). If no date or time can be found,
7
17
  # +nil+ will be returned.
8
18
  #
@@ -11,15 +21,15 @@ module Chronic
11
21
  # [<tt>:context</tt>]
12
22
  # <tt>:past</tt> or <tt>:future</tt> (defaults to <tt>:future</tt>)
13
23
  #
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
24
+ # If your string represents a birthday, you can set <tt>:context</tt> to <tt>:past</tt>
25
+ # and if an ambiguous string is given, it will assume it is in the
16
26
  # past. Specify <tt>:future</tt> or omit to set a future context.
17
27
  #
18
28
  # [<tt>:now</tt>]
19
29
  # Time (defaults to Time.now)
20
30
  #
21
31
  # By setting <tt>:now</tt> to a Time, all computations will be based off
22
- # of that time instead of Time.now
32
+ # of that time instead of Time.now. If set to nil, Chronic will use Time.now.
23
33
  #
24
34
  # [<tt>:guess</tt>]
25
35
  # +true+ or +false+ (defaults to +true+)
@@ -27,83 +37,70 @@ module Chronic
27
37
  # By default, the parser will guess a single point in time for the
28
38
  # given date or time. If you'd rather have the entire time span returned,
29
39
  # set <tt>:guess</tt> to +false+ and a Chronic::Span will be returned.
30
- #
40
+ #
31
41
  # [<tt>:ambiguous_time_range</tt>]
32
42
  # Integer or <tt>:none</tt> (defaults to <tt>6</tt> (6am-6pm))
33
43
  #
34
- # If an Integer is given, ambiguous times (like 5:00) will be
44
+ # If an Integer is given, ambiguous times (like 5:00) will be
35
45
  # assumed to be within the range of that time in the AM to that time
36
46
  # in the PM. For example, if you set it to <tt>7</tt>, then the parser will
37
47
  # look for the time between 7am and 7pm. In the case of 5:00, it would
38
48
  # 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
49
+ # will be made, and the first matching instance of that time will
40
50
  # be used.
51
+ #
52
+ # [<tt>:endian_precedence</tt>]
53
+ # Array (defaults to <tt>[:middle, :little]</tt>)
54
+ #
55
+ # By default, Chronic will parse "03/04/2011" as the fourth day
56
+ # of the third month. Alternatively you can tell Chronic to parse
57
+ # this as the third day of the fourth month by altering the
58
+ # <tt>:endian_precedence</tt> to <tt>[:little, :middle]</tt>.
41
59
  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
-
60
+ @text = text
61
+ options = DEFAULT_OPTIONS.merge specified_options
62
+
49
63
  # 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
64
+ (specified_options.keys-DEFAULT_OPTIONS.keys).each {|key| raise(InvalidArgumentException, "#{key} is not a valid option key.")}
65
+
53
66
  [: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 =)
67
+
68
+ options[:now] ||= Chronic.time_class.now
56
69
  @now = options[:now]
57
-
70
+
58
71
  # 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, TimeZone].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
-
72
+ text = pre_normalize(text)
73
+
74
+ # tokenize words
75
+ @tokens = tokenize(text, options)
76
+
76
77
  if Chronic.debug
77
78
  puts "+---------------------------------------------------"
78
79
  puts "| " + @tokens.to_s
79
80
  puts "+---------------------------------------------------"
80
81
  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
82
+
83
+ span = tokens_to_span(@tokens, options)
84
+
91
85
  if options[:guess]
92
- return self.guess(span)
86
+ guess span
93
87
  else
94
- return span
88
+ span
95
89
  end
96
90
  end
97
-
91
+
98
92
  # Clean up the specified input text by stripping unwanted characters,
99
93
  # converting idioms to their canonical form, converting number words
100
94
  # to numbers (three => 3), and converting ordinal words to numeric
101
95
  # ordinals (third => 3rd)
102
96
  def pre_normalize(text) #:nodoc:
103
97
  normalized_text = text.to_s.downcase
98
+ normalized_text.gsub!(/['"\.,]/, '')
99
+ normalized_text.gsub!(/\bsecond (of|day|month|hour|minute|second)\b/, '2nd \1')
104
100
  normalized_text = numericize_numbers(normalized_text)
105
- normalized_text.gsub!(/['"\.]/, '')
101
+ normalized_text.gsub!(/ \-(\d{4})\b/, ' tzminus\1')
106
102
  normalized_text.gsub!(/([\/\-\,\@])/) { ' ' + $1 + ' ' }
103
+ normalized_text.gsub!(/\b0(\d+:\d+\s*pm?\b)/, '\1')
107
104
  normalized_text.gsub!(/\btoday\b/, 'this day')
108
105
  normalized_text.gsub!(/\btomm?orr?ow\b/, 'next day')
109
106
  normalized_text.gsub!(/\byesterday\b/, 'last day')
@@ -117,27 +114,25 @@ module Chronic
117
114
  normalized_text.gsub!(/\b(?:in|during) the (morning)\b/, '\1')
118
115
  normalized_text.gsub!(/\b(?:in the|during the|at) (afternoon|evening|night)\b/, '\1')
119
116
  normalized_text.gsub!(/\btonight\b/, 'this night')
120
- normalized_text.gsub!(/(?=\w)([ap]m|oclock)\b/, ' \1')
117
+ normalized_text.gsub!(/\b\d+:?\d*[ap]\b/,'\0m')
118
+ normalized_text.gsub!(/(\d)([ap]m|oclock)\b/, '\1 \2')
121
119
  normalized_text.gsub!(/\b(hence|after|from)\b/, 'future')
122
- normalized_text = numericize_ordinals(normalized_text)
120
+ normalized_text
123
121
  end
124
-
122
+
125
123
  # Convert number words to numbers (three => 3)
126
124
  def numericize_numbers(text) #:nodoc:
127
125
  Numerizer.numerize(text)
128
126
  end
129
-
130
- # Convert ordinal words to numeric ordinals (third => 3rd)
131
- def numericize_ordinals(text) #:nodoc:
132
- text
133
- end
134
-
135
- # Split the text on spaces and convert each word into
136
- # a Token
137
- def base_tokenize(text) #:nodoc:
138
- text.split(' ').map { |word| Token.new(word) }
127
+
128
+ def tokenize(text, options) #:nodoc:
129
+ tokens = text.split(' ').map { |word| Token.new(word) }
130
+ [Repeater, Grabber, Pointer, Scalar, Ordinal, Separator, TimeZone].each do |tok|
131
+ tokens = tok.scan(tokens, options)
132
+ end
133
+ tokens.delete_if { |token| !token.tagged? }
139
134
  end
140
-
135
+
141
136
  # Guess a specific time within the given span
142
137
  def guess(span) #:nodoc:
143
138
  return nil if span.nil?
@@ -148,92 +143,15 @@ module Chronic
148
143
  end
149
144
  end
150
145
  end
151
-
152
- class Token #:nodoc:
153
- attr_accessor :word, :tags
154
-
155
- def initialize(word)
156
- @word = word
157
- @tags = []
158
- end
159
-
160
- # Tag this token with the specified tag
161
- def tag(new_tag)
162
- @tags << new_tag
163
- end
164
-
165
- # Remove all tags of the given class
166
- def untag(tag_class)
167
- @tags = @tags.select { |m| !m.kind_of? tag_class }
168
- end
169
-
170
- # Return true if this token has any tags
171
- def tagged?
172
- @tags.size > 0
173
- end
174
-
175
- # Return the Tag that matches the given class
176
- def get_tag(tag_class)
177
- matches = @tags.select { |m| m.kind_of? tag_class }
178
- #matches.size < 2 || raise("Multiple identical tags found")
179
- return matches.first
180
- end
181
-
182
- # Print this Token in a pretty way
183
- def to_s
184
- @word << '(' << @tags.join(', ') << ') '
185
- end
186
- end
187
-
188
- # A Span represents a range of time. Since this class extends
189
- # Range, you can use #begin and #end to get the beginning and
190
- # ending times of the span (they will be of class Time)
191
- class Span < Range
192
- # Returns the width of this span in seconds
193
- def width
194
- (self.end - self.begin).to_i
195
- end
196
-
197
- # Add a number of seconds to this span, returning the
198
- # resulting Span
199
- def +(seconds)
200
- Span.new(self.begin + seconds, self.end + seconds)
201
- end
202
-
203
- # Subtract a number of seconds to this span, returning the
204
- # resulting Span
205
- def -(seconds)
206
- self + -seconds
207
- end
208
-
209
- # Prints this span in a nice fashion
210
- def to_s
211
- '(' << self.begin.to_s << '..' << self.end.to_s << ')'
212
- end
213
- end
214
146
 
215
- # Tokens are tagged with subclassed instances of this class when
216
- # they match specific criteria
217
- class Tag #:nodoc:
218
- attr_accessor :type
219
-
220
- def initialize(type)
221
- @type = type
222
- end
223
-
224
- def start=(s)
225
- @now = s
226
- end
227
- end
228
-
229
147
  # Internal exception
230
148
  class ChronicPain < Exception #:nodoc:
231
-
149
+
232
150
  end
233
-
151
+
234
152
  # This exception is raised if an invalid argument is provided to
235
153
  # any of Chronic's methods
236
154
  class InvalidArgumentException < Exception
237
-
155
+
238
156
  end
239
- end
157
+ end
@@ -1,26 +1,22 @@
1
- #module Chronic
2
-
3
- class Chronic::Grabber < Chronic::Tag #:nodoc:
4
- def self.scan(tokens)
1
+ module Chronic
2
+ class Grabber < Tag #:nodoc:
3
+ def self.scan(tokens, options)
5
4
  tokens.each_index do |i|
6
- if t = self.scan_for_all(tokens[i]) then tokens[i].tag(t); next end
5
+ if t = scan_for_all(tokens[i]) then tokens[i].tag(t); next end
7
6
  end
8
- tokens
9
7
  end
10
-
8
+
11
9
  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
10
+ scan_for token, self,
11
+ {
12
+ /last/ => :last,
13
+ /this/ => :this,
14
+ /next/ => :next
15
+ }
19
16
  end
20
-
17
+
21
18
  def to_s
22
19
  'grabber-' << @type.to_s
23
20
  end
24
21
  end
25
-
26
- #end
22
+ end