Skip to content

Commit

Permalink
Implement day-increment duration calculations
Browse files Browse the repository at this point in the history
Here's the syntax:
```ruby
Biz.time(5, :days).after(Time.now)
```

Behavior notes:
* The next business time at least 24 hours in the indicated direction
  will be returned.
* If the equivalent time in the specified number of days isn't a
  business time, the *next* business time in the direction of the
  calculation (e.g. before or after) will be returned.
  • Loading branch information
craiglittle committed Mar 20, 2015
1 parent b66bf78 commit cdfc461
Show file tree
Hide file tree
Showing 18 changed files with 310 additions and 108 deletions.
10 changes: 5 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,9 @@ Biz.time(30, :minutes).before(Time.utc(2015, 1, 1, 11, 45))
# Find the time an amount of business time *after* a specified starting time
Biz.time(2, :hours).after(Time.utc(2015, 12, 25, 9, 30))

# Calculations can be performed in seconds, minutes, hours, or days
Biz.time(1, :day).after(Time.utc(2015, 1, 8, 10))

# Find the amount of business time between two times
Biz.within(Time.utc(2015, 3, 7), Time.utc(2015, 3, 14)).in_seconds

Expand All @@ -90,9 +93,7 @@ which you can use to do your own custom calculations or just get a better idea
of what's happening under the hood:

