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 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