Skip to content

Commit ca99529

Browse files
committed
add rollback in past and substraction
1 parent b833e05 commit ca99529

File tree

6 files changed

+106
-7
lines changed

6 files changed

+106
-7
lines changed

Rakefile

+5
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,8 @@ require 'rspec/core/rake_task'
44
RSpec::Core::RakeTask.new(:spec)
55

66
task :default => :spec
7+
8+
desc "Open an irb session preloaded with working_hours"
9+
task :console do
10+
sh "irb -rubygems -I lib -r working_hours.rb"
11+
end

lib/working_hours.rb

-1
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,5 @@
55
require "working_hours/core_ext/fixnum"
66

77
module WorkingHours
8-
UnknownDuration = Class.new StandardError
98
InvalidConfiguration = Class.new StandardError
109
end

lib/working_hours/computation.rb

+36-1
Original file line numberDiff line numberDiff line change
@@ -40,12 +40,27 @@ def add_seconds origin, seconds
4040
end
4141
end
4242
end
43+
while seconds < 0
44+
# roll to previous business period
45+
time = return_to_working_time(time)
46+
# look at working ranges
47+
time_in_day = time.seconds_since_midnight
48+
config[:working_hours][time.wday].reverse_each do |from, to|
49+
if time_in_day > from and time_in_day <= to
50+
# take all we can
51+
take = [time_in_day - from, -seconds].min
52+
# advance time
53+
time -= take
54+
# decrease seconds
55+
seconds += take
56+
end
57+
end
58+
end
4359
convert_to_original_format time, origin
4460
end
4561

4662
def advance_to_working_time time
4763
time = in_config_zone(time)
48-
return time if in_working_hours? time
4964
loop do
5065
# skip holidays and weekends
5166
while not working_day?(time)
@@ -54,13 +69,33 @@ def advance_to_working_time time
5469
# find first working range after time
5570
time_in_day = time.seconds_since_midnight
5671
(Config.precompiled[:working_hours][time.wday] || {}).each do |from, to|
72+
return time if time_in_day >= from and time_in_day < to
5773
return time + (from - time_in_day) if from >= time_in_day
5874
end
5975
# if none is found, go to next day and loop
6076
time = (time + 1.day).beginning_of_day
6177
end
6278
end
6379

80+
def return_to_working_time time
81+
time = in_config_zone(time)
82+
loop do
83+
# skip holidays and weekends
84+
while not working_day?(time)
85+
time = (time - 1.day).end_of_day
86+
end
87+
# find last working range before time
88+
time_in_day = time.seconds_since_midnight
89+
(Config.precompiled[:working_hours][time.wday] || {}).reverse_each do |from, to|
90+
# round is used to suppress miliseconds hack from `end_of_day`
91+
return time.round if time_in_day > from and time_in_day <= to
92+
return (time - (time_in_day - to)).round if to <= time_in_day
93+
end
94+
# if none is found, go to previous day and loop
95+
time = (time - 1.day).end_of_day
96+
end
97+
end
98+
6499
def working_day? time
65100
time = in_config_zone(time)
66101
Config.precompiled[:working_hours][time.wday].present? and not Config.precompiled[:holidays].include?(time.to_date)

lib/working_hours/duration.rb

+18-1
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,24 @@ class Duration
1010
SUPPORTED_KINDS = [:days, :hours, :minutes, :seconds]
1111

1212
def initialize(value, kind)
13-
raise WorkingHours::UnknownDuration unless SUPPORTED_KINDS.include?(kind)
13+
raise ArgumentError.new("Invalid working time unit: #{kind}") unless SUPPORTED_KINDS.include?(kind)
1414
@value = value
1515
@kind = kind
1616
end
1717

18+
def -@
19+
Duration.new(-value, kind)
20+
end
21+
22+
def ==(other)
23+
self.class == other.class and kind == other.kind and value == other.value
24+
end
25+
alias :eql? :==
26+
27+
def hash
28+
[self.class, kind, value].hash
29+
end
30+
1831
def +(other)
1932
unless other.respond_to?(:in_time_zone)
2033
raise TypeError.new("Can't convert #{other.class} to a time")
@@ -26,5 +39,9 @@ def from_now
2639
self + Time.now
2740
end
2841

42+
def ago
43+
(-self) + Time.now
44+
end
45+
2946
end
3047
end

spec/working_hours/computation_spec.rb

+37
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,11 @@
5959
expect(add_hours(time, 2)).to eq(Time.utc(1991, 11, 15, 16, 00, 42))
6060
end
6161

