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,319 @@
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 ZTime
8
+ # @firm will be used to indicate user provided am/pm
9
+ attr_accessor :firm
10
+
11
+ # @time is always stored on 24 hour clock, but we could initialize a Time object with ZTime.new("1020", :pm)
12
+ # we will convert this to 24 hour clock and set @firm = true
13
+ def initialize(hhmmss = nil, am_pm = nil)
14
+ t = hhmmss ? hhmmss : ::Time.new.strftime("%H%M%S")
15
+ t.gsub!(/:/,'') # remove any hyphens, so a user can initialize with something like "2008-10-23"
16
+ self.time = t
17
+ if am_pm then adjust_for(am_pm) end
18
+ end
19
+
20
+ def time
21
+ @time
22
+ end
23
+
24
+ def time=(hhmmss)
25
+ @time = lazy(hhmmss)
26
+ @firm = false
27
+ validate
28
+ end
29
+
30
+ def hour_str
31
+ @time[0..1]
32
+ end
33
+
34
+ def minute_str
35
+ @time[2..3]
36
+ end
37
+
38
+ def second_str
39
+ @time[4..5]
40
+ end
41
+
42
+ def hour
43
+ hour_str.to_i
44
+ end
45
+
46
+ def minute
47
+ minute_str.to_i
48
+ end
49
+
50
+ def second
51
+ second_str.to_i
52
+ end
53
+
54
+ # add_ methods return new ZTime object
55
+ # add_ methods take an optional block, the block will be passed the number of days that have passed;
56
+ # i.e. adding 48 hours will pass a 2 to the block, this is handy for something like this:
57
+ # time.add_hours(15) {|x| date.add_days(x)}
58
+ def add_minutes(number, &block)
59
+ # new minute is going to be (current minute + number) % 60
60
+ # number of hours to add is (current minute + number) / 60
61
+ hours_to_add = (self.minute + number) / 60
62
+ # note add_hours returns a new time object
63
+ if block_given?
64
+ o = self.add_hours(hours_to_add, &block)
65
+ else
66
+ o = self.add_hours(hours_to_add)
67
+ end
68
+ o.change_minute_to((o.minute + number) % 60) # modifies self
69
+ end
70
+
71
+ def add_hours(number, &block)
72
+ o = self.dup
73
+ if block_given?
74
+ yield((o.hour + number) / 24)
75
+ end
76
+ o.change_hour_to((o.hour + number) % 24)
77
+ end
78
+
79
+
80
+ # NOTE: change_ methods modify self.
81
+ def change_hour_to(h)
82
+ self.time = h.to_s2 + minute_str + second_str
83
+ self
84
+ end
85
+
86
+ def change_minute_to(m)
87
+ self.time = hour_str + m.to_s2 + second_str
88
+ self
89
+ end
90
+
91
+ def change_second_to(s)
92
+ self.time = hour_str + minute_str + s.to_s2
93
+ self
94
+ end
95
+
96
+ def readable
97
+ @time[0..1] + ":" + @time[2..3] + ":" + @time[4..5]
98
+ end
99
+
100
+ def readable_12hr
101
+ hour_on_12hr_clock.to_s2 + ":" + @time[2..3] + " #{am_pm}"
102
+ end
103
+
104
+ def hour_on_12hr_clock
105
+ h = hour % 12
106
+ h += 12 if h == 0
107
+ h
108
+ end
109
+
110
+ def is_am?
111
+ hour < 12 # 0 through 11 on 24hr clock
112
+ end
113
+
114
+ def am_pm
115
+ is_am? ? "am" : "pm"
116
+ end
117
+
118
+
119
+ def <(t2)
120
+ (self.hour < t2.hour) || (self.hour == t2.hour && (self.minute < t2.minute || (self.minute == t2.minute && self.second < t2.second)))
121
+ end
122
+
123
+ def <=(t2)
124
+ (self.hour < t2.hour) || (self.hour == t2.hour && (self.minute < t2.minute || (self.minute == t2.minute && self.second <= t2.second)))
125
+ end
126
+
127
+ def >(t2)
128
+ (self.hour > t2.hour) || (self.hour == t2.hour && (self.minute > t2.minute || (self.minute == t2.minute && self.second > t2.second)))
129
+ end
130
+
131
+ def >=(t2)
132
+ (self.hour > t2.hour) || (self.hour == t2.hour && (self.minute > t2.minute || (self.minute == t2.minute && self.second >= t2.second)))
133
+ end
134
+
135
+ def ==(t2)
136
+ self.hour == t2.hour && self.minute == t2.minute && self.second == t2.second
137
+ end
138
+
139
+ def <=>(t2)
140
+ if self < t2
141
+ -1
142
+ elsif self > t2
143
+ 1
144
+ else
145
+ 0
146
+ end
147
+ end
148
+
149
+ class << self
150
+
151
+ # send an array of ZTime objects, this will make a guess at whether they should be am/pm if the user did not specify
152
+ # NOTE ORDER IS IMPORTANT: times[0] is assumed to be BEFORE times[1]
153
+ def am_pm_modifier(*time_array)
154
+ # find firm time indices
155
+ firm_time_indices = []
156
+ time_array.each_with_index {|t,i| firm_time_indices << i if t.firm}
157
+
158
+ if firm_time_indices.empty?
159
+ # pure guess
160
+ # DO WE REALLY WANT TO DO THIS?
161
+ time_array.each_index do |i|
162
+ # user gave us nothing
163
+ next if i == 0
164
+ time_array[i].guess_modify_such_that_is_after(time_array[i-1])
165
+ end
166
+ else
167
+ # first handle soft times up to first firm time
168
+ min_boundary = 0
169
+ max_boundary = firm_time_indices[0]
170
+ (min_boundary...max_boundary).to_a.reverse.each do |i| # this says, iterate backwards starting from max_boundary, but not including it, until the min boundary
171
+ time_array[i].modify_such_that_is_before(time_array[i+1])
172
+ end
173
+
174
+ firm_time_indices.each_index do |j|
175
+ # now handle all times after first firm time until the next firm time
176
+ min_boundary = firm_time_indices[j]
177
+ max_boundary = firm_time_indices[j+1] || time_array.size
178
+ (min_boundary + 1...max_boundary).each do |i| # any boundary problems here? What if there is only 1 time? Nope.
179
+ time_array[i].modify_such_that_is_after(time_array[i-1])
180
+ end
181
+ end
182
+ end
183
+ end
184
+
185
+ def am_to_24hr(h)
186
+ # note 12am is 00
187
+ h % 12
188
+ end
189
+
190
+ def pm_to_24hr(h)
191
+ h == 12 ? 12 : h + 12
192
+ end
193
+ end
194
+
195
+ # this can very easily be cleaned up
196
+ def modify_such_that_is_before(time2)
197
+ raise "ZTime#modify_such_that_is_before says: trying to modify time that has @firm set" if @firm
198
+ raise "ZTime#modify_such_that_is_before says: time2 does not have @firm set" if !time2.firm
199
+ # self cannot have @firm set, so all hours will be between 1 and 12
200
+ # time2 is an end time, self could be its current setting, or off by 12 hours
201
+
202
+ # self to time2 --> self to time2
203
+ # 12 to 2am --> 1200 to 0200
204
+ # 12 to 12am --> 1200 to 0000
205
+ # 1220 to 12am --> 1220 to 0000
206
+ # 11 to 2am or 1100 to 0200
207
+ if self > time2
208
+ if self.hour == 12 && time2.hour == 0
209
+ # do nothing
210
+ else
211
+ self.hour == 12 ? change_hour_to(0) : change_hour_to(self.hour + 12)
212
+ end
213
+ elsif self < time2
214
+ if time2.hour >= 12 && ZTime.new((time2.hour - 12).to_s2 + time2.minute_str + time2.second_str) > self
215
+ # 4 to 5pm or 0400 to 1700
216
+ change_hour_to(self.hour + 12)
217
+ else
218
+ # 4 to 1pm or 0400 to 1300
219
+ # do nothing
220
+ end
221
+ else
222
+ # the times are equal, and self can only be between 0100 and 1200, so move self forward 12 hours, unless hour is 12
223
+ self.hour == 12 ? change_hour_to(0) : change_hour_to(self.hour + 12)
224
+ end
225
+ self.firm = true
226
+ self
227
+ end
228
+
229
+ def modify_such_that_is_after(time1)
230
+ raise "ZTime#modify_such_that_is_after says: trying to modify time that has @firm set" if @firm
231
+ raise "ZTime#modify_such_that_is_after says: time1 does not have @firm set" if !time1.firm
232
+ # time1 to self --> time1 to self
233
+ # 8pm to 835 --> 2000 to 835
234
+ # 835pm to 835 --> 2035 to 835
235
+ # 10pm to 11 --> 2200 to 1100
236
+ # 1021pm to 1223--> 2221 to 1223
237
+ # 930am to 5 ---> 0930 to 0500
238
+ # 930pm to 5 ---> 2130 to 0500
239
+ if self < time1
240
+ unless time1.hour >= 12 && ZTime.new((time1.hour - 12).to_s2 + time1.minute_str + time1.second_str) >= self
241
+ self.hour == 12 ? change_hour_to(0) : change_hour_to(self.hour + 12)
242
+ end
243
+ elsif self > time1
244
+ # # time1 to self --> time1 to self
245
+ # # 10am to 11 --> 1000 to 1100
246
+ # #
247
+ # if time1.hour >= 12 && ZTime.new((time1.hour - 12).to_s2 + time1.minute_str + time1.second_str) > self
248
+ # change_hour_to(self.hour + 12)
249
+ # else
250
+ # # do nothing
251
+ # end
252
+ else
253
+ # the times are equal, and self can only be between 0100 and 1200, so move self forward 12 hours, unless hour is 12
254
+ self.hour == 12 ? change_hour_to(0) : change_hour_to(self.hour + 12)
255
+ end
256
+ self.firm = true
257
+ self
258
+ end
259
+
260
+ # use this if we don't have a firm time to modify off
261
+ def guess_modify_such_that_is_after(time1)
262
+ # time1 to self time1 to self
263
+ # 9 to 5 --> 0900 to 0500
264
+ # 9 to 9 --> 0900 to 0900
265
+ # 12 to 12 --> 1200 to 1200
266
+ # 12 to 6 ---> 1200 to 0600
267
+ if time1 >= self
268
+ # crossed boundary at noon
269
+ self.hour == 12 ? change_hour_to(0) : change_hour_to(self.hour + 12)
270
+ end
271
+ end
272
+
273
+ private
274
+
275
+ def adjust_for(am_pm)
276
+ # how does validation work? Well, we already know that @time is valid, and once we modify we call time= which will
277
+ # perform validation on the new time. That won't catch something like this though: ZTime.new("2215", :am)
278
+ # so we will check for that here.
279
+ # If user is providing :am or :pm, the hour must be between 1 and 12
280
+ raise "ZTime#adjust_for says: you specified am or pm with 1 > hour > 12" unless hour >= 1 && hour <= 12
281
+ if am_pm == :am || am_pm == 'am'
282
+ change_hour_to(ZTime.am_to_24hr(self.hour))
283
+ elsif am_pm == :pm || am_pm == 'pm'
284
+ change_hour_to(ZTime.pm_to_24hr(self.hour))
285
+ else
286
+ raise "ZTime#adjust_for says: you passed an invalid value for am_pm, use :am or :pm"
287
+ end
288
+ @firm = true
289
+ end
290
+
291
+ def validate
292
+ raise "ZTime#validate says: invalid time" unless valid
293
+ end
294
+
295
+ def valid
296
+ @time.length == 6 && @time !~ /\D/ && valid_hour && valid_minute && valid_second
297
+ end
298
+
299
+ def valid_hour
300
+ hour >= 0 and hour < 24
301
+ end
302
+
303
+ def valid_minute
304
+ minute >= 0 and minute < 60
305
+ end
306
+
307
+ def valid_second
308
+ second >= 0 and second < 60
309
+ end
310
+
311
+ def lazy(s)
312
+ # someone isn't following directions, but we will let it slide
313
+ s.length == 1 && s = "0#{s}0000" # only provided h
314
+ s.length == 2 && s << "0000" # only provided hh
315
+ s.length == 4 && s << "00" # only provided hhmm
316
+ return s
317
+ end
318
+ end
319
+ end
data/lib/nickel.rb CHANGED
@@ -1,41 +1,37 @@
1
- require 'rubygems'
2
- require 'mapricot'
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
+ # Usage:
6
+ #
7
+ # Nickel.parse "some query", Time.local(2011, 7, 1)
8
+ #
9
+ # The second term is optional.
3
10
 
4
- module Nickel
5
- VERSION = "0.0.3"
11
+ require 'logger'
12
+ require 'date'
6
13
 
7
- def self.query(q, current_time = Time.now)
8
- raise InvalidDateTimeError unless [DateTime, Time].include?(current_time.class)
9
- url = "http://naturalinputs.com/query?q=#{URI.escape(q)}&t=#{current_time.strftime("%Y%m%dT%H%M%S")}"
10
- Mapricot.parser = :libxml
11
- Api::NaturalInputsResponse.new(:url => url)
12
- end
13
- end
14
+ path = File.expand_path(File.join(File.dirname(__FILE__), 'nickel'))
14
15
 
16
+ require File.join(path, 'ruby_ext', 'to_s2.rb')
17
+ require File.join(path, 'ruby_ext', 'calling_method.rb')
18
+ require File.join(path, 'zdate.rb')
19
+ require File.join(path, 'ztime.rb')
20
+ require File.join(path, 'instance_from_hash')
21
+ require File.join(path, 'query_constants')
22
+ require File.join(path, 'query')
23
+ require File.join(path, 'construct')
24
+ require File.join(path, 'construct_finder')
25
+ require File.join(path, 'construct_interpreter')
26
+ require File.join(path, 'occurrence')
27
+ require File.join(path, 'nlp.rb')
15
28
 