```ruby
Biz.periods
.after(Time.utc(2015, 1, 10, 10))
.timeline.forward
Biz.periods.after(Time.utc(2015, 1, 10, 10)).timeline
.until(Time.utc(2015, 1, 17, 10)).to_a

#=> [#<Biz::TimeSegment start_time=2015-01-10 18:00:00 UTC end_time=2015-01-10 22:00:00 UTC>,
Expand All @@ -103,8 +104,7 @@ Biz.periods
# #<Biz::TimeSegment start_time=2015-01-15 21:00:00 UTC end_time=2015-01-16 01:00:00 UTC>]

Biz.periods
.before(Time.utc(2015, 5, 5, 12, 34, 57))
.timeline.backward
.before(Time.utc(2015, 5, 5, 12, 34, 57)).timeline
.for(Biz::Duration.minutes(3_598)).to_a

#=> [#<Biz::TimeSegment start_time=2015-05-05 07:00:00 UTC end_time=2015-05-05 12:34:57 UTC>,
Expand Down
5 changes: 2 additions & 3 deletions lib/biz/calculation/duration_within.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,9 @@ module Biz
module Calculation
class DurationWithin < SimpleDelegator

def initialize(periods, calculation_period)
def initialize(schedule, calculation_period)
super(
periods.after(calculation_period.start_time)
.timeline.forward
schedule.periods.after(calculation_period.start_time).timeline
.until(calculation_period.end_time)
.map(&:duration)
.reduce(Duration.new(0), :+)
Expand Down
104 changes: 87 additions & 17 deletions lib/biz/calculation/for_duration.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,30 +2,100 @@ module Biz
module Calculation
class ForDuration

attr_reader :periods,
:duration
UNITS = Set.new(%i[second seconds minute minutes hour hours day days])

def initialize(periods, duration)
unless duration.positive?
fail ArgumentError, 'Duration adjustment must be positive.'
include AbstractType

def self.with_unit(schedule, scalar, unit)
unless UNITS.include?(unit)
fail ArgumentError, 'The unit is not supported.'
end

@periods = periods
@duration = duration
send(unit, schedule, scalar)
end

def self.unit
name.split('::').last.downcase.to_sym
end

def initialize(schedule, scalar)
@schedule = schedule
@scalar = scalar
end

def before(time)
periods.before(time)
.timeline.backward
.for(duration).to_a
.last.start_time
abstract_method :before,
:after

protected

attr_reader :schedule,
:scalar

private

def unit
self.class.unit
end

def after(time)
periods.after(time)
.timeline.forward
.for(duration).to_a
.last.end_time
[
*%i[second seconds minute minutes hour hours].map { |unit|
const_set(unit.to_s.capitalize,
Class.new(self) do
def before(time)
timeline(:before, time).last.start_time
end

def after(time)
timeline(:after, time).last.end_time
end

private

def timeline(direction, time)
schedule.periods.send(direction, time).timeline
.for(Duration.send(unit, scalar)).to_a
end
end
)
},
*%i[day days].map { |unit|
const_set(unit.to_s.capitalize,
Class.new(self) do
def before(time)
periods(:before, time).first.end_time
end

def after(time)
periods(:after, time).first.start_time
end

private

def periods(direction, time)
schedule.periods.send(direction, advanced_date(direction, time))
end

def advanced_date(direction, time)
schedule.in_zone.on_date(
schedule.dates.days(scalar).send(direction, local(time)),
day_time(time)
)
end

def day_time(time)
DayTime.from_time(local(time))
end

def local(time)
schedule.in_zone.local(time)
end
end
)
}
].each do |unit_class|
define_singleton_method(unit_class.unit) { |schedule, scalar|
unit_class.new(schedule, scalar)
}
end

end
Expand Down
2 changes: 1 addition & 1 deletion lib/biz/core_ext/fixnum.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ module Biz
module CoreExt
module Fixnum

%i[second seconds minute minutes hour hours].each do |unit|
Calculation::ForDuration::UNITS.each do |unit|
define_method("business_#{unit}") { Biz.time(self, unit) }
end

Expand Down
14 changes: 14 additions & 0 deletions lib/biz/dates.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
module Biz
class Dates < SimpleDelegator

def initialize(schedule)
super(
Clavius::Schedule.new do |c|
c.weekdays = schedule.weekdays
c.excluded = schedule.holidays.map(&:date)
end
)
end

end
end
42 changes: 23 additions & 19 deletions lib/biz/day_time.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,34 +8,38 @@ class DayTime

extend Forwardable

def self.from_hour(hour)
new(hour * Time::MINUTES_IN_HOUR)
end

def self.from_timestamp(timestamp)
timestamp.match(TIMESTAMP_PATTERN) { |match|
new(match[:hour].to_i * Time::MINUTES_IN_HOUR + match[:minute].to_i)
}
end
class << self

def self.midnight
MIDNIGHT
end
def from_time(time)
new(time.hour * Time::MINUTES_IN_HOUR + time.min)
end

def self.noon
NOON
end
def from_hour(hour)
new(hour * Time::MINUTES_IN_HOUR)
end

def self.endnight
ENDNIGHT
end
def from_timestamp(timestamp)
timestamp.match(TIMESTAMP_PATTERN) { |match|
new(match[:hour].to_i * Time::MINUTES_IN_HOUR + match[:minute].to_i)
}
end

class << self
def midnight
MIDNIGHT
end

alias_method :am, :midnight

def noon
NOON
end

alias_method :pm, :noon

def endnight
ENDNIGHT
end

end

attr_reader :day_minute
Expand Down
10 changes: 0 additions & 10 deletions lib/biz/duration.rb
Original file line number Diff line number Diff line change
@@ -1,23 +1,13 @@
module Biz
class Duration

UNITS = Set.new(%i[second seconds minute minutes hour hours])

include Equalizer.new(:seconds)
include Comparable

extend Forwardable

class << self

def with_unit(scalar, unit)
unless UNITS.include?(unit)
fail ArgumentError, 'The unit is not supported.'
end

public_send(unit, scalar)
end

def seconds(seconds)
new(seconds)
end
Expand Down
4 changes: 4 additions & 0 deletions lib/biz/periods/after.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@ module Biz
class Periods
class After < Abstract

def timeline
super.forward
end

private

def weeks
Expand Down
4 changes: 4 additions & 0 deletions lib/biz/periods/before.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@ module Biz
class Periods
class Before < Abstract

def timeline
super.backward
end

private

def weeks
Expand Down
14 changes: 6 additions & 8 deletions lib/biz/schedule.rb
Original file line number Diff line number Diff line change
Expand Up @@ -25,17 +25,11 @@ def dates
alias_method :date, :dates

def time(scalar, unit)
Calculation::ForDuration.new(
periods,
Duration.with_unit(scalar, unit)
)
Calculation::ForDuration.with_unit(self, scalar, unit)
end

def within(origin, terminus)
Calculation::DurationWithin.new(
periods,
TimeSegment.new(origin, terminus)
)
Calculation::DurationWithin.new(self, TimeSegment.new(origin, terminus))
end

def in_hours?(time)
Expand All @@ -44,6 +38,10 @@ def in_hours?(time)

alias_method :business_hours?, :in_hours?

def in_zone
Time.new(time_zone)
end

protected

attr_reader :configuration
Expand Down
2 changes: 1 addition & 1 deletion spec/calculation/duration_within_spec.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
RSpec.describe Biz::Calculation::DurationWithin do
subject(:calculation) {
described_class.new(schedule.periods, calculation_period)
described_class.new(schedule, calculation_period)
}

context 'when the calculation start time is after the end time' do
Expand Down
Loading

0 comments on commit cdfc461

Please sign in to comment.