chronic 0.2.3 → 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.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
|