tickle 1.2.0 → 2.0.0.rc1
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.
- 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
|