tickle 1.2.0 → 2.0.0.rc1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.gitignore +2 -1
- data/.travis.yml +6 -4
- data/CHANGES.md +6 -24
- data/Gemfile +13 -4
- data/LICENCE +1 -1
- data/README.md +2 -2
- data/Rakefile +4 -7
- data/benchmarks/main.rb +71 -0
- data/lib/ext/array.rb +6 -0
- data/lib/ext/date_and_time.rb +64 -0
- data/lib/ext/string.rb +39 -0
- data/lib/tickle.rb +24 -117
- data/lib/tickle/filters.rb +58 -0
- data/lib/tickle/handler.rb +223 -80
- data/lib/tickle/helpers.rb +51 -0
- data/lib/tickle/patterns.rb +115 -0
- data/lib/tickle/repeater.rb +232 -116
- data/lib/tickle/tickle.rb +98 -220
- data/lib/tickle/tickled.rb +174 -0
- data/lib/tickle/token.rb +94 -0
- data/lib/tickle/version.rb +1 -1
- data/spec/helpers_spec.rb +36 -0
- data/spec/patterns_spec.rb +240 -0
- data/spec/spec_helper.rb +41 -0
- data/spec/tickle_spec.rb +543 -0
- data/spec/token_spec.rb +82 -0
- data/test/helper.rb +0 -2
- data/test/test_parsing.rb +14 -23
- data/tickle.gemspec +2 -9
- metadata +32 -113
data/lib/tickle/tickle.rb
CHANGED
@@ -19,260 +19,156 @@
|
|
19
19
|
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
20
|
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
21
21
|
|
22
|
-
require 'numerizer'
|
23
|
-
|
24
22
|
module Tickle
|
23
|
+
|
24
|
+
require_relative "patterns.rb"
|
25
|
+
require 'numerizer'
|
26
|
+
require_relative "helpers.rb"
|
27
|
+
require_relative "token.rb"
|
28
|
+
require_relative "tickled.rb"
|
29
|
+
|
30
|
+
|
25
31
|
class << self
|
32
|
+
|
33
|
+
|
26
34
|
# == Configuration options
|
27
35
|
#
|
28
|
-
#
|
29
|
-
#
|
30
|
-
#
|
31
|
-
#
|
36
|
+
# @param [String] text The string Tickle should parse.
|
37
|
+
# @param [Hash] specified_options See actual defaults below.
|
38
|
+
# @option specified_options [Date,Time,String] :start (Time.now) Start date for future occurrences. Must be in valid date format.
|
39
|
+
# @option specified_options [Date,Time,String] :until (nil) Last date to run occurrences until. Must be in valid date format.
|
40
|
+
# @option specified_options [true,false] :next_only (false)
|
41
|
+
# @option specified_options [Date,Time] :now (Time.now)
|
42
|
+
# @return [Hash]
|
32
43
|
#
|
33
|
-
#
|
34
|
-
#
|
35
|
-
#
|
36
|
-
# end
|
44
|
+
# @example Use by calling Tickle.parse and passing natural language with or without options.
|
45
|
+
# Tickle.parse("every Tuesday")
|
46
|
+
# # => {:next=>2014-08-26 12:00:00 0100, :expression=>"tuesday", :starting=>2014-08-25 16:31:12 0100, :until=>nil}
|
37
47
|
#
|
38
|
-
def
|
39
|
-
# get options and set defaults if necessary. Ability to set now is mostly for debugging
|
40
|
-
default_options = {:start => Time.now, :next_only => false, :until => nil, :now => Time.now}
|
41
|
-
options = default_options.merge specified_options
|
42
|
-
|
43
|
-
# ensure an expression was provided
|
44
|
-
raise(InvalidArgumentException, 'date expression is required') unless text
|
45
|
-
|
46
|
-
# ensure the specified options are valid
|
47
|
-
specified_options.keys.each do |key|
|
48
|
-
raise(InvalidArgumentException, "#{key} is not a valid option key.") unless default_options.keys.include?(key)
|
49
|
-
end
|
50
|
-
raise(InvalidArgumentException, ':start specified is not a valid datetime.') unless (is_date(specified_options[:start]) || Chronic.parse(specified_options[:start])) if specified_options[:start]
|
51
|
-
|
52
|
-
# check to see if a valid datetime was passed
|
53
|
-
return text if text.is_a?(Date) || text.is_a?(Time)
|
48
|
+
def _parse( tickled )
|
54
49
|
|
55
50
|
# check to see if this event starts some other time and reset now
|
56
|
-
|
57
|
-
|
58
|
-
Tickle.dwrite("start: #{@start}, until: #{@until}, now: #{options[:now].to_date}")
|
51
|
+
scan_expression! tickled
|
59
52
|
|
60
|
-
|
61
|
-
|
62
|
-
raise(InvalidDateExpression, "the start date (#{@start.to_date}) cannot occur after the end date") if @until && @start.to_date > @until.to_date
|
53
|
+
fail(InvalidDateExpression, "the start date (#{@start.to_date}) cannot occur in the past for a future event") if @start && @start.to_date < Date.today
|
54
|
+
fail(InvalidDateExpression, "the start date (#{@start.to_date}) cannot occur after the end date") if @until && @start.to_date > @until.to_date
|
63
55
|
|
64
56
|
# no need to guess at expression if the start_date is in the future
|
65
57
|
best_guess = nil
|
66
|
-
if @start.to_date >
|
58
|
+
if @start.to_date > tickled.now.to_date
|
67
59
|
best_guess = @start
|
68
60
|
else
|
69
61
|
# put the text into a normal format to ease scanning using Chronic
|
70
|
-
|
71
|
-
|
72
|
-
# split into tokens
|
73
|
-
@tokens = base_tokenize(event)
|
74
|
-
|
62
|
+
tickled.filtered = tickled.event.filter
|
63
|
+
# split into tokens and then
|
75
64
|
# process each original word for implied word
|
76
|
-
post_tokenize
|
77
|
-
|
78
|
-
@tokens.each {|x| Tickle.dwrite("raw: #{x.inspect}")}
|
65
|
+
@tokens = post_tokenize Token.tokenize(tickled.filtered)
|
79
66
|
|
80
67
|
# scan the tokens with each token scanner
|
81
|
-
@tokens =
|
68
|
+
@tokens = Token.scan!(@tokens)
|
82
69
|
|
83
70
|
# remove all tokens without a type
|
84
71
|
@tokens.reject! {|token| token.type.nil? }
|
85
72
|
|
86
73
|
# combine number and ordinals into single number
|
87
|
-
combine_multiple_numbers
|
88
|
-
|
89
|
-
@tokens.each {|x| Tickle.dwrite("processed: #{x.inspect}")}
|
74
|
+
@tokens = Helpers.combine_multiple_numbers(@tokens)
|
90
75
|
|
91
76
|
# if we can't guess it maybe chronic can
|
92
|
-
|
77
|
+
_guess = guess(@tokens, @start)
|
78
|
+
best_guess = _guess || chronic_parse(tickled.event) # TODO fix this call
|
93
79
|
end
|
94
80
|
|
95
|
-
|
96
|
-
|
81
|
+
fail(InvalidDateExpression, "the next occurrence takes place after the end date specified") if @until && (best_guess.to_date > @until.to_date)
|
97
82
|
if !best_guess
|
98
83
|
return nil
|
99
|
-
elsif
|
100
|
-
return {:next => best_guess.to_time, :expression => event.
|
84
|
+
elsif !tickled.next_only?
|
85
|
+
return {:next => best_guess.to_time, :expression => tickled.event.filter, :starting => @start, :until => @until}
|
101
86
|
else
|
102
87
|
return best_guess
|
103
88
|
end
|
104
89
|
end
|
105
90
|
|
91
|
+
|
106
92
|
# scans the expression for a variety of natural formats, such as 'every thursday starting tomorrow until May 15th
|
107
|
-
def scan_expression(
|
108
|
-
starting =
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
(\s
|
124
|
-
(?:\bend|until)
|
125
|
-
(?:s|ing)?
|
126
|
-
) # 2
|
127
|
-
(.*) # 3
|
128
|
-
/ix
|
129
|
-
if text =~ start_every_regex
|
130
|
-
starting = text.match(start_every_regex)[2].strip
|
131
|
-
text = text.match(start_every_regex)[4].strip
|
132
|
-
event, ending = process_for_ending(text)
|
133
|
-
elsif text =~ every_start_regex
|
134
|
-
event = text.match(every_start_regex)[2].strip
|
135
|
-
text = text.match(every_start_regex)[4].strip
|
136
|
-
starting, ending = process_for_ending(text)
|
137
|
-
elsif text =~ start_ending_regex
|
138
|
-
md = text.match start_ending_regex
|
139
|
-
starting = md.captures[1]
|
140
|
-
ending = md.captures.last.strip
|
141
|
-
event = 'day'
|
142
|
-
else
|
143
|
-
event, ending = process_for_ending(text)
|
93
|
+
def scan_expression!(tickled)
|
94
|
+
starting,ending,event = nil, nil, nil
|
95
|
+
if (md = Patterns::START_EVERY_REGEX.match tickled)
|
96
|
+
starting = md[:start].strip
|
97
|
+
text = md[:event].strip
|
98
|
+
event, ending = process_for_ending(text)
|
99
|
+
elsif (md = Patterns::EVERY_START_REGEX.match tickled)
|
100
|
+
event = md[:event].strip
|
101
|
+
text = md[:start].strip
|
102
|
+
starting, ending = process_for_ending(text)
|
103
|
+
elsif (md = Patterns::START_ENDING_REGEX.match tickled)
|
104
|
+
starting = md[:start].strip
|
105
|
+
ending = md[:finish].strip
|
106
|
+
event = 'day'
|
107
|
+
else
|
108
|
+
event, ending = process_for_ending(text)
|
144
109
|
end
|
145
|
-
|
110
|
+
tickled.starting = starting unless starting.nil?
|
111
|
+
tickled.event = event unless event.nil?
|
112
|
+
tickled.ending = ending unless ending.nil?
|
146
113
|
# they gave a phrase so if we can't interpret then we need to raise an error
|
147
|
-
if starting
|
148
|
-
|
149
|
-
@start ||= nil # initialize the variable to quell warnings
|
150
|
-
@start = chronic_parse(pre_filter(starting))
|
114
|
+
if tickled.starting && !tickled.starting.to_s.blank?
|
115
|
+
@start = chronic_parse(tickled.starting,tickled, :start)
|
151
116
|
if @start
|
152
117
|
@start.to_time
|
153
118
|
else
|
154
|
-
|
119
|
+
fail(InvalidDateExpression,"the starting date expression \"#{tickled.starting}\" could not be interpretted")
|
155
120
|
end
|
156
121
|
else
|
157
|
-
@start =
|
122
|
+
@start = tickled.start && tickled.start.to_time
|
158
123
|
end
|
159
124
|
|
160
|
-
|
161
|
-
|
125
|
+
|
126
|
+
if tickled.ending && !tickled.ending.blank?
|
127
|
+
@until = chronic_parse(tickled.ending.filter,tickled, :until)
|
162
128
|
if @until
|
163
129
|
@until.to_time
|
164
130
|
else
|
165
|
-
|
131
|
+
fail(InvalidDateExpression,"the ending date expression \"#{tickled.ending}\" could not be interpretted")
|
166
132
|
end
|
167
133
|
else
|
168
|
-
@until =
|
134
|
+
@until =
|
135
|
+
if tickled.starting && !tickled.starting.to_s.blank?
|
136
|
+
if tickled.until && !tickled.until.to_s.blank?
|
137
|
+
if tickled.until.to_time > tickled.starting.to_time
|
138
|
+
tickled.until.to_time
|
139
|
+
end
|
140
|
+
end
|
141
|
+
end
|
169
142
|
end
|
170
143
|
|
171
144
|
@next = nil
|
172
|
-
|
173
|
-
return event
|
145
|
+
tickled
|
174
146
|
end
|
175
147
|
|
148
|
+
|
176
149
|
# process the remaining expression to see if an until, end, ending is specified
|
177
150
|
def process_for_ending(text)
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
else
|
182
|
-
return text, nil
|
183
|
-
end
|
184
|
-
end
|
185
|
-
|
186
|
-
# Normalize natural string removing prefix language
|
187
|
-
def pre_filter(text)
|
188
|
-
return nil unless text
|
189
|
-
|
190
|
-
text.gsub!(/every(\s)?/, '')
|
191
|
-
text.gsub!(/each(\s)?/, '')
|
192
|
-
text.gsub!(/repeat(s|ing)?(\s)?/, '')
|
193
|
-
text.gsub!(/on the(\s)?/, '')
|
194
|
-
text.gsub!(/([^\w\d\s])+/, '')
|
195
|
-
normalize_us_holidays(text.downcase.strip)
|
196
|
-
end
|
197
|
-
|
198
|
-
# Split the text on spaces and convert each word into
|
199
|
-
# a Token
|
200
|
-
def base_tokenize(text) #:nodoc:
|
201
|
-
text.split(' ').map { |word| Token.new(word) }
|
151
|
+
(md = Patterns::PROCESS_FOR_ENDING.match text) ?
|
152
|
+
[ md[:target], md[:ending] ] :
|
153
|
+
[text, nil]
|
202
154
|
end
|
203
155
|
|
204
156
|
# normalizes each token
|
205
|
-
def post_tokenize
|
206
|
-
|
207
|
-
|
157
|
+
def post_tokenize(tokens)
|
158
|
+
_tokens = tokens.map(&:clone)
|
159
|
+
_tokens.each do |token|
|
160
|
+
token.normalize!
|
208
161
|
end
|
162
|
+
_tokens
|
209
163
|
end
|
210
164
|
|
211
|
-
# Clean up the specified input text by stripping unwanted characters,
|
212
|
-
# converting idioms to their canonical form, converting number words
|
213
|
-
# to numbers (three => 3), and converting ordinal words to numeric
|
214
|
-
# ordinals (third => 3rd)
|
215
|
-
def normalize(text) #:nodoc:
|
216
|
-
normalized_text = text.to_s.downcase
|
217
|
-
normalized_text = Numerizer.numerize(normalized_text)
|
218
|
-
normalized_text.gsub!(/['"\.]/, '')
|
219
|
-
normalized_text.gsub!(/([\/\-\,\@])/) { ' ' + $1 + ' ' }
|
220
|
-
normalized_text
|
221
|
-
end
|
222
|
-
|
223
|
-
# Converts natural language US Holidays into a date expression to be
|
224
|
-
# parsed.
|
225
|
-
def normalize_us_holidays(text) #:nodoc:
|
226
|
-
normalized_text = text.to_s.downcase
|
227
|
-
normalized_text.gsub!(/\bnew\syear'?s?(\s)?(day)?\b/, "january 1, #{next_appropriate_year(1, 1)}")
|
228
|
-
normalized_text.gsub!(/\bnew\syear'?s?(\s)?(eve)?\b/, "december 31, #{next_appropriate_year(12, 31)}")
|
229
|
-
normalized_text.gsub!(/\bm(artin\s)?l(uther\s)?k(ing)?(\sday)?\b/, 'third monday in january')
|
230
|
-
normalized_text.gsub!(/\binauguration(\sday)?\b/, 'january 20')
|
231
|
-
normalized_text.gsub!(/\bpresident'?s?(\sday)?\b/, 'third monday in february')
|
232
|
-
normalized_text.gsub!(/\bmemorial\sday\b/, '4th monday of may')
|
233
|
-
normalized_text.gsub!(/\bindepend(e|a)nce\sday\b/, "july 4, #{next_appropriate_year(7, 4)}")
|
234
|
-
normalized_text.gsub!(/\blabor\sday\b/, 'first monday in september')
|
235
|
-
normalized_text.gsub!(/\bcolumbus\sday\b/, 'second monday in october')
|
236
|
-
normalized_text.gsub!(/\bveterans?\sday\b/, "november 11, #{next_appropriate_year(11, 1)}")
|
237
|
-
normalized_text.gsub!(/\bthanksgiving(\sday)?\b/, 'fourth thursday in november')
|
238
|
-
normalized_text.gsub!(/\bchristmas\seve\b/, "december 24, #{next_appropriate_year(12, 24)}")
|
239
|
-
normalized_text.gsub!(/\bchristmas(\sday)?\b/, "december 25, #{next_appropriate_year(12, 25)}")
|
240
|
-
normalized_text.gsub!(/\bsuper\sbowl(\ssunday)?\b/, 'first sunday in february')
|
241
|
-
normalized_text.gsub!(/\bgroundhog(\sday)?\b/, "february 2, #{next_appropriate_year(2, 2)}")
|
242
|
-
normalized_text.gsub!(/\bvalentine'?s?(\sday)?\b/, "february 14, #{next_appropriate_year(2, 14)}")
|
243
|
-
normalized_text.gsub!(/\bs(ain)?t\spatrick'?s?(\sday)?\b/, "march 17, #{next_appropriate_year(3, 17)}")
|
244
|
-
normalized_text.gsub!(/\bapril\sfool'?s?(\sday)?\b/, "april 1, #{next_appropriate_year(4, 1)}")
|
245
|
-
normalized_text.gsub!(/\bearth\sday\b/, "april 22, #{next_appropriate_year(4, 22)}")
|
246
|
-
normalized_text.gsub!(/\barbor\sday\b/, 'fourth friday in april')
|
247
|
-
normalized_text.gsub!(/\bcinco\sde\smayo\b/, "may 5, #{next_appropriate_year(5, 5)}")
|
248
|
-
normalized_text.gsub!(/\bmother'?s?\sday\b/, 'second sunday in may')
|
249
|
-
normalized_text.gsub!(/\bflag\sday\b/, "june 14, #{next_appropriate_year(6, 14)}")
|
250
|
-
normalized_text.gsub!(/\bfather'?s?\sday\b/, 'third sunday in june')
|
251
|
-
normalized_text.gsub!(/\bhalloween\b/, "october 31, #{next_appropriate_year(10, 31)}")
|
252
|
-
normalized_text.gsub!(/\belection\sday\b/, 'second tuesday in november')
|
253
|
-
normalized_text.gsub!(/\bkwanzaa\b/, "january 1, #{next_appropriate_year(1, 1)}")
|
254
|
-
normalized_text
|
255
|
-
end
|
256
|
-
|
257
|
-
# Turns compound numbers, like 'twenty first' => 21
|
258
|
-
def combine_multiple_numbers
|
259
|
-
if [:number, :ordinal].all? {|type| token_types.include? type}
|
260
|
-
number = token_of_type(:number)
|
261
|
-
ordinal = token_of_type(:ordinal)
|
262
|
-
combined_original = "#{number.original} #{ordinal.original}"
|
263
|
-
combined_word = (number.start.to_s[0] + ordinal.word)
|
264
|
-
combined_value = (number.start.to_s[0] + ordinal.start.to_s)
|
265
|
-
new_number_token = Token.new(combined_original, combined_word, :ordinal, combined_value, 365)
|
266
|
-
@tokens.reject! {|token| (token.type == :number || token.type == :ordinal)}
|
267
|
-
@tokens << new_number_token
|
268
|
-
end
|
269
|
-
end
|
270
165
|
|
271
166
|
# Returns an array of types for all tokens
|
272
167
|
def token_types
|
273
168
|
@tokens.map(&:type)
|
274
169
|
end
|
275
170
|
|
171
|
+
|
276
172
|
# Returns the next available month based on the current day of the month.
|
277
173
|
# For example, if get_next_month(15) is called and the start date is the 10th, then it will return the 15th of this month.
|
278
174
|
# However, if get_next_month(15) is called and the start date is the 18th, it will return the 15th of next month.
|
@@ -280,56 +176,38 @@ module Tickle
|
|
280
176
|
month = number.to_i < @start.day ? (@start.month == 12 ? 1 : @start.month + 1) : @start.month
|
281
177
|
end
|
282
178
|
|
179
|
+
|
283
180
|
def next_appropriate_year(month, day)
|
284
|
-
|
285
|
-
year = (Date.new(start.year.to_i, month.to_i, day.to_i) == start.to_date) ? start.year + 1 : start.year
|
181
|
+
year = (Date.new(@start.year.to_i, month.to_i, day.to_i) == @start.to_date) ? @start.year + 1 : @start.year
|
286
182
|
return year
|
287
183
|
end
|
288
184
|
|
289
|
-
# Return the number of days in a specified month.
|
290
|
-
# If no month is specified, current month is used.
|
291
|
-
def days_in_month(month=nil)
|
292
|
-
month ||= Date.today.month
|
293
|
-
days_in_mon = Date.civil(Date.today.year, month, -1).day
|
294
|
-
end
|
295
185
|
|
296
186
|
private
|
297
187
|
|
188
|
+
|
298
189
|
# slightly modified chronic parser to ensure that the date found is in the future
|
299
190
|
# first we check to see if an explicit date was passed and, if so, dont do anything.
|
300
191
|
# if, however, a date expression was passed we evaluate and shift forward if needed
|
301
|
-
def chronic_parse(exp)
|
302
|
-
|
303
|
-
result =
|
304
|
-
|
192
|
+
def chronic_parse(exp, tickled, start_or_until)
|
193
|
+
exp = Ordinal.new exp
|
194
|
+
result =
|
195
|
+
if r = Chronic.parse(exp.ordinal_as_number, :now => tickled.now)
|
196
|
+
r
|
197
|
+
elsif r = (start_or_until && tickled[start_or_until])
|
198
|
+
r
|
199
|
+
elsif r = (start_or_until == :start && tickled.now)
|
200
|
+
r
|
201
|
+
end
|
202
|
+
if result && result.to_time < Time.now
|
203
|
+
result = Time.local(result.year + 1, result.month, result.day, result.hour, result.min, result.sec)
|
204
|
+
end
|
305
205
|
result
|
306
206
|
end
|
307
207
|
|
308
208
|
end
|
309
209
|
|
310
|
-
class Token #:nodoc:
|
311
|
-
attr_accessor :original, :word, :type, :interval, :start
|
312
210
|
|
313
|
-
def initialize(original, word=nil, type=nil, start=nil, interval=nil)
|
314
|
-
@original = original
|
315
|
-
@word = word
|
316
|
-
@type = type
|
317
|
-
@interval = interval
|
318
|
-
@start = start
|
319
|
-
end
|
320
|
-
|
321
|
-
# Updates an existing token. Mostly used by the repeater class.
|
322
|
-
def update(type, start=nil, interval=nil)
|
323
|
-
@start = start
|
324
|
-
@type = type
|
325
|
-
@interval = interval
|
326
|
-
end
|
327
|
-
end
|
328
|
-
|
329
|
-
# This exception is raised if an invalid argument is provided to
|
330
|
-
# any of Tickle's methods
|
331
|
-
class InvalidArgumentException < Exception
|
332
|
-
end
|
333
211
|
|
334
212
|
# This exception is raised if there is an issue with the parsing
|
335
213
|
# output from the date expression provided
|
@@ -0,0 +1,174 @@
|
|
1
|
+
require 'chronic'
|
2
|
+
require 'texttube/base'
|
3
|
+
require_relative "filters.rb"
|
4
|
+
require 'chronic'
|
5
|
+
|
6
|
+
module Tickle
|
7
|
+
|
8
|
+
|
9
|
+
|
10
|
+
# Contains the initial input and the result of parsing it.
|
11
|
+
class Tickled < TextTube::Base
|
12
|
+
register Filters
|
13
|
+
|
14
|
+
# @param [String] asked The string Tickle should parse.
|
15
|
+
# @param [Hash] options
|
16
|
+
# @see Tickle.parse for specific options
|
17
|
+
# @see ::Hash#new
|
18
|
+
def initialize(asked, options={}, &block)
|
19
|
+
fail ArgumentError, "You must pass a string to Tickled.new" if asked.nil?
|
20
|
+
|
21
|
+
|
22
|
+
default_options = {
|
23
|
+
:start => Time.now,
|
24
|
+
:next_only => false,
|
25
|
+
:until => nil,
|
26
|
+
:now => Time.now,
|
27
|
+
}
|
28
|
+
|
29
|
+
unless options.nil? || options.empty?
|
30
|
+
# ensure the specified options are valid
|
31
|
+
options.keys.each do |key|
|
32
|
+
fail(ArgumentError, "#{key} is not a valid option key.") unless default_options.keys.include?(key)
|
33
|
+
end
|
34
|
+
|
35
|
+
[:start,:until,:now].each do |key|
|
36
|
+
if options.has_key? key
|
37
|
+
test_for_correctness options[key], key
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
t =
|
43
|
+
if asked.respond_to?(:to_time)
|
44
|
+
asked
|
45
|
+
elsif (t = Time.parse(asked) rescue nil) # a legitimate use!
|
46
|
+
t
|
47
|
+
elsif (t = Chronic.parse("#{asked}") rescue nil) # another legitimate use!
|
48
|
+
t
|
49
|
+
end
|
50
|
+
|
51
|
+
unless t.nil?
|
52
|
+
define_singleton_method :to_time do
|
53
|
+
@as_time ||= t
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
@opts = default_options.merge(options)
|
58
|
+
super(asked.to_s)
|
59
|
+
end
|
60
|
+
|
61
|
+
|
62
|
+
def asked
|
63
|
+
self
|
64
|
+
end
|
65
|
+
|
66
|
+
|
67
|
+
def parser= parser
|
68
|
+
@parser = parser
|
69
|
+
end
|
70
|
+
|
71
|
+
def parse!
|
72
|
+
@parser.parse self
|
73
|
+
end
|
74
|
+
|
75
|
+
def now=( value )
|
76
|
+
@opts[:now] = value
|
77
|
+
end
|
78
|
+
|
79
|
+
def now
|
80
|
+
@opts[:now]
|
81
|
+
end
|
82
|
+
|
83
|
+
def next_only=( value )
|
84
|
+
@opts[:next_only] = value
|
85
|
+
end
|
86
|
+
|
87
|
+
|
88
|
+
def next_only?
|
89
|
+
@opts[:next_only]
|
90
|
+
end
|
91
|
+
|
92
|
+
|
93
|
+
# param [Date,Time,String,nil] v The value given for the key.
|
94
|
+
# param [Symbol] key The name of the key being tested.
|
95
|
+
def test_for_correctness( v, key )
|
96
|
+
# Must be be a time or a string or be able to convert to a time
|
97
|
+
# If it is a string, must parse ok by Chronic
|
98
|
+
fail ArgumentError, "The value (#{v}) given for :#{key} does not appear to be a valid date or time." unless v.respond_to?(:to_time) or (v.respond_to?(:downcase) and ::Chronic.parse(v))
|
99
|
+
end
|
100
|
+
|
101
|
+
|
102
|
+
def asked
|
103
|
+
self
|
104
|
+
end
|
105
|
+
|
106
|
+
|
107
|
+
# @param [Date,Time,String]
|
108
|
+
def asked=( text )
|
109
|
+
#test_for_correctness text, :asked
|
110
|
+
@opts[:asked] = text
|
111
|
+
end
|
112
|
+
|
113
|
+
def start
|
114
|
+
@opts[:start] ||= Time.now
|
115
|
+
end
|
116
|
+
|
117
|
+
def start=( value )
|
118
|
+
@opts[:start] = test_for_correctness value, :start
|
119
|
+
end
|
120
|
+
|
121
|
+
def until
|
122
|
+
@opts[:until] ||= Tickled.new( Time.now )
|
123
|
+
end
|
124
|
+
|
125
|
+
def until=( value )
|
126
|
+
@opts[:until] = test_for_correctness value, :until
|
127
|
+
end
|
128
|
+
|
129
|
+
|
130
|
+
[:starting, :ending, :event].each do |meth|
|
131
|
+
define_method meth do
|
132
|
+
@opts[meth]
|
133
|
+
end
|
134
|
+
define_method "#{meth}=" do |value|
|
135
|
+
@opts[meth] = Tickled.new value
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|
139
|
+
|
140
|
+
def event
|
141
|
+
@opts[:event] ||= self
|
142
|
+
end
|
143
|
+
def event= value
|
144
|
+
@opts[:event] = Tickled.new value
|
145
|
+
end
|
146
|
+
|
147
|
+
|
148
|
+
def filtered=(filtered_text)
|
149
|
+
@filtered = filtered_text
|
150
|
+
end
|
151
|
+
|
152
|
+
def filtered
|
153
|
+
@filtered
|
154
|
+
end
|
155
|
+
|
156
|
+
|
157
|
+
def to_s
|
158
|
+
self
|
159
|
+
end
|
160
|
+
|
161
|
+
def blank?
|
162
|
+
if respond_to? :empty?
|
163
|
+
empty? || !self
|
164
|
+
elsif respond_to? :localtime
|
165
|
+
false
|
166
|
+
end
|
167
|
+
end
|
168
|
+
|
169
|
+
# def inspect
|
170
|
+
# "#{self} #{@opts.inspect}"
|
171
|
+
# end
|
172
|
+
|
173
|
+
end
|
174
|
+
end
|