biz 0.0.1 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 7ed9d4ce91d53a0f30a134390760ffcf9e7ac335
4
- data.tar.gz: b0d3f931a675c643e350d41f5305d43d1938ad5c
3
+ metadata.gz: 29c21b59e6fde3b429ebf855ccebec03742d0619
4
+ data.tar.gz: 8adbadda565386c89c078b146a69ccbf0e45972f
5
5
  SHA512:
6
- metadata.gz: 59e13d60fa485c816607922bdafa26093c182f9f2609fdfca481e617afdfcd54bab2b2355015f3f6c342183c19fb72e164ecfd6d9f0dadd5e46e16c9fd3aa1b5
7
- data.tar.gz: 2d0de21b4a82ec4e5f0eec132bf6533c2eba3b6eb575baf2f8a07f098cc3e6941021185f10a68a9996b73ecd956262a9bb210784b1beece02bf741bb37c25ab8
6
+ metadata.gz: 171eb369d1f45cba0e0e7e8a8d3c8d19043b18b25830f900df21030fc2b7c110c2d97d9ebcada7cb927ad704a961a0c1bcf532c56c5f630d09e9623e823607c8
7
+ data.tar.gz: 7a5ae606a3ce3bed526007dab290e301271ad5eb1829c485984b70d35bb78533e3477180e4e01807a9acc1bf63f9239ae13a5f034fcdb3389c4d7dcea1652b21
data/README.md CHANGED
@@ -1,14 +1,28 @@
1
- # Biz
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
- TODO: Write a gem description
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
- ```ruby
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
- TODO: Write usage instructions here
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
- 1. Fork it ( https://github.com/[my-github-username]/biz/fork )
28
- 2. Create your feature branch (`git checkout -b my-new-feature`)
29
- 3. Commit your changes (`git commit -am 'Add some feature'`)
30
- 4. Push to the branch (`git push origin my-new-feature`)
31
- 5. Create a new Pull Request
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 "biz/version"
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
- # Your code goes here...
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,8 @@
1
+ module Biz
2
+ module Calculation
3
+ end
4
+ end
5
+
6
+ require 'biz/calculation/for_duration'
7
+ require 'biz/calculation/duration_within'
8
+ require 'biz/calculation/active'
@@ -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
@@ -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
@@ -0,0 +1,11 @@
1
+ module Biz
2
+ module CoreExt
3
+ module Fixnum
4
+
5
+ %i[second seconds minute minutes hour hours].each do |unit|
6
+ define_method("business_#{unit}") { Biz.time(self, unit) }
7
+ end
8
+
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,11 @@
1
+ module Biz
2
+ module CoreExt
3
+ module Time
4
+
5
+ def business_hours?
6
+ Biz.business_hours?(self)
7
+ end
8
+
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,11 @@
1
+ module Biz
2
+ class Date
3
+
4
+ EPOCH = ::Date.new(2006, 1, 1)
5
+
6
+ def self.from_day(day)
7
+ EPOCH + day
8
+ end
9
+
10
+ end
11
+ end
@@ -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