chronic 0.2.3 → 0.3.0
Sign up to get free protection for your applications and to get access to all the features.
- 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.rb +21 -19
- 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 +13 -12
- 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 +49 -42
- 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/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 +300 -204
- metadata +74 -52
- data/History.txt +0 -53
- data/README.txt +0 -149
- data/test/suite.rb +0 -9
data/lib/chronic.rb
CHANGED
@@ -9,6 +9,8 @@
|
|
9
9
|
|
10
10
|
$:.unshift File.dirname(__FILE__) # For use/testing when no gem is installed
|
11
11
|
|
12
|
+
require 'time'
|
13
|
+
|
12
14
|
require 'chronic/chronic'
|
13
15
|
require 'chronic/handlers'
|
14
16
|
|
@@ -21,6 +23,7 @@ require 'chronic/repeaters/repeater_month_name'
|
|
21
23
|
require 'chronic/repeaters/repeater_fortnight'
|
22
24
|
require 'chronic/repeaters/repeater_week'
|
23
25
|
require 'chronic/repeaters/repeater_weekend'
|
26
|
+
require 'chronic/repeaters/repeater_weekday'
|
24
27
|
require 'chronic/repeaters/repeater_day'
|
25
28
|
require 'chronic/repeaters/repeater_day_name'
|
26
29
|
require 'chronic/repeaters/repeater_day_portion'
|
@@ -36,19 +39,18 @@ require 'chronic/ordinal'
|
|
36
39
|
require 'chronic/separator'
|
37
40
|
require 'chronic/time_zone'
|
38
41
|
|
39
|
-
require 'numerizer/numerizer'
|
42
|
+
require 'chronic/numerizer/numerizer'
|
40
43
|
|
41
44
|
module Chronic
|
42
|
-
VERSION = "0.
|
43
|
-
|
44
|
-
def self.debug; false; end
|
45
|
-
end
|
45
|
+
VERSION = "0.3.0"
|
46
46
|
|
47
|
-
|
47
|
+
class << self
|
48
|
+
attr_accessor :debug
|
49
|
+
attr_accessor :time_class
|
50
|
+
end
|
48
51
|
|
49
|
-
|
50
|
-
|
51
|
-
puts
|
52
|
+
self.debug = false
|
53
|
+
self.time_class = Time
|
52
54
|
end
|
53
55
|
|
54
56
|
# class Time
|
@@ -56,8 +58,8 @@ end
|
|
56
58
|
# # extra_seconds = second > 60 ? second - 60 : 0
|
57
59
|
# # extra_minutes = minute > 59 ? minute - 59 : 0
|
58
60
|
# # extra_hours = hour > 23 ? hour - 23 : 0
|
59
|
-
# # extra_days = day >
|
60
|
-
#
|
61
|
+
# # extra_days = day >
|
62
|
+
#
|
61
63
|
# if month > 12
|
62
64
|
# if month % 12 == 0
|
63
65
|
# year += (month - 12) / 12
|
@@ -67,7 +69,7 @@ end
|
|
67
69
|
# month = month % 12
|
68
70
|
# end
|
69
71
|
# end
|
70
|
-
#
|
72
|
+
#
|
71
73
|
# base = Time.local(year, month)
|
72
74
|
# puts base
|
73
75
|
# offset = ((day - 1) * 24 * 60 * 60) + (hour * 60 * 60) + (minute * 60) + second
|
@@ -84,17 +86,17 @@ class Time
|
|
84
86
|
minute += second / 60
|
85
87
|
second = second % 60
|
86
88
|
end
|
87
|
-
|
89
|
+
|
88
90
|
if minute >= 60
|
89
91
|
hour += minute / 60
|
90
92
|
minute = minute % 60
|
91
93
|
end
|
92
|
-
|
94
|
+
|
93
95
|
if hour >= 24
|
94
96
|
day += hour / 24
|
95
97
|
hour = hour % 24
|
96
98
|
end
|
97
|
-
|
99
|
+
|
98
100
|
# determine if there is a day overflow. this is complicated by our crappy calendar
|
99
101
|
# system (non-constant number of days per month)
|
100
102
|
day <= 56 || raise("day must be no more than 56 (makes month resolution easier)")
|
@@ -109,7 +111,7 @@ class Time
|
|
109
111
|
day = day % days_this_month
|
110
112
|
end
|
111
113
|
end
|
112
|
-
|
114
|
+
|
113
115
|
if month > 12
|
114
116
|
if month % 12 == 0
|
115
117
|
year += (month - 12) / 12
|
@@ -119,7 +121,7 @@ class Time
|
|
119
121
|
month = month % 12
|
120
122
|
end
|
121
123
|
end
|
122
|
-
|
123
|
-
|
124
|
+
|
125
|
+
Chronic.time_class.local(year, month, day, hour, minute, second)
|
124
126
|
end
|
125
|
-
end
|
127
|
+
end
|
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
data/lib/chronic/handlers.rb
CHANGED
@@ -1,90 +1,133 @@
|
|
1
1
|
module Chronic
|
2
2
|
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
3
|
+
class << self
|
4
|
+
|
5
|
+
def definitions(options={}) #:nodoc:
|
6
|
+
options[:endian_precedence] = [:middle, :little] if options[:endian_precedence].nil?
|
7
|
+
|
8
|
+
# ensure the endian precedence is exactly two elements long
|
9
|
+
raise ChronicPain, "More than two elements specified for endian precedence array" unless options[:endian_precedence].length == 2
|
10
|
+
|
11
|
+
# handler for dd/mm/yyyy
|
12
|
+
@little_endian_handler ||= Handler.new([:scalar_day, :separator_slash_or_dash, :scalar_month, :separator_slash_or_dash, :scalar_year, :separator_at?, 'time?'], :handle_sd_sm_sy)
|
13
|
+
|
14
|
+
# handler for mm/dd/yyyy
|
15
|
+
@middle_endian_handler ||= Handler.new([:scalar_month, :separator_slash_or_dash, :scalar_day, :separator_slash_or_dash, :scalar_year, :separator_at?, 'time?'], :handle_sm_sd_sy)
|
16
|
+
|
17
|
+
# ensure we have valid endian values
|
18
|
+
options[:endian_precedence].each do |e|
|
19
|
+
raise ChronicPain, "Unknown endian type: #{e.to_s}" unless instance_variable_defined?(endian_variable_name_for(e))
|
20
|
+
end
|
21
|
+
|
22
|
+
@definitions ||=
|
7
23
|
{:time => [Handler.new([:repeater_time, :repeater_day_portion?], nil)],
|
8
|
-
|
9
|
-
:date => [Handler.new([:repeater_day_name, :repeater_month_name, :scalar_day, :repeater_time, :time_zone, :scalar_year], :handle_rdn_rmn_sd_t_tz_sy),
|
24
|
+
|
25
|
+
:date => [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),
|
10
26
|
Handler.new([:repeater_month_name, :scalar_day, :scalar_year], :handle_rmn_sd_sy),
|
11
27
|
Handler.new([:repeater_month_name, :scalar_day, :scalar_year, :separator_at?, 'time?'], :handle_rmn_sd_sy),
|
12
28
|
Handler.new([:repeater_month_name, :scalar_day, :separator_at?, 'time?'], :handle_rmn_sd),
|
29
|
+
Handler.new([:repeater_time, :repeater_day_portion?, :separator_on?, :repeater_month_name, :scalar_day], :handle_rmn_sd_on),
|
13
30
|
Handler.new([:repeater_month_name, :ordinal_day, :separator_at?, 'time?'], :handle_rmn_od),
|
31
|
+
Handler.new([:repeater_time, :repeater_day_portion?, :separator_on?, :repeater_month_name, :ordinal_day], :handle_rmn_od_on),
|
14
32
|
Handler.new([:repeater_month_name, :scalar_year], :handle_rmn_sy),
|
15
33
|
Handler.new([:scalar_day, :repeater_month_name, :scalar_year, :separator_at?, 'time?'], :handle_sd_rmn_sy),
|
16
|
-
|
17
|
-
|
34
|
+
@middle_endian_handler,
|
35
|
+
@little_endian_handler,
|
18
36
|
Handler.new([:scalar_year, :separator_slash_or_dash, :scalar_month, :separator_slash_or_dash, :scalar_day, :separator_at?, 'time?'], :handle_sy_sm_sd),
|
19
37
|
Handler.new([:scalar_month, :separator_slash_or_dash, :scalar_year], :handle_sm_sy)],
|
20
|
-
|
38
|
+
|
21
39
|
# tonight at 7pm
|
22
40
|
:anchor => [Handler.new([:grabber?, :repeater, :separator_at?, :repeater?, :repeater?], :handle_r),
|
23
41
|
Handler.new([:grabber?, :repeater, :repeater, :separator_at?, :repeater?, :repeater?], :handle_r),
|
24
42
|
Handler.new([:repeater, :grabber, :repeater], :handle_r_g_r)],
|
25
|
-
|
43
|
+
|
26
44
|
# 3 weeks from now, in 2 months
|
27
45
|
:arrow => [Handler.new([:scalar, :repeater, :pointer], :handle_s_r_p),
|
28
46
|
Handler.new([:pointer, :scalar, :repeater], :handle_p_s_r),
|
29
47
|
Handler.new([:scalar, :repeater, :pointer, 'anchor'], :handle_s_r_p_a)],
|
30
|
-
|
48
|
+
|
31
49
|
# 3rd week in march
|
32
50
|
:narrow => [Handler.new([:ordinal, :repeater, :separator_in, :repeater], :handle_o_r_s_r),
|
33
51
|
Handler.new([:ordinal, :repeater, :grabber, :repeater], :handle_o_r_g_r)]
|
34
52
|
}
|
53
|
+
|
54
|
+
apply_endian_precedences(options[:endian_precedence])
|
55
|
+
|
56
|
+
@definitions
|
35
57
|
end
|
36
|
-
|
37
|
-
def tokens_to_span(tokens, options) #:nodoc:
|
58
|
+
|
59
|
+
def tokens_to_span(tokens, options) #:nodoc:
|
38
60
|
# maybe it's a specific date
|
39
|
-
|
40
|
-
self.definitions
|
41
|
-
|
61
|
+
|
62
|
+
definitions = self.definitions(options)
|
63
|
+
definitions[:date].each do |handler|
|
64
|
+
if handler.match(tokens, definitions)
|
42
65
|
puts "-date" if Chronic.debug
|
43
66
|
good_tokens = tokens.select { |o| !o.get_tag Separator }
|
44
67
|
return self.send(handler.handler_method, good_tokens, options)
|
45
68
|
end
|
46
69
|
end
|
47
|
-
|
70
|
+
|
48
71
|
# I guess it's not a specific date, maybe it's just an anchor
|
49
|
-
|
50
|
-
|
51
|
-
if handler.match(tokens,
|
72
|
+
|
73
|
+
definitions[:anchor].each do |handler|
|
74
|
+
if handler.match(tokens, definitions)
|
52
75
|
puts "-anchor" if Chronic.debug
|
53
76
|
good_tokens = tokens.select { |o| !o.get_tag Separator }
|
54
77
|
return self.send(handler.handler_method, good_tokens, options)
|
55
78
|
end
|
56
79
|
end
|
57
|
-
|
80
|
+
|
58
81
|
# not an anchor, perhaps it's an arrow
|
59
|
-
|
60
|
-
|
61
|
-
if handler.match(tokens,
|
82
|
+
|
83
|
+
definitions[:arrow].each do |handler|
|
84
|
+
if handler.match(tokens, definitions)
|
62
85
|
puts "-arrow" if Chronic.debug
|
63
86
|
good_tokens = tokens.reject { |o| o.get_tag(SeparatorAt) || o.get_tag(SeparatorSlashOrDash) || o.get_tag(SeparatorComma) }
|
64
87
|
return self.send(handler.handler_method, good_tokens, options)
|
65
88
|
end
|
66
89
|
end
|
67
|
-
|
90
|
+
|
68
91
|
# not an arrow, let's hope it's a narrow
|
69
|
-
|
70
|
-
|
71
|
-
if handler.match(tokens,
|
92
|
+
|
93
|
+
definitions[:narrow].each do |handler|
|
94
|
+
if handler.match(tokens, definitions)
|
72
95
|
puts "-narrow" if Chronic.debug
|
73
96
|
#good_tokens = tokens.select { |o| !o.get_tag Separator }
|
74
97
|
return self.send(handler.handler_method, tokens, options)
|
75
98
|
end
|
76
99
|
end
|
77
|
-
|
100
|
+
|
78
101
|
# I guess you're out of luck!
|
79
102
|
puts "-none" if Chronic.debug
|
80
103
|
return nil
|
81
104
|
end
|
82
|
-
|
105
|
+
|
83
106
|
#--------------
|
84
|
-
|
107
|
+
|
108
|
+
def apply_endian_precedences(precedences)
|
109
|
+
date_defs = @definitions[:date]
|
110
|
+
|
111
|
+
# map the precedence array to indices on @definitions[:date]
|
112
|
+
indices = precedences.map { |e|
|
113
|
+
handler = instance_variable_get(endian_variable_name_for(e))
|
114
|
+
date_defs.index(handler)
|
115
|
+
}
|
116
|
+
|
117
|
+
# swap the handlers if we discover they are at odds with the desired preferences
|
118
|
+
swap(date_defs, indices.first, indices.last) if indices.first > indices.last
|
119
|
+
end
|
120
|
+
|
121
|
+
def endian_variable_name_for(e)
|
122
|
+
"@#{e.to_s}_endian_handler".to_sym
|
123
|
+
end
|
124
|
+
|
125
|
+
# exchange two elements in an array
|
126
|
+
def swap(arr, a, b); arr[a], arr[b] = arr[b], arr[a]; end
|
127
|
+
|
85
128
|
def day_or_time(day_start, time_tokens, options)
|
86
129
|
outer_span = Span.new(day_start, day_start + (24 * 60 * 60))
|
87
|
-
|
130
|
+
|
88
131
|
if !time_tokens.empty?
|
89
132
|
@now = outer_span.begin
|
90
133
|
time = get_anchor(dealias_and_disambiguate_times(time_tokens, options), options)
|
@@ -93,30 +136,46 @@ module Chronic
|
|
93
136
|
return outer_span
|
94
137
|
end
|
95
138
|
end
|
96
|
-
|
139
|
+
|
97
140
|
#--------------
|
98
|
-
|
141
|
+
|
99
142
|
def handle_m_d(month, day, time_tokens, options) #:nodoc:
|
100
143
|
month.start = @now
|
101
144
|
span = month.this(options[:context])
|
102
|
-
|
103
|
-
day_start =
|
104
|
-
|
145
|
+
|
146
|
+
day_start = Chronic.time_class.local(span.begin.year, span.begin.month, day)
|
147
|
+
|
105
148
|
day_or_time(day_start, time_tokens, options)
|
106
149
|
end
|
107
|
-
|
150
|
+
|
108
151
|
def handle_rmn_sd(tokens, options) #:nodoc:
|
109
152
|
handle_m_d(tokens[0].get_tag(RepeaterMonthName), tokens[1].get_tag(ScalarDay).type, tokens[2..tokens.size], options)
|
110
153
|
end
|
111
|
-
|
154
|
+
|
155
|
+
def handle_rmn_sd_on(tokens, options) #:nodoc:
|
156
|
+
if tokens.size > 3
|
157
|
+
handle_m_d(tokens[2].get_tag(RepeaterMonthName), tokens[3].get_tag(ScalarDay).type, tokens[0..1], options)
|
158
|
+
else
|
159
|
+
handle_m_d(tokens[1].get_tag(RepeaterMonthName), tokens[2].get_tag(ScalarDay).type, tokens[0..0], options)
|
160
|
+
end
|
161
|
+
end
|
162
|
+
|
112
163
|
def handle_rmn_od(tokens, options) #:nodoc:
|
113
164
|
handle_m_d(tokens[0].get_tag(RepeaterMonthName), tokens[1].get_tag(OrdinalDay).type, tokens[2..tokens.size], options)
|
114
165
|
end
|
115
|
-
|
166
|
+
|
167
|
+
def handle_rmn_od_on(tokens, options) #:nodoc:
|
168
|
+
if tokens.size > 3
|
169
|
+
handle_m_d(tokens[2].get_tag(RepeaterMonthName), tokens[3].get_tag(OrdinalDay).type, tokens[0..1], options)
|
170
|
+
else
|
171
|
+
handle_m_d(tokens[1].get_tag(RepeaterMonthName), tokens[2].get_tag(OrdinalDay).type, tokens[0..0], options)
|
172
|
+
end
|
173
|
+
end
|
174
|
+
|
116
175
|
def handle_rmn_sy(tokens, options) #:nodoc:
|
117
176
|
month = tokens[0].get_tag(RepeaterMonthName).index
|
118
177
|
year = tokens[1].get_tag(ScalarYear).type
|
119
|
-
|
178
|
+
|
120
179
|
if month == 12
|
121
180
|
next_month_year = year + 1
|
122
181
|
next_month_month = 1
|
@@ -124,79 +183,71 @@ module Chronic
|
|
124
183
|
next_month_year = year
|
125
184
|
next_month_month = month + 1
|
126
185
|
end
|
127
|
-
|
186
|
+
|
128
187
|
begin
|
129
|
-
Span.new(
|
188
|
+
Span.new(Chronic.time_class.local(year, month), Chronic.time_class.local(next_month_year, next_month_month))
|
130
189
|
rescue ArgumentError
|
131
190
|
nil
|
132
191
|
end
|
133
192
|
end
|
134
|
-
|
193
|
+
|
135
194
|
def handle_rdn_rmn_sd_t_tz_sy(tokens, options) #:nodoc:
|
136
|
-
|
137
|
-
|
138
|
-
year = tokens[5].get_tag(ScalarYear).type
|
139
|
-
|
140
|
-
begin
|
141
|
-
day_start = Time.local(year, month, day)
|
142
|
-
day_or_time(day_start, [tokens[3]], options)
|
143
|
-
rescue ArgumentError
|
144
|
-
nil
|
145
|
-
end
|
195
|
+
t = Chronic.time_class.parse(@text)
|
196
|
+
Span.new(t, t + 1)
|
146
197
|
end
|
147
|
-
|
198
|
+
|
148
199
|
def handle_rmn_sd_sy(tokens, options) #:nodoc:
|
149
200
|
month = tokens[0].get_tag(RepeaterMonthName).index
|
150
201
|
day = tokens[1].get_tag(ScalarDay).type
|
151
202
|
year = tokens[2].get_tag(ScalarYear).type
|
152
|
-
|
203
|
+
|
153
204
|
time_tokens = tokens.last(tokens.size - 3)
|
154
|
-
|
205
|
+
|
155
206
|
begin
|
156
|
-
day_start =
|
207
|
+
day_start = Chronic.time_class.local(year, month, day)
|
157
208
|
day_or_time(day_start, time_tokens, options)
|
158
209
|
rescue ArgumentError
|
159
210
|
nil
|
160
211
|
end
|
161
212
|
end
|
162
|
-
|
213
|
+
|
163
214
|
def handle_sd_rmn_sy(tokens, options) #:nodoc:
|
164
215
|
new_tokens = [tokens[1], tokens[0], tokens[2]]
|
165
216
|
time_tokens = tokens.last(tokens.size - 3)
|
166
217
|
self.handle_rmn_sd_sy(new_tokens + time_tokens, options)
|
167
218
|
end
|
168
|
-
|
219
|
+
|
169
220
|
def handle_sm_sd_sy(tokens, options) #:nodoc:
|
170
221
|
month = tokens[0].get_tag(ScalarMonth).type
|
171
222
|
day = tokens[1].get_tag(ScalarDay).type
|
172
223
|
year = tokens[2].get_tag(ScalarYear).type
|
173
|
-
|
224
|
+
|
174
225
|
time_tokens = tokens.last(tokens.size - 3)
|
175
|
-
|
226
|
+
|
176
227
|
begin
|
177
|
-
day_start =
|
228
|
+
day_start = Chronic.time_class.local(year, month, day) #:nodoc:
|
178
229
|
day_or_time(day_start, time_tokens, options)
|
179
230
|
rescue ArgumentError
|
180
231
|
nil
|
181
232
|
end
|
182
233
|
end
|
183
|
-
|
234
|
+
|
184
235
|
def handle_sd_sm_sy(tokens, options) #:nodoc:
|
185
236
|
new_tokens = [tokens[1], tokens[0], tokens[2]]
|
186
237
|
time_tokens = tokens.last(tokens.size - 3)
|
187
238
|
self.handle_sm_sd_sy(new_tokens + time_tokens, options)
|
188
239
|
end
|
189
|
-
|
240
|
+
|
190
241
|
def handle_sy_sm_sd(tokens, options) #:nodoc:
|
191
242
|
new_tokens = [tokens[1], tokens[2], tokens[0]]
|
192
243
|
time_tokens = tokens.last(tokens.size - 3)
|
193
244
|
self.handle_sm_sd_sy(new_tokens + time_tokens, options)
|
194
245
|
end
|
195
|
-
|
246
|
+
|
196
247
|
def handle_sm_sy(tokens, options) #:nodoc:
|
197
248
|
month = tokens[0].get_tag(ScalarMonth).type
|
198
249
|
year = tokens[1].get_tag(ScalarYear).type
|
199
|
-
|
250
|
+
|
200
251
|
if month == 12
|
201
252
|
next_month_year = year + 1
|
202
253
|
next_month_month = 1
|
@@ -204,40 +255,40 @@ module Chronic
|
|
204
255
|
next_month_year = year
|
205
256
|
next_month_month = month + 1
|
206
257
|
end
|
207
|
-
|
258
|
+
|
208
259
|
begin
|
209
|
-
Span.new(
|
260
|
+
Span.new(Chronic.time_class.local(year, month), Chronic.time_class.local(next_month_year, next_month_month))
|
210
261
|
rescue ArgumentError
|
211
262
|
nil
|
212
263
|
end
|
213
264
|
end
|
214
|
-
|
265
|
+
|
215
266
|
# anchors
|
216
|
-
|
267
|
+
|
217
268
|
def handle_r(tokens, options) #:nodoc:
|
218
269
|
dd_tokens = dealias_and_disambiguate_times(tokens, options)
|
219
270
|
self.get_anchor(dd_tokens, options)
|
220
271
|
end
|
221
|
-
|
272
|
+
|
222
273
|
def handle_r_g_r(tokens, options) #:nodoc:
|
223
274
|
new_tokens = [tokens[1], tokens[0], tokens[2]]
|
224
275
|
self.handle_r(new_tokens, options)
|
225
276
|
end
|
226
|
-
|
277
|
+
|
227
278
|
# arrows
|
228
|
-
|
279
|
+
|
229
280
|
def handle_srp(tokens, span, options) #:nodoc:
|
230
281
|
distance = tokens[0].get_tag(Scalar).type
|
231
282
|
repeater = tokens[1].get_tag(Repeater)
|
232
283
|
pointer = tokens[2].get_tag(Pointer).type
|
233
|
-
|
284
|
+
|
234
285
|
repeater.offset(span, distance, pointer)
|
235
286
|
end
|
236
|
-
|
287
|
+
|
237
288
|
def handle_s_r_p(tokens, options) #:nodoc:
|
238
289
|
repeater = tokens[1].get_tag(Repeater)
|
239
|
-
|
240
|
-
# span =
|
290
|
+
|
291
|
+
# span =
|
241
292
|
# case true
|
242
293
|
# when [RepeaterYear, RepeaterSeason, RepeaterSeasonName, RepeaterMonth, RepeaterMonthName, RepeaterFortnight, RepeaterWeek].include?(repeater.class)
|
243
294
|
# self.parse("this hour", :guess => false, :now => @now)
|
@@ -248,24 +299,24 @@ module Chronic
|
|
248
299
|
# else
|
249
300
|
# raise(ChronicPain, "Invalid repeater: #{repeater.class}")
|
250
301
|
# end
|
251
|
-
|
302
|
+
|
252
303
|
span = self.parse("this second", :guess => false, :now => @now)
|
253
|
-
|
304
|
+
|
254
305
|
self.handle_srp(tokens, span, options)
|
255
306
|
end
|
256
|
-
|
307
|
+
|
257
308
|
def handle_p_s_r(tokens, options) #:nodoc:
|
258
309
|
new_tokens = [tokens[1], tokens[2], tokens[0]]
|
259
310
|
self.handle_s_r_p(new_tokens, options)
|
260
311
|
end
|
261
|
-
|
312
|
+
|
262
313
|
def handle_s_r_p_a(tokens, options) #:nodoc:
|
263
314
|
anchor_span = get_anchor(tokens[3..tokens.size - 1], options)
|
264
315
|
self.handle_srp(tokens, anchor_span, options)
|
265
316
|
end
|
266
|
-
|
317
|
+
|
267
318
|
# narrows
|
268
|
-
|
319
|
+
|
269
320
|
def handle_orr(tokens, outer_span, options) #:nodoc:
|
270
321
|
repeater = tokens[1].get_tag(Repeater)
|
271
322
|
repeater.start = outer_span.begin - 1
|
@@ -280,34 +331,34 @@ module Chronic
|
|
280
331
|
end
|
281
332
|
span
|
282
333
|
end
|
283
|
-
|
334
|
+
|
284
335
|
def handle_o_r_s_r(tokens, options) #:nodoc:
|
285
336
|
outer_span = get_anchor([tokens[3]], options)
|
286
337
|
handle_orr(tokens[0..1], outer_span, options)
|
287
338
|
end
|
288
|
-
|
339
|
+
|
289
340
|
def handle_o_r_g_r(tokens, options) #:nodoc:
|
290
341
|
outer_span = get_anchor(tokens[2..3], options)
|
291
342
|
handle_orr(tokens[0..1], outer_span, options)
|
292
343
|
end
|
293
|
-
|
344
|
+
|
294
345
|
# support methods
|
295
|
-
|
346
|
+
|
296
347
|
def get_anchor(tokens, options) #:nodoc:
|
297
348
|
grabber = Grabber.new(:this)
|
298
349
|
pointer = :future
|
299
|
-
|
350
|
+
|
300
351
|
repeaters = self.get_repeaters(tokens)
|
301
352
|
repeaters.size.times { tokens.pop }
|
302
|
-
|
353
|
+
|
303
354
|
if tokens.first && tokens.first.get_tag(Grabber)
|
304
355
|
grabber = tokens.first.get_tag(Grabber)
|
305
356
|
tokens.pop
|
306
357
|
end
|
307
|
-
|
358
|
+
|
308
359
|
head = repeaters.shift
|
309
360
|
head.start = @now
|
310
|
-
|
361
|
+
|
311
362
|
case grabber.type
|
312
363
|
when :last
|
313
364
|
outer_span = head.next(:past)
|
@@ -321,11 +372,11 @@ module Chronic
|
|
321
372
|
outer_span = head.next(:future)
|
322
373
|
else raise(ChronicPain, "Invalid grabber")
|
323
374
|
end
|
324
|
-
|
375
|
+
|
325
376
|
puts "--#{outer_span}" if Chronic.debug
|
326
377
|
anchor = find_within(repeaters, outer_span, pointer)
|
327
378
|
end
|
328
|
-
|
379
|
+
|
329
380
|
def get_repeaters(tokens) #:nodoc:
|
330
381
|
repeaters = []
|
331
382
|
tokens.each do |token|
|
@@ -335,30 +386,30 @@ module Chronic
|
|
335
386
|
end
|
336
387
|
repeaters.sort.reverse
|
337
388
|
end
|
338
|
-
|
389
|
+
|
339
390
|
# Recursively finds repeaters within other repeaters.
|
340
391
|
# Returns a Span representing the innermost time span
|
341
392
|
# or nil if no repeater union could be found
|
342
393
|
def find_within(tags, span, pointer) #:nodoc:
|
343
394
|
puts "--#{span}" if Chronic.debug
|
344
395
|
return span if tags.empty?
|
345
|
-
|
396
|
+
|
346
397
|
head, *rest = tags
|
347
398
|
head.start = pointer == :future ? span.begin : span.end
|
348
399
|
h = head.this(:none)
|
349
|
-
|
400
|
+
|
350
401
|
if span.include?(h.begin) || span.include?(h.end)
|
351
402
|
return find_within(rest, h, pointer)
|
352
403
|
else
|
353
404
|
return nil
|
354
405
|
end
|
355
406
|
end
|
356
|
-
|
407
|
+
|
357
408
|
def dealias_and_disambiguate_times(tokens, options) #:nodoc:
|
358
409
|
# handle aliases of am/pm
|
359
410
|
# 5:00 in the morning -> 5:00 am
|
360
411
|
# 7:00 in the evening -> 7:00 pm
|
361
|
-
|
412
|
+
|
362
413
|
day_portion_index = nil
|
363
414
|
tokens.each_with_index do |t, i|
|
364
415
|
if t.get_tag(RepeaterDayPortion)
|
@@ -366,7 +417,7 @@ module Chronic
|
|
366
417
|
break
|
367
418
|
end
|
368
419
|
end
|
369
|
-
|
420
|
+
|
370
421
|
time_index = nil
|
371
422
|
tokens.each_with_index do |t, i|
|
372
423
|
if t.get_tag(RepeaterTime)
|
@@ -374,11 +425,11 @@ module Chronic
|
|
374
425
|
break
|
375
426
|
end
|
376
427
|
end
|
377
|
-
|
428
|
+
|
378
429
|
if (day_portion_index && time_index)
|
379
430
|
t1 = tokens[day_portion_index]
|
380
431
|
t1tag = t1.get_tag(RepeaterDayPortion)
|
381
|
-
|
432
|
+
|
382
433
|
if [:morning].include?(t1tag.type)
|
383
434
|
puts '--morning->am' if Chronic.debug
|
384
435
|
t1.untag(RepeaterDayPortion)
|
@@ -389,7 +440,7 @@ module Chronic
|
|
389
440
|
t1.tag(RepeaterDayPortion.new(:pm))
|
390
441
|
end
|
391
442
|
end
|
392
|
-
|
443
|
+
|
393
444
|
# tokens.each_with_index do |t0, i|
|
394
445
|
# t1 = tokens[i + 1]
|
395
446
|
# if t1 && (t1tag = t1.get_tag(RepeaterDayPortion)) && t0.get_tag(RepeaterTime)
|
@@ -404,7 +455,7 @@ module Chronic
|
|
404
455
|
# end
|
405
456
|
# end
|
406
457
|
# end
|
407
|
-
|
458
|
+
|
408
459
|
# handle ambiguous times if :ambiguous_time_range is specified
|
409
460
|
if options[:ambiguous_time_range] != :none
|
410
461
|
ttokens = []
|
@@ -419,25 +470,25 @@ module Chronic
|
|
419
470
|
end
|
420
471
|
tokens = ttokens
|
421
472
|
end
|
422
|
-
|
473
|
+
|
423
474
|
tokens
|
424
475
|
end
|
425
|
-
|
476
|
+
|
426
477
|
end
|
427
|
-
|
478
|
+
|
428
479
|
class Handler #:nodoc:
|
429
480
|
attr_accessor :pattern, :handler_method
|
430
|
-
|
481
|
+
|
431
482
|
def initialize(pattern, handler_method)
|
432
483
|
@pattern = pattern
|
433
484
|
@handler_method = handler_method
|
434
485
|
end
|
435
|
-
|
486
|
+
|
436
487
|
def constantize(name)
|
437
488
|
camel = name.to_s.gsub(/(^|_)(.)/) { $2.upcase }
|
438
489
|
::Chronic.module_eval(camel, __FILE__, __LINE__)
|
439
490
|
end
|
440
|
-
|
491
|
+
|
441
492
|
def match(tokens, definitions)
|
442
493
|
token_index = 0
|
443
494
|
@pattern.each do |element|
|
@@ -464,6 +515,10 @@ module Chronic
|
|
464
515
|
return false if token_index != tokens.size
|
465
516
|
return true
|
466
517
|
end
|
518
|
+
|
519
|
+
def ==(other)
|
520
|
+
self.pattern == other.pattern
|
521
|
+
end
|
467
522
|
end
|
468
|
-
|
469
|
-
end
|
523
|
+
|
524
|
+
end
|