dcparker-days_and_times 1.0.0
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/History.txt +4 -0
- data/License.txt +20 -0
- data/Manifest.txt +13 -0
- data/README.txt +68 -0
- data/Rakefile +36 -0
- data/lib/days_and_times.rb +13 -0
- data/lib/days_and_times/duration.rb +367 -0
- data/lib/days_and_times/numeric.rb +48 -0
- data/lib/days_and_times/object.rb +19 -0
- data/lib/days_and_times/time.rb +137 -0
- data/spec/days_and_times/numeric_spec.rb +9 -0
- data/spec/days_and_times_spec.rb +80 -0
- data/spec/spec_helper.rb +1 -0
- metadata +77 -0
data/History.txt
ADDED
data/License.txt
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2008 BehindLogic
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/Manifest.txt
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
History.txt
|
2
|
+
License.txt
|
3
|
+
Manifest.txt
|
4
|
+
README.txt
|
5
|
+
Rakefile
|
6
|
+
lib/days_and_times.rb
|
7
|
+
lib/days_and_times/duration.rb
|
8
|
+
lib/days_and_times/numeric.rb
|
9
|
+
lib/days_and_times/object.rb
|
10
|
+
lib/days_and_times/time.rb
|
11
|
+
spec/days_and_times/numeric_spec.rb
|
12
|
+
spec/days_and_times_spec.rb
|
13
|
+
spec/spec_helper.rb
|
data/README.txt
ADDED
@@ -0,0 +1,68 @@
|
|
1
|
+
= days_and_times
|
2
|
+
|
3
|
+
* http://github.com/dcparker/days_and_times
|
4
|
+
|
5
|
+
== DESCRIPTION:
|
6
|
+
|
7
|
+
Natural language method chaining for Time, Durations and the like.
|
8
|
+
|
9
|
+
== FEATURES/PROBLEMS:
|
10
|
+
|
11
|
+
* Acts on singular and plural of the following methods: second, minute, hour, day, week.
|
12
|
+
* Months are variable and do not lend themselves to concrete mathmatics, so they are not implemented.
|
13
|
+
* The Duration class holds many treasures and enables us in many ways.
|
14
|
+
* Perform mathematical operations between numbers and durations, durations and durations, and durations and numbers, and the 'unit' will be respected as expected in the algebraic rules.
|
15
|
+
* Iterators are implemented on Duration objects, respecting the unit of time the duration was instantiated with, or using a customized iterator unit.
|
16
|
+
* Durations can be 'anchored' to a starting time for convenience in many usages.
|
17
|
+
|
18
|
+
== SYNOPSIS:
|
19
|
+
|
20
|
+
1.day #=> A duration of 1 day
|
21
|
+
7.days #=> A duration of 7 days
|
22
|
+
1.week #=> A duration of 1 week
|
23
|
+
1.week - 2.days #=> A duration of 5 days
|
24
|
+
1.week.from(Now()) #=> The time of 1 week from this moment
|
25
|
+
1.week.from(Today()) #=> The time of 1 week from the beginning of today
|
26
|
+
3.minutes.ago.until(7.minutes.from(Now())) #=> duration 3 minutes ago to 7 minutes from now
|
27
|
+
3.minutes.ago.until(7.minutes.from(Now())) - 2.minutes #=> duration 3 minutes ago to 5 minutes from now
|
28
|
+
4.weeks.from(2.days.from(Now())).until(8.weeks.from(Yesterday())) #=> A duration, starting in 4 weeks and 2 days, and ending 8 weeks from yesterday
|
29
|
+
1.week - 1.second #=> A duration of 6 days, 23 hours, 59 minutes, and 59 seconds
|
30
|
+
4.weeks / 2 #=> A duration of 2 weeks
|
31
|
+
4.weeks / 2.weeks #=> The integer 2
|
32
|
+
8.weeks.each {|week| ...} #=> Runs code for each week contained in the duration (of 8 weeks)
|
33
|
+
8.weeks.starting(Now()).each {|week| ...} #=> Runs code for each week in the duration, but each week is also anchored to a starting time, in sequence through the duration.
|
34
|
+
1.week.each {|week| ...} #=> Automatically chooses week as its iterator
|
35
|
+
7.days.each {|day| ...} #=> Automatically chooses day as its iterator
|
36
|
+
1.week.each_day {|day| ...} #=> Forcing the week to iterate through days
|
37
|
+
1.week.each(10.hours) {|ten_hour_segment| ...} #=> Using a custom iterator of 10 hours. There would be 17 of them, but notice that the last iteration will only be 8 hours.
|
38
|
+
# ... and more!
|
39
|
+
|
40
|
+
== INSTALL:
|
41
|
+
|
42
|
+
gem sources -a http://gems.github.com
|
43
|
+
sudo gem install dcparker-days_and_times
|
44
|
+
|
45
|
+
== LICENSE:
|
46
|
+
|
47
|
+
(The MIT License)
|
48
|
+
|
49
|
+
Copyright (c) 2008 BehindLogic
|
50
|
+
|
51
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
52
|
+
a copy of this software and associated documentation files (the
|
53
|
+
'Software'), to deal in the Software without restriction, including
|
54
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
55
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
56
|
+
permit persons to whom the Software is furnished to do so, subject to
|
57
|
+
the following conditions:
|
58
|
+
|
59
|
+
The above copyright notice and this permission notice shall be
|
60
|
+
included in all copies or substantial portions of the Software.
|
61
|
+
|
62
|
+
THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
|
63
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
64
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
65
|
+
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
66
|
+
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
67
|
+
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
68
|
+
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/Rakefile
ADDED
@@ -0,0 +1,36 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'hoe'
|
3
|
+
require './lib/days_and_times.rb'
|
4
|
+
|
5
|
+
Dir['tasks/**/*.rake'].each { |rake| load rake }
|
6
|
+
|
7
|
+
Hoe.new('days_and_times', DaysAndTimes.VERSION) do |p|
|
8
|
+
p.author = 'Daniel Parker'
|
9
|
+
p.email = 'gems@behindlogic.com'
|
10
|
+
p.summary = "Natural language method chaining for Time, Durations and the like."
|
11
|
+
p.description = "Natural language method chaining for Time, Durations and the like."
|
12
|
+
p.url = 'http://github.com/dcparker/days_and_times'
|
13
|
+
p.changes = p.paragraphs_of('History.txt', 0..1).join("\n\n")
|
14
|
+
end
|
15
|
+
|
16
|
+
desc "Generate gemspec"
|
17
|
+
task :gemspec do |x|
|
18
|
+
# Check the manifest before generating the gemspec
|
19
|
+
manifest = %x[rake check_manifest]
|
20
|
+
manifest.gsub!(/\(in [^\)]+\)\n/, "")
|
21
|
+
|
22
|
+
unless manifest.empty?
|
23
|
+
print "\n", "#"*68, "\n"
|
24
|
+
print <<-EOS
|
25
|
+
Manifest.txt is not up-to-date. Please review the changes below.
|
26
|
+
If the changes are correct, run 'rake check_manifest | patch'
|
27
|
+
and then run this command again.
|
28
|
+
EOS
|
29
|
+
print "#"*68, "\n\n"
|
30
|
+
puts manifest
|
31
|
+
else
|
32
|
+
gemspec = `rake debug_gem`
|
33
|
+
gemspec.gsub!(/\(in [^\)]+\)\n/, "")
|
34
|
+
File.open("days_and_times.gemspec", 'w') {|f| f.write(gemspec) }
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
$:.unshift(File.dirname(__FILE__)) unless
|
2
|
+
$:.include?(File.dirname(__FILE__)) || $:.include?(File.expand_path(File.dirname(__FILE__)))
|
3
|
+
|
4
|
+
module DaysAndTimes
|
5
|
+
def self.VERSION
|
6
|
+
'1.0.0'
|
7
|
+
end
|
8
|
+
end
|
9
|
+
|
10
|
+
require 'days_and_times/duration'
|
11
|
+
require 'days_and_times/numeric'
|
12
|
+
require 'days_and_times/time'
|
13
|
+
require 'days_and_times/object'
|
@@ -0,0 +1,367 @@
|
|
1
|
+
require 'time'
|
2
|
+
class Duration
|
3
|
+
# Length is the length of the time span, in seconds
|
4
|
+
# Unit is a length of time (in seconds) to use in collection methods
|
5
|
+
# StartTime is an optional attribute that can 'anchor' a duration
|
6
|
+
# to a specific real time.
|
7
|
+
attr_accessor :length, :unit, :start_time
|
8
|
+
def initialize(count=0,unit=1,start_time=nil,auto_klass={})
|
9
|
+
if unit.is_a?(Time) || unit.is_a?(DateTime)
|
10
|
+
start_time = unit
|
11
|
+
unit = 1
|
12
|
+
end
|
13
|
+
options = {:count => count || 0, :unit => unit || 1, :start_time => start_time}.merge(count.is_a?(Hash) ? count : {})
|
14
|
+
|
15
|
+
@unit = options[:unit]
|
16
|
+
@length = (@unit * options[:count].to_f).round
|
17
|
+
@start_time = options[:start_time]
|
18
|
+
end
|
19
|
+
def self.new(*args)
|
20
|
+
a = super
|
21
|
+
if self.name == 'Duration' && (args.last.is_a?(Hash) ? args.last[:auto_class] == true : true)
|
22
|
+
a.send(:auto_class)
|
23
|
+
else
|
24
|
+
a
|
25
|
+
end
|
26
|
+
end
|
27
|
+
def self.length
|
28
|
+
1
|
29
|
+
end
|
30
|
+
|
31
|
+
# * * * * * * * * * * * * * * * *
|
32
|
+
# A Duration is a LENGTH of Time
|
33
|
+
# -This is measured in seconds,
|
34
|
+
# but set in terms of the unit
|
35
|
+
# currently being used.
|
36
|
+
def length=(value)
|
37
|
+
if value.respond_to?(:to_f)
|
38
|
+
@length = (self.unit * value.to_f).round
|
39
|
+
else
|
40
|
+
raise TypeError, "Can't set a Duration's length to a #{value.class.name} object."
|
41
|
+
end
|
42
|
+
end
|
43
|
+
def length
|
44
|
+
length = @length.to_f / self.unit
|
45
|
+
length.to_i == length ? length.to_i : length
|
46
|
+
end
|
47
|
+
def abs_length=(value)
|
48
|
+
if value.respond_to?(:to_i)
|
49
|
+
@length = value.to_i
|
50
|
+
else
|
51
|
+
raise TypeError, "Can't set a Duration's length to a #{value.class.name} object."
|
52
|
+
end
|
53
|
+
end
|
54
|
+
def abs_length
|
55
|
+
@length
|
56
|
+
end
|
57
|
+
def to_i
|
58
|
+
self.abs_length.to_i
|
59
|
+
end
|
60
|
+
def to_f
|
61
|
+
self.abs_length.to_f
|
62
|
+
end
|
63
|
+
def to_s
|
64
|
+
"#{self.length} #{self.class.name}"
|
65
|
+
end
|
66
|
+
def coerce(*args)
|
67
|
+
to_f.coerce(*args)
|
68
|
+
end
|
69
|
+
|
70
|
+
def ===(other)
|
71
|
+
self.to_f == other.to_f
|
72
|
+
end
|
73
|
+
def inspect
|
74
|
+
"#<#{self.class.name}:#{self.object_id} (length=#{self.length.inspect}) #{self.instance_variables.reject {|d| d=='@length' || self.instance_variable_get(d).nil?}.collect {|iv| "#{iv}=#{self.instance_variable_get(iv).inspect}"}.join(' ')}>"
|
75
|
+
end
|
76
|
+
# * * * * * * * * * * * * * * * *
|
77
|
+
|
78
|
+
# * * * * * * * * * * * * * * * * * * * * *
|
79
|
+
# A Duration's calculations utilize a UNIT
|
80
|
+
# -This is stored as the number of
|
81
|
+
# seconds equal to the unit's value.
|
82
|
+
def unit=(value)
|
83
|
+
if value.respond_to?(:to_i)
|
84
|
+
@unit = value.to_i
|
85
|
+
else
|
86
|
+
raise TypeError, "Can't set a Duration's unit to a #{value.class.name} object."
|
87
|
+
end
|
88
|
+
end
|
89
|
+
def -(value)
|
90
|
+
if value.respond_to?(:to_i)
|
91
|
+
auto_class(Duration.new(@length - value.to_i))
|
92
|
+
else
|
93
|
+
raise TypeError, "Can't convert #{value.class.name} to an integer."
|
94
|
+
end
|
95
|
+
end
|
96
|
+
def +(value)
|
97
|
+
if value.respond_to?(:to_i)
|
98
|
+
auto_class(Duration.new(@length + value.to_i))
|
99
|
+
else
|
100
|
+
raise TypeError, "Can't convert #{value.class.name} to an integer."
|
101
|
+
end
|
102
|
+
end
|
103
|
+
def *(value)
|
104
|
+
if value.is_a?(Duration)
|
105
|
+
@length * value.length * value.unit
|
106
|
+
elsif value.respond_to?(:to_i)
|
107
|
+
auto_class(Duration.new(@length * value))
|
108
|
+
else
|
109
|
+
raise TypeError, "Can't convert #{value.class.name} to an integer."
|
110
|
+
end
|
111
|
+
end
|
112
|
+
def /(value)
|
113
|
+
if value.is_a?(Duration)
|
114
|
+
@length / (value.length * value.unit)
|
115
|
+
elsif value.respond_to?(:to_i)
|
116
|
+
auto_class(Duration.new(@length / value))
|
117
|
+
else
|
118
|
+
raise TypeError, "Can't convert #{value.class.name} to an integer."
|
119
|
+
end
|
120
|
+
end
|
121
|
+
def in_weeks
|
122
|
+
self.unit = Week.length
|
123
|
+
auto_class(self)
|
124
|
+
end
|
125
|
+
def in_days
|
126
|
+
self.unit = Day.length
|
127
|
+
auto_class(self)
|
128
|
+
end
|
129
|
+
def in_hours
|
130
|
+
self.unit = Hour.length
|
131
|
+
auto_class(self)
|
132
|
+
end
|
133
|
+
def in_minutes
|
134
|
+
self.unit = Minute.length
|
135
|
+
auto_class(self)
|
136
|
+
end
|
137
|
+
def in_seconds
|
138
|
+
self.unit = Second.length
|
139
|
+
auto_class(self)
|
140
|
+
end
|
141
|
+
def weeks
|
142
|
+
@length.to_f / Week.length
|
143
|
+
end
|
144
|
+
def days
|
145
|
+
@length.to_f / Day.length
|
146
|
+
end
|
147
|
+
def hours
|
148
|
+
@length.to_f / Hour.length
|
149
|
+
end
|
150
|
+
def minutes
|
151
|
+
@length.to_f / Minute.length
|
152
|
+
end
|
153
|
+
def seconds
|
154
|
+
@length.to_f / Second.length
|
155
|
+
end
|
156
|
+
# * * * * * * * * * * * * * * * * * * * * *
|
157
|
+
|
158
|
+
# * * * * * * * * * * * * * * * * * * * * * * *
|
159
|
+
# A Duration can be 'anchored' to a START_TIME
|
160
|
+
# -This start_time is a Time object
|
161
|
+
def start_time=(value)
|
162
|
+
if value.is_a?(Time) || value.is_a?(DateTime)
|
163
|
+
@start_time = value.to_time
|
164
|
+
else
|
165
|
+
raise TypeError, "A Duration's start_time must be a Time or DateTime object."
|
166
|
+
end
|
167
|
+
end
|
168
|
+
def end_time=(value)
|
169
|
+
if value.is_a?(Time) || value.is_a?(DateTime)
|
170
|
+
@start_time = value.to_time - self #Subtracts this duration from the end_time to get the start_time
|
171
|
+
else
|
172
|
+
raise TypeError, "A Duration's end_time must be a Time or DateTime object."
|
173
|
+
end
|
174
|
+
end
|
175
|
+
def end_time
|
176
|
+
@start_time + self
|
177
|
+
end
|
178
|
+
def anchored?
|
179
|
+
!self.start_time.nil?
|
180
|
+
end
|
181
|
+
# * * * * * * * * * * * * * * * * * * * * * * *
|
182
|
+
|
183
|
+
# * * * * * * * * * * * * * * * * * * * * * * * *
|
184
|
+
# Calculations using Duration as an intermediate
|
185
|
+
def from(time)
|
186
|
+
time + @length
|
187
|
+
end
|
188
|
+
def before(time)
|
189
|
+
time - @length
|
190
|
+
end
|
191
|
+
def from_now
|
192
|
+
self.from(Time.now)
|
193
|
+
end
|
194
|
+
def ago
|
195
|
+
self.before(Time.now)
|
196
|
+
end
|
197
|
+
def starting(time)
|
198
|
+
self.start_time = time
|
199
|
+
self
|
200
|
+
end
|
201
|
+
def ending(time)
|
202
|
+
self.end_time = time
|
203
|
+
self
|
204
|
+
end
|
205
|
+
# * * * * * * * * * * * * * * * * * * * * * * * *
|
206
|
+
|
207
|
+
# * * * * * * * * * * * * * * * * * * * * * * * * * * *
|
208
|
+
# A Duration can be treated as a 'collection' of units
|
209
|
+
def each_week(&block)
|
210
|
+
self.each(Week.length,&block)
|
211
|
+
end
|
212
|
+
def each_day(&block)
|
213
|
+
self.each(Day.length,&block)
|
214
|
+
end
|
215
|
+
def each_hour(&block)
|
216
|
+
self.each(Hour.length,&block)
|
217
|
+
end
|
218
|
+
def each_minute(&block)
|
219
|
+
self.each(Minute.length,&block)
|
220
|
+
end
|
221
|
+
def each_second(&block)
|
222
|
+
self.each(Second.length,&block)
|
223
|
+
end
|
224
|
+
def collect(use_unit=self.class.length,&block)
|
225
|
+
ary = []
|
226
|
+
self.each(use_unit) do |x|
|
227
|
+
ary << (block_given? ? yield(x) : x)
|
228
|
+
end
|
229
|
+
ary
|
230
|
+
end
|
231
|
+
def each(use_unit=self.class.length)
|
232
|
+
remainder = @length.to_f % use_unit
|
233
|
+
ret = []
|
234
|
+
if self.start_time.nil?
|
235
|
+
(@length.to_f / use_unit).to_i.times do |i|
|
236
|
+
ret << Duration.new(1, use_unit)
|
237
|
+
yield(ret[-1])
|
238
|
+
end
|
239
|
+
else
|
240
|
+
(@length.to_f / use_unit).to_i.times do |i|
|
241
|
+
ret << Duration.new(1, use_unit, (self.start_time + (use_unit * i)))
|
242
|
+
yield(ret[-1])
|
243
|
+
end
|
244
|
+
end
|
245
|
+
if remainder > 0
|
246
|
+
ret << (self.start_time.nil? ? Duration.new(remainder, 1) : Duration.new(remainder, 1, (self.start_time + @length - remainder)))
|
247
|
+
yield(ret[-1])
|
248
|
+
end
|
249
|
+
ret
|
250
|
+
end
|
251
|
+
# * * * * * * * * * * * * * * * * * * * * * * * * * * *
|
252
|
+
|
253
|
+
# * * * * * * * * * * * * * * * * * * * * * * * * * * *
|
254
|
+
# Through some ingenious metacoding (see 'def bind_object_method') below,
|
255
|
+
# it is possible to create a new method on a Duration object
|
256
|
+
# to a method on another object, in order to gather information
|
257
|
+
# based on the duration mentioned.
|
258
|
+
def create_find_within_method_for(other, method_name, other_method_name)
|
259
|
+
self.bind_object_method(other, method_name, other_method_name, [[], ['self.start_time', 'self.end_time']])
|
260
|
+
end
|
261
|
+
def self.create_find_within_method_for(other, method_name, other_method_name)
|
262
|
+
self.bind_class_object_method(other, method_name, other_method_name, [[], ['self.start_time', 'self.end_time']])
|
263
|
+
end
|
264
|
+
# * * * * * * * * * * * * * * * * * * * * * * * * * * *
|
265
|
+
|
266
|
+
def method_missing(method_name, *args)
|
267
|
+
# Delegate any missing methods to the start_time Time object, if we have a start_time and the method exists there.
|
268
|
+
return self.start_time.send(method_name, *args) if self.anchored? && self.start_time.respond_to?(method_name)
|
269
|
+
super
|
270
|
+
end
|
271
|
+
|
272
|
+
private
|
273
|
+
def auto_class(obj=self)
|
274
|
+
new_obj = case obj.unit
|
275
|
+
when 1
|
276
|
+
obj.class.name == 'Seconds' ? obj : (obj.length == 1 ? Second.new(obj.start_time) : Seconds.new(obj.length,obj.start_time))
|
277
|
+
when 60
|
278
|
+
obj.class.name == 'Minutes' ? obj : (obj.length == 1 ? Minute.new(obj.start_time) : Minutes.new(obj.length,obj.start_time))
|
279
|
+
when 3600
|
280
|
+
obj.class.name == 'Hours' ? obj : (obj.length == 1 ? Hour.new(obj.start_time) : Hours.new(obj.length,obj.start_time))
|
281
|
+
when 86400
|
282
|
+
obj.class.name == 'Days' ? obj : (obj.length == 1 ? Day.new(obj.start_time) : Days.new(obj.length,obj.start_time))
|
283
|
+
when 604800
|
284
|
+
obj.class.name == 'Weeks' ? obj : (obj.length == 1 ? Week.new(obj.start_time) : Weeks.new(obj.length,obj.start_time))
|
285
|
+
else
|
286
|
+
obj.class.name == 'Duration' ? obj : Duration.new(obj.length,obj.unit,obj.start_time,{:auto_class => false})
|
287
|
+
end
|
288
|
+
# Now, auto-transform class if possible:
|
289
|
+
case
|
290
|
+
when !['Weeks', 'Week'].include?(new_obj.class.name) && new_obj.to_i.remainder(Week.length) == 0
|
291
|
+
new_obj.to_i == Week.length ? Week.new(new_obj.start_time) : Weeks.new(new_obj.to_f / Week.length,new_obj.start_time)
|
292
|
+
when !['Weeks', 'Week', 'Days', 'Day'].include?(new_obj.class.name) && new_obj.to_i.remainder(Day.length) == 0
|
293
|
+
new_obj.to_i == Day.length ? Day.new(new_obj.start_time) : Days.new(new_obj.to_f / Day.length,new_obj.start_time)
|
294
|
+
when !['Weeks', 'Week', 'Days', 'Day', 'Hours', 'Hour'].include?(new_obj.class.name) && new_obj.to_i.remainder(Hour.length) == 0
|
295
|
+
new_obj.to_i == Hour.length ? Hour.new(new_obj.start_time) : Hours.new(new_obj.to_f / Hour.length,new_obj.start_time)
|
296
|
+
when !['Weeks', 'Week', 'Days', 'Day', 'Hours', 'Hour', 'Minutes', 'Minute'].include?(new_obj.class.name) && new_obj.to_f.remainder(Minute.length) == 0
|
297
|
+
new_obj.to_i == Minute.length ? Minute.new(new_obj.start_time) : Minutes.new(new_obj.to_f / Minute.length,new_obj.start_time)
|
298
|
+
else
|
299
|
+
new_obj
|
300
|
+
end
|
301
|
+
end
|
302
|
+
end
|
303
|
+
class Weeks < Duration
|
304
|
+
def initialize(count=1,start_time=nil)
|
305
|
+
super(count,Week.length,start_time)
|
306
|
+
end
|
307
|
+
def self.length
|
308
|
+
604800
|
309
|
+
end
|
310
|
+
end
|
311
|
+
class Week < Weeks
|
312
|
+
def initialize(start_time=nil)
|
313
|
+
super(1,start_time)
|
314
|
+
end
|
315
|
+
end
|
316
|
+
class Days < Duration
|
317
|
+
def initialize(count=1,start_time=nil)
|
318
|
+
super(count,Day.length,start_time)
|
319
|
+
end
|
320
|
+
def self.length
|
321
|
+
86400
|
322
|
+
end
|
323
|
+
end
|
324
|
+
class Day < Days
|
325
|
+
def initialize(start_time=nil)
|
326
|
+
super(1,start_time)
|
327
|
+
end
|
328
|
+
end
|
329
|
+
class Hours < Duration
|
330
|
+
def initialize(count=1,start_time=nil)
|
331
|
+
super(count,Hour.length,start_time)
|
332
|
+
end
|
333
|
+
def self.length
|
334
|
+
3600
|
335
|
+
end
|
336
|
+
end
|
337
|
+
class Hour < Hours
|
338
|
+
def initialize(start_time=nil)
|
339
|
+
super(1,start_time)
|
340
|
+
end
|
341
|
+
end
|
342
|
+
class Minutes < Duration
|
343
|
+
def initialize(count=1,start_time=nil)
|
344
|
+
super(count,Minute.length,start_time)
|
345
|
+
end
|
346
|
+
def self.length
|
347
|
+
60
|
348
|
+
end
|
349
|
+
end
|
350
|
+
class Minute < Minutes
|
351
|
+
def initialize(start_time=nil)
|
352
|
+
super(1,start_time)
|
353
|
+
end
|
354
|
+
end
|
355
|
+
class Seconds < Duration
|
356
|
+
def initialize(count=1,start_time=nil)
|
357
|
+
super(count,Second.length,start_time)
|
358
|
+
end
|
359
|
+
def self.length
|
360
|
+
1
|
361
|
+
end
|
362
|
+
end
|
363
|
+
class Second < Seconds
|
364
|
+
def initialize(start_time=nil)
|
365
|
+
super(1,start_time)
|
366
|
+
end
|
367
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
require 'days_and_times/duration'
|
2
|
+
class Numeric
|
3
|
+
def weeks
|
4
|
+
self == 1 ? Week.new : Weeks.new(self)
|
5
|
+
end
|
6
|
+
def week
|
7
|
+
self.weeks
|
8
|
+
end
|
9
|
+
|
10
|
+
def days
|
11
|
+
self == 1 ? Day.new : Days.new(self)
|
12
|
+
end
|
13
|
+
def day
|
14
|
+
self.days
|
15
|
+
end
|
16
|
+
|
17
|
+
def hours
|
18
|
+
self == 1 ? Hour.new : Hours.new(self)
|
19
|
+
end
|
20
|
+
def hour
|
21
|
+
self.hours
|
22
|
+
end
|
23
|
+
|
24
|
+
def minutes
|
25
|
+
self == 1 ? Minute.new : Minutes.new(self)
|
26
|
+
end
|
27
|
+
def minute
|
28
|
+
self.minutes
|
29
|
+
end
|
30
|
+
|
31
|
+
def seconds
|
32
|
+
self == 1 ? Second.new : Seconds.new(self)
|
33
|
+
end
|
34
|
+
def second
|
35
|
+
self.seconds
|
36
|
+
end
|
37
|
+
|
38
|
+
def is_multiple_of?(num)
|
39
|
+
self % num == 0
|
40
|
+
end
|
41
|
+
|
42
|
+
def am
|
43
|
+
Time.parse("#{self}:00:00")
|
44
|
+
end
|
45
|
+
def pm
|
46
|
+
Time.parse("#{self+12}:00:00")
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
class Object
|
2
|
+
def bind_class_object_method(other, self_method_name, other_method_name, args=[[],[]])
|
3
|
+
# Since I can't pass the 'other' object into eval as a string, I have to
|
4
|
+
# set a class instance variable and copy the contents to a class variable
|
5
|
+
# so that the generated method will play nicely with subclasses.
|
6
|
+
self.instance_variable_set("@#{self_method_name.to_s}_OBJ", other)
|
7
|
+
self.send :eval, "@@#{self_method_name.to_s}_OBJ = @#{self_method_name.to_s}_OBJ
|
8
|
+
def #{self_method_name.to_s}(#{args[0].join(', ')})
|
9
|
+
@@#{self_method_name.to_s}_OBJ.#{other_method_name.to_s}(#{args[1].join(', ')})
|
10
|
+
end"
|
11
|
+
self
|
12
|
+
end
|
13
|
+
def bind_object_method(other, self_method_name, other_method_name, args=[[],[]])
|
14
|
+
self.instance_variable_set("@#{self_method_name}_OBJ", other)
|
15
|
+
eval "def self.#{self_method_name}(#{args[0].join(', ')})
|
16
|
+
@#{self_method_name}_OBJ.#{other_method_name}(#{args[1].join(', ')})
|
17
|
+
end"
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,137 @@
|
|
1
|
+
require 'time'
|
2
|
+
class Time
|
3
|
+
def to_time
|
4
|
+
self
|
5
|
+
end
|
6
|
+
|
7
|
+
# Returns a new Time where one or more of the elements have been changed according to the +options+ parameter. The time options
|
8
|
+
# (hour, minute, sec, usec) reset cascadingly, so if only the hour is passed, then minute, sec, and usec is set to 0. If the hour and
|
9
|
+
# minute is passed, then sec and usec is set to 0.
|
10
|
+
def change(options)
|
11
|
+
::Time.send(
|
12
|
+
self.utc? ? :utc_time : :local_time,
|
13
|
+
options[:year] || self.year,
|
14
|
+
options[:month] || self.month,
|
15
|
+
options[:day] || options[:mday] || self.day, # mday is deprecated
|
16
|
+
options[:hour] || self.hour,
|
17
|
+
options[:min] || (options[:hour] ? 0 : self.min),
|
18
|
+
options[:sec] || ((options[:hour] || options[:min]) ? 0 : self.sec),
|
19
|
+
options[:usec] || ((options[:hour] || options[:min] || options[:sec]) ? 0 : self.usec)
|
20
|
+
)
|
21
|
+
end
|
22
|
+
|
23
|
+
def day_name
|
24
|
+
self.strftime("%A")
|
25
|
+
end
|
26
|
+
def month_name
|
27
|
+
self.strftime("%B")
|
28
|
+
end
|
29
|
+
|
30
|
+
# Seconds since midnight: Time.now.seconds_since_midnight
|
31
|
+
def seconds_since_midnight
|
32
|
+
self.to_i - self.change(:hour => 0).to_i + (self.usec/1.0e+6)
|
33
|
+
end
|
34
|
+
|
35
|
+
# Returns a new Time representing the start of the day (0:00)
|
36
|
+
def beginning_of_day
|
37
|
+
(self - self.seconds_since_midnight).change(:usec => 0)
|
38
|
+
end
|
39
|
+
alias :midnight :beginning_of_day
|
40
|
+
alias :at_midnight :beginning_of_day
|
41
|
+
alias :at_beginning_of_day :beginning_of_day
|
42
|
+
|
43
|
+
# Returns a new Time if requested year can be accomodated by Ruby's Time class
|
44
|
+
# (i.e., if year is within either 1970..2038 or 1902..2038, depending on system architecture);
|
45
|
+
# otherwise returns a DateTime
|
46
|
+
def self.time_with_datetime_fallback(utc_or_local, year, month=1, day=1, hour=0, min=0, sec=0, usec=0)
|
47
|
+
::Time.send(utc_or_local, year, month, day, hour, min, sec, usec)
|
48
|
+
rescue
|
49
|
+
offset = if utc_or_local.to_sym == :utc then 0 else ::DateTime.now.offset end
|
50
|
+
::DateTime.civil(year, month, day, hour, min, sec, offset, 0)
|
51
|
+
end
|
52
|
+
|
53
|
+
# wraps class method time_with_datetime_fallback with utc_or_local == :utc
|
54
|
+
def self.utc_time(*args)
|
55
|
+
time_with_datetime_fallback(:utc, *args)
|
56
|
+
end
|
57
|
+
|
58
|
+
# wraps class method time_with_datetime_fallback with utc_or_local == :local
|
59
|
+
def self.local_time(*args)
|
60
|
+
time_with_datetime_fallback(:local, *args)
|
61
|
+
end
|
62
|
+
|
63
|
+
def self.tomorrow
|
64
|
+
Time.now.beginning_of_day + 1.day
|
65
|
+
end
|
66
|
+
def tomorrow
|
67
|
+
self.beginning_of_day + 1.day
|
68
|
+
end
|
69
|
+
def self.yesterday
|
70
|
+
Time.now.beginning_of_day - 1.day
|
71
|
+
end
|
72
|
+
def yesterday
|
73
|
+
self.beginning_of_day - 1.day
|
74
|
+
end
|
75
|
+
def self.today
|
76
|
+
Time.now.beginning_of_day
|
77
|
+
end
|
78
|
+
def self.next_month
|
79
|
+
today.change(:day => 1, :month => today.month + 1)
|
80
|
+
end
|
81
|
+
|
82
|
+
def until(end_time)
|
83
|
+
Duration.new(end_time - self, self)
|
84
|
+
end
|
85
|
+
def through(duration)
|
86
|
+
self.until(duration)
|
87
|
+
end
|
88
|
+
def for(duration)
|
89
|
+
raise TypeError, "must be a Duration object." unless duration.is_a?(Duration)
|
90
|
+
duration.start_time = self
|
91
|
+
duration
|
92
|
+
end
|
93
|
+
def is_today?
|
94
|
+
self.beginning_of_day == Time.today
|
95
|
+
end
|
96
|
+
def strfsql
|
97
|
+
self.strftime("%Y-#{self.strftime("%m").to_i.to_s}-#{self.strftime("%d").to_i.to_s}")
|
98
|
+
end
|
99
|
+
def self.from_tzid(tzid) #We aren't handling the Time Zone part here...
|
100
|
+
if tzid =~ /(\d\d\d\d)(\d\d)(\d\d)T(\d\d)(\d\d)(\d\d)Z/ # yyyymmddThhmmss
|
101
|
+
Time.xmlschema("#{$1}-#{$2}-#{$3}T#{$4}:#{$5}:#{$6}")
|
102
|
+
else
|
103
|
+
return nil
|
104
|
+
end
|
105
|
+
end
|
106
|
+
def humanize_time
|
107
|
+
self.strftime("%M").to_i > 0 ? self.strftime("#{self.strftime("%I").to_i.to_s}:%M%p").downcase : self.strftime("#{self.strftime("%I").to_i.to_s}%p").downcase
|
108
|
+
end
|
109
|
+
def humanize_date(length_profile='medium') #There may be decent reason to change how this works entirely...
|
110
|
+
case length_profile
|
111
|
+
when 'abbr' || 'abbreviated'
|
112
|
+
self.strftime("%m/%d/%y")
|
113
|
+
when 'short'
|
114
|
+
self.strftime("%b #{self.strftime("%d").to_i.to_s}")
|
115
|
+
when 'medium'
|
116
|
+
self.strftime("%B #{self.strftime("%d").to_i.to_s}")
|
117
|
+
when 'long'
|
118
|
+
self.strftime("%B #{self.strftime("%d").to_i.to_s}, %Y")
|
119
|
+
end
|
120
|
+
end
|
121
|
+
def humanize_date_time
|
122
|
+
self.humanize_date + ' ' + self.humanize_time
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
def Today
|
127
|
+
Time.today
|
128
|
+
end
|
129
|
+
def Yesterday
|
130
|
+
Time.yesterday
|
131
|
+
end
|
132
|
+
def Tomorrow
|
133
|
+
Time.tomorrow
|
134
|
+
end
|
135
|
+
def Now
|
136
|
+
Time.now
|
137
|
+
end
|
@@ -0,0 +1,9 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/../spec_helper.rb'
|
2
|
+
|
3
|
+
describe Numeric do
|
4
|
+
it "should generate am and pm times" do
|
5
|
+
3.am.to_s.should eql(Time.parse("3:00:00").to_s)
|
6
|
+
3.pm.to_s.should eql(Time.parse("15:00:00").to_s)
|
7
|
+
19.am.to_s.should eql(Time.parse("19:00:00").to_s)
|
8
|
+
end
|
9
|
+
end
|
@@ -0,0 +1,80 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/spec_helper.rb'
|
2
|
+
|
3
|
+
describe Duration do
|
4
|
+
it "should be backward-compatible with previous english time statements" do
|
5
|
+
10.minutes.from_now.inspect.should eql((Time.now + 600).inspect)
|
6
|
+
3.days.ago.inspect.should eql((Time.now - 3*24*60*60).inspect)
|
7
|
+
# More english statements need to be written!!
|
8
|
+
end
|
9
|
+
|
10
|
+
it "should store a duration correctly in minutes if told to do so, even when created in seconds" do
|
11
|
+
132.seconds.in_minutes.should be_is_a(Minutes)
|
12
|
+
132.seconds.in_minutes.unit.should eql(60)
|
13
|
+
132.seconds.in_minutes.length.to_s.should eql('2.2')
|
14
|
+
end
|
15
|
+
|
16
|
+
it "should add a duration properly to a Time object" do
|
17
|
+
(Time.now+1.day).inspect.should eql((Time.now+86400).inspect)
|
18
|
+
end
|
19
|
+
|
20
|
+
it "should properly represent time in string, depending on the unit" do
|
21
|
+
23.seconds.to_s.should eql("23 Seconds")
|
22
|
+
end
|
23
|
+
|
24
|
+
it "should properly perform mathmatical operations, yet considering the unit" do
|
25
|
+
(2.days / 2 === 1.day).should eql(true)
|
26
|
+
(2.days / 2).in_days.length.should eql(1.day.length)
|
27
|
+
end
|
28
|
+
|
29
|
+
it "should automatically map to Day, Minute, Hour, etc if the length matches" do
|
30
|
+
(1.week - 6.days).class.name.should eql('Day')
|
31
|
+
end
|
32
|
+
|
33
|
+
it "should render durations properly" do
|
34
|
+
1.day.should === Duration.new(1, 86400) #=> A duration of 1 day
|
35
|
+
7.days.should === Duration.new(7, 86400) #=> A duration of 7 days
|
36
|
+
1.week.should === Duration.new(1, 604800) #=> A duration of 1 week
|
37
|
+
end
|
38
|
+
|
39
|
+
it "should perform mathematics on unanchored durations" do
|
40
|
+
(1.week - 2.days).should === Duration.new(5, 86400) #=> A duration of 5 days
|
41
|
+
end
|
42
|
+
|
43
|
+
it "should perform mathematics on an anchored duration" do
|
44
|
+
(3.minutes.ago.until(7.minutes.from(Now())) - 2.minutes).should === Duration.new(8, 60) #=> duration 3 minutes ago to 5 minutes from now
|
45
|
+
end
|
46
|
+
|
47
|
+
it "should generate durations from numeric and time arguments" do
|
48
|
+
1.week.from(Now()).to_s.should eql((Time.now + 604800).to_s) #=> The time of 1 week from this moment
|
49
|
+
1.week.from(Today()).should eql((Time.now + 604800).beginning_of_day) #=> The time of 1 week from the beginning of today
|
50
|
+
3.minutes.ago.until(7.minutes.from(Now())).should === Duration.new(10, 60) #=> duration 3 minutes ago to 7 minutes from now
|
51
|
+
4.weeks.from(2.days.from(Now())).until(8.weeks.from(Yesterday())) #=> A duration, starting in 4 weeks and 2 days, and ending 8 weeks from yesterday
|
52
|
+
end
|
53
|
+
|
54
|
+
it "should perform cross-unit calculations" do
|
55
|
+
1.week - 1.second #=> A duration of 6 days, 23 hours, 59 minutes, and 59 seconds
|
56
|
+
end
|
57
|
+
|
58
|
+
it "should perform algebraic calculations respecting the unit" do
|
59
|
+
4.weeks / 2 #=> A duration of 2 weeks
|
60
|
+
4.weeks / 2.weeks #=> The integer 2
|
61
|
+
end
|
62
|
+
|
63
|
+
it "should perform iterations respecting the default unit and custom units" do
|
64
|
+
8.weeks.each {|week|
|
65
|
+
week.should === Duration.new(1, 604800)
|
66
|
+
} #=> Runs code for each week contained in the duration (of 8 weeks)
|
67
|
+
8.weeks.starting(Now()).each {|week| } #=> Runs code for each week in the duration, but each week is also anchored to a starting time, in sequence through the duration.
|
68
|
+
1.week.each {|week| } #=> Automatically chooses week as its iterator
|
69
|
+
7.days.each {|day| } #=> Automatically chooses day as its iterator
|
70
|
+
1.week.each_day {|day| } #=> Forcing the week to iterate through days
|
71
|
+
end
|
72
|
+
|
73
|
+
it "should perform iterations with a remainder that also runs" do
|
74
|
+
count = 0
|
75
|
+
1.week.each(10.hours.to_f) {|ten_hour_segment|
|
76
|
+
count += 1
|
77
|
+
ten_hour_segment.should === Duration.new(10, 3600) unless count == 17
|
78
|
+
}[-1].should === Duration.new(8, 3600) #=> Using a custom iterator of 10 hours. There would be 17 of them, but notice that the last iteration will only be 8 hours.
|
79
|
+
end
|
80
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/../lib/days_and_times'
|
metadata
ADDED
@@ -0,0 +1,77 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: dcparker-days_and_times
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 1.0.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Daniel Parker
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
|
12
|
+
date: 2008-08-06 00:00:00 -07:00
|
13
|
+
default_executable:
|
14
|
+
dependencies:
|
15
|
+
- !ruby/object:Gem::Dependency
|
16
|
+
name: hoe
|
17
|
+
version_requirement:
|
18
|
+
version_requirements: !ruby/object:Gem::Requirement
|
19
|
+
requirements:
|
20
|
+
- - ">="
|
21
|
+
- !ruby/object:Gem::Version
|
22
|
+
version: 1.7.0
|
23
|
+
version:
|
24
|
+
description: Natural language method chaining for Time, Durations and the like.
|
25
|
+
email: gems@behindlogic.com
|
26
|
+
executables: []
|
27
|
+
|
28
|
+
extensions: []
|
29
|
+
|
30
|
+
extra_rdoc_files:
|
31
|
+
- History.txt
|
32
|
+
- License.txt
|
33
|
+
- Manifest.txt
|
34
|
+
- README.txt
|
35
|
+
files:
|
36
|
+
- History.txt
|
37
|
+
- License.txt
|
38
|
+
- Manifest.txt
|
39
|
+
- README.txt
|
40
|
+
- Rakefile
|
41
|
+
- lib/days_and_times.rb
|
42
|
+
- lib/days_and_times/duration.rb
|
43
|
+
- lib/days_and_times/numeric.rb
|
44
|
+
- lib/days_and_times/object.rb
|
45
|
+
- lib/days_and_times/time.rb
|
46
|
+
- spec/days_and_times/numeric_spec.rb
|
47
|
+
- spec/days_and_times_spec.rb
|
48
|
+
- spec/spec_helper.rb
|
49
|
+
has_rdoc: true
|
50
|
+
homepage: http://github.com/dcparker/days_and_times
|
51
|
+
post_install_message:
|
52
|
+
rdoc_options:
|
53
|
+
- --main
|
54
|
+
- README.txt
|
55
|
+
require_paths:
|
56
|
+
- lib
|
57
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - ">="
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: "0"
|
62
|
+
version:
|
63
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
64
|
+
requirements:
|
65
|
+
- - ">="
|
66
|
+
- !ruby/object:Gem::Version
|
67
|
+
version: "0"
|
68
|
+
version:
|
69
|
+
requirements: []
|
70
|
+
|
71
|
+
rubyforge_project: days_and_times
|
72
|
+
rubygems_version: 1.2.0
|
73
|
+
signing_key:
|
74
|
+
specification_version: 2
|
75
|
+
summary: Natural language method chaining for Time, Durations and the like.
|
76
|
+
test_files: []
|
77
|
+
|