biz 0.0.1 → 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +110 -11
- data/lib/biz.rb +54 -2
- data/lib/biz/calculation.rb +8 -0
- data/lib/biz/calculation/active.rb +20 -0
- data/lib/biz/calculation/duration_within.rb +17 -0
- data/lib/biz/calculation/for_duration.rb +33 -0
- data/lib/biz/configuration.rb +74 -0
- data/lib/biz/core_ext.rb +12 -0
- data/lib/biz/core_ext/date.rb +15 -0
- data/lib/biz/core_ext/fixnum.rb +11 -0
- data/lib/biz/core_ext/time.rb +11 -0
- data/lib/biz/date.rb +11 -0
- data/lib/biz/day.rb +51 -0
- data/lib/biz/day_of_week.rb +81 -0
- data/lib/biz/day_time.rb +95 -0
- data/lib/biz/duration.rb +100 -0
- data/lib/biz/holiday.rb +26 -0
- data/lib/biz/interval.rb +35 -0
- data/lib/biz/periods.rb +24 -0
- data/lib/biz/periods/abstract.rb +49 -0
- data/lib/biz/periods/after.rb +24 -0
- data/lib/biz/periods/before.rb +27 -0
- data/lib/biz/schedule.rb +37 -0
- data/lib/biz/time.rb +49 -0
- data/lib/biz/time_segment.rb +63 -0
- data/lib/biz/timeline.rb +24 -0
- data/lib/biz/timeline/abstract.rb +41 -0
- data/lib/biz/timeline/backward.rb +24 -0
- data/lib/biz/timeline/forward.rb +24 -0
- data/lib/biz/version.rb +3 -1
- data/lib/biz/week.rb +67 -0
- data/lib/biz/week_time.rb +26 -0
- data/lib/biz/week_time/abstract.rb +75 -0
- data/lib/biz/week_time/end.rb +20 -0
- data/lib/biz/week_time/start.rb +22 -0
- metadata +122 -5
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 29c21b59e6fde3b429ebf855ccebec03742d0619
|
4
|
+
data.tar.gz: 8adbadda565386c89c078b146a69ccbf0e45972f
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 171eb369d1f45cba0e0e7e8a8d3c8d19043b18b25830f900df21030fc2b7c110c2d97d9ebcada7cb927ad704a961a0c1bcf532c56c5f630d09e9623e823607c8
|
7
|
+
data.tar.gz: 7a5ae606a3ce3bed526007dab290e301271ad5eb1829c485984b70d35bb78533e3477180e4e01807a9acc1bf63f9239ae13a5f034fcdb3389c4d7dcea1652b21
|
data/README.md
CHANGED
@@ -1,14 +1,28 @@
|
|
1
|
-
#
|
1
|
+
# biz
|
2
|
+
[![Build Status](https://magnum.travis-ci.com/zendesk/biz.svg?token=FPvAz1WHPkjgRp2szEGq&branch=master)](https://magnum.travis-ci.com/zendesk/biz)
|
3
|
+
[![Code Climate](https://codeclimate.com/repos/54ac74216956802dc40027d6/badges/591180c7fa5da2a8aa3d/gpa.svg)](https://codeclimate.com/repos/54ac74216956802dc40027d6/feed)
|
4
|
+
[![Test Coverage](https://codeclimate.com/repos/54ac74216956802dc40027d6/badges/591180c7fa5da2a8aa3d/coverage.svg)](https://codeclimate.com/repos/54ac74216956802dc40027d6/feed)
|
2
5
|
|
3
|
-
|
6
|
+
Time calculations using business hours.
|
7
|
+
|
8
|
+
## Features
|
9
|
+
|
10
|
+
* Second-level precision on all calculations.
|
11
|
+
* Accurate handling of Daylight Saving Time.
|
12
|
+
* Support for intervals spanning any period of the day, including the entirety.
|
13
|
+
* Support for interday intervals and holidays.
|
14
|
+
* Thread-safe.
|
15
|
+
|
16
|
+
## Anti-Features
|
17
|
+
|
18
|
+
* No dependency on ActiveSupport.
|
19
|
+
* No monkey patching by default.
|
4
20
|
|
5
21
|
## Installation
|
6
22
|
|
7
23
|
Add this line to your application's Gemfile:
|
8
24
|
|
9
|
-
|
10
|
-
gem 'biz'
|
11
|
-
```
|
25
|
+
gem 'biz'
|
12
26
|
|
13
27
|
And then execute:
|
14
28
|
|
@@ -18,14 +32,99 @@ Or install it yourself as:
|
|
18
32
|
|
19
33
|
$ gem install biz
|
20
34
|
|
35
|
+
## Configuration
|
36
|
+
|
37
|
+
```ruby
|
38
|
+
Biz.configure do |config|
|
39
|
+
config.business_hours = {
|
40
|
+
mon: {'09:00' => '17:00'},
|
41
|
+
tue: {'00:00' => '24:00'},
|
42
|
+
wed: {'09:00' => '17:00'},
|
43
|
+
thu: {'09:00' => '12:00', '13:00' => '17:00'},
|
44
|
+
sat: {'10:00' => '14:00'}
|
45
|
+
}
|
46
|
+
|
47
|
+
config.holidays = [Date.new(2014, 1, 1), Date.new(2014, 12, 25)]
|
48
|
+
|
49
|
+
config.time_zone = 'America/Los_Angeles'
|
50
|
+
end
|
51
|
+
```
|
52
|
+
|
53
|
+
If global configuration isn't your thing, configure an instance instead:
|
54
|
+
|
55
|
+
```ruby
|
56
|
+
Biz::Schedule.new do |config|
|
57
|
+
# ...
|
58
|
+
end
|
59
|
+
```
|
60
|
+
|
21
61
|
## Usage
|
22
62
|
|
23
|
-
|
63
|
+
```ruby
|
64
|
+
# Find the time an amount of business time *before* a specified starting time
|
65
|
+
Biz.time(30, :minutes).before(Time.utc(2015, 1, 1, 11, 45))
|
66
|
+
|
67
|
+
# Find the time an amount of business time *after* a specified starting time
|
68
|
+
Biz.time(2, :hours).after(Time.utc(2015, 12, 25, 9, 30))
|
69
|
+
|
70
|
+
# Find the amount of business time between two times
|
71
|
+
Biz.within(Time.utc(2015, 3, 7), Time.utc(2015, 3, 14)).in_seconds
|
72
|
+
|
73
|
+
# Determine if a time is in business hours
|
74
|
+
Biz.business_hours?(Time.utc(2015, 1, 10, 9))
|
75
|
+
```
|
76
|
+
|
77
|
+
## Core Extensions
|
78
|
+
|
79
|
+
Optional extensions to core classes (`Date`, `Fixnum`, and `Time`) are available
|
80
|
+
for additional expressiveness:
|
81
|
+
|
82
|
+
```ruby
|
83
|
+
require 'biz/core_ext'
|
84
|
+
|
85
|
+
75.business_seconds.after(Time.utc(2015, 3, 5, 12, 30))
|
86
|
+
|
87
|
+
30.business_minutes.before(Time.utc(2015, 1, 1, 11, 45))
|
88
|
+
|
89
|
+
5.business_hours.after(Time.utc(2015, 4, 7, 8, 20))
|
90
|
+
|
91
|
+
Time.utc(2015, 8, 20, 9, 30).business_hours?
|
92
|
+
|
93
|
+
Date.new(2015, 12, 10).business_day?
|
94
|
+
```
|
24
95
|
|
25
96
|
## Contributing
|
26
97
|
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
98
|
+
Pull requests are welcome, but consider asking for a feature or bug fix first
|
99
|
+
through the issue tracker. When contributing code, please squash sloppy commits
|
100
|
+
aggressively and follow [Tim Pope's guidelines](http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html)
|
101
|
+
for commit messages.
|
102
|
+
|
103
|
+
There are a number of ways to get started after cloning the repository.
|
104
|
+
|
105
|
+
To set up your environment:
|
106
|
+
|
107
|
+
script/bootstrap
|
108
|
+
|
109
|
+
To run the spec suite:
|
110
|
+
|
111
|
+
script/spec
|
112
|
+
|
113
|
+
To open a console with the gem and sample schedule loaded:
|
114
|
+
|
115
|
+
script/console
|
116
|
+
|
117
|
+
## Copyright and license
|
118
|
+
|
119
|
+
Copyright 2015 Zendesk
|
120
|
+
|
121
|
+
Licensed under the Apache License, Version 2.0 (the "License"); you may not use
|
122
|
+
this file except in compliance with the License.
|
123
|
+
|
124
|
+
You may obtain a copy of the License at
|
125
|
+
http://www.apache.org/licenses/LICENSE-2.0.
|
126
|
+
|
127
|
+
Unless required by applicable law or agreed to in writing, software distributed
|
128
|
+
under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
|
129
|
+
CONDITIONS OF ANY KIND, either express or implied. See the License for the
|
130
|
+
specific language governing permissions and limitations under the License.
|
data/lib/biz.rb
CHANGED
@@ -1,5 +1,57 @@
|
|
1
|
-
require
|
1
|
+
require 'date'
|
2
|
+
require 'delegate'
|
3
|
+
require 'forwardable'
|
4
|
+
require 'set'
|
5
|
+
|
6
|
+
require 'abstract_type'
|
7
|
+
require 'equalizer'
|
8
|
+
require 'memoizable'
|
9
|
+
require 'tzinfo'
|
2
10
|
|
3
11
|
module Biz
|
4
|
-
|
12
|
+
class << self
|
13
|
+
|
14
|
+
extend Forwardable
|
15
|
+
|
16
|
+
def configure(&block)
|
17
|
+
Thread.current[:biz_schedule] = Schedule.new(&block)
|
18
|
+
end
|
19
|
+
|
20
|
+
delegate %i[
|
21
|
+
intervals
|
22
|
+
holidays
|
23
|
+
time_zone
|
24
|
+
periods
|
25
|
+
time
|
26
|
+
within
|
27
|
+
business_hours?
|
28
|
+
] => :schedule
|
29
|
+
|
30
|
+
private
|
31
|
+
|
32
|
+
def schedule
|
33
|
+
Thread.current[:biz_schedule] or fail 'Biz has not been configured.'
|
34
|
+
end
|
35
|
+
|
36
|
+
end
|
5
37
|
end
|
38
|
+
|
39
|
+
require 'biz/version'
|
40
|
+
|
41
|
+
require 'biz/date'
|
42
|
+
require 'biz/time'
|
43
|
+
|
44
|
+
require 'biz/calculation'
|
45
|
+
require 'biz/configuration'
|
46
|
+
require 'biz/day'
|
47
|
+
require 'biz/day_of_week'
|
48
|
+
require 'biz/day_time'
|
49
|
+
require 'biz/duration'
|
50
|
+
require 'biz/holiday'
|
51
|
+
require 'biz/interval'
|
52
|
+
require 'biz/periods'
|
53
|
+
require 'biz/schedule'
|
54
|
+
require 'biz/timeline'
|
55
|
+
require 'biz/time_segment'
|
56
|
+
require 'biz/week'
|
57
|
+
require 'biz/week_time'
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module Biz
|
2
|
+
module Calculation
|
3
|
+
class Active
|
4
|
+
|
5
|
+
attr_reader :schedule,
|
6
|
+
:time
|
7
|
+
|
8
|
+
def initialize(schedule, time)
|
9
|
+
@schedule = schedule
|
10
|
+
@time = time
|
11
|
+
end
|
12
|
+
|
13
|
+
def active?
|
14
|
+
schedule.intervals.any? { |interval| interval.contains?(time) } &&
|
15
|
+
schedule.holidays.none? { |holiday| holiday.contains?(time) }
|
16
|
+
end
|
17
|
+
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
module Biz
|
2
|
+
module Calculation
|
3
|
+
class DurationWithin < SimpleDelegator
|
4
|
+
|
5
|
+
def initialize(schedule, calculation_period)
|
6
|
+
super(
|
7
|
+
schedule.periods.after(calculation_period.start_time)
|
8
|
+
.timeline.forward
|
9
|
+
.until(calculation_period.end_time)
|
10
|
+
.map(&:duration)
|
11
|
+
.reduce(Duration.new(0), :+)
|
12
|
+
)
|
13
|
+
end
|
14
|
+
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
module Biz
|
2
|
+
module Calculation
|
3
|
+
class ForDuration
|
4
|
+
|
5
|
+
attr_reader :schedule,
|
6
|
+
:duration
|
7
|
+
|
8
|
+
def initialize(schedule, duration)
|
9
|
+
unless duration.positive?
|
10
|
+
fail ArgumentError, 'Duration adjustment must be positive.'
|
11
|
+
end
|
12
|
+
|
13
|
+
@schedule = schedule
|
14
|
+
@duration = duration
|
15
|
+
end
|
16
|
+
|
17
|
+
def before(time)
|
18
|
+
schedule.periods.before(time)
|
19
|
+
.timeline.backward
|
20
|
+
.for(duration).to_a
|
21
|
+
.last.start_time
|
22
|
+
end
|
23
|
+
|
24
|
+
def after(time)
|
25
|
+
schedule.periods.after(time)
|
26
|
+
.timeline.forward
|
27
|
+
.for(duration).to_a
|
28
|
+
.last.end_time
|
29
|
+
end
|
30
|
+
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,74 @@
|
|
1
|
+
module Biz
|
2
|
+
class Configuration
|
3
|
+
|
4
|
+
include Memoizable
|
5
|
+
|
6
|
+
def initialize
|
7
|
+
@raw = Raw.new.tap do |raw| yield raw end
|
8
|
+
end
|
9
|
+
|
10
|
+
def intervals
|
11
|
+
raw.business_hours.flat_map { |weekday, hours|
|
12
|
+
weekday_intervals(weekday, hours)
|
13
|
+
}.sort_by(&:start_time)
|
14
|
+
end
|
15
|
+
|
16
|
+
def holidays
|
17
|
+
raw.holidays.map { |date| Holiday.new(date, time_zone) }
|
18
|
+
end
|
19
|
+
|
20
|
+
def time_zone
|
21
|
+
TZInfo::TimezoneProxy.new(raw.time_zone)
|
22
|
+
end
|
23
|
+
|
24
|
+
protected
|
25
|
+
|
26
|
+
attr_reader :raw
|
27
|
+
|
28
|
+
private
|
29
|
+
|
30
|
+
def weekday_intervals(weekday, hours)
|
31
|
+
hours.map { |start_timestamp, end_timestamp|
|
32
|
+
Interval.new(
|
33
|
+
WeekTime.start(
|
34
|
+
DayOfWeek.from_symbol(weekday).start_minute +
|
35
|
+
DayTime.from_timestamp(start_timestamp).day_minute
|
36
|
+
),
|
37
|
+
WeekTime.end(
|
38
|
+
DayOfWeek.from_symbol(weekday).start_minute +
|
39
|
+
DayTime.from_timestamp(end_timestamp).day_minute
|
40
|
+
),
|
41
|
+
time_zone
|
42
|
+
)
|
43
|
+
}
|
44
|
+
end
|
45
|
+
|
46
|
+
memoize :intervals,
|
47
|
+
:holidays
|
48
|
+
|
49
|
+
Raw = Struct.new(:business_hours, :holidays, :time_zone) do
|
50
|
+
module Default
|
51
|
+
|
52
|
+
BUSINESS_HOURS = {
|
53
|
+
mon: {'09:00' => '17:00'},
|
54
|
+
tue: {'09:00' => '17:00'},
|
55
|
+
wed: {'09:00' => '17:00'},
|
56
|
+
thu: {'09:00' => '17:00'},
|
57
|
+
fri: {'09:00' => '17:00'}
|
58
|
+
}
|
59
|
+
HOLIDAYS = []
|
60
|
+
TIME_ZONE = 'Etc/UTC'
|
61
|
+
|
62
|
+
end
|
63
|
+
|
64
|
+
def initialize(*)
|
65
|
+
super
|
66
|
+
|
67
|
+
self.business_hours ||= Default::BUSINESS_HOURS
|
68
|
+
self.holidays ||= Default::HOLIDAYS
|
69
|
+
self.time_zone ||= Default::TIME_ZONE
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
end
|
74
|
+
end
|
data/lib/biz/core_ext.rb
ADDED
@@ -0,0 +1,12 @@
|
|
1
|
+
module Biz
|
2
|
+
module CoreExt
|
3
|
+
end
|
4
|
+
end
|
5
|
+
|
6
|
+
require 'biz/core_ext/date'
|
7
|
+
require 'biz/core_ext/fixnum'
|
8
|
+
require 'biz/core_ext/time'
|
9
|
+
|
10
|
+
Date.class_eval do include Biz::CoreExt::Date end
|
11
|
+
Fixnum.class_eval do include Biz::CoreExt::Fixnum end
|
12
|
+
Time.class_eval do include Biz::CoreExt::Time end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
module Biz
|
2
|
+
module CoreExt
|
3
|
+
module Date
|
4
|
+
|
5
|
+
def business_day?
|
6
|
+
Biz.periods
|
7
|
+
.after(Biz::Time.new(Biz.time_zone).on_date(self, DayTime.midnight))
|
8
|
+
.timeline.forward
|
9
|
+
.until(Biz::Time.new(Biz.time_zone).on_date(self, DayTime.endnight))
|
10
|
+
.any?
|
11
|
+
end
|
12
|
+
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
data/lib/biz/date.rb
ADDED
data/lib/biz/day.rb
ADDED
@@ -0,0 +1,51 @@
|
|
1
|
+
module Biz
|
2
|
+
class Day
|
3
|
+
|
4
|
+
include Comparable
|
5
|
+
|
6
|
+
extend Forwardable
|
7
|
+
|
8
|
+
def self.from_date(date)
|
9
|
+
new((date - Date::EPOCH).to_i)
|
10
|
+
end
|
11
|
+
|
12
|
+
def self.from_time(time)
|
13
|
+
from_date(time.to_date)
|
14
|
+
end
|
15
|
+
|
16
|
+
class << self
|
17
|
+
|
18
|
+
alias_method :since_epoch, :from_time
|
19
|
+
|
20
|
+
end
|
21
|
+
|
22
|
+
attr_reader :day
|
23
|
+
|
24
|
+
delegate %i[
|
25
|
+
to_s
|
26
|
+
to_i
|
27
|
+
to_int
|
28
|
+
] => :day
|
29
|
+
|
30
|
+
def initialize(day)
|
31
|
+
@day = Integer(day)
|
32
|
+
end
|
33
|
+
|
34
|
+
def to_date
|
35
|
+
Date.from_day(day)
|
36
|
+
end
|
37
|
+
|
38
|
+
def coerce(other)
|
39
|
+
[self.class.new(other), self]
|
40
|
+
end
|
41
|
+
|
42
|
+
protected
|
43
|
+
|
44
|
+
def <=>(other)
|
45
|
+
return nil unless other.respond_to?(:to_i)
|
46
|
+
|
47
|
+
day <=> other.to_i
|
48
|
+
end
|
49
|
+
|
50
|
+
end
|
51
|
+
end
|