-
-
Save pleax/e9c0da1a6e92dd12cbc7 to your computer and use it in GitHub Desktop.
My solution for RPCFN#10: Business Hours
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment