secret
Last active

My solution for RPCFN#10: Business Hours

  • Download Gist
business_hours.rb
Ruby
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110
require 'time'
require 'date'
 
class BusinessHours
 
class OpenHours
 
attr_reader :open, :close
 
def initialize(open, close)
@open, @close = open, close
end
 
def duration
@duration ||= @open < @close ? @close - @open : 0
end
 
CLOSED = new(0, 0)
 
def self.parse(open, close)
open = Time.parse(open)
close = Time.parse(close)
open = TimeUtils::seconds_from_midnight(open)
close = TimeUtils::seconds_from_midnight(close)
new(open, close)
end
 
def offset(seconds)
self.class.new([@open, seconds].max, @close)
end
 
end
 
module TimeUtils
 
class << self
 
def seconds_from_midnight(time)
time.hour*60*60 + time.min*60 + time.sec
end
def time_from_midnight(seconds)
hours, seconds = seconds.divmod(60 * 60)
minutes, seconds = seconds.divmod(60)
[hours, minutes, seconds]
end
 
end
 
end
 
WEEK_DAYS = Time::RFC2822_DAY_NAME.map { |m| m.downcase.to_sym }
 
def initialize(start_time, end_time)
open_hours = OpenHours.parse(start_time, end_time)
 
@week = {}
WEEK_DAYS.each do |day|
@week[day] = open_hours
end
 
@specific_days = {}
end
 
def update(day, start_time, end_time)
set_open_hours day, OpenHours.parse(start_time, end_time)
end
 
def closed(*days)
days.each do |day|
set_open_hours day, OpenHours::CLOSED
end
end
 
def calculate_deadline(job_duration, start_date_time)
start_date_time = Time.parse(start_date_time)
 
today = Date.civil(start_date_time.year, start_date_time.month, start_date_time.day)
open_hours = get_open_hours(today).offset(TimeUtils::seconds_from_midnight(start_date_time))
 
# here is possible to use strict greater operator if you want to stop on edge of previous business day.
# see "BusinessHours schedule without exceptions should flip the edge" spec
while job_duration >= open_hours.duration
job_duration -= open_hours.duration
 
today = today.next
open_hours = get_open_hours(today)
end
 
Time.local(today.year, today.month, today.day, *TimeUtils::time_from_midnight(open_hours.open + job_duration))
end
 
private
 
def get_open_hours(date)
@specific_days[date] || @week[WEEK_DAYS[date.wday]]
end
 
def set_open_hours(day, open_hours)
case day
when Symbol
@week[day] = open_hours
when String
@specific_days[Date.parse(day)] = open_hours
end
end
 
end
business_hours_spec.rb
Ruby
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163
require 'lib/business_hours.rb'
 
describe BusinessHours do
 
subject { BusinessHours.new("9:00 AM", "3:00 PM") }
 
it { should respond_to(:update) }
 
it { should respond_to(:closed) }
 
it { should respond_to(:calculate_deadline) }
 
context "schedule without exceptions" do
before { @hours = BusinessHours.new("9:00 AM", "3:00 PM") }
 
it "should handle start time during open hours" do
@hours.calculate_deadline(1*60*60, "Jun 7, 2010 9:10 AM").should == Time.parse("Jun 7, 2010 10:10 AM")
end
 
it "should handle start time before open hours" do
@hours.calculate_deadline(2*60*60, "Jun 7, 2010 8:45 AM").should == Time.parse("Jun 7, 2010 11:00 AM")
end
 
it "should handle start time after open hours" do
@hours.calculate_deadline(2*60*60, "Jun 7, 2010 10:45 PM").should == Time.parse("Jun 8, 2010 11:00 AM")
end
 
it "should finish job next day if not enough time left" do
@hours.calculate_deadline(2*60*60, "Jun 7, 2010 2:45 PM").should == Time.parse("Jun 8, 2010 10:45 AM")
end
 
it "should process huge job for several days" do
@hours.calculate_deadline(20*60*60, "Jun 7, 2010 10:45 AM").should == Time.parse("Jun 10, 2010 12:45 PM")
end
 
it "should flip the edge" do
@hours.calculate_deadline(6*60*60, "Jun 7, 2010 9:00 AM").should == Time.parse("Jun 8, 2010 9:00 AM")
end
 
# this is also possible, but I prefer previous variant
#
# it "should NOT flip the edge" do
# @hours.calculate_deadline(6*60*60, "Jun 7, 2010 9:00 AM").should == Time.parse("Jun 7, 2010 3:00 PM")
# end
 
context "on dst changes" do
 
it "should respect changing to dst" do
@hours.calculate_deadline(8*60*60, "March 27, 2010 2:00 PM").should == Time.parse("March 29, 2010 10:00 AM")
end
 
it "should respect changing to dst" do
@hours.calculate_deadline(2*60*60, "March 27, 2010 2:00 PM").should == Time.parse("March 28, 2010 10:00 AM")
end
 
it "should respect changing from dst" do
@hours.calculate_deadline(8*60*60, "October 31, 2010 2:00 PM").should == Time.parse("November 2, 2010 10:00 AM")
end
 
it "should respect changing from dst" do
@hours.calculate_deadline(2*60*60, "October 31, 2010 2:00 PM").should == Time.parse("November 1, 2010 10:00 AM")
end
 
end
 
end
 
context "schedule with closed weekdays" do
before do
@hours = BusinessHours.new("9:00 AM", "3:00 PM")
@hours.closed :sun, :wed
end
 
it "should skip closed days" do
@hours.calculate_deadline(2*60*60, "Jun 5, 2010 2:45 PM").should == Time.parse("Jun 7, 2010 10:45 AM")
end
 
it "should skip closed days even if work scheduled to closed day" do
@hours.calculate_deadline(2*60*60, "Jun 6, 2010 11:45 AM").should == Time.parse("Jun 7, 2010 11:00 AM")
end
end
 
context "schedule with closed specific days" do
 
before do
@hours = BusinessHours.new("9:00 AM", "3:00 PM")
@hours.closed "Dec 25, 2010"
end
 
it "should skip closed days" do
@hours.calculate_deadline(2*60*60, "Dec 24, 2010 2:45 PM").should == Time.parse("Dec 26, 2010 10:45 AM")
end
 
it "should skip closed days even if work scheduled to closed day" do
@hours.calculate_deadline(2*60*60, "Dec 25, 2010 11:45 AM").should == Time.parse("Dec 26, 2010 11:00 AM")
end
 
end
 
context "schedule with both closed weekdays and specific days" do
 
before do
@hours = BusinessHours.new("9:00 AM", "3:00 PM")
@hours.closed :sun, :wed, "Dec 25, 2010"
end
 
it "should skip closed days" do
@hours.calculate_deadline(2*60*60, "Dec 24, 2010 2:45 PM").should == Time.parse("Dec 27, 2010 10:45 AM")
end
end
 
context "schedule with different open hours in weekdays" do
before do
@hours = BusinessHours.new("9:00 AM", "3:00 PM")
@hours.update :fri, "10:00 AM", "5:00 PM"
end
 
it "should spend open hours" do
@hours.calculate_deadline(14*60*60, "Jun 3, 2010 9:00 AM").should == Time.parse("Jun 5, 2010 10:00 AM")
end
 
end
 
context "schedule with different open hours in specific days" do
before do
@hours = BusinessHours.new("9:00 AM", "3:00 PM")
@hours.update "Dec 24, 2010", "8:00 AM", "1:00 PM"
end
 
it "should spend open hours" do
@hours.calculate_deadline(12*60*60, "Dec 23, 2010 9:00 AM").should == Time.parse("Dec 25, 2010 10:00 AM")
end
 
it "should spend open hours if started at the day" do
@hours.calculate_deadline(6*60*60, "Dec 24, 2010 12:00 PM").should == Time.parse("Dec 25, 2010 2:00 PM")
end
 
end
 
context "original tests" do
 
before do
@hours = BusinessHours.new("9:00 AM", "3:00 PM")
@hours.update :fri, "10:00 AM", "5:00 PM"
@hours.update "Dec 24, 2010", "8:00 AM", "1:00 PM"
@hours.closed :sun, :wed, "Dec 25, 2010"
end
 
it "should pass test #1" do
@hours.calculate_deadline(2*60*60, "Jun 7, 2010 9:10 AM").should == Time.parse("Mon Jun 07 11:10:00 2010")
end
 
it "should pass test #2" do
@hours.calculate_deadline(15*60, "Jun 8, 2010 2:48 PM").should == Time.parse("Thu Jun 10 09:03:00 2010")
end
 
it "should pass test #3" do
@hours.calculate_deadline(7*60*60, "Dec 24, 2010 6:45 AM").should == Time.parse("Mon Dec 27 11:00:00 2010")
end
 
end
 
end

Please sign in to comment on this gist.

Something went wrong with that request. Please try again.