timeframe 0.0.1
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/.document +5 -0
- data/.gitignore +21 -0
- data/LICENSE +20 -0
- data/README.rdoc +13 -0
- data/Rakefile +46 -0
- data/VERSION +1 -0
- data/lib/timeframe.rb +297 -0
- data/lib/timeframe/ykk.rb +6 -0
- data/spec/spec.opts +1 -0
- data/spec/spec_helper.rb +10 -0
- data/spec/timeframe_spec.rb +248 -0
- metadata +114 -0
data/.document
ADDED
data/.gitignore
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2009 Andy Rossmeissl
|
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/README.rdoc
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
= timeframe
|
2
|
+
|
3
|
+
A Ruby class for describing and interacting with timeframes.
|
4
|
+
|
5
|
+
== Documentation
|
6
|
+
|
7
|
+
== Acknoweldgements
|
8
|
+
|
9
|
+
The good parts of Timeframe all came from the gentlemen at Fingertips[http://fngtps.com].
|
10
|
+
|
11
|
+
== Copyright
|
12
|
+
|
13
|
+
Copyright (c) 2010 Andy Rossmeissl.
|
data/Rakefile
ADDED
@@ -0,0 +1,46 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'rake'
|
3
|
+
|
4
|
+
begin
|
5
|
+
require 'jeweler'
|
6
|
+
Jeweler::Tasks.new do |gem|
|
7
|
+
gem.name = "timeframe"
|
8
|
+
gem.summary = %Q{Date intervals}
|
9
|
+
gem.description = %Q{A Ruby class for describing and interacting with timeframes.}
|
10
|
+
gem.email = "andy@rossmeissl.net"
|
11
|
+
gem.homepage = "http://github.com/rossmeissl/timeframe"
|
12
|
+
gem.authors = ["Andy Rossmeissl"]
|
13
|
+
gem.add_development_dependency "rspec", ">= 1.2.9"
|
14
|
+
gem.add_dependency 'activesupport', '= 3.0.0.beta4'
|
15
|
+
gem.add_dependency 'andand'
|
16
|
+
end
|
17
|
+
Jeweler::GemcutterTasks.new
|
18
|
+
rescue LoadError
|
19
|
+
puts "Jeweler (or a dependency) not available. Install it with: gem install jeweler"
|
20
|
+
end
|
21
|
+
|
22
|
+
require 'spec/rake/spectask'
|
23
|
+
Spec::Rake::SpecTask.new(:spec) do |spec|
|
24
|
+
spec.libs << 'lib' << 'spec'
|
25
|
+
spec.spec_files = FileList['spec/**/*_spec.rb']
|
26
|
+
end
|
27
|
+
|
28
|
+
Spec::Rake::SpecTask.new(:rcov) do |spec|
|
29
|
+
spec.libs << 'lib' << 'spec'
|
30
|
+
spec.pattern = 'spec/**/*_spec.rb'
|
31
|
+
spec.rcov = true
|
32
|
+
end
|
33
|
+
|
34
|
+
task :spec => :check_dependencies
|
35
|
+
|
36
|
+
task :default => :spec
|
37
|
+
|
38
|
+
require 'rake/rdoctask'
|
39
|
+
Rake::RDocTask.new do |rdoc|
|
40
|
+
version = File.exist?('VERSION') ? File.read('VERSION') : ""
|
41
|
+
|
42
|
+
rdoc.rdoc_dir = 'rdoc'
|
43
|
+
rdoc.title = "timeframe #{version}"
|
44
|
+
rdoc.rdoc_files.include('README*')
|
45
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
46
|
+
end
|
data/VERSION
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
0.0.1
|
data/lib/timeframe.rb
ADDED
@@ -0,0 +1,297 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'active_support/version'
|
3
|
+
%w{
|
4
|
+
active_support/core_ext/array/extract_options
|
5
|
+
active_support/core_ext/string/conversions
|
6
|
+
active_support/core_ext/date/conversions
|
7
|
+
active_support/core_ext/integer/time
|
8
|
+
active_support/core_ext/numeric/time
|
9
|
+
}.each do |active_support_3_requirement|
|
10
|
+
require active_support_3_requirement
|
11
|
+
end if ActiveSupport::VERSION::MAJOR == 3
|
12
|
+
require 'andand'
|
13
|
+
require 'timeframe/ykk'
|
14
|
+
|
15
|
+
# Encapsulates a timeframe between two dates. The dates provided to the class are always until the last date. That means
|
16
|
+
# that the last date is excluded.
|
17
|
+
#
|
18
|
+
# # from 2007-10-01 00:00:00.000 to 2007-10-31 23:59:59.999
|
19
|
+
# Timeframe.new(Date(2007,10,1), Date(2007,11,1))
|
20
|
+
# # and holds 31 days
|
21
|
+
# Timeframe.new(Date(2007,10,1), Date(2007,11,1)).days #=> 31
|
22
|
+
class Timeframe
|
23
|
+
attr_accessor :from, :to
|
24
|
+
|
25
|
+
# Creates a new instance of Timeframe. You can either pass a start and end Date or a Hash with named arguments,
|
26
|
+
# with the following options:
|
27
|
+
#
|
28
|
+
# <tt>:month</tt>: Start date becomes the first day of this month, and the end date becomes the first day of
|
29
|
+
# the next month. If no <tt>:year</tt> is specified, the current year is used.
|
30
|
+
# <tt>:year</tt>: Start date becomes the first day of this year, and the end date becomes the first day of the
|
31
|
+
# next year.
|
32
|
+
#
|
33
|
+
# By default, Timeframe.new will die if the resulting Timeframe would cross year boundaries. This can be overridden
|
34
|
+
# by setting the <tt>:skip_year_boundary_crossing_check</tt> option.
|
35
|
+
#
|
36
|
+
# Examples:
|
37
|
+
#
|
38
|
+
# Timeframe.new Date.new(2007, 2, 1), Date.new(2007, 4, 1) # February and March
|
39
|
+
# Timeframe.new :year => 2004 # The year 2004
|
40
|
+
# Timeframe.new :month => 4 # April
|
41
|
+
# Timeframe.new :year => 2004, :month => 2 # Feburary 2004
|
42
|
+
def initialize(*args)
|
43
|
+
options = args.extract_options!
|
44
|
+
|
45
|
+
if month = options[:month]
|
46
|
+
month = Date.parse(month).month if month.is_a? String
|
47
|
+
year = options[:year] || Date.today.year
|
48
|
+
from = Date.new(year, month, 1)
|
49
|
+
to = from.next_month
|
50
|
+
elsif year = options[:year]
|
51
|
+
from = Date.new(year, 1, 1)
|
52
|
+
to = Date.new(year+1, 1, 1)
|
53
|
+
end
|
54
|
+
|
55
|
+
from ||= args.shift.andand.to_date
|
56
|
+
to ||= args.shift.andand.to_date
|
57
|
+
|
58
|
+
raise ArgumentError, "Please supply a start and end date, `#{args.map(&:inspect).to_sentence}' is not enough" if from.nil? or to.nil?
|
59
|
+
raise ArgumentError, "Start date #{from} should be earlier than end date #{to}" if from > to
|
60
|
+
raise ArgumentError, 'Timeframes that cross year boundaries are dangerous' unless options[:skip_year_boundary_crossing_check] or from.year == to.yesterday.year or from == to
|
61
|
+
|
62
|
+
@from, @to = from, to
|
63
|
+
end
|
64
|
+
|
65
|
+
def inspect # :nodoc:
|
66
|
+
"<Timeframe(#{object_id}) #{days} days starting #{from} ending #{to}>"
|
67
|
+
end
|
68
|
+
|
69
|
+
# The number of days in the timeframe
|
70
|
+
#
|
71
|
+
# Timeframe.new(Date.new(2007, 11, 1), Date.new(2007, 12, 1)).days #=> 30
|
72
|
+
# Timeframe.new(:month => 1).days #=> 31
|
73
|
+
# Timeframe.new(:year => 2004).days #=> 366
|
74
|
+
def days
|
75
|
+
(to - from).to_i
|
76
|
+
end
|
77
|
+
|
78
|
+
# Returns a string representation of the timeframe
|
79
|
+
def to_s
|
80
|
+
if [from.day, from.month, to.day, to.month].uniq == [1]
|
81
|
+
from.year.to_s
|
82
|
+
elsif from.day == 1 and to.day == 1 and to.month - from.month == 1
|
83
|
+
"#{Date::MONTHNAMES[from.month]} #{from.year}"
|
84
|
+
else
|
85
|
+
"the period from #{from.strftime('%d %B')} to #{to.yesterday.strftime('%d %B %Y')}"
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
# Returns true when a Date or other Timeframe is included in this Timeframe
|
90
|
+
def include?(obj)
|
91
|
+
# puts "checking to see if #{date} is between #{from} and #{to}" if Emitter::DEBUG
|
92
|
+
case obj
|
93
|
+
when Date
|
94
|
+
(from...to).include?(obj)
|
95
|
+
when Time
|
96
|
+
# (from...to).include?(obj.to_date)
|
97
|
+
raise "this wasn't previously supported, but it could be"
|
98
|
+
when Timeframe
|
99
|
+
from <= obj.from and to >= obj.to
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
# Returns true when the parameter Timeframe is properly included in the Timeframe
|
104
|
+
def proper_include?(other_timeframe)
|
105
|
+
raise ArgumentError, 'Proper inclusion only makes sense when testing other Timeframes' unless other_timeframe.is_a? Timeframe
|
106
|
+
(from < other_timeframe.from) and (to > other_timeframe.to)
|
107
|
+
end
|
108
|
+
|
109
|
+
# Returns true when this timeframe is equal to the other timeframe
|
110
|
+
def ==(other)
|
111
|
+
# puts "checking to see if #{self} is equal to #{other}" if Emitter::DEBUG
|
112
|
+
return false unless other.is_a?(Timeframe)
|
113
|
+
from == other.from and to == other.to
|
114
|
+
end
|
115
|
+
alias :eql? :==
|
116
|
+
|
117
|
+
# Calculates a hash value for the Timeframe, used for equality checking and Hash lookups.
|
118
|
+
#--
|
119
|
+
# This needs to be an integer or else it won't use #eql?
|
120
|
+
def hash
|
121
|
+
from.hash + to.hash
|
122
|
+
end
|
123
|
+
alias :to_param :hash
|
124
|
+
|
125
|
+
# Returns an array of month-long subtimeframes
|
126
|
+
#--
|
127
|
+
# TODO: rename to month_subtimeframes
|
128
|
+
def months
|
129
|
+
raise ArgumentError, "Please only provide whole-month timeframes to Timeframe#months" unless from.day == 1 and to.day == 1
|
130
|
+
raise ArgumentError, 'Timeframes that cross year boundaries are dangerous during Timeframe#months' unless from.year == to.yesterday.year
|
131
|
+
year = from.year # therefore this only works in the from year
|
132
|
+
(from.month..to.yesterday.month).map { |m| Timeframe.new :month => m, :year => year }
|
133
|
+
end
|
134
|
+
|
135
|
+
# Returns the relevant year as a Timeframe
|
136
|
+
def year
|
137
|
+
raise ArgumentError, 'Timeframes that cross year boundaries are dangerous during Timeframe#year' unless from.year == to.yesterday.year
|
138
|
+
Timeframe.new :year => from.year
|
139
|
+
end
|
140
|
+
|
141
|
+
# Divides a Timeframe into component parts, each no more than a month long.
|
142
|
+
#--
|
143
|
+
# multiyear safe
|
144
|
+
def month_subtimeframes
|
145
|
+
(from.year..to.yesterday.year).map do |year|
|
146
|
+
(1..12).map do |month|
|
147
|
+
Timeframe.new(:year => year, :month => month) & self
|
148
|
+
end
|
149
|
+
end.flatten.compact
|
150
|
+
end
|
151
|
+
|
152
|
+
# Like #month_subtimeframes, but will discard partial months
|
153
|
+
# multiyear safe
|
154
|
+
def full_month_subtimeframes
|
155
|
+
month_subtimeframes.map { |st| Timeframe.new(:year => st.from.year, :month => st.from.month) }
|
156
|
+
end
|
157
|
+
|
158
|
+
# Divides a Timeframe into component parts, each no more than a year long.
|
159
|
+
#--
|
160
|
+
# multiyear safe
|
161
|
+
def year_subtimeframes
|
162
|
+
(from.year..to.yesterday.year).map do |year|
|
163
|
+
Timeframe.new(:year => year) & self
|
164
|
+
end
|
165
|
+
end
|
166
|
+
|
167
|
+
# Like #year_subtimeframes, but will discard partial years
|
168
|
+
#--
|
169
|
+
# multiyear safe
|
170
|
+
def full_year_subtimeframes
|
171
|
+
(from.year..to.yesterday.year).map do |year|
|
172
|
+
Timeframe.new :year => year
|
173
|
+
end
|
174
|
+
end
|
175
|
+
|
176
|
+
# Crop a Timeframe to end no later than the provided date.
|
177
|
+
#--
|
178
|
+
# multiyear safe
|
179
|
+
def ending_no_later_than(date)
|
180
|
+
if to < date
|
181
|
+
self
|
182
|
+
elsif from >= date
|
183
|
+
nil
|
184
|
+
else
|
185
|
+
Timeframe.multiyear from, date
|
186
|
+
end
|
187
|
+
end
|
188
|
+
|
189
|
+
# Returns a timeframe representing the intersection of the given timeframes
|
190
|
+
def &(other_timeframe)
|
191
|
+
this_timeframe = self
|
192
|
+
if other_timeframe == this_timeframe
|
193
|
+
this_timeframe
|
194
|
+
elsif this_timeframe.from > other_timeframe.from and this_timeframe.to < other_timeframe.to
|
195
|
+
this_timeframe
|
196
|
+
elsif other_timeframe.from > this_timeframe.from and other_timeframe.to < this_timeframe.to
|
197
|
+
other_timeframe
|
198
|
+
elsif this_timeframe.from >= other_timeframe.to or this_timeframe.to <= other_timeframe.from
|
199
|
+
nil
|
200
|
+
else
|
201
|
+
Timeframe.new [this_timeframe.from, other_timeframe.from].max, [this_timeframe.to, other_timeframe.to].min, :skip_year_boundary_crossing_check => true
|
202
|
+
end
|
203
|
+
end
|
204
|
+
|
205
|
+
# Returns the fraction (as a Float) of another Timeframe that this Timeframe represents
|
206
|
+
def /(other_timeframe)
|
207
|
+
raise ArgumentError, 'You can only divide a Timeframe by another Timeframe' unless other_timeframe.is_a? Timeframe
|
208
|
+
self.days.to_f / other_timeframe.days.to_f
|
209
|
+
end
|
210
|
+
|
211
|
+
# Crop a Timeframe by another Timeframe
|
212
|
+
def crop(container)
|
213
|
+
raise ArgumentError, 'You can only crop a timeframe by another timeframe' unless container.is_a? Timeframe
|
214
|
+
self.class.new [from, container.from].max, [to, container.to].min
|
215
|
+
end
|
216
|
+
|
217
|
+
# Returns an array of Timeframes representing the gaps left in the Timeframe after removing all given Timeframes
|
218
|
+
def gaps_left_by(*timeframes)
|
219
|
+
# remove extraneous timeframes
|
220
|
+
timeframes.reject! { |t| t.to <= from }
|
221
|
+
timeframes.reject! { |t| t.from >= to }
|
222
|
+
|
223
|
+
# crop timeframes
|
224
|
+
timeframes.map! { |t| t.crop self }
|
225
|
+
|
226
|
+
# remove proper subtimeframes
|
227
|
+
timeframes.reject! { |t| timeframes.detect { |u| u.proper_include? t } }
|
228
|
+
|
229
|
+
# escape
|
230
|
+
return [self] if timeframes.empty?
|
231
|
+
|
232
|
+
timeframes.sort! { |x, y| x.from <=> y.from }
|
233
|
+
|
234
|
+
timeframes.collect(&:to).unshift(from).extend(Ykk).ykk(timeframes.collect(&:from).push(to)) do |gap|
|
235
|
+
Timeframe.new(*gap) if gap[1] > gap[0]
|
236
|
+
end.compact
|
237
|
+
end
|
238
|
+
|
239
|
+
# Returns true if the union of the given Timeframes includes the Timeframe
|
240
|
+
def covered_by?(*timeframes)
|
241
|
+
gaps_left_by(*timeframes).empty?
|
242
|
+
end
|
243
|
+
|
244
|
+
# Returns the same Timeframe, only a year earlier
|
245
|
+
def last_year
|
246
|
+
self.class.new((from - 1.year), (to - 1.year))
|
247
|
+
end
|
248
|
+
|
249
|
+
def to_json # :nodoc:
|
250
|
+
{ :from => from, :to => to }.to_json
|
251
|
+
end
|
252
|
+
|
253
|
+
class << self
|
254
|
+
# Shortcut method to return the Timeframe representing the current year (as defined by Time.now)
|
255
|
+
def this_year
|
256
|
+
new :year => Time.now.year
|
257
|
+
end
|
258
|
+
|
259
|
+
# Construct a new Timeframe, but constrain it by another
|
260
|
+
def constrained_new(from, to, constraint)
|
261
|
+
raise ArgumentError, 'Need Date, Date, Timeframe as args' unless from.is_a? Date and to.is_a? Date and constraint.is_a? Timeframe
|
262
|
+
raise ArgumentError, "Start date #{from} should be earlier than end date #{to}" if from > to
|
263
|
+
if to <= constraint.from or from >= constraint.to
|
264
|
+
new constraint.from, constraint.from
|
265
|
+
elsif from.year == to.yesterday.year
|
266
|
+
new(from, to) & constraint
|
267
|
+
elsif from.year < constraint.from.year and constraint.from.year < to.yesterday.year
|
268
|
+
constraint
|
269
|
+
else
|
270
|
+
new [constraint.from, from].max, [constraint.to, to].min
|
271
|
+
end
|
272
|
+
end
|
273
|
+
|
274
|
+
# Shortcut for #new that automatically skips year boundary crossing checks
|
275
|
+
def multiyear(from, to)
|
276
|
+
from = Date.parse(from) if from.is_a?(String)
|
277
|
+
to = Date.parse(to) if to.is_a?(String)
|
278
|
+
new from, to, :skip_year_boundary_crossing_check => true
|
279
|
+
end
|
280
|
+
|
281
|
+
# Create a multiyear timeframe +/- number of years around today
|
282
|
+
def mid(number)
|
283
|
+
from = Time.zone.today - number.years
|
284
|
+
to = Time.zone.today + number.years
|
285
|
+
multiyear from, to
|
286
|
+
end
|
287
|
+
|
288
|
+
# Construct a new Timeframe by parsing an ISO 8601 time interval string
|
289
|
+
# http://en.wikipedia.org/wiki/ISO_8601#Time_intervals
|
290
|
+
def interval(interval)
|
291
|
+
raise ArgumentError, 'Intervals should be specified as a string' unless interval.is_a? String
|
292
|
+
raise ArgumentError, 'Intervals should be specified according to ISO 8601, method 1, eliding times' unless interval =~ /^\d\d\d\d-\d\d-\d\d\/\d\d\d\d-\d\d-\d\d$/
|
293
|
+
|
294
|
+
new(*interval.split('/').map { |date| Date.parse date })
|
295
|
+
end
|
296
|
+
end
|
297
|
+
end
|
data/spec/spec.opts
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
--color
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,248 @@
|
|
1
|
+
require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
|
2
|
+
require 'date'
|
3
|
+
|
4
|
+
describe Timeframe do
|
5
|
+
describe 'initialization' do
|
6
|
+
it 'should create a timeframe using date strings' do
|
7
|
+
tf = Timeframe.new('2008-02-14', '2008-05-10')
|
8
|
+
tf.from.should == Date.parse('2008-02-14')
|
9
|
+
tf.to.should == Date.parse('2008-05-10')
|
10
|
+
end
|
11
|
+
it 'should create a timeframe using date objects' do
|
12
|
+
start = Date.parse('2008-02-14')
|
13
|
+
finish = Date.parse('2008-05-10')
|
14
|
+
tf = Timeframe.new(start, finish)
|
15
|
+
tf.from.should == start
|
16
|
+
tf.to.should == finish
|
17
|
+
end
|
18
|
+
it "should accept months" do
|
19
|
+
timeframe = Timeframe.new(:month => 1)
|
20
|
+
timeframe.from.should == Date.today.change(:month => 1, :day => 1)
|
21
|
+
timeframe.to.should == Date.today.change(:month => 2, :day => 1)
|
22
|
+
end
|
23
|
+
it "should accept month names" do
|
24
|
+
timeframe = Timeframe.new(:month => 'february')
|
25
|
+
timeframe.from.should == Date.today.change(:month => 2, :day => 1)
|
26
|
+
timeframe.to.should == Date.today.change(:month => 3, :day => 1)
|
27
|
+
end
|
28
|
+
it "should accept years" do
|
29
|
+
timeframe = Timeframe.new(:year => 2004)
|
30
|
+
timeframe.from.should == Date.new(2004, 1, 1)
|
31
|
+
timeframe.to.should == Date.new(2005, 1, 1)
|
32
|
+
end
|
33
|
+
it "should accept years and months" do
|
34
|
+
timeframe = Timeframe.new(:year => 2005, :month => 5)
|
35
|
+
timeframe.from.should == Date.new(2005, 5, 1)
|
36
|
+
timeframe.to.should == Date.new(2005, 6, 1)
|
37
|
+
end
|
38
|
+
it "should not accept just one date argument" do
|
39
|
+
lambda {
|
40
|
+
Timeframe.new Date.new(2007, 2, 1)
|
41
|
+
}.should raise_error(ArgumentError, /supply/)
|
42
|
+
end
|
43
|
+
it "should not accept end date that is earlier than start date" do
|
44
|
+
lambda {
|
45
|
+
timeframe = Timeframe.new Date.new(2008, 1, 1), Date.new(2007, 1, 1)
|
46
|
+
}.should raise_error(ArgumentError, /earlier/)
|
47
|
+
end
|
48
|
+
it "should not accept timeframes that cross year boundaries" do
|
49
|
+
lambda {
|
50
|
+
timeframe = Timeframe.new Date.new(2007, 1, 1), Date.new(2008, 1, 2)
|
51
|
+
}.should raise_error(ArgumentError, /cross/)
|
52
|
+
end
|
53
|
+
it "should optionally accept timeframes that cross year boundaries" do
|
54
|
+
lambda {
|
55
|
+
timeframe = Timeframe.new Date.new(2007, 1, 1), Date.new(2008, 1, 2), :skip_year_boundary_crossing_check => true
|
56
|
+
}.should_not raise_error
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
describe '#inspect' do
|
61
|
+
it 'should return the time frame in readable text' do
|
62
|
+
start = Date.parse('2008-02-14')
|
63
|
+
finish = Date.parse('2008-05-10')
|
64
|
+
tf = Timeframe.new(start, finish)
|
65
|
+
tf.inspect.should =~ /<Timeframe\(-?\d+\) 86 days starting 2008-02-14 ending 2008-05-10>/
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
describe '.constrained_new' do
|
70
|
+
let(:start) { Date.parse('2008-02-14') }
|
71
|
+
let(:finish) { Date.parse('2008-05-10') }
|
72
|
+
let(:constraint_start) { Date.parse('2008-01-01') }
|
73
|
+
let(:constraint_finish) { Date.parse('2008-12-01') }
|
74
|
+
let(:constraint) { Timeframe.new(constraint_start, constraint_finish) }
|
75
|
+
|
76
|
+
it "should allow for constrained creation" do
|
77
|
+
constraint = Timeframe.new :year => 2008
|
78
|
+
may = Timeframe.new Date.new(2008,5,1), Date.new(2008,6,1)
|
79
|
+
january = Timeframe.new Date.new(2008,1,1), Date.new(2008,2,1)
|
80
|
+
Timeframe.constrained_new(may.from, may.to, constraint).should == may
|
81
|
+
Timeframe.constrained_new(Date.new(2007,1,1), Date.new(2010,1,1), constraint).should == constraint
|
82
|
+
Timeframe.constrained_new(Date.new(2007,11,1), Date.new(2008,2,1), constraint).should == january
|
83
|
+
end
|
84
|
+
it 'should return a timeframe spanning start and end date if within constraint' do
|
85
|
+
tf = Timeframe.constrained_new(start, finish, constraint)
|
86
|
+
tf.from.should == start
|
87
|
+
tf.to.should == finish
|
88
|
+
end
|
89
|
+
it 'should return a timeframe spanning constraint start and end date if outside constraint' do
|
90
|
+
constraint = Timeframe.new(start, finish)
|
91
|
+
tf = Timeframe.constrained_new(constraint_start, constraint_finish, constraint)
|
92
|
+
tf.from.should == start
|
93
|
+
tf.to.should == finish
|
94
|
+
end
|
95
|
+
it 'should return a timeframe starting at constraint start' do
|
96
|
+
start = Date.parse('2008-01-01')
|
97
|
+
constraint_start = Date.parse('2008-01-14')
|
98
|
+
constraint = Timeframe.new(constraint_start, constraint_finish)
|
99
|
+
tf = Timeframe.constrained_new(start, finish, constraint)
|
100
|
+
tf.from.should == constraint_start
|
101
|
+
tf.to.should == finish
|
102
|
+
end
|
103
|
+
it 'should return a timeframe ending at constraint end' do
|
104
|
+
constraint_finish = Date.parse('2008-04-14')
|
105
|
+
constraint = Timeframe.new(constraint_start, constraint_finish)
|
106
|
+
tf = Timeframe.constrained_new(start, finish, constraint)
|
107
|
+
tf.from.should == start
|
108
|
+
tf.to.should == constraint_finish
|
109
|
+
end
|
110
|
+
it "should return a 0-length timeframe when constraining a timeframe by a disjoint timeframe" do
|
111
|
+
constraint = Timeframe.new :year => 2010
|
112
|
+
timeframe = Timeframe.constrained_new(
|
113
|
+
Date.new(2009,1,1), Date.new(2010,1,1), constraint)
|
114
|
+
timeframe.days.should == 0
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
describe '.this_year' do
|
119
|
+
it "should return the current year" do
|
120
|
+
Timeframe.this_year.should == Timeframe.new(:year => Time.now.year)
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
describe '#days' do
|
125
|
+
it "should return them number of days included" do
|
126
|
+
#TODO: make these separate "it" blocks, per best practices
|
127
|
+
Timeframe.new(Date.new(2007, 1, 1), Date.new(2008, 1, 1)).days.should == 365
|
128
|
+
Timeframe.new(Date.new(2008, 1, 1), Date.new(2009, 1, 1)).days.should == 366 #leap year
|
129
|
+
Timeframe.new(Date.new(2007, 11, 1), Date.new(2007, 12, 1)).days.should == 30
|
130
|
+
Timeframe.new(Date.new(2007, 11, 1), Date.new(2008, 1, 1)).days.should == 61
|
131
|
+
Timeframe.new(Date.new(2007, 2, 1), Date.new(2007, 3, 1)).days.should == 28
|
132
|
+
Timeframe.new(Date.new(2008, 2, 1), Date.new(2008, 3, 1)).days.should == 29
|
133
|
+
Timeframe.new(Date.new(2008, 2, 1), Date.new(2008, 2, 1)).days.should == 0
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
137
|
+
describe '#include?' do
|
138
|
+
it "should know if a certain date is included in the Timeframe" do
|
139
|
+
#TODO: make these separate "it" blocks, per best practices
|
140
|
+
timeframe = Timeframe.new :year => 2008, :month => 2
|
141
|
+
[
|
142
|
+
Date.new(2008, 2, 1),
|
143
|
+
Date.new(2008, 2, 5),
|
144
|
+
Date.new(2008, 2, 29)
|
145
|
+
].each do |date|
|
146
|
+
timeframe.include?(date).should == true
|
147
|
+
end
|
148
|
+
[
|
149
|
+
Date.new(2008, 1, 1),
|
150
|
+
Date.new(2007, 2, 1),
|
151
|
+
Date.new(2008, 3, 1)
|
152
|
+
].each do |date|
|
153
|
+
timeframe.include?(date).should == false
|
154
|
+
end
|
155
|
+
end
|
156
|
+
end
|
157
|
+
|
158
|
+
describe '#==' do
|
159
|
+
it "should be able to know if it's equal to another Timeframe object" do
|
160
|
+
Timeframe.new(:year => 2007).should == Timeframe.new(:year => 2007)
|
161
|
+
Timeframe.new(:year => 2004, :month => 1).should == Timeframe.new(:year => 2004, :month => 1)
|
162
|
+
end
|
163
|
+
|
164
|
+
it "should hash equal hash values when the timeframe is equal" do
|
165
|
+
Timeframe.new(:year => 2007).hash.should == Timeframe.new(:year => 2007).hash
|
166
|
+
Timeframe.new(:year => 2004, :month => 1).hash.should == Timeframe.new(:year => 2004, :month => 1).hash
|
167
|
+
end
|
168
|
+
end
|
169
|
+
|
170
|
+
describe '#months' do
|
171
|
+
it "should return an array of month-long subtimeframes" do
|
172
|
+
Timeframe.new(:year => 2009).months.length.should == 12
|
173
|
+
end
|
174
|
+
it "should not return an array of month-long subtimeframes if provided an inappropriate range" do
|
175
|
+
lambda {
|
176
|
+
Timeframe.new(Date.new(2009, 3, 2), Date.new(2009, 3, 5)).months
|
177
|
+
}.should raise_error(ArgumentError, /whole/)
|
178
|
+
lambda {
|
179
|
+
Timeframe.new(Date.new(2009, 1, 1), Date.new(2012, 1, 1), :skip_year_boundary_crossing_check => true).months
|
180
|
+
}.should raise_error(ArgumentError, /dangerous during/)
|
181
|
+
end
|
182
|
+
end
|
183
|
+
|
184
|
+
describe '#year' do
|
185
|
+
it "should return the relevant year of a timeframe" do
|
186
|
+
Timeframe.new(Date.new(2009, 2, 1), Date.new(2009, 4, 1)).year.should == Timeframe.new(:year => 2009)
|
187
|
+
end
|
188
|
+
it "should not return the relevant year of a timeframe if provided an inappropriate range" do
|
189
|
+
lambda {
|
190
|
+
Timeframe.new(Date.new(2009, 1, 1), Date.new(2012, 1, 1), :skip_year_boundary_crossing_check => true).year
|
191
|
+
}.should raise_error(ArgumentError, /dangerous during/)
|
192
|
+
end
|
193
|
+
end
|
194
|
+
|
195
|
+
describe '#&' do
|
196
|
+
it "should return its intersection with another timeframe" do
|
197
|
+
#TODO: make these separate "it" blocks, per best practices
|
198
|
+
(Timeframe.new(:month => 4) & Timeframe.new(:month => 6)).should be_nil
|
199
|
+
(Timeframe.new(:month => 4) & Timeframe.new(:month => 5)).should be_nil
|
200
|
+
(Timeframe.new(:month => 4) & Timeframe.new(:month => 4)).should == Timeframe.new(:month => 4)
|
201
|
+
(Timeframe.new(:year => Time.now.year) & Timeframe.new(:month => 4)).should == Timeframe.new(:month => 4)
|
202
|
+
(Timeframe.new(Date.new(2009, 2, 1), Date.new(2009, 6, 1)) & Timeframe.new(Date.new(2009, 4, 1), Date.new(2009, 8, 1))).should == Timeframe.new(Date.new(2009, 4, 1), Date.new(2009, 6, 1))
|
203
|
+
end
|
204
|
+
end
|
205
|
+
|
206
|
+
describe '#/' do
|
207
|
+
it "should return a fraction of another timeframe" do
|
208
|
+
(Timeframe.new(:month => 4, :year => 2009) / Timeframe.new(:year => 2009)).should == (30.0 / 365.0)
|
209
|
+
end
|
210
|
+
end
|
211
|
+
|
212
|
+
describe '#gaps_left_by' do
|
213
|
+
it "should be able to ascertain gaps left by a list of other Timeframes" do
|
214
|
+
Timeframe.new(:year => 2009).gaps_left_by(
|
215
|
+
Timeframe.new(:year => 2009, :month => 3),
|
216
|
+
Timeframe.new(:year => 2009, :month => 5),
|
217
|
+
Timeframe.new(Date.new(2009, 8, 1), Date.new(2009, 11, 1)),
|
218
|
+
Timeframe.new(Date.new(2009, 9, 1), Date.new(2009, 10, 1))
|
219
|
+
).should ==
|
220
|
+
[ Timeframe.new(Date.new(2009, 1, 1), Date.new(2009, 3, 1)),
|
221
|
+
Timeframe.new(Date.new(2009, 4, 1), Date.new(2009, 5, 1)),
|
222
|
+
Timeframe.new(Date.new(2009, 6, 1), Date.new(2009, 8, 1)),
|
223
|
+
Timeframe.new(Date.new(2009, 11, 1), Date.new(2010, 1, 1)) ]
|
224
|
+
end
|
225
|
+
end
|
226
|
+
|
227
|
+
describe '#covered_by?' do
|
228
|
+
it "should be able to ascertain gaps left by a list of other Timeframes" do
|
229
|
+
Timeframe.new(:year => 2009).covered_by?(
|
230
|
+
Timeframe.new(:month => 1, :year => 2009),
|
231
|
+
Timeframe.new(Date.new(2009, 2, 1), Date.new(2010, 1, 1))
|
232
|
+
).should be_true
|
233
|
+
end
|
234
|
+
end
|
235
|
+
|
236
|
+
describe '#last_year' do
|
237
|
+
it "should return its predecessor in a previous year" do
|
238
|
+
Timeframe.this_year.last_year.should ==
|
239
|
+
Timeframe.new(Date.new(Date.today.year - 1, 1, 1), Date.new(Date.today.year, 1, 1))
|
240
|
+
end
|
241
|
+
end
|
242
|
+
|
243
|
+
describe 'Timeframe:class#interval' do
|
244
|
+
it 'should parse ISO 8601 interval format' do
|
245
|
+
Timeframe.interval('2009-01-01/2010-01-01').should == Timeframe.new(:year => 2009)
|
246
|
+
end
|
247
|
+
end
|
248
|
+
end
|
metadata
ADDED
@@ -0,0 +1,114 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: timeframe
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
prerelease: false
|
5
|
+
segments:
|
6
|
+
- 0
|
7
|
+
- 0
|
8
|
+
- 1
|
9
|
+
version: 0.0.1
|
10
|
+
platform: ruby
|
11
|
+
authors:
|
12
|
+
- Andy Rossmeissl
|
13
|
+
autorequire:
|
14
|
+
bindir: bin
|
15
|
+
cert_chain: []
|
16
|
+
|
17
|
+
date: 2010-07-15 00:00:00 -04:00
|
18
|
+
default_executable:
|
19
|
+
dependencies:
|
20
|
+
- !ruby/object:Gem::Dependency
|
21
|
+
name: rspec
|
22
|
+
prerelease: false
|
23
|
+
requirement: &id001 !ruby/object:Gem::Requirement
|
24
|
+
requirements:
|
25
|
+
- - ">="
|
26
|
+
- !ruby/object:Gem::Version
|
27
|
+
segments:
|
28
|
+
- 1
|
29
|
+
- 2
|
30
|
+
- 9
|
31
|
+
version: 1.2.9
|
32
|
+
type: :development
|
33
|
+
version_requirements: *id001
|
34
|
+
- !ruby/object:Gem::Dependency
|
35
|
+
name: activesupport
|
36
|
+
prerelease: false
|
37
|
+
requirement: &id002 !ruby/object:Gem::Requirement
|
38
|
+
requirements:
|
39
|
+
- - "="
|
40
|
+
- !ruby/object:Gem::Version
|
41
|
+
segments:
|
42
|
+
- 3
|
43
|
+
- 0
|
44
|
+
- 0
|
45
|
+
- beta4
|
46
|
+
version: 3.0.0.beta4
|
47
|
+
type: :runtime
|
48
|
+
version_requirements: *id002
|
49
|
+
- !ruby/object:Gem::Dependency
|
50
|
+
name: andand
|
51
|
+
prerelease: false
|
52
|
+
requirement: &id003 !ruby/object:Gem::Requirement
|
53
|
+
requirements:
|
54
|
+
- - ">="
|
55
|
+
- !ruby/object:Gem::Version
|
56
|
+
segments:
|
57
|
+
- 0
|
58
|
+
version: "0"
|
59
|
+
type: :runtime
|
60
|
+
version_requirements: *id003
|
61
|
+
description: A Ruby class for describing and interacting with timeframes.
|
62
|
+
email: andy@rossmeissl.net
|
63
|
+
executables: []
|
64
|
+
|
65
|
+
extensions: []
|
66
|
+
|
67
|
+
extra_rdoc_files:
|
68
|
+
- LICENSE
|
69
|
+
- README.rdoc
|
70
|
+
files:
|
71
|
+
- .document
|
72
|
+
- .gitignore
|
73
|
+
- LICENSE
|
74
|
+
- README.rdoc
|
75
|
+
- Rakefile
|
76
|
+
- VERSION
|
77
|
+
- lib/timeframe.rb
|
78
|
+
- lib/timeframe/ykk.rb
|
79
|
+
- spec/spec.opts
|
80
|
+
- spec/spec_helper.rb
|
81
|
+
- spec/timeframe_spec.rb
|
82
|
+
has_rdoc: true
|
83
|
+
homepage: http://github.com/rossmeissl/timeframe
|
84
|
+
licenses: []
|
85
|
+
|
86
|
+
post_install_message:
|
87
|
+
rdoc_options:
|
88
|
+
- --charset=UTF-8
|
89
|
+
require_paths:
|
90
|
+
- lib
|
91
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
92
|
+
requirements:
|
93
|
+
- - ">="
|
94
|
+
- !ruby/object:Gem::Version
|
95
|
+
segments:
|
96
|
+
- 0
|
97
|
+
version: "0"
|
98
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
99
|
+
requirements:
|
100
|
+
- - ">="
|
101
|
+
- !ruby/object:Gem::Version
|
102
|
+
segments:
|
103
|
+
- 0
|
104
|
+
version: "0"
|
105
|
+
requirements: []
|
106
|
+
|
107
|
+
rubyforge_project:
|
108
|
+
rubygems_version: 1.3.6
|
109
|
+
signing_key:
|
110
|
+
specification_version: 3
|
111
|
+
summary: Date intervals
|
112
|
+
test_files:
|
113
|
+
- spec/spec_helper.rb
|
114
|
+
- spec/timeframe_spec.rb
|