16
29
  module Nickel
17
- module Api
18
- class NaturalInputsResponse < Mapricot::Base
19
- has_one :message
20
- has_many :occurrences, :xml
30
+ class << self
31
+ def parse(query, date_time = Time.now)
32
+ n = NLP.new(query, date_time)
33
+ n.parse
34
+ n
21
35
  end
22
-
23
- class Occurrence < Mapricot::Base
24
- has_one :type
25
- has_one :start_date
26
- has_one :end_date
27
- has_one :start_time
28
- has_one :end_time
29
- has_one :day_of_week
30
- has_one :week_of_month, :integer
31
- has_one :date_of_month, :integer
32
- has_one :interval, :integer
33
- end
34
- end
36
+ end
35
37
  end
36
-
37
- class InvalidDateTimeError < StandardError
38
- def message
39
- "You must pass in a ruby DateTime or Time class object"
40
- end
41
- end
data/nickel.gemspec CHANGED
@@ -1,20 +1,40 @@
1
1
  Gem::Specification.new do |s|
2
2
  s.name = "nickel"
3
- s.version = "0.0.3"
4
- s.summary = "Natural language date and time parsing"
3
+ s.version = "0.0.4"
4
+ s.summary = "Natural language date, time, and message parsing."
5
5
  s.email = "lzell11@gmail.com"
