-
-
Save eregon/951938000e732ca79825 to your computer and use it in GitHub Desktop.
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
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 .` |
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" | |
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 |
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 "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 | |
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
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 |
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
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 |
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
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 |
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
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 |
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
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 |
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
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