From be6a3fdcc49c063f91310904da557a9d47e70f06 Mon Sep 17 00:00:00 2001 From: Martin Edenhofer Date: Mon, 25 Mar 2013 13:00:29 +0100 Subject: [PATCH] Moved to external module for business time calculation. --- lib/business_time/business_minutes.rb | 70 +++++++++++ lib/business_time/core_ext/fixnum_minute.rb | 11 ++ lib/business_time/core_ext/time_fix.rb | 131 ++++++++++++++++++++ lib/time_calculation.rb | 40 ++++++ test/unit/working_time_test.rb | 112 +++++++++++++++-- 5 files changed, 351 insertions(+), 13 deletions(-) create mode 100644 lib/business_time/business_minutes.rb create mode 100644 lib/business_time/core_ext/fixnum_minute.rb create mode 100644 lib/business_time/core_ext/time_fix.rb create mode 100644 lib/time_calculation.rb diff --git a/lib/business_time/business_minutes.rb b/lib/business_time/business_minutes.rb new file mode 100644 index 000000000..33d30b05f --- /dev/null +++ b/lib/business_time/business_minutes.rb @@ -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 diff --git a/lib/business_time/core_ext/fixnum_minute.rb b/lib/business_time/core_ext/fixnum_minute.rb new file mode 100644 index 000000000..af6a894de --- /dev/null +++ b/lib/business_time/core_ext/fixnum_minute.rb @@ -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 \ No newline at end of file diff --git a/lib/business_time/core_ext/time_fix.rb b/lib/business_time/core_ext/time_fix.rb new file mode 100644 index 000000000..9c03d6d07 --- /dev/null +++ b/lib/business_time/core_ext/time_fix.rb @@ -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 \ No newline at end of file diff --git a/lib/time_calculation.rb b/lib/time_calculation.rb new file mode 100644 index 000000000..18b00ad72 --- /dev/null +++ b/lib/time_calculation.rb @@ -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 diff --git a/test/unit/working_time_test.rb b/test/unit/working_time_test.rb index dba620a5f..90f8cb124 100644 --- a/test/unit/working_time_test.rb +++ b/test/unit/working_time_test.rb @@ -1,5 +1,6 @@ # encoding: utf-8 require 'test_helper' +require 'time_calculation' class WorkingTimeTest < ActiveSupport::TestCase test 'working time' do @@ -9,32 +10,117 @@ class WorkingTimeTest < ActiveSupport::TestCase { :start => '2012-12-17 08:00:00', :end => '2012-12-18 08:00:00', - :diff => 480, + :diff => 600, :config => { - :work_week => [:mon, :tue, :wed, :thu, :fri ], - :beginning_of_workday => '8:00 am', - :end_of_workday => '6:00 pm', + '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-23 08:00:00', - :end => '2012-12-24 10:30:42', - :diff => 0, + :start => '2012-12-17 08:00:00', + :end => '2012-12-17 09:00:00', + :diff => 60, :config => { - :work_week => [:mon, :tue, :wed, :thu, :fri ], - :beginning_of_workday => '8:00 am', - :end_of_workday => '6:00 pm', - :holidays => [ + 'Mon' => true, + 'Tue' => true, + 'Wed' => true, + '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' ], }, }, ] tests.each { |test| -# diff = some_method( test[:start], test[:end], test[:config] ) -# assert_equal( diff, test[:diff], 'diff' ) + TimeCalculation.config( test[:config] ) + diff = TimeCalculation.business_time_diff( test[:start], test[:end] ) + assert_equal( diff, test[:diff], 'diff' ) } 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