6
6
  s.homepage = "http://github.com/lzell/nickel"
7
- s.description = "A client for naturalinputs.com. Extracts date, time, and message information from naturally worded text."
7
+ s.description = "Extracts date, time, and message information from naturally worded text."
8
8
  s.has_rdoc = true
9
9
  s.authors = ["Lou Zell"]
10
- s.files = ["README.rdoc",
11
- "History.txt",
12
- "License.txt",
13
- "nickel.gemspec",
14
- "test/nickel_spec.rb",
15
- "lib/nickel.rb"]
10
+ s.files =
11
+ [
12
+ "History.txt",
13
+ "License.txt",
14
+ "README.rdoc",
15
+ "Rakefile",
16
+ "nickel.gemspec",
17
+ "lib/nickel.rb",
18
+ "lib/nickel/construct.rb",
19
+ "lib/nickel/construct_finder.rb",
20
+ "lib/nickel/construct_interpreter.rb",
21
+ "lib/nickel/instance_from_hash.rb",
22
+ "lib/nickel/nlp.rb",
23
+ "lib/nickel/occurrence.rb",
24
+ "lib/nickel/query.rb",
25
+ "lib/nickel/query_constants.rb",
26
+ "lib/nickel/zdate.rb",
27
+ "lib/nickel/ztime.rb",
28
+ "lib/nickel/ruby_ext/calling_method.rb",
29
+ "lib/nickel/ruby_ext/to_s2.rb",
30
+ "test/compare.rb",
31
+ "test/nlp_test.rb",
32
+ "test/nlp_tests_helper.rb",
33
+ "test/zdate_test.rb",
34
+ "test/ztime_test.rb",
35
+ "spec/nickel_spec.rb"
36
+ ]
16
37
  s.require_paths = ["lib"]
17
38
  s.rdoc_options = ["--main", "README.rdoc", "--title", "Nickel"]
18
39
  s.extra_rdoc_files = ["README.rdoc"]
19
- s.add_dependency("mapricot")
20
40
  end
@@ -1,5 +1,5 @@
1
- require 'rubygems'
2
- require 'spec'
1
+ # Run with:
2
+ # ~/dev/nickel(master)$ rspec test/nickel_spec.rb
3
3
  require File.expand_path(File.dirname(__FILE__) + "/../lib/nickel")
