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.
- data/HISTORY.md +76 -0
- data/LICENSE +21 -0
- data/README.md +165 -0
- data/Rakefile +145 -18
- data/benchmark/benchmark.rb +13 -0
- data/chronic.gemspec +85 -0
- data/lib/chronic/chronic.rb +59 -49
- data/lib/chronic/grabber.rb +2 -2
- data/lib/chronic/handlers.rb +167 -112
- data/lib/{numerizer → chronic/numerizer}/numerizer.rb +17 -23
- data/lib/chronic/ordinal.rb +6 -6
- data/lib/chronic/pointer.rb +3 -3
- data/lib/chronic/repeater.rb +26 -12
- data/lib/chronic/repeaters/repeater_day.rb +17 -12
- data/lib/chronic/repeaters/repeater_day_name.rb +17 -12
- data/lib/chronic/repeaters/repeater_day_portion.rb +15 -14
- data/lib/chronic/repeaters/repeater_fortnight.rb +14 -9
- data/lib/chronic/repeaters/repeater_hour.rb +15 -10
- data/lib/chronic/repeaters/repeater_minute.rb +15 -10
- data/lib/chronic/repeaters/repeater_month.rb +20 -15
- data/lib/chronic/repeaters/repeater_month_name.rb +21 -16
- data/lib/chronic/repeaters/repeater_season.rb +136 -9
- data/lib/chronic/repeaters/repeater_season_name.rb +38 -17
- data/lib/chronic/repeaters/repeater_second.rb +15 -10
- data/lib/chronic/repeaters/repeater_time.rb +53 -43
- data/lib/chronic/repeaters/repeater_week.rb +16 -11
- data/lib/chronic/repeaters/repeater_weekday.rb +77 -0
- data/lib/chronic/repeaters/repeater_weekend.rb +14 -9
- data/lib/chronic/repeaters/repeater_year.rb +19 -13
- data/lib/chronic/scalar.rb +16 -14
- data/lib/chronic/separator.rb +25 -10
- data/lib/chronic/time_zone.rb +4 -3
- data/lib/chronic.rb +21 -19
- data/test/helper.rb +7 -0
- data/test/test_Chronic.rb +17 -18
- data/test/test_DaylightSavings.rb +118 -0
- data/test/test_Handler.rb +37 -38
- data/test/test_Numerizer.rb +8 -5
- data/test/test_RepeaterDayName.rb +15 -16
- data/test/test_RepeaterFortnight.rb +16 -17
- data/test/test_RepeaterHour.rb +18 -19
- data/test/test_RepeaterMinute.rb +34 -0
- data/test/test_RepeaterMonth.rb +16 -17
- data/test/test_RepeaterMonthName.rb +17 -18
- data/test/test_RepeaterTime.rb +20 -22
- data/test/test_RepeaterWeek.rb +16 -17
- data/test/test_RepeaterWeekday.rb +55 -0
- data/test/test_RepeaterWeekend.rb +21 -22
- data/test/test_RepeaterYear.rb +17 -18
- data/test/test_Span.rb +5 -6
- data/test/test_Time.rb +11 -12
- data/test/test_Token.rb +5 -6
- data/test/test_parsing.rb +314 -196
- metadata +74 -49
- data/History.txt +0 -49
- data/README.txt +0 -149
- data/test/suite.rb +0 -9
data/lib/chronic/chronic.rb
CHANGED
|
@@ -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 =>
|
|
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!(
|
|
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
|
data/lib/chronic/grabber.rb
CHANGED