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.
@@ -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