nickel 0.0.3 → 0.0.4
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/License.txt +2 -2
- data/README.rdoc +24 -12
- data/Rakefile +22 -0
- data/lib/nickel/construct.rb +121 -0
- data/lib/nickel/construct_finder.rb +1145 -0
- data/lib/nickel/construct_interpreter.rb +345 -0
- data/lib/nickel/instance_from_hash.rb +13 -0
- data/lib/nickel/nlp.rb +73 -0
- data/lib/nickel/occurrence.rb +90 -0
- data/lib/nickel/query.rb +1143 -0
- data/lib/nickel/query_constants.rb +26 -0
- data/lib/nickel/ruby_ext/calling_method.rb +10 -0
- data/lib/nickel/ruby_ext/to_s2.rb +12 -0
- data/lib/nickel/zdate.rb +503 -0
- data/lib/nickel/ztime.rb +319 -0
- data/lib/nickel.rb +30 -34
- data/nickel.gemspec +30 -10
- data/{test → spec}/nickel_spec.rb +4 -4
- data/test/compare.rb +109 -0
- data/test/nlp_test.rb +813 -0
- data/test/nlp_tests_helper.rb +55 -0
- data/test/zdate_test.rb +43 -0
- data/test/ztime_test.rb +428 -0
- metadata +34 -22
@@ -0,0 +1,345 @@
|
|
1
|
+
# Ruby Nickel Library
|
2
|
+
# Copyright (c) 2008-2011 Lou Zell, lzell11@gmail.com, http://hazelmade.com
|
3
|
+
# MIT License [http://www.opensource.org/licenses/mit-license.php]
|
4
|
+
|
5
|
+
module Nickel
|
6
|
+
|
7
|
+
class ConstructInterpreter
|
8
|
+
|
9
|
+
attr_reader :occurrences, :constructs, :curdate
|
10
|
+
|
11
|
+
def initialize(constructs, curdate)
|
12
|
+
@constructs = constructs
|
13
|
+
@curdate = curdate
|
14
|
+
@occurrences = [] # output
|
15
|
+
initialize_index_to_type_map
|
16
|
+
initialize_user_input_style
|
17
|
+
initialize_arrays_of_construct_indices
|
18
|
+
initialize_sorted_time_map
|
19
|
+
finalize_constructs
|
20
|
+
end
|
21
|
+
|
22
|
+
def run
|
23
|
+
if found_dates
|
24
|
+
occurrences_from_dates
|
25
|
+
elsif found_one_date_span
|
26
|
+
occurrences_from_one_date_span
|
27
|
+
elsif found_recurrences_and_optional_date_span
|
28
|
+
occurrences_from_recurrences_and_optional_date_span
|
29
|
+
elsif found_wrappers_only
|
30
|
+
occurrences_from_wrappers_only
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
private
|
35
|
+
def initialize_index_to_type_map
|
36
|
+
# The @index_to_type_map hash looks like this: {0 => :date, 1 => :timespan, ...}
|
37
|
+
# Each key represents the index in @constructs and the value represents that constructs class.
|
38
|
+
@index_to_type_map = {}
|
39
|
+
@constructs.each_with_index do |c,i|
|
40
|
+
@index_to_type_map[i] = case c.class.name
|
41
|
+
when "Nickel::DateConstruct" then :date
|
42
|
+
when "Nickel::TimeConstruct" then :time
|
43
|
+
when "Nickel::DateSpanConstruct" then :datespan
|
44
|
+
when "Nickel::TimeSpanConstruct" then :timespan
|
45
|
+
when "Nickel::RecurrenceConstruct" then :recurrence
|
46
|
+
when "Nickel::WrapperConstruct" then :wrapper
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def initialize_user_input_style
|
52
|
+
# Initializes @user_input_style.
|
53
|
+
# Determine user input style, i.e. "DATE TIME DATE TIME" OR "TIME DATE TIME DATE".
|
54
|
+
# Determine user input style, i.e. "DATE TIME DATE TIME" OR "TIME DATE TIME DATE".
|
55
|
+
case (@index_to_type_map[0] == :wrapper ? @index_to_type_map[1] : @index_to_type_map[0])
|
56
|
+
when :date then @user_input_style = :datetime
|
57
|
+
when :time then @user_input_style = :timedate
|
58
|
+
when :datespan then @user_input_style = :datetime
|
59
|
+
when :timespan then @user_input_style = :timedate
|
60
|
+
when :recurrence then @user_input_style = :datetime
|
61
|
+
else
|
62
|
+
# We only have wrappers. It doesn't matter which user style we choose.
|
63
|
+
@user_input_style = :datetime
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
def initialize_arrays_of_construct_indices
|
68
|
+
@dci,@tci,@dsci,@tsci,@rci,@wci = [],[],[],[],[],[]
|
69
|
+
@index_to_type_map.each do |i, type|
|
70
|
+
case type
|
71
|
+
when :date then @dci << i
|
72
|
+
when :time then @tci << i
|
73
|
+
when :datespan then @dsci << i
|
74
|
+
when :timespan then @tsci << i
|
75
|
+
when :recurrence then @rci << i
|
76
|
+
when :wrapper then @wci << i
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
def initialize_sorted_time_map
|
82
|
+
# Sorted time map has date/datespan/recurrence construct indices as keys, and
|
83
|
+
# an array of time/timespan indices as values.
|
84
|
+
@sorted_time_map = {}
|
85
|
+
|
86
|
+
# Get all indices of date/datespan/recurrence constructs in the order they occurred.
|
87
|
+
date_indices = (@dci + @dsci + @rci).sort
|
88
|
+
|
89
|
+
# What is inhert_on about? If a user enters something like "wed and fri at 4pm" they
|
90
|
+
# really want 4pm to be associated with wed and fri. For all dates that we don't find
|
91
|
+
# associated times, we append the dates index to the inherit_on array. Then, once we
|
92
|
+
# find a date associated with times, we copy the sorted_time_map at that index to the
|
93
|
+
# other indices in the inherit_on array.
|
94
|
+
#
|
95
|
+
# If @user_input_style is :datetime, then inherit_on will hold date indices that must inherit
|
96
|
+
# from the next date with associated times. If @user_input_style is :timedate, then
|
97
|
+
# inherit_from will hold the last date index with associated times, and subsequent dates that
|
98
|
+
# do not have associated times will inherit from this index.
|
99
|
+
@user_input_style == :datetime ? inherit_on = [] : inherit_from = nil
|
100
|
+
|
101
|
+
# Iterate date_indices and populate @sorted_time_map
|
102
|
+
date_indices.each do |i|
|
103
|
+
# Do not change i.
|
104
|
+
j = i
|
105
|
+
|
106
|
+
# Now find all time and time span construct indices between this index and a boundary.
|
107
|
+
# Boundaries are any other index in date_indices, -1 (passed the first construct),
|
108
|
+
# and @constructs.size (passed the last construct).
|
109
|
+
map_to_indices = []
|
110
|
+
while (j = move_time_map_index(j)) && j != -1 && j != @constructs.size && !date_indices.include?(j) # boundaries
|
111
|
+
(index_references_time(j) || index_references_timespan(j)) && map_to_indices << j
|
112
|
+
end
|
113
|
+
|
114
|
+
# NOTE: time/timespan indices are sorted by the order which they appeared, e.g.
|
115
|
+
# their construct index number.
|
116
|
+
@sorted_time_map[i] = map_to_indices.sort
|
117
|
+
if @user_input_style == :datetime
|
118
|
+
inherit_on = handle_datetime_time_map_inheritance(inherit_on, i)
|
119
|
+
else
|
120
|
+
inherit_from = handle_timedate_time_map_inheritance(inherit_from, i)
|
121
|
+
end
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
def move_time_map_index(index)
|
126
|
+
# The time map index must be moved based on user input style. For instance,
|
127
|
+
# if a user enters dates then times, we must attach times to preceding dates,
|
128
|
+
# meaning move forward.
|
129
|
+
if @user_input_style == :datetime then index + 1
|
130
|
+
elsif @user_input_style == :timedate then index - 1
|
131
|
+
else raise "ConstructInterpreter#move_time_map_index says: @user_input_style is not valid"
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
def handle_datetime_time_map_inheritance(inherit_on, date_index)
|
136
|
+
if @sorted_time_map[date_index].empty?
|
137
|
+
# There are no times for this date, mark to be inherited
|
138
|
+
inherit_on << date_index
|
139
|
+
else
|
140
|
+
# There are times for this date, use them for any indices marked as inherit_on.
|
141
|
+
# Then clear the inherit_on array.
|
142
|
+
inherit_on.each {|k| @sorted_time_map[k] = @sorted_time_map[date_index]}
|
143
|
+
inherit_on = []
|
144
|
+
end
|
145
|
+
inherit_on
|
146
|
+
end
|
147
|
+
|
148
|
+
def handle_timedate_time_map_inheritance(inherit_from, date_index)
|
149
|
+
if @sorted_time_map[date_index].empty?
|
150
|
+
# There are no times for this date, try inheriting from last batch of times.
|
151
|
+
@sorted_time_map[date_index] = @sorted_time_map[inherit_from] if inherit_from
|
152
|
+
else
|
153
|
+
inherit_from = date_index
|
154
|
+
end
|
155
|
+
inherit_from
|
156
|
+
end
|
157
|
+
|
158
|
+
def index_references_time(index)
|
159
|
+
@index_to_type_map[index] == :time
|
160
|
+
end
|
161
|
+
|
162
|
+
def index_references_timespan(index)
|
163
|
+
@index_to_type_map[index] == :timespan
|
164
|
+
end
|
165
|
+
|
166
|
+
# Returns either @time or @start_time, depending on whether tindex references a Time or TimeSpan construct
|
167
|
+
def start_time_from_tindex(tindex)
|
168
|
+
if index_references_time(tindex)
|
169
|
+
return @constructs[tindex].time
|
170
|
+
elsif index_references_timespan(tindex)
|
171
|
+
return @constructs[tindex].start_time
|
172
|
+
else
|
173
|
+
raise "ConstructInterpreter#start_time_from_tindex says: tindex does not reference a time or time span"
|
174
|
+
end
|
175
|
+
end
|
176
|
+
|
177
|
+
# If guess is false, either start time or end time (but not both) must already be firm.
|
178
|
+
# The time that is not firm will be modified according to the firm time and set to firm.
|
179
|
+
def finalize_timespan_constructs(guess = false)
|
180
|
+
@tsci.each do |i|
|
181
|
+
st, et = @constructs[i].start_time, @constructs[i].end_time
|
182
|
+
if st.firm && et.firm
|
183
|
+
next # nothing to do if start and end times are both firm
|
184
|
+
elsif !st.firm && et.firm
|
185
|
+
st.modify_such_that_is_before(et)
|
186
|
+
elsif st.firm && !et.firm
|
187
|
+
et.modify_such_that_is_after(st)
|
188
|
+
else
|
189
|
+
et.guess_modify_such_that_is_after(st) if guess
|
190
|
+
end
|
191
|
+
end
|
192
|
+
end
|
193
|
+
|
194
|
+
# One of this methods functions will be to assign proper am/pm values to time
|
195
|
+
# and timespan constructs if they were not specified.
|
196
|
+
def finalize_constructs
|
197
|
+
|
198
|
+
# First assign am/pm values to timespan constructs independent of
|
199
|
+
# other times in timemap.
|
200
|
+
finalize_timespan_constructs
|
201
|
+
|
202
|
+
# Next we need to burn through the time map, find any start times
|
203
|
+
# that are not firm, and set them based on previous firm times.
|
204
|
+
# Note that @sorted_time_map has the format {date_index => [array, of, time, indices]}
|
205
|
+
@sorted_time_map.each_value do |time_indices|
|
206
|
+
# The time_indices array holds TimeConstruct and TimeSpanConstruct indices.
|
207
|
+
# The time_array will hold an array of ZTime objects to modify (potentially)
|
208
|
+
time_array = []
|
209
|
+
time_indices.each {|tindex| time_array << start_time_from_tindex(tindex)}
|
210
|
+
ZTime.am_pm_modifier(*time_array)
|
211
|
+
end
|
212
|
+
|
213
|
+
# Finally, we need to modify the timespans based on the the time info from am_pm_modifier.
|
214
|
+
# We also need to guess at any timespans that didn't get any help from am_pm_modifier.
|
215
|
+
# i.e. we originally guessed at timespans independently of other time info in time map;
|
216
|
+
# now that we have modified start times based on other info in time map, we can refine the
|
217
|
+
# end times in our time spans. If we didn't pick them up before.
|
218
|
+
finalize_timespan_constructs(true)
|
219
|
+
end
|
220
|
+
|
221
|
+
# The @sorted_time_map hash has keys of date/datespans/recurrence indices (in this case date),
|
222
|
+
# and an array of time and time span indices as values. This checks to make sure that array of
|
223
|
+
# times is not empty, and if it is there are no times associated with this date construct.
|
224
|
+
# Huh? That does not explain this method... at all.
|
225
|
+
def create_occurrence_for_each_time_in_time_map(occ_base, dindex, &block)
|
226
|
+
if !@sorted_time_map[dindex].empty?
|
227
|
+
@sorted_time_map[dindex].each do |tindex| # tindex may be time index or time span index
|
228
|
+
occ = occ_base.dup
|
229
|
+
occ.start_time = start_time_from_tindex(tindex)
|
230
|
+
if index_references_time(tindex)
|
231
|
+
occ.start_time = @constructs[tindex].time
|
232
|
+
elsif index_references_timespan(tindex)
|
233
|
+
occ.start_time = @constructs[tindex].start_time
|
234
|
+
occ.end_time = @constructs[tindex].end_time
|
235
|
+
end
|
236
|
+
yield(occ)
|
237
|
+
end
|
238
|
+
else
|
239
|
+
yield(occ_base)
|
240
|
+
end
|
241
|
+
end
|
242
|
+
|
243
|
+
def found_dates
|
244
|
+
# One or more date constructs, NO date spans, NO recurrence,
|
245
|
+
# possible wrappers, possible time constructs, possible time spans
|
246
|
+
@dci.size > 0 && @dsci.size == 0 && @rci.size == 0
|
247
|
+
end
|
248
|
+
|
249
|
+
def occurrences_from_dates
|
250
|
+
@dci.each do |dindex|
|
251
|
+
occ_base = Occurrence.new(:type => :single, :start_date => @constructs[dindex].date)
|
252
|
+
create_occurrence_for_each_time_in_time_map(occ_base, dindex) {|occ| @occurrences << occ}
|
253
|
+
end
|
254
|
+
end
|
255
|
+
|
256
|
+
def found_one_date_span
|
257
|
+
@dci.size == 0 && @dsci.size == 1 && @rci.size == 0
|
258
|
+
end
|
259
|
+
|
260
|
+
def occurrences_from_one_date_span
|
261
|
+
occ_base = Occurrence.new(:type => :daily,
|
262
|
+
:start_date => @constructs[@dsci[0]].start_date,
|
263
|
+
:end_date => @constructs[@dsci[0]].end_date,
|
264
|
+
:interval => 1)
|
265
|
+
create_occurrence_for_each_time_in_time_map(occ_base, @dsci[0]) {|occ| @occurrences << occ}
|
266
|
+
end
|
267
|
+
|
268
|
+
def found_recurrences_and_optional_date_span
|
269
|
+
@dsci.size <= 1 && @rci.size >= 1 # dates are optional
|
270
|
+
end
|
271
|
+
|
272
|
+
def occurrences_from_recurrences_and_optional_date_span
|
273
|
+
if @dsci.size == 1
|
274
|
+
# If a date span exists, it functions as wrapper.
|
275
|
+
occ_base_opts = {:start_date => @constructs[@dsci[0]].start_date, :end_date => @constructs[@dsci[0]].end_date}
|
276
|
+
else
|
277
|
+
# Perhaps there are type 0 or type 1 wrappers to provide start/end dates.
|
278
|
+
occ_base_opts = occ_base_opts_from_wrappers
|
279
|
+
end
|
280
|
+
|
281
|
+
@rci.each do |rcindex|
|
282
|
+
# Construct#interpret returns an array of hashes, each hash represents a single occurrence.
|
283
|
+
@constructs[rcindex].interpret.each do |rec_occ_base_opts|
|
284
|
+
# RecurrenceConstruct#interpret returns base_opts for each occurrence,
|
285
|
+
# but they must be merged with start/end dates, if supplied.
|
286
|
+
occ_base = Occurrence.new(rec_occ_base_opts.merge(occ_base_opts))
|
287
|
+
# Attach times:
|
288
|
+
create_occurrence_for_each_time_in_time_map(occ_base, rcindex) {|occ| @occurrences << occ}
|
289
|
+
end
|
290
|
+
end
|
291
|
+
end
|
292
|
+
|
293
|
+
def found_wrappers_only
|
294
|
+
# This should really be "found length wrappers only", because @dci.size must be zero,
|
295
|
+
# and start/end wrappers require a date.
|
296
|
+
@dsci.size == 0 && @rci.size == 0 && @wci.size > 0 && @dci.size == 0
|
297
|
+
end
|
298
|
+
|
299
|
+
def occurrences_from_wrappers_only
|
300
|
+
occ_base = {:type => :daily, :interval => 1}
|
301
|
+
@occurrences << Occurrence.new(occ_base.merge(occ_base_opts_from_wrappers))
|
302
|
+
end
|
303
|
+
|
304
|
+
def occ_base_opts_from_wrappers
|
305
|
+
base_opts = {}
|
306
|
+
# Must do type 0 and 1 wrappers first, imagine something like
|
307
|
+
# "every friday starting next friday for 6 months".
|
308
|
+
@wci.each do |wi|
|
309
|
+
# Make sure the construct after the wrapper is a date.
|
310
|
+
if @constructs[wi].wrapper_type == 0 && @dci.include?(wi + 1)
|
311
|
+
base_opts[:start_date] = @constructs[wi + 1].date
|
312
|
+
elsif @constructs[wi].wrapper_type == 1 && @dci.include?(wi + 1)
|
313
|
+
base_opts[:end_date] = @constructs[wi + 1].date
|
314
|
+
end
|
315
|
+
end
|
316
|
+
|
317
|
+
# Now pick up wrapper types 2,3,4
|
318
|
+
@wci.each do |wi|
|
319
|
+
if @constructs[wi].wrapper_type >= 2
|
320
|
+
if base_opts[:start_date].nil? && base_opts[:end_date].nil? # span must start today
|
321
|
+
base_opts[:start_date] = @curdate.dup
|
322
|
+
base_opts[:end_date] = case @constructs[wi].wrapper_type
|
323
|
+
when 2 then @curdate.add_days(@constructs[wi].wrapper_length)
|
324
|
+
when 3 then @curdate.add_weeks(@constructs[wi].wrapper_length)
|
325
|
+
when 4 then @curdate.add_months(@constructs[wi].wrapper_length)
|
326
|
+
end
|
327
|
+
elsif base_opts[:start_date] && base_opts[:end_date].nil?
|
328
|
+
base_opts[:end_date] = case @constructs[wi].wrapper_type
|
329
|
+
when 2 then base_opts[:start_date].add_days(@constructs[wi].wrapper_length)
|
330
|
+
when 3 then base_opts[:start_date].add_weeks(@constructs[wi].wrapper_length)
|
331
|
+
when 4 then base_opts[:start_date].add_months(@constructs[wi].wrapper_length)
|
332
|
+
end
|
333
|
+
elsif base_opts[:start_date].nil? && base_opts[:end_date] # for 6 months until jan 3rd
|
334
|
+
base_opts[:start_date] = case @constructs[wi].wrapper_type
|
335
|
+
when 2 then base_opts[:end_date].sub_days(@constructs[wi].wrapper_length)
|
336
|
+
when 3 then base_opts[:end_date].sub_weeks(@constructs[wi].wrapper_length)
|
337
|
+
when 4 then base_opts[:end_date].sub_months(@constructs[wi].wrapper_length)
|
338
|
+
end
|
339
|
+
end
|
340
|
+
end
|
341
|
+
end
|
342
|
+
base_opts
|
343
|
+
end
|
344
|
+
end
|
345
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
# Ruby Nickel Library
|
2
|
+
# Copyright (c) 2008-2011 Lou Zell, lzell11@gmail.com, http://hazelmade.com
|
3
|
+
# MIT License [http://www.opensource.org/licenses/mit-license.php]
|
4
|
+
|
5
|
+
module InstanceFromHash
|
6
|
+
|
7
|
+
def initialize(h)
|
8
|
+
h.each do |k,v|
|
9
|
+
instance_variable_set("@#{k}", v)
|
10
|
+
end
|
11
|
+
super()
|
12
|
+
end
|
13
|
+
end
|
data/lib/nickel/nlp.rb
ADDED
@@ -0,0 +1,73 @@
|
|
1
|
+
# Ruby Nickel Library
|
2
|
+
# Copyright (c) 2008-2011 Lou Zell, lzell11@gmail.com, http://hazelmade.com
|
3
|
+
# MIT License [http://www.opensource.org/licenses/mit-license.php]
|
4
|
+
|
5
|
+
module Nickel
|
6
|
+
|
7
|
+
class NLP
|
8
|
+
|
9
|
+
attr_reader :query, :input_date, :input_time, :nlp_query
|
10
|
+
attr_reader :construct_finder, :construct_interpreter
|
11
|
+
attr_reader :occurrences, :output
|
12
|
+
|
13
|
+
# Never, EVER change the default behavior to false; <-- then why did I put it here?
|
14
|
+
@use_date_correction = true
|
15
|
+
class << self; attr_accessor :use_date_correction; end
|
16
|
+
|
17
|
+
def initialize(query, date_time = Time.now)
|
18
|
+
raise InvalidDateTimeError unless [DateTime, Time].include?(date_time.class)
|
19
|
+
str_time = date_time.strftime("%Y%m%dT%H%M%S")
|
20
|
+
validate_input query, str_time
|
21
|
+
@query = query.dup
|
22
|
+
@input_date = ZDate.new str_time[0..7] # up to T, note format is already verified
|
23
|
+
@input_time = ZTime.new str_time[9..14] # after T
|
24
|
+
#setup_logger
|
25
|
+
end
|
26
|
+
|
27
|
+
def parse
|
28
|
+
@nlp_query = NLPQuery.new(@query).standardize # standardizes the query
|
29
|
+
@construct_finder = ConstructFinder.new(@nlp_query, @input_date, @input_time)
|
30
|
+
@construct_finder.run
|
31
|
+
@nlp_query.extract_message(@construct_finder.constructs)
|
32
|
+
@construct_interpreter = ConstructInterpreter.new(@construct_finder.constructs, @input_date) # input_date only needed for wrappers
|
33
|
+
@construct_interpreter.run
|
34
|
+
@occurrences = Occurrence.finalizer(@construct_interpreter.occurrences, @input_date) # finds start and end dates
|
35
|
+
@occurrences.sort! {|x,y| if x.start_date > y.start_date then 1 elsif x.start_date < y.start_date then -1 else 0 end} # sorts occurrences by start date
|
36
|
+
@output = @occurrences # legacy
|
37
|
+
@occurrences
|
38
|
+
end
|
39
|
+
|
40
|
+
def setup_logger
|
41
|
+
@logger = Logger.new(STDOUT)
|
42
|
+
def @logger.blue(a)
|
43
|
+
self.warn "\e[44m #{a.inspect} \e[0m"
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def inspect
|
48
|
+
"message: \"#{message}\", occurrences: #{occurrences.inspect}"
|
49
|
+
end
|
50
|
+
|
51
|
+
# Pass :inspect or :pretty_inspect as the inspect_method
|
52
|
+
def debug_str(inspect_method = :inspect)
|
53
|
+
"Current Date: #{self.input_date.readable}\nCurrent Time: #{self.input_time.readable}\n\nQuery: #{self.query}\nStandardized Query: #{self.nlp_query}\nQuery changed in: #{self.nlp_query.changed_in.inspect}\n\nConstructs Found: #{s = "\n"; self.construct_finder.constructs.each{|x| s << x.send(inspect_method) + "\n"}; s}\n\n@construct_interpreter: #{self.construct_interpreter.send(inspect_method)}"
|
54
|
+
end
|
55
|
+
|
56
|
+
def message
|
57
|
+
@nlp_query.message
|
58
|
+
end
|
59
|
+
|
60
|
+
private
|
61
|
+
def validate_input query, date_time
|
62
|
+
raise "Empty NLP query" unless query.length > 0
|
63
|
+
raise "NLP says: date_time is not in the correct format" unless date_time =~ /^\d{8}T\d{6}$/
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
class InvalidDateTimeError < StandardError
|
68
|
+
def message
|
69
|
+
"You must pass in a ruby DateTime or Time class object"
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
@@ -0,0 +1,90 @@
|
|
1
|
+
# Ruby Nickel Library
|
2
|
+
# Copyright (c) 2008-2011 Lou Zell, lzell11@gmail.com, http://hazelmade.com
|
3
|
+
# MIT License [http://www.opensource.org/licenses/mit-license.php]
|
4
|
+
|
5
|
+
module Nickel
|
6
|
+
|
7
|
+
class Occurrence
|
8
|
+
include InstanceFromHash
|
9
|
+
|
10
|
+
# Some notes about this class, @type can take the following values:
|
11
|
+
# :single, :daily, :weekly, :daymonthly, :datemonthly,
|
12
|
+
attr_accessor :type, :start_date, :end_date, :start_time, :end_time, :interval, :day_of_week, :week_of_month, :date_of_month
|
13
|
+
|
14
|
+
def initialize(h)
|
15
|
+
@start_date = nil # prevents warning in testing; but why is the warning there in the first place? Because I should be using instance_variable_defined in finalize method instead of checking for nil vals
|
16
|
+
super(h)
|
17
|
+
end
|
18
|
+
|
19
|
+
def inspect
|
20
|
+
str = %(\#<Occurrence type: #{type})
|
21
|
+
str << %(, start_date: "#{start_date.date}") if start_date
|
22
|
+
str << %(, end_date: "#{end_date.date}") if end_date
|
23
|
+
str << %(, start_time: "#{start_time.time}") if start_time
|
24
|
+
str << %(, end_time: "#{end_time.time}") if end_time
|
25
|
+
str << %(, interval: #{interval}) if interval
|
26
|
+
str << %(, day_of_week: #{day_of_week}) if day_of_week
|
27
|
+
str << %(, week_of_month: #{week_of_month}) if week_of_month
|
28
|
+
str << %(, date_of_month: #{date_of_month}) if date_of_month
|
29
|
+
str << ">"
|
30
|
+
str
|
31
|
+
end
|
32
|
+
|
33
|
+
|
34
|
+
def finalize(cur_date)
|
35
|
+
#@end_date = nil if @end_date.nil?
|
36
|
+
# one of the purposes of this method is to find a start date if it is not already specified
|
37
|
+
|
38
|
+
# case type
|
39
|
+
# when :daily then finalize_daily
|
40
|
+
# when :weekly then finalize_weekly
|
41
|
+
# when :daymonthly then finalize_daymonthly
|
42
|
+
# when :datemonthly then finalize_datemonthly
|
43
|
+
# end
|
44
|
+
|
45
|
+
|
46
|
+
if @type == :daily && @start_date.nil?
|
47
|
+
@start_date = cur_date
|
48
|
+
elsif @type == :weekly
|
49
|
+
if @start_date.nil?
|
50
|
+
@start_date = cur_date.this(@day_of_week)
|
51
|
+
else
|
52
|
+
@start_date = @start_date.this(@day_of_week) # this is needed in case someone said "every monday and wed starting DATE"; we want to find the first occurrence after DATE
|
53
|
+
end
|
54
|
+
if instance_variable_defined?("@end_date")
|
55
|
+
@end_date = @end_date.prev(@day_of_week) # find the real end date, if someone says "every monday until dec 1"; find the actual last occurrence
|
56
|
+
end
|
57
|
+
elsif @type == :datemonthly
|
58
|
+
if @start_date.nil?
|
59
|
+
if cur_date.day <= @date_of_month
|
60
|
+
@start_date = cur_date.add_days(@date_of_month - cur_date.day)
|
61
|
+
else
|
62
|
+
@start_date = cur_date.add_months(1).beginning_of_month.add_days(@date_of_month - 1)
|
63
|
+
end
|
64
|
+
else
|
65
|
+
if @start_date.day <= @date_of_month
|
66
|
+
@start_date = @start_date.add_days(@date_of_month - @start_date.day)
|
67
|
+
else
|
68
|
+
@start_date = @start_date.add_months(1).beginning_of_month.add_days(@date_of_month - 1)
|
69
|
+
end
|
70
|
+
end
|
71
|
+
elsif @type == :daymonthly
|
72
|
+
# in this case we also want to change @week_of_month val to -1 if it is currently 5. I used 5 to represent "last" in the previous version of the parser, but a more standard format is to use -1
|
73
|
+
@week_of_month = -1 if @week_of_month == 5
|
74
|
+
if @start_date.nil?
|
75
|
+
@start_date = cur_date.get_date_from_day_and_week_of_month(@day_of_week, @week_of_month)
|
76
|
+
else
|
77
|
+
@start_date = @start_date.get_date_from_day_and_week_of_month(@day_of_week, @week_of_month)
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
end
|
82
|
+
|
83
|
+
class << self
|
84
|
+
def finalizer(occurrences, cur_date)
|
85
|
+
occurrences.each {|occ| occ.finalize(cur_date)}
|
86
|
+
occurrences
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|