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.
- data/HISTORY.md +103 -0
- data/LICENSE +21 -0
- data/Manifest.txt +16 -5
- data/README.md +171 -0
- data/Rakefile +136 -15
- data/benchmark/benchmark.rb +13 -0
- data/chronic.gemspec +82 -0
- data/lib/chronic/chronic.rb +65 -147
- data/lib/chronic/grabber.rb +13 -17
- data/lib/chronic/handlers.rb +216 -138
- data/lib/chronic/mini_date.rb +27 -0
- data/lib/chronic/numerizer.rb +120 -0
- data/lib/chronic/ordinal.rb +11 -16
- data/lib/chronic/pointer.rb +11 -13
- data/lib/chronic/repeater.rb +117 -106
- data/lib/chronic/repeaters/repeater_day.rb +50 -43
- data/lib/chronic/repeaters/repeater_day_name.rb +49 -42
- data/lib/chronic/repeaters/repeater_day_portion.rb +82 -80
- data/lib/chronic/repeaters/repeater_fortnight.rb +60 -53
- data/lib/chronic/repeaters/repeater_hour.rb +50 -43
- data/lib/chronic/repeaters/repeater_minute.rb +50 -43
- data/lib/chronic/repeaters/repeater_month.rb +63 -56
- data/lib/chronic/repeaters/repeater_month_name.rb +91 -82
- data/lib/chronic/repeaters/repeater_season.rb +125 -20
- data/lib/chronic/repeaters/repeater_season_name.rb +43 -22
- data/lib/chronic/repeaters/repeater_second.rb +40 -33
- data/lib/chronic/repeaters/repeater_time.rb +118 -106
- data/lib/chronic/repeaters/repeater_week.rb +64 -57
- data/lib/chronic/repeaters/repeater_weekday.rb +86 -0
- data/lib/chronic/repeaters/repeater_weekend.rb +58 -51
- data/lib/chronic/repeaters/repeater_year.rb +58 -50
- data/lib/chronic/scalar.rb +37 -27
- data/lib/chronic/separator.rb +34 -37
- data/lib/chronic/span.rb +31 -0
- data/lib/chronic/tag.rb +26 -0
- data/lib/chronic/time_zone.rb +9 -10
- data/lib/chronic/token.rb +35 -0
- data/lib/chronic.rb +34 -25
- data/test/helper.rb +6 -0
- data/test/test_Chronic.rb +22 -18
- data/test/test_DaylightSavings.rb +118 -0
- data/test/test_Handler.rb +37 -38
- data/test/test_Numerizer.rb +66 -42
- data/test/test_RepeaterDayName.rb +15 -16
- data/test/test_RepeaterFortnight.rb +16 -17
- data/test/test_RepeaterHour.rb +22 -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 +395 -208
- metadata +68 -52
- data/History.txt +0 -53
- data/README.txt +0 -149
- data/lib/numerizer/numerizer.rb +0 -103
- data/test/suite.rb +0 -9
data/lib/chronic/chronic.rb
CHANGED
|
@@ -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
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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
|
|
51
|
-
|
|
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
|
-
|
|
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 =
|
|
60
|
-
|
|
61
|
-
#
|
|
62
|
-
@tokens =
|
|
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
|
-
|
|
83
|
-
|
|
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
|
-
|
|
86
|
+
guess span
|
|
93
87
|
else
|
|
94
|
-
|
|
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!(
|
|
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
|
|
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
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
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
|
data/lib/chronic/grabber.rb
CHANGED
|
@@ -1,26 +1,22 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
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 =
|
|
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
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
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
|