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