62+
it 'can substract working hours' do
63+
time = Time.utc(1991, 11, 18, 14, 00, 42) # Monday
64+
expect(add_hours(time, -7)).to eq(Time.utc(1991, 11, 15, 15, 00, 42)) # Friday
65+
end
66+
6267
it 'accepts time given from any time zone' do
6368
time = Time.utc(1991, 11, 15, 7, 0, 0) # Friday 7 am UTC
6469
WorkingHours::Config.time_zone = 'Tokyo' # But we are at tokyo, so it's already 4 pm
@@ -119,6 +124,38 @@
119124
end
120125
end
121126

127+
describe '.return_to_working_time' do
128+
it 'jumps non-working day' do
129+
WorkingHours::Config.holidays = [Date.new(2014, 5, 1)]
130+
expect(return_to_working_time(Time.utc(2014, 5, 1, 12, 0))).to eq(Time.utc(2014, 4, 30, 17))
131+
expect(return_to_working_time(Time.utc(2014, 6, 1, 12, 0))).to eq(Time.utc(2014, 5, 30, 17))
132+
end
133+
134+
it 'returns self during working hours' do
135+
expect(return_to_working_time(Time.utc(2014, 4, 7, 9, 1))).to eq(Time.utc(2014, 4, 7, 9, 1))
136+
expect(return_to_working_time(Time.utc(2014, 4, 7, 17, 0))).to eq(Time.utc(2014, 4, 7, 17, 0))
137+
end
138+
139+
it 'jumps outside working hours' do
140+
expect(return_to_working_time(Time.utc(2014, 4, 7, 17, 1))).to eq(Time.utc(2014, 4, 7, 17, 0))
141+
expect(return_to_working_time(Time.utc(2014, 4, 8, 9, 0))).to eq(Time.utc(2014, 4, 7, 17, 0))
142+
end
143+
144+
it 'move between timespans' do
145+
WorkingHours::Config.working_hours = {mon: {'07:00' => '12:00', '13:00' => '18:00'}}
146+
expect(return_to_working_time(Time.utc(2014, 4, 7, 13, 1))).to eq(Time.utc(2014, 4, 7, 13, 1))
147+
expect(return_to_working_time(Time.utc(2014, 4, 7, 13, 0))).to eq(Time.utc(2014, 4, 7, 12, 0))
148+
expect(return_to_working_time(Time.utc(2014, 4, 7, 12, 1))).to eq(Time.utc(2014, 4, 7, 12, 0))
149+
expect(return_to_working_time(Time.utc(2014, 4, 7, 12, 0))).to eq(Time.utc(2014, 4, 7, 12, 0))
150+
end
151+
152+
it 'works with any input timezone (converts to config)' do
153+
# Monday 1 am (-09:00) is 10am in UTC time, working time!
154+
expect(return_to_working_time(Time.new(2014, 4, 7, 1, 0, 0 , "-09:00"))).to eq(Time.utc(2014, 4, 7, 10))
155+
expect(return_to_working_time(Time.new(2014, 4, 7, 22, 0, 0 , "+02:00"))).to eq(Time.utc(2014, 4, 7, 17))
156+
end
157+
end
158+
122159
describe '.working_day?' do
123160
it 'returns true on working day' do
124161
expect(working_day?(Date.new(2014, 4, 7))).to be(true)

spec/working_hours/duration_spec.rb

+10-4
Original file line numberDiff line numberDiff line change
@@ -32,12 +32,15 @@
3232
it 'should not work with anything else' do
3333
expect {
3434
duration = WorkingHours::Duration.new(42, :foo)
35-
}.to raise_error WorkingHours::UnknownDuration
35+
}.to raise_error ArgumentError, "Invalid working time unit: foo"
3636
end
3737
end
3838

39-
describe 'substraction' do
40-
pending
39+
describe '#-@' do
40+
it 'inverses value' do
41+
expect(-2.working.days).to eq(WorkingHours::Duration.new(-2, :days))
42+
expect(-(-1.working.hour)).to eq(WorkingHours::Duration.new(1, :hours))
43+
end
4144
end
4245

4346
describe '#from_now' do
@@ -48,7 +51,10 @@
4851
end
4952

5053
describe '#ago' do
51-
pending
54+
it "performs substraction with Time.now" do
55+
Timecop.freeze(Time.utc(1991, 11, 15, 21)) # we are Friday 21 pm UTC
56+
expect(7.working.day.ago).to eq(Time.utc(1991, 11, 6, 21))
57+
end
5258
end
5359

5460
end

0 commit comments

Comments
 (0)