working_hours 0.9.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +17 -0
- data/.rspec +2 -0
- data/.travis.yml +13 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +150 -0
- data/Rakefile +11 -0
- data/gemfiles/Gemfile.activesupport-3.2.x +5 -0
- data/gemfiles/Gemfile.activesupport-4.0.x +5 -0
- data/gemfiles/Gemfile.activesupport-4.1.x +5 -0
- data/gemfiles/Gemfile.activesupport-head +5 -0
- data/lib/working_hours/computation.rb +186 -0
- data/lib/working_hours/config.rb +152 -0
- data/lib/working_hours/core_ext/date_and_time.rb +57 -0
- data/lib/working_hours/core_ext/fixnum.rb +15 -0
- data/lib/working_hours/deep_freeze.rb +12 -0
- data/lib/working_hours/duration.rb +44 -0
- data/lib/working_hours/duration_proxy.rb +23 -0
- data/lib/working_hours/module.rb +7 -0
- data/lib/working_hours/version.rb +3 -0
- data/lib/working_hours.rb +3 -0
- data/spec/spec_helper.rb +24 -0
- data/spec/working_hours/computation_spec.rb +316 -0
- data/spec/working_hours/config_spec.rb +231 -0
- data/spec/working_hours/core_ext/date_and_time_spec.rb +181 -0
- data/spec/working_hours/core_ext/fixnum_spec.rb +13 -0
- data/spec/working_hours/duration_proxy_spec.rb +31 -0
- data/spec/working_hours/duration_spec.rb +78 -0
- data/spec/working_hours_spec.rb +14 -0
- data/working_hours.gemspec +28 -0
- metadata +168 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 4c941c0a9419ac6c9518eb678158e34a1bd7a1c2
|
4
|
+
data.tar.gz: 2d67809674b2e66e74a8df2300feba9d6afc0ed4
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 22c7e703ea00a9237d6c6050dce76e48e7521c7041358eb2d6ae7d4f1fcf2a79db0557c80001e499e18a7a63af9c2f36fce7cff71204fd3bdad91040c0855300
|
7
|
+
data.tar.gz: 45fb3490c70fa7e22d6b92f7eebdc60ef296b185378ab62a56fd0247ca04cfb91fb4b27bf7c95179de0edb6506e5f4944aa3613d60f8d5e6d81f7a4d9a618623
|
data/.gitignore
ADDED
data/.rspec
ADDED
data/.travis.yml
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
language: ruby
|
2
|
+
rvm:
|
3
|
+
- 2.0.0
|
4
|
+
- 2.1.2
|
5
|
+
- ruby-head
|
6
|
+
gemfile:
|
7
|
+
- gemfiles/Gemfile.activesupport-3.2.x
|
8
|
+
- gemfiles/Gemfile.activesupport-4.0.x
|
9
|
+
- gemfiles/Gemfile.activesupport-4.1.x
|
10
|
+
- gemfiles/Gemfile.activesupport-edge
|
11
|
+
matrix:
|
12
|
+
allow_failures:
|
13
|
+
- gemfile: gemfiles/Gemfile.activesupport-edge
|
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2014 Intrepidd
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,150 @@
|
|
1
|
+
# WorkingHours
|
2
|
+
|
3
|
+
[![Build Status](https://travis-ci.org/Intrepidd/working_hours.svg?branch=master)](https://travis-ci.org/Intrepidd/working_hours)
|
4
|
+
|
5
|
+
A modern ruby gem allowing to do time calculation with working hours.
|
6
|
+
|
7
|
+
Compatible and tested with:
|
8
|
+
- Ruby `2.0`, `2.1`
|
9
|
+
- ActiveSupport `3.2.x`, `4.0.x` and `4.1.x`
|
10
|
+
|
11
|
+
## Installation
|
12
|
+
|
13
|
+
Gemfile:
|
14
|
+
|
15
|
+
```ruby
|
16
|
+
gem 'working_hours'
|
17
|
+
```
|
18
|
+
|
19
|
+
## Usage
|
20
|
+
|
21
|
+
```ruby
|
22
|
+
require 'working_hours'
|
23
|
+
|
24
|
+
# Move forward
|
25
|
+
1.working.day.from_now
|
26
|
+
2.working.hours.from_now
|
27
|
+
15.working.minutes.from_now
|
28
|
+
|
29
|
+
# Move backward
|
30
|
+
1.working.day.ago
|
31
|
+
2.working.hours.ago
|
32
|
+
15.working.minutes.ago
|
33
|
+
|
34
|
+
# Start from custom Date or Time
|
35
|
+
Date.new(2014, 12, 31) + 8.working.days # => Mon, 12 Jan 2015
|
36
|
+
Time.utc(2014, 8, 4, 8, 32) - 4.working.hours # => 2014-08-01 13:00:00
|
37
|
+
|
38
|
+
# Compute working days between two dates
|
39
|
+
friday.working_days_until(monday) # => 1
|
40
|
+
# Time is considered at end of day, so:
|
41
|
+
# - friday to saturday = 0 working days
|
42
|
+
# - sunday to monday = 1 working days
|
43
|
+
|
44
|
+
# Compute working duration (in seconds) between two times
|
45
|
+
from = Time.utc(2014, 8, 3, 8, 32) # sunday 8:32am
|
46
|
+
to = Time.utc(2014, 8, 4, 10, 32) # monday 10:32am
|
47
|
+
from.working_time_until(to) # => 5520 (1.hour + 32.minutes)
|
48
|
+
|
49
|
+
# Know if a day is worked
|
50
|
+
Date.new(2014, 12, 28).working_day? # => false
|
51
|
+
|
52
|
+
# Know if a time is worked
|
53
|
+
Time.utc(2014, 8, 4, 7, 16).in_working_hours? # => false
|
54
|
+
```
|
55
|
+
|
56
|
+
## Configuration
|
57
|
+
|
58
|
+
The working hours configuration is thread local and consists of a hash defining working periods for each day, a time zone and a list of days off.
|
59
|
+
|
60
|
+
```ruby
|
61
|
+
# Configure working hours
|
62
|
+
WorkingHours::Config.working_hours = {
|
63
|
+
:tue => {'09:00' => '12:00', '13:00' => '17:00'},
|
64
|
+
:wed => {'09:00' => '12:00', '13:00' => '17:00'},
|
65
|
+
:thu => {'09:00' => '12:00', '13:00' => '17:00'},
|
66
|
+
:fri => {'09:00' => '12:00', '13:00' => '17:00'},
|
67
|
+
:sat => {'10:00' => '15:00'}
|
68
|
+
}
|
69
|
+
|
70
|
+
# Configure timezone (uses activesupport, defaults to UTC)
|
71
|
+
WorkingHours::Config.time_zone = 'Paris'
|
72
|
+
|
73
|
+
# Configure holidays
|
74
|
+
WorkingHours::Config.holidays = [Date.new(2014, 12, 31)]
|
75
|
+
```
|
76
|
+
|
77
|
+
> Once the config has been set, internal objects are frozen and you **can't** modify them (ex: `holidays << Date.today`). This is because the configuration is precompiled to a computing friendly form and changes would not be taken into account. To change the config you **must use** one of the 3 setters shown above.
|
78
|
+
|
79
|
+
## No core extensions / monkey patching
|
80
|
+
|
81
|
+
Core extensions (monkey patching to add methods on Time, Date, Numbers, etc.) are handy but not appreciated by everyone. WorkingHours can also be used **without any monkey patching**:
|
82
|
+
|
83
|
+
```ruby
|
84
|
+
require 'working_hours/module'
|
85
|
+
|
86
|
+
# Move forward
|
87
|
+
WorkingHours::Duration.new(1, :days).from_now
|
88
|
+
WorkingHours::Duration.new(2, :hours).from_now
|
89
|
+
WorkingHours::Duration.new(15, :minutes).from_now
|
90
|
+
|
91
|
+
# Move backward
|
92
|
+
WorkingHours::Duration.new(1, :days).ago
|
93
|
+
WorkingHours::Duration.new(2, :hours).ago
|
94
|
+
WorkingHours::Duration.new(15, :minutes).ago
|
95
|
+
|
96
|
+
# Start from custom Date or Time
|
97
|
+
WorkingHours::Duration.new(8, :days).since(Date.new(2014, 12, 31)) # => Mon, 12 Jan 2015
|
98
|
+
WorkingHours::Duration.new(4, :hours).until(Time.utc(2014, 8, 4, 8, 32)) # => 2014-08-01 13:00:00
|
99
|
+
|
100
|
+
# Compute working days between two dates
|
101
|
+
WorkingHours.working_days_between(friday, monday) # => 1
|
102
|
+
# Time is considered at end of day, so:
|
103
|
+
# - friday to saturday = 0 working days
|
104
|
+
# - sunday to monday = 1 working days
|
105
|
+
|
106
|
+
# Compute working duration (in seconds) between two times
|
107
|
+
from = Time.utc(2014, 8, 3, 8, 32) # sunday 8:32am
|
108
|
+
to = Time.utc(2014, 8, 4, 10, 32) # monday 10:32am
|
109
|
+
WorkingHours.working_time_between(from, to) # => 5520 (1.hour + 32.minutes)
|
110
|
+
|
111
|
+
# Know if a day is worked
|
112
|
+
WorkingHours.working_day?(Date.new(2014, 12, 28)) # => false
|
113
|
+
|
114
|
+
# Know if a time is worked
|
115
|
+
WorkingHours.in_working_hours?(Time.utc(2014, 8, 4, 7, 16)) # => false
|
116
|
+
```
|
117
|
+
|
118
|
+
## Use in your class/module
|
119
|
+
|
120
|
+
If you want to use working hours inside a specific class or module, you can include its computation methods like this:
|
121
|
+
|
122
|
+
```ruby
|
123
|
+
require 'working_hours'
|
124
|
+
|
125
|
+
class Order
|
126
|
+
include WorkingHours::Computation
|
127
|
+
|
128
|
+
def shipping_date_estimate
|
129
|
+
order_date + 2.working.days
|
130
|
+
end
|
131
|
+
end
|
132
|
+
```
|
133
|
+
|
134
|
+
> This also works with zero monkey patch by requiring `working_hours/module`
|
135
|
+
|
136
|
+
## Timezones
|
137
|
+
|
138
|
+
This gem uses a simple but efficient approach in dealing with timezones. When you define your working hours **you have to choose** a timezome associated with it (in the config example, the working hours are in Paris time). Then, any time used in calcultation will be converted to this timezone first, so you don't have to worry if your times are local or UTC as long as they are correct :)
|
139
|
+
|
140
|
+
## Alternatives
|
141
|
+
|
142
|
+
There is a gem called [business_time](https://github.com/bokmann/business_time) already available to do this kind of computation and it was of great help to us. But we decided to start another one because business_time is suffering from a [few](https://github.com/bokmann/business_time/issues/50) [bugs](https://github.com/bokmann/business_time/pull/84) and inconsistencies regarding timezones. It also lacks essential features to us (like working minutes computation).
|
143
|
+
|
144
|
+
## Contributing
|
145
|
+
|
146
|
+
1. Fork it ( http://github.com/<my-github-username>/working_hours/fork )
|
147
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
148
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
149
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
150
|
+
5. Create new Pull Request
|
data/Rakefile
ADDED
@@ -0,0 +1,11 @@
|
|
1
|
+
require "bundler/gem_tasks"
|
2
|
+
require 'rspec/core/rake_task'
|
3
|
+
|
4
|
+
RSpec::Core::RakeTask.new(:spec)
|
5
|
+
|
6
|
+
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
|
@@ -0,0 +1,186 @@
|
|
1
|
+
require 'active_support/all'
|
2
|
+
require 'working_hours/config'
|
3
|
+
|
4
|
+
module WorkingHours
|
5
|
+
module Computation
|
6
|
+
|
7
|
+
def add_days origin, days
|
8
|
+
time = in_config_zone(origin)
|
9
|
+
while days > 0
|
10
|
+
time += 1.day
|
11
|
+
days -= 1 if working_day?(time)
|
12
|
+
end
|
13
|
+
while days < 0
|
14
|
+
time -= 1.day
|
15
|
+
days += 1 if working_day?(time)
|
16
|
+
end
|
17
|
+
convert_to_original_format time, origin
|
18
|
+
end
|
19
|
+
|
20
|
+
def add_hours origin, hours
|
21
|
+
add_minutes origin, hours * 60
|
22
|
+
end
|
23
|
+
|
24
|
+
def add_minutes origin, minutes
|
25
|
+
add_seconds origin, minutes * 60
|
26
|
+
end
|
27
|
+
|
28
|
+
def add_seconds origin, seconds
|
29
|
+
time = in_config_zone(origin).round
|
30
|
+
while seconds > 0
|
31
|
+
# roll to next business period
|
32
|
+
time = advance_to_working_time(time)
|
33
|
+
# look at working ranges
|
34
|
+
time_in_day = time.seconds_since_midnight
|
35
|
+
wh_config[:working_hours][time.wday].each do |from, to|
|
36
|
+
if time_in_day >= from and time_in_day < to
|
37
|
+
# take all we can
|
38
|
+
take = [to - time_in_day, seconds].min
|
39
|
+
# advance time
|
40
|
+
time += take
|
41
|
+
# decrease seconds
|
42
|
+
seconds -= take
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
while seconds < 0
|
47
|
+
# roll to previous business period
|
48
|
+
time = return_to_working_time(time)
|
49
|
+
# look at working ranges
|
50
|
+
time_in_day = time.seconds_since_midnight
|
51
|
+
wh_config[:working_hours][time.wday].reverse_each do |from, to|
|
52
|
+
if time_in_day > from and time_in_day <= to
|
53
|
+
# take all we can
|
54
|
+
take = [time_in_day - from, -seconds].min
|
55
|
+
# advance time
|
56
|
+
time -= take
|
57
|
+
# decrease seconds
|
58
|
+
seconds += take
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
convert_to_original_format time, origin
|
63
|
+
end
|
64
|
+
|
65
|
+
def advance_to_working_time time
|
66
|
+
time = in_config_zone(time).round
|
67
|
+
loop do
|
68
|
+
# skip holidays and weekends
|
69
|
+
while not working_day?(time)
|
70
|
+
time = (time + 1.day).beginning_of_day
|
71
|
+
end
|
72
|
+
# find first working range after time
|
73
|
+
time_in_day = time.seconds_since_midnight
|
74
|
+
(Config.precompiled[:working_hours][time.wday] || {}).each do |from, to|
|
75
|
+
return time if time_in_day >= from and time_in_day < to
|
76
|
+
return time + (from - time_in_day) if from >= time_in_day
|
77
|
+
end
|
78
|
+
# if none is found, go to next day and loop
|
79
|
+
time = (time + 1.day).beginning_of_day
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
def return_to_working_time time
|
84
|
+
time = in_config_zone(time).round
|
85
|
+
loop do
|
86
|
+
# skip holidays and weekends
|
87
|
+
while not working_day?(time)
|
88
|
+
time = (time - 1.day).end_of_day
|
89
|
+
end
|
90
|
+
# find last working range before time
|
91
|
+
time_in_day = time.seconds_since_midnight
|
92
|
+
(Config.precompiled[:working_hours][time.wday] || {}).reverse_each do |from, to|
|
93
|
+
# round is used to suppress miliseconds hack from `end_of_day`
|
94
|
+
return time.round if time_in_day > from and time_in_day <= to
|
95
|
+
return (time - (time_in_day - to)).round if to <= time_in_day
|
96
|
+
end
|
97
|
+
# if none is found, go to previous day and loop
|
98
|
+
time = (time - 1.day).end_of_day
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
def working_day? time
|
103
|
+
time = in_config_zone(time)
|
104
|
+
Config.precompiled[:working_hours][time.wday].present? and not Config.precompiled[:holidays].include?(time.to_date)
|
105
|
+
end
|
106
|
+
|
107
|
+
def in_working_hours? time
|
108
|
+
time = in_config_zone(time)
|
109
|
+
return false if not working_day?(time)
|
110
|
+
time_in_day = time.seconds_since_midnight
|
111
|
+
Config.precompiled[:working_hours][time.wday].each do |from, to|
|
112
|
+
return true if time_in_day >= from and time_in_day < to
|
113
|
+
end
|
114
|
+
false
|
115
|
+
end
|
116
|
+
|
117
|
+
def working_days_between from, to
|
118
|
+
if to < from
|
119
|
+
-working_days_between(to, from)
|
120
|
+
else
|
121
|
+
from = in_config_zone(from)
|
122
|
+
to = in_config_zone(to)
|
123
|
+
days = 0
|
124
|
+
while from.to_date < to.to_date
|
125
|
+
from += 1.day
|
126
|
+
days += 1 if working_day?(from)
|
127
|
+
end
|
128
|
+
days
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
def working_time_between from, to
|
133
|
+
if to < from
|
134
|
+
-working_time_between(to, from)
|
135
|
+
else
|
136
|
+
from = advance_to_working_time(in_config_zone(from))
|
137
|
+
to = in_config_zone(to).round
|
138
|
+
distance = 0
|
139
|
+
while from < to
|
140
|
+
# look at working ranges
|
141
|
+
time_in_day = from.seconds_since_midnight
|
142
|
+
wh_config[:working_hours][from.wday].each do |begins, ends|
|
143
|
+
if time_in_day >= begins and time_in_day < ends
|
144
|
+
# take all we can
|
145
|
+
take = [ends - time_in_day, to - from].min
|
146
|
+
# advance time
|
147
|
+
from += take
|
148
|
+
# increase counter
|
149
|
+
distance += take
|
150
|
+
end
|
151
|
+
end
|
152
|
+
# roll to next business period
|
153
|
+
from = advance_to_working_time(from)
|
154
|
+
end
|
155
|
+
distance
|
156
|
+
end
|
157
|
+
end
|
158
|
+
|
159
|
+
private
|
160
|
+
|
161
|
+
def wh_config
|
162
|
+
WorkingHours::Config.precompiled
|
163
|
+
end
|
164
|
+
|
165
|
+
# fix for ActiveRecord < 4, doesn't implement in_time_zone for Date
|
166
|
+
def in_config_zone time
|
167
|
+
if time.respond_to? :in_time_zone
|
168
|
+
time.in_time_zone(wh_config[:time_zone])
|
169
|
+
elsif time.is_a? Date
|
170
|
+
wh_config[:time_zone].local(time.year, time.month, time.day)
|
171
|
+
else
|
172
|
+
raise TypeError.new("Can't convert #{time.class} to a Time")
|
173
|
+
end
|
174
|
+
end
|
175
|
+
|
176
|
+
def convert_to_original_format time, original
|
177
|
+
case original
|
178
|
+
when Date then time.to_date
|
179
|
+
when DateTime then time.to_datetime
|
180
|
+
when Time then time.to_time
|
181
|
+
else time
|
182
|
+
end
|
183
|
+
end
|
184
|
+
|
185
|
+
end
|
186
|
+
end
|
@@ -0,0 +1,152 @@
|
|
1
|
+
require 'set'
|
2
|
+
require 'working_hours/deep_freeze'
|
3
|
+
|
4
|
+
module WorkingHours
|
5
|
+
InvalidConfiguration = Class.new StandardError
|
6
|
+
|
7
|
+
class Config
|
8
|
+
extend WorkingHours::DeepFreeze
|
9
|
+
|
10
|
+
TIME_FORMAT = /\A([0-1][0-9]|2[0-3]):([0-5][0-9])\z/
|
11
|
+
DAYS_OF_WEEK = [:sun, :mon, :tue, :wed, :thu, :fri, :sat]
|
12
|
+
|
13
|
+
class << self
|
14
|
+
|
15
|
+
def working_hours
|
16
|
+
config[:working_hours]
|
17
|
+
end
|
18
|
+
|
19
|
+
def working_hours=(val)
|
20
|
+
validate_working_hours! val
|
21
|
+
config[:working_hours] = deep_freeze(val)
|
22
|
+
config.delete :precompiled
|
23
|
+
end
|
24
|
+
|
25
|
+
def holidays
|
26
|
+
config[:holidays]
|
27
|
+
end
|
28
|
+
|
29
|
+
def holidays=(val)
|
30
|
+
validate_holidays! val
|
31
|
+
config[:holidays] = deep_freeze(val)
|
32
|
+
config.delete :precompiled
|
33
|
+
end
|
34
|
+
|
35
|
+
# Returns an optimized for computing version
|
36
|
+
def precompiled
|
37
|
+
config[:precompiled] ||= begin
|
38
|
+
compiled = {working_hours: []}
|
39
|
+
working_hours.each do |day, hours|
|
40
|
+
compiled[:working_hours][DAYS_OF_WEEK.index(day)] = {}
|
41
|
+
hours.each do |start, finish|
|
42
|
+
compiled[:working_hours][DAYS_OF_WEEK.index(day)][compile_time(start)] = compile_time(finish)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
compiled[:holidays] = Set.new(holidays)
|
46
|
+
compiled[:time_zone] = time_zone
|
47
|
+
compiled
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def time_zone
|
52
|
+
config[:time_zone]
|
53
|
+
end
|
54
|
+
|
55
|
+
def time_zone=(val)
|
56
|
+
zone = validate_time_zone! val
|
57
|
+
config[:time_zone] = zone.freeze
|
58
|
+
config.delete :precompiled
|
59
|
+
end
|
60
|
+
|
61
|
+
def reset!
|
62
|
+
Thread.current[:working_hours] = default_config
|
63
|
+
end
|
64
|
+
|
65
|
+
private
|
66
|
+
|
67
|
+
def config
|
68
|
+
Thread.current[:working_hours] ||= default_config
|
69
|
+
end
|
70
|
+
|
71
|
+
def default_config
|
72
|
+
{
|
73
|
+
working_hours: {
|
74
|
+
mon: {'09:00' => '17:00'},
|
75
|
+
tue: {'09:00' => '17:00'},
|
76
|
+
wed: {'09:00' => '17:00'},
|
77
|
+
thu: {'09:00' => '17:00'},
|
78
|
+
fri: {'09:00' => '17:00'}
|
79
|
+
}.freeze,
|
80
|
+
holidays: [].freeze,
|
81
|
+
time_zone: ActiveSupport::TimeZone['UTC'].freeze
|
82
|
+
}
|
83
|
+
end
|
84
|
+
|
85
|
+
def compile_time time
|
86
|
+
hour = time[TIME_FORMAT,1].to_i
|
87
|
+
min = time[TIME_FORMAT,2].to_i
|
88
|
+
hour * 3600 + min * 60
|
89
|
+
end
|
90
|
+
|
91
|
+
def validate_working_hours! week
|
92
|
+
if week.empty?
|
93
|
+
raise InvalidConfiguration.new "No working hours given"
|
94
|
+
end
|
95
|
+
if (invalid_keys = (week.keys - DAYS_OF_WEEK)).any?
|
96
|
+
raise InvalidConfiguration.new "Invalid day identifier(s): #{invalid_keys.join(', ')} - must be 3 letter symbols"
|
97
|
+
end
|
98
|
+
week.each do |day, hours|
|
99
|
+
if not hours.is_a? Hash
|
100
|
+
raise InvalidConfiguration.new "Invalid type for `#{day}`: #{hours.class} - must be Hash"
|
101
|
+
elsif hours.empty?
|
102
|
+
raise InvalidConfiguration.new "No working hours given for day `#{day}`"
|
103
|
+
end
|
104
|
+
last_time = nil
|
105
|
+
hours.each do |start, finish|
|
106
|
+
if not start =~ TIME_FORMAT
|
107
|
+
raise InvalidConfiguration.new "Invalid time: #{start} - must be 'HH:MM'"
|
108
|
+
elsif not finish =~ TIME_FORMAT
|
109
|
+
raise InvalidConfiguration.new "Invalid time: #{finish} - must be 'HH:MM'"
|
110
|
+
elsif start >= finish
|
111
|
+
raise InvalidConfiguration.new "Invalid range: #{start} => #{finish} - ends before it starts"
|
112
|
+
elsif last_time and start < last_time
|
113
|
+
raise InvalidConfiguration.new "Invalid range: #{start} => #{finish} - overlaps previous range"
|
114
|
+
end
|
115
|
+
last_time = finish
|
116
|
+
end
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
def validate_holidays! holidays
|
121
|
+
if not holidays.is_a? Array
|
122
|
+
raise InvalidConfiguration.new "Invalid type for holidays: #{holidays.class} - must be Array"
|
123
|
+
end
|
124
|
+
holidays.each do |day|
|
125
|
+
if not day.is_a? Date
|
126
|
+
raise InvalidConfiguration.new "Invalid holiday: #{day} - must be Date"
|
127
|
+
end
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
def validate_time_zone! zone
|
132
|
+
if zone.is_a? String
|
133
|
+
res = ActiveSupport::TimeZone[zone]
|
134
|
+
if res.nil?
|
135
|
+
raise InvalidConfiguration.new "Unknown time zone: #{zone}"
|
136
|
+
end
|
137
|
+
elsif zone.is_a? ActiveSupport::TimeZone
|
138
|
+
res = zone
|
139
|
+
else
|
140
|
+
raise InvalidConfiguration.new "Invalid time zone: #{zone.inspect} - must be String or ActiveSupport::TimeZone"
|
141
|
+
end
|
142
|
+
res
|
143
|
+
end
|
144
|
+
|
145
|
+
end
|
146
|
+
|
147
|
+
private
|
148
|
+
|
149
|
+
def initialize
|
150
|
+
end
|
151
|
+
end
|
152
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
require 'active_support/time_with_zone'
|
2
|
+
require 'working_hours/module'
|
3
|
+
|
4
|
+
module WorkingHours
|
5
|
+
module CoreExt
|
6
|
+
module DateAndTime
|
7
|
+
|
8
|
+
def +(other)
|
9
|
+
if (other.is_a?(WorkingHours::Duration))
|
10
|
+
other.since(self)
|
11
|
+
else
|
12
|
+
super(other)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
def -(other)
|
17
|
+
if (other.is_a?(WorkingHours::Duration))
|
18
|
+
other.until(self)
|
19
|
+
else
|
20
|
+
super(other)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def working_days_until(other)
|
25
|
+
WorkingHours.working_days_between(self, other)
|
26
|
+
end
|
27
|
+
|
28
|
+
def working_time_until(other)
|
29
|
+
WorkingHours.working_time_between(self, other)
|
30
|
+
end
|
31
|
+
|
32
|
+
def working_day?
|
33
|
+
WorkingHours.working_day?(self)
|
34
|
+
end
|
35
|
+
|
36
|
+
def in_working_hours?
|
37
|
+
WorkingHours.in_working_hours?(self)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
class Date
|
44
|
+
prepend WorkingHours::CoreExt::DateAndTime
|
45
|
+
end
|
46
|
+
|
47
|
+
class DateTime
|
48
|
+
prepend WorkingHours::CoreExt::DateAndTime
|
49
|
+
end
|
50
|
+
|
51
|
+
class Time
|
52
|
+
prepend WorkingHours::CoreExt::DateAndTime
|
53
|
+
end
|
54
|
+
|
55
|
+
class ActiveSupport::TimeWithZone
|
56
|
+
prepend WorkingHours::CoreExt::DateAndTime
|
57
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
module WorkingHours
|
2
|
+
module DeepFreeze
|
3
|
+
def deep_freeze object
|
4
|
+
if object.is_a? Array
|
5
|
+
object.replace(object.dup.each { |_, value| deep_freeze(value) })
|
6
|
+
elsif object.is_a? Hash
|
7
|
+
object.replace(object.dup.each { |_, value| deep_freeze(value) })
|
8
|
+
end
|
9
|
+
object.freeze
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|