Skip to content

Instantly share code, notes, and snippets.

@eregon

eregon/README Secret

Created June 26, 2010 12:26
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save eregon/951938000e732ca79825 to your computer and use it in GitHub Desktop.
Save eregon/951938000e732ca79825 to your computer and use it in GitHub Desktop.
RPCFN #10: Business Hours
Benoit Daloze
Compatible with 1.8 and 1.9
Need rspec2 (gem install rspec --pre) for the spec
Run the specs with `rspec .`
require "time"
Dir[File.expand_path("../*.rb", __FILE__)].each { |f| require f unless File.identical?(f, __FILE__) }
class BusinessHours
attr_reader :base_rule, :rules
NO_DAY = TimeRange.new(0..0)
ALL_DAY = TimeRange.new(0..24)
MIN = 60
HOUR = 60 * MIN
DAY = 24 * HOUR
def initialize(opening, closing, &block)
@rules = []
@base_rule = Rule.new(opening, closing)
instance_exec(&block) if block
end
def update(time, opening, closing)
@rules << SpecialDay.new(time, opening, closing)
end
def closed(*days)
days.each { |day|
@rules << ClosingDay.new(day)
}
end
def calculate_deadline(duration, start_time)
start_time, duration = Time.parse(start_time), Rational(duration, HOUR)
open, close = work_time(start_time).to_a
# if start_time is out of opening hours
open = [[BusinessHours.parse_hour(start_time), open].max, close].min
start_time = start_time.beginning_of_day + open*HOUR
today_work = close - open
until today_work >= duration
duration -= today_work
open, close = work_time(start_time += DAY).to_a
start_time = start_time.beginning_of_day + open*HOUR
today_work = close - open
end
start_time + duration*HOUR
end
def work_time(day)
work_time = @rules.map { |rule| rule.time_range(day) }.reduce(:&)
work_time = @base_rule.time_range(day) if work_time.nil? or work_time == ALL_DAY
work_time
end
def self.parse_day(day)
case day
when Symbol
WeekDay.new(day)
when String
Time.parse(day)
end
end
def self.parse_hour(hour) #=> Rational: (0-23h)+min/60
case hour
when Numeric
hour
when Time
hour.hour + Rational(hour.min, MIN)
when /\A(\d{1,2}):(\d{2}) (AM|PM)\z/
$1.to_i+($3 == "PM" ? 12 : 0) + Rational($2.to_i, MIN)
end
end
end
require "business_hours"
describe BusinessHours do
let(:hours) {
BusinessHours.new("9:00 AM", "3:00 PM") {
update :fri, "10:00 AM", "5:00 PM"
update "Fri Dec 24, 2010", "8:00 AM", "1:00 PM"
closed :sun, :wed, "Dec 25, 2010"
}
}
it "work_time" do
hours.work_time(Time.parse("Mon Jun 7, 2010")).should == (9..3+12)
hours.work_time(Time.parse("Fri Jun 11, 2010")).should == (10..5+12)
hours.work_time(Time.parse("Fri Dec 24, 2010")).should == (8..1+12)
hours.work_time(Time.parse("Sat Dec 25, 2010")).should == (0..0)
hours.work_time(Time.parse("Wed Jun 9, 2010")).should == (0..0)
hours.work_time(Time.parse("Sun Jun 13, 2010")).should == (0..0)
end
it "init test" do
hours = BusinessHours.new("8:00 AM", "5:20 PM")
hours.base_rule.opening.should == 8
hours.base_rule.closing.should == 17+Rational(20,60)
hours.calculate_deadline( 5*60, "Dec 21, 2009 3:00 PM").should == Time.parse("Mon Dec 21 15:05:00 2009")
end
it "basic test" 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 "closed test" do
hours.calculate_deadline( 15*60, "Jun 8, 2010 2:48 PM").should == Time.parse("Thu Jun 10 09:03:00 2010")
end
it "long test" 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
class ClosingDay
def initialize(day)
@day = BusinessHours.parse_day(day)
end
def time_range(day)
if @day.same_day? day
BusinessHours::NO_DAY
else
BusinessHours::ALL_DAY
end
end
end
class Rule
attr_reader :opening, :closing
def initialize(opening, closing)
@opening, @closing = BusinessHours.parse_hour(opening), BusinessHours.parse_hour(closing)
end
def time_range(day)
TimeRange.new @opening..@closing
end
end
class SpecialDay
def initialize(day, opening, closing)
@day = BusinessHours.parse_day(day)
@opening, @closing = BusinessHours.parse_hour(opening), BusinessHours.parse_hour(closing)
end
def time_range(day)
if @day.same_day? day
TimeRange.new @opening..@closing, @day
else
BusinessHours::ALL_DAY
end
end
end
class Time
def same_day? time
year == time.year and month == time.month and day == time.day
end
def beginning_of_day
Time.local year, month, day
end
end
class TimeRange < Range
PRIORITIES = {
:low => -1,
:normal => 0,
:high => 1
}
attr_reader :priority
def initialize(range, priority = PRIORITIES[:normal])
super(range.first, range.last)
@priority = case priority
when WeekDay
PRIORITIES[:normal]
when Time
PRIORITIES[:high]
else
priority
end
end
def == range
first == range.first and last == range.last
end
def & range
if [first, last, range.first, range.last].all? { |e| Numeric === e }
if @priority != range.priority
@priority > range.priority ? self : range
else
f, l = [first,range.first].max, [last,range.last].min
TimeRange.new( f <= l ? (f..l) : (0..0), @priority )
end
end
end
def to_a
[first, last]
end
end
class WeekDay
def initialize(sym)
@sym = sym
end
def same_day?(time)
time.strftime("%a").downcase == @sym.to_s
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment