Permalink
Browse files

time and duration ranges

  • Loading branch information...
1 parent 1bbc6f1 commit acad5113c8bec8fea33c5667dac305465195d0b0 @kristianmandrup committed Sep 25, 2012
View
@@ -4,7 +4,7 @@ gem 'chronic'
gem 'chronic_duration'
gem 'activesupport', '>= 3.0.0'
gem 'spanner'
-gem 'sugar-high', '~> 0.7.1' # for range intersect
+gem 'sugar-high', '~> 0.7.2' # for range intersect
gem 'xduration', '~> 2.2'
group :test, :development do
View
@@ -104,6 +104,7 @@ GEM
hike (~> 1.2)
rack (~> 1.0)
tilt (~> 1.1, != 1.3.0)
+ sugar-high (0.7.2)
thor (0.15.4)
tilt (1.3.3)
treetop (1.4.10)
@@ -130,4 +131,5 @@ DEPENDENCIES
rspec (>= 2.8.0)
simplecov (>= 0.5)
spanner
+ sugar-high (~> 0.7.2)
xduration (~> 2.2)
View
@@ -309,6 +309,23 @@ TimeSpan.start_field = :start
TimeSpan.end_field = :end
```
+## Ranges
+
+A Range can be converted into either a `Timespan` or a `DurationRange`
+
+### DurationRange
+
+```ruby
+dr = (1..5).days # => DurationRange 1..5, :days
+ts =(1..5).days(:timespan) # => Timespan start_date: 1.day.from_now, end_date: 5.days.from_now
+
+dr.between?(4.days) # => true
+```
+
+You can also use Range#intersect from *sugar-high* gem to test intersection of time ranges ;)
+
+### Timespan
+
## Contributing to Timespan
* Check out the latest master to make sure the feature hasn't been implemented or the bug hasn't been fixed yet.
View
@@ -1 +1 @@
-0.4.7
+0.4.8
View
@@ -5,6 +5,10 @@
# Range intersection that works with dates!
require 'sugar-high/range'
+require 'sugar-high/delegate'
+require 'sugar-high/kind_of'
+
+require 'timespan/core_ext'
require 'timespan/units'
require 'timespan/compare'
@@ -47,7 +51,6 @@ class TimeParseError < StandardError; end
def initialize options = {}
@is_new = true
-
@init_options = options
validate! if options == {}
@@ -64,6 +67,26 @@ def initialize options = {}
end
class << self
+ def max_date
+ @max_date ||= Time.now + 10.years
+ end
+
+ def min_date
+ @min_date ||= Time.now
+ end
+
+ def max_date= date
+ @max_date = date if valid_date?(date)
+ end
+
+ def min_date= date
+ @max_date = date if valid_date?(date)
+ end
+
+ def asap options = {}
+ self.new options.merge(asap: true)
+ end
+
def from start, duration, options = {}
start = case start.to_sym
when :now, :asap
@@ -101,6 +124,12 @@ def untill ending
self.new start_date: Date.now, end_date: ending
end
alias_method :until, :untill
+
+ protected
+
+ def valid_date? date
+ date.any_kind_of?(Date, Time, DateTime)
+ end
end
def start_time= time
@@ -147,15 +176,18 @@ def untill time
alias_method :until, :untill
def convert_to_time time
+ time = Duration.new time if time.kind_of? Numeric
case time
when String
Chronic.parse(time)
when Date, DateTime
time.to_time
+ when Duration
+ (Time.now + time).to_time
when Time
time
else
- raise ArgumentError, "A valid time must be either a String, Date, Time or DateTime, was: #{time.inspect}"
+ raise ArgumentError, "A valid time must be either a String, Duration, Date, Time or DateTime, was: #{time.inspect} (#{time.class})"
end
end
@@ -174,18 +206,38 @@ def configure! options = {}
dur = options[first_from(DURATION_KEYS, options)]
asap = options[:asap]
- self.asap = asap if asap
+ if options[:at_least]
+ to = Timespan.max_date
+ from = Time.now + options[:at_least]
+ end
+
+ if options[:at_most]
+ to = Time.now + options[:at_most]
+ from = Time.now
+ end
+
+ if options[:between]
+ from = Time.now + options[:between].min
+ to = Time.now + options[:between].max
+ end
+
+ # puts "configure: to:#{to}, from:#{from}, dur:#{dur}, asap:#{asap}"
+
+ @asap = asap if asap
self.duration = dur if dur
self.start_time = from if from
self.end_time = to if to
+ # puts "configured: start:#{self.start_time}, end:#{self.end_time}, duration:#{self.duration}, asap:#{self.asap?}"
+
default_from_now!
calculate_miss!
rescue ArgumentError => e
raise TimeParseError, e.message
- rescue Exception => e
- calculate_miss!
- validate!
+ # rescue Exception => e
+ # puts "Exception: #{e}"
+ # calculate_miss!
+ # validate!
end
def default_from_now!
@@ -0,0 +1 @@
+require 'timespan/core_ext/range'
@@ -4,4 +4,8 @@ def __evolve_to_timespan__
object = self
::Timespan.new :from => serializer.from(object), :to => serializer.to(object), asap: serializer.asap(object)
end
+
+ def __evolve_to_duration_range__
+ ::DurationRange.new Range.new(self[:from], self[:to]), :seconds
+ end
end
@@ -0,0 +1,115 @@
+class TimespanRange < DelegateDecorator
+ attr_accessor :unit, :range
+
+ def initialize range, unit
+ super(range)
+ @range = Timespan.new between: range
+ @unit = unit.to_s.singularize.to_sym
+ end
+end
+
+class DurationRange < DelegateDecorator
+ attr_accessor :unit, :range
+
+ def initialize range, unit
+ super(range)
+ @unit = unit.to_s.singularize.to_sym
+ @range = range
+ end
+
+ def __evolve_to_duration_range__
+ self
+ end
+
+ def mongoize
+ {:from => range.min, :to => range.max}
+ end
+
+ def between? duration
+ obj = case duration
+ when Duration
+ duration
+ else
+ Duration.new duration
+ end
+ obj.total >= min && obj.total <= max
+ end
+
+ class << self
+ # See http://mongoid.org/en/mongoid/docs/upgrading.html
+
+ # Serialize a Hash (with DurationRange keys) or a DurationRange to
+ # a BSON serializable type.
+ #
+ # @param [Timespan, Hash, Integer, String] value
+ # @return [Hash] Timespan in seconds
+ def mongoize object
+ case object
+ when DurationRange then object.mongoize
+ when Hash
+ range = Range.new parse(object[:from]).total, parse(object[:to]).total
+ ::DurationRange.new mongoize(range)
+ when Range
+ object.send(:seconds)
+ else
+ object
+ end
+ end
+
+ # Deserialize a Timespan given the hash stored by Mongodb
+ #
+ # @param [Hash] Timespan as hash
+ # @return [Timespan] deserialized Timespan
+ def demongoize(object)
+ return if !object
+ case object
+ when Hash
+ object.__evolve_to_duration_range__
+ else
+ parse object
+ end
+ end
+
+ # Converts the object that was supplied to a criteria and converts it
+ # into a database friendly form.
+ def evolve(object)
+ object.__evolve_to_duration_range__.mongoize
+ end
+
+ protected
+
+ def parse duration
+ if duration.kind_of? Numeric
+ return Duration.new duration
+ else
+ case duration
+ when Timespan
+ duration.duration
+ when Duration
+ duration
+ when Hash
+ Duration.new duration
+ when Time
+ duration.to_i
+ when DateTime, Date
+ duration.to_time.to_i
+ when String
+ Duration.new parse_duration(duration)
+ else
+ raise ArgumentError, "Unsupported duration type: #{duration.inspect} of class #{duration.class}"
+ end
+ end
+ end
+
+ end
+end
+
+
+class Range
+ [:seconds, :minutes, :hours, :days, :weeks, :months, :years].each do |unit|
+ define_method unit do |type = :duration|
+ timerange = Range.new self.min.send(unit), self.max.send(unit)
+ type == :timespan ? TimespanRange.new(timerange, unit) : DurationRange.new(timerange, unit)
+ end
+ end
+end
@@ -0,0 +1,60 @@
+require 'timespan/mongoid/spec_helper'
+
+describe Range do
+ subject { timerange }
+
+ describe 'create DurationRange' do
+ let(:range) { (1..5) }
+ let (:timerange) { range.days }
+
+ specify { subject.should be_a DurationRange }
+
+ its(:min) { should be_a Fixnum }
+ its(:max) { should be_a Fixnum }
+
+ specify { subject.min.should == 1.day }
+ specify { subject.max.should == 5.days }
+ end
+end
+
+describe DurationRange do
+ subject { timerange }
+
+ let(:range) { (1..5) }
+
+ context 'day range' do
+ let (:timerange) { range.days }
+
+ its(:range) { should be_a Range }
+ its(:min) { should == 1.day }
+ its(:max) { should == 5.days }
+ its(:unit) { should == :day }
+
+ specify { subject.between?(4.days).should be_true }
+ end
+
+ context 'week range' do
+ let (:timerange) { range.weeks }
+
+ its(:range) { should be_a Range }
+ its(:min) { should == 1.week }
+ its(:max) { should == 5.weeks }
+ its(:unit) { should == :week }
+ end
+
+ context 'month range' do
+ let (:timerange) { range.months }
+
+ its(:min) { should == 1.month }
+ its(:max) { should == 5.months }
+ its(:unit) { should == :month }
+ end
+
+ context 'year range' do
+ let (:timerange) { range.years }
+
+ its(:min) { should == 1.year }
+ its(:max) { should == 5.years }
+ its(:unit) { should == :year }
+ end
+end
Oops, something went wrong.

0 comments on commit acad511

Please sign in to comment.