Moved to external module for business time calculation.

This commit is contained in:
Martin Edenhofer 2013-03-25 13:00:29 +01:00
parent b30ef07940
commit be6a3fdcc4
5 changed files with 351 additions and 13 deletions

View file

@ -0,0 +1,70 @@
module BusinessTime
class BusinessMinutes
def initialize(minutes)
@minutes = minutes
end
def ago
Time.zone ? before(Time.zone.now) : before(Time.now)
end
def from_now
Time.zone ? after(Time.zone.now) : after(Time.now)
end
def after(time)
after_time = Time.roll_forward(time)
# Step through the hours, skipping over non-business hours
@minutes.times do
after_time = after_time + 1.minute
# Ignore minutes before opening and after closing
if (after_time > Time.end_of_workday(after_time))
after_time = after_time + off_minutes
end
# Ignore weekends and holidays
while !Time.workday?(after_time)
after_time = after_time + 1.day
end
end
after_time
end
alias_method :since, :after
def before(time)
before_time = Time.roll_forward(time)
# Step through the hours, skipping over non-business hours
@minutes.times do
before_time = before_time - 1.minute
# Ignore hours before opening and after closing
if (before_time < Time.beginning_of_workday(before_time))
before_time = before_time - off_minutes
end
# Ignore weekends and holidays
while !Time.workday?(before_time)
before_time = before_time - 1.day
end
end
before_time
end
private
def off_minutes
return @gap if @gap
if Time.zone
gap_end = Time.zone.parse(BusinessTime::Config.beginning_of_workday)
gap_begin = (Time.zone.parse(BusinessTime::Config.end_of_workday)-1.day)
else
gap_end = Time.parse(BusinessTime::Config.beginning_of_workday)
gap_begin = (Time.parse(BusinessTime::Config.end_of_workday) - 1.day)
end
@gap = gap_end - gap_begin
end
end
end

View file

@ -0,0 +1,11 @@
# hook into fixnum so we can say things like:
# 5.business_minutes.from_now
# 4.business_minutes.before(some_date_time)
class Fixnum
include BusinessTime
def business_minutes
BusinessMinutes.new(self)
end
alias_method :business_minute, :business_minutes
end

View file

@ -0,0 +1,131 @@
# Add workday and weekday concepts to the Time class
class Time
class << self
# Gives the time at the end of the workday, assuming that this time falls on a
# workday.
# Note: It pretends that this day is a workday whether or not it really is a
# workday.
def end_of_workday(day)
format = "%B %d %Y #{BusinessTime::Config.end_of_workday}"
Time.zone ? Time.zone.parse(day.strftime(format)) :
Time.parse(day.strftime(format))
end
# Gives the time at the beginning of the workday, assuming that this time
# falls on a workday.
# Note: It pretends that this day is a workday whether or not it really is a
# workday.
def beginning_of_workday(day)
format = "%B %d %Y #{BusinessTime::Config.beginning_of_workday}"
Time.zone ? Time.zone.parse(day.strftime(format)) :
Time.parse(day.strftime(format))
end
# True if this time is on a workday (between 00:00:00 and 23:59:59), even if
# this time falls outside of normal business hours.
def workday?(day)
Time.weekday?(day) &&
!BusinessTime::Config.holidays.include?(day.to_date)
end
# True if this time falls on a weekday.
def weekday?(day)
BusinessTime::Config.weekdays.include? day.wday
end
def before_business_hours?(time)
time < beginning_of_workday(time)
end
def after_business_hours?(time)
time > end_of_workday(time)
end
# Rolls forward to the next beginning_of_workday
# when the time is outside of business hours
def roll_forward(time)
if (Time.before_business_hours?(time) || !Time.workday?(time))
next_business_time = Time.beginning_of_workday(time)
elsif Time.after_business_hours?(time + 1)
next_business_time = Time.beginning_of_workday(time) + 1.day
else
next_business_time = time.clone
end
while !Time.workday?(next_business_time)
puts '4'
next_business_time += 1.day
end
next_business_time
end
end
end
class Time
def business_time_until(to_time)
# Make sure that we will calculate time from A to B "clockwise"
direction = 1
if self < to_time
time_a = self
time_b = to_time
else
time_a = to_time
time_b = self
direction = -1
end
# Align both times to the closest business hours
time_a = Time::roll_forward(time_a)
time_b = Time::roll_forward(time_b)
# If same date, then calculate difference straight forward
if time_a.to_date == time_b.to_date
result = time_b - time_a
return result *= direction
end
# Both times are in different dates
result = Time.parse(time_a.strftime('%Y-%m-%d ') + BusinessTime::Config.end_of_workday) - time_a # First day
result += time_b - Time.parse(time_b.strftime('%Y-%m-%d ') + BusinessTime::Config.beginning_of_workday) # Last day
# All days in between
puts "--- #{time_a}-#{time_b} - #{direction}"
# duration_of_working_day = Time.parse(BusinessTime::Config.end_of_workday) - Time.parse(BusinessTime::Config.beginning_of_workday)
# result += (time_a.to_date.business_days_until(time_b.to_date) - 1) * duration_of_working_day
result = 0
# All days in between
time_c = time_a
while time_c.to_i < time_b.to_i do
end_of_workday = Time.end_of_workday(time_c)
if !Time.workday?(time_c)
time_c = Time.beginning_of_workday(time_c) + 1.day
puts 'VACATIONS! ' + time_c.to_s
end
if time_c.to_date == time_b.to_date
if end_of_workday < time_b
result += end_of_workday - time_c
break
else
result += time_b - time_c
break
end
else
result += end_of_workday - time_c
time_c = Time::roll_forward(end_of_workday)
end
result += 1 if end_of_workday.to_s =~ /23:59:59/
end
# Make sure that sign is correct
result *= direction
end
end

40
lib/time_calculation.rb Normal file
View file

@ -0,0 +1,40 @@
require 'business_time'
require 'business_time/business_minutes'
require 'business_time/core_ext/fixnum_minute'
require 'business_time/core_ext/time_fix'
module TimeCalculation
def self.config(config)
BusinessTime::Config.beginning_of_workday = config['beginning_of_workday']
BusinessTime::Config.end_of_workday = config['end_of_workday']
days = []
['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'].each {|day|
if config[day]
days.push day.downcase.to_sym
end
}
BusinessTime::Config.work_week = days
holidays = []
if config['holidays']
config['holidays'].each {|holiday|
date = Date.parse( holiday )
holidays.push date.to_date
}
end
BusinessTime::Config.holidays = holidays
end
def self.business_time_diff(start_time, end_time)
start_time = Time.parse( start_time.to_s + 'UTC' )
end_time = Time.parse( end_time.to_s + 'UTC' )
diff = start_time.business_time_until( end_time ) / 60
diff.round
end
def self.dest_time(start_time, diff_in_min)
start_time = Time.parse( start_time.to_s + 'UTC' )
dest_time = diff_in_min.round.business_minute.after( start_time )
return dest_time
end
end

View file

@ -1,5 +1,6 @@
# encoding: utf-8 # encoding: utf-8
require 'test_helper' require 'test_helper'
require 'time_calculation'
class WorkingTimeTest < ActiveSupport::TestCase class WorkingTimeTest < ActiveSupport::TestCase
test 'working time' do test 'working time' do
@ -9,32 +10,117 @@ class WorkingTimeTest < ActiveSupport::TestCase
{ {
:start => '2012-12-17 08:00:00', :start => '2012-12-17 08:00:00',
:end => '2012-12-18 08:00:00', :end => '2012-12-18 08:00:00',
:diff => 480, :diff => 600,
:config => { :config => {
:work_week => [:mon, :tue, :wed, :thu, :fri ], 'Mon' => true,
:beginning_of_workday => '8:00 am', 'Tue' => true,
:end_of_workday => '6:00 pm', 'Wed' => true,
'Thu' => true,
'Fri' => true,
'beginning_of_workday' => '8:00 am',
'end_of_workday' => '6:00 pm',
}, },
}, },
# test 2 # test 2
{ {
:start => '2012-12-23 08:00:00', :start => '2012-12-17 08:00:00',
:end => '2012-12-24 10:30:42', :end => '2012-12-17 09:00:00',
:diff => 0, :diff => 60,
:config => { :config => {
:work_week => [:mon, :tue, :wed, :thu, :fri ], 'Mon' => true,
:beginning_of_workday => '8:00 am', 'Tue' => true,
:end_of_workday => '6:00 pm', 'Wed' => true,
:holidays => [ 'Thu' => true,
'Fri' => true,
'beginning_of_workday' => '8:00 am',
'end_of_workday' => '6:00 pm',
},
},
# test 3
{
:start => '2012-12-17 08:00:00',
:end => '2012-12-17 08:15:00',
:diff => 15,
:config => {
'Mon' => true,
'Tue' => true,
'Wed' => true,
'Thu' => true,
'Fri' => true,
'beginning_of_workday' => '8:00 am',
'end_of_workday' => '6:00 pm',
},
},
# test 4
{
:start => '2012-12-23 08:00:00',
:end => '2012-12-27 10:30:42',
# :diff => 0,
:diff => 151,
:config => {
'Mon' => true,
'Tue' => true,
'Wed' => true,
'Thu' => true,
'Fri' => true,
'beginning_of_workday' => '8:00 am',
'end_of_workday' => '6:00 pm',
'holidays' => [
'2012-12-24', '2012-12-25', '2012-12-26' '2012-12-24', '2012-12-25', '2012-12-26'
], ],
}, },
}, },
] ]
tests.each { |test| tests.each { |test|
# diff = some_method( test[:start], test[:end], test[:config] ) TimeCalculation.config( test[:config] )
# assert_equal( diff, test[:diff], 'diff' ) diff = TimeCalculation.business_time_diff( test[:start], test[:end] )
assert_equal( diff, test[:diff], 'diff' )
} }
end end
test 'dest time' do
tests = [
# test 1
{
:start => '2012-12-17 08:00:00',
:dest_time => '2012-12-17 18:00:00',
:diff => 600,
:config => {
'Mon' => true,
'Tue' => true,
'Wed' => true,
'Thu' => true,
'Fri' => true,
'beginning_of_workday' => '8:00 am',
'end_of_workday' => '6:00 pm',
},
},
# test 2
{
:start => '2012-12-17 08:00:00',
:dest_time => '2012-12-18 08:30:00',
:diff => 630,
:config => {
'Mon' => true,
'Tue' => true,
'Wed' => true,
'Thu' => true,
'Fri' => true,
'beginning_of_workday' => '8:00 am',
'end_of_workday' => '6:00 pm',
},
},
]
tests.each { |test|
TimeCalculation.config( test[:config] )
dest_time = TimeCalculation.dest_time( test[:start], test[:diff] )
assert_equal( dest_time, Time.parse( test[:dest_time] + ' UTC' ), 'dest time' )
}
end
end end