Skip to content

Instantly share code, notes, and snippets.

@pleax
Created May 28, 2010 16:07
Show Gist options
  • Star 7 You must be signed in to star a gist
  • Fork 4 You must be signed in to fork a gist
  • Save pleax/e9c0da1a6e92dd12cbc7 to your computer and use it in GitHub Desktop.
Save pleax/e9c0da1a6e92dd12cbc7 to your computer and use it in GitHub Desktop.
My solution for RPCFN#10: Business Hours
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
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