4
4
 
5
5
 
@@ -161,5 +161,5 @@ describe "Setting current time" do
161
161
  Nickel.query "lunch 3 days from now", Date.new(2009,05,28)
162
162
  }.should raise_error("You must pass in a ruby DateTime or Time class object")
163
163
  end
164
-
165
- end
164
+ end
165
+
data/test/compare.rb ADDED
@@ -0,0 +1,109 @@
1
+ module Compare
2
+
3
+ # Use this to compare two objects, it will check their classes, instance vars, methods and instance var values
4
+ # to make sure the objects are "the same", they DO NOT have to share the same object_id, that's the point.
5
+ # NOTE: This won't work on any base classes (e.g. String, Fixnum, Array)
6
+ # I could put in a quick fix, it would check for base class and then just use "==" operator
7
+ class Objects
8
+
9
+ def initialize(object1, object2)
10
+ @o1, @o2 = object1, object2
11
+ end
12
+
13
+ # class level access
14
+ class << self
15
+ def same?(object1, object2)
16
+ new(object1, object2).same?
17
+ end
18
+ end
19
+
20
+ def same?
21
+ same_class? && same_content?
22
+ end
23
+
24
+ def same_class?
25
+ @o1.class.name == @o2.class.name
26
+ end
27
+
28
+ def same_content?
29
+ if @o1.class.name =~ /Array|Fixnum|Hash|String/
30
+ @o1 == @o2
31
+ else
32
+ same_methods? && same_instance_variables? && same_instance_variable_values?
33
+ end
34
+ end
35
+
36
+ def same_methods?
37
+ # @o1.methods == @o2.methods WOW! that was a bug, [1,2] is not the same as [2,1] <-- I find it hard to believe I didn't know this!
38
+ @o1.methods.sort == @o2.methods.sort
39
+ end
40
+
41
+ def same_instance_variables?
42
+ # @o1.instance_variables == @o2.instance_variables # BAD BAD BAD
43
+ @o1.instance_variables.sort == @o2.instance_variables.sort
44
+ end
45
+
46
+ def same_instance_variable_values?
47
+ same = true
48
+ @o1.instance_variables.each do |ivar|
49
+ o1_var_val = @o1.instance_variable_get(ivar)
50
+ o2_var_val = @o2.instance_variable_get(ivar)
51
+ if o1_var_val == o2_var_val # if they are the same by "==" operator, we are fine, note that Z::Time now has == so it won't go through recursion, meaning we may miss @firm setting
52
+ elsif o1_var_val.class.name !~ /Array|Fixnum|Hash|String/ && Objects.same?(o1_var_val, o2_var_val) # instance vars are objects other than base class! Recursion!
53
+ else
54
+ same = false # no match by "==" or by compare objects
55
+ end
56
+ end
57
+ return same
58
+ end
59
+ end
60
+
61
+ # Use this to compare an array of objects, e.g. [object1, object2] and [object3, object4]
62
+ # note order is not important, both of these cases would return true:
63
+ # object1 same as object3
64
+ # object2 same as object4
65
+ # OR
66
+ # object1 same as object4
67
+ # object2 same as object3
68
+ class ArrayofObjects
69
+
70
+ def initialize(array1, array2)
71
+ @a1, @a2 = array1.dup, array2.dup
72
+ end
73
+
74
+ class << self
75
+ def same?(array1, array2)
76
+ new(array1, array2).same?
77
+ end
78
+ end
79
+
80
+ def same?
81
+ equal_size? && equal_objects?
82
+ end
83
+
84
+ def equal_size?
85
+ @a1.size == @a2.size
86
+ end
87
+
88
+ def equal_objects?
89
+ same = true
90
+ @a1.size.times do
91
+ unless first_element_in_a1_has_match_in_a2 then same = false end
92
+ end
93
+ same
94
+ end
95
+
96
+ def first_element_in_a1_has_match_in_a2
97
+ has_match = false
98
+ @a2.size.times do |i|
99
+ if Objects.same?(@a1[0], @a2[i])
100
+ has_match = true
101
+ @a1.shift # we are removing the matching elements from @a1 and @a2 so they don't match anything else in next iterations
102
+ @a2.delete_at(i)
103
+ break
104
+ end
105
+ end
106
+ has_match
107
+ end
108
+ end
109
+ end