fast_business_time 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 3bc591afe0f524f5b53fe24cc3bfa7e9d4bd9bfa
4
+ data.tar.gz: f33004151bfd14b10379e8f4a4afbd40fb956829
5
+ SHA512:
6
+ metadata.gz: b4d448c57b2840b2d408508c18d2e28343310fa9533b85dc5949b5b01833d1af3248ed0e19b3be51f1b9a3e2f070a14945e323dbaebe741c26f5777d3f9ad6ae
7
+ data.tar.gz: ffa4ebf541fa04f1b6fb9456856bd5c2b689c2ef7144331ffafce6cafcc625ee7582b9d0c9afd3653fbb8c35062ecfbd574c7f958c59ba74a8d6964b08fa5bd0
data/.gitignore ADDED
@@ -0,0 +1,9 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
@@ -0,0 +1,49 @@
1
+ # Contributor Code of Conduct
2
+
3
+ As contributors and maintainers of this project, and in the interest of
4
+ fostering an open and welcoming community, we pledge to respect all people who
5
+ contribute through reporting issues, posting feature requests, updating
6
+ documentation, submitting pull requests or patches, and other activities.
7
+
8
+ We are committed to making participation in this project a harassment-free
9
+ experience for everyone, regardless of level of experience, gender, gender
10
+ identity and expression, sexual orientation, disability, personal appearance,
11
+ body size, race, ethnicity, age, religion, or nationality.
12
+
13
+ Examples of unacceptable behavior by participants include:
14
+
15
+ * The use of sexualized language or imagery
16
+ * Personal attacks
17
+ * Trolling or insulting/derogatory comments
18
+ * Public or private harassment
19
+ * Publishing other's private information, such as physical or electronic
20
+ addresses, without explicit permission
21
+ * Other unethical or unprofessional conduct
22
+
23
+ Project maintainers have the right and responsibility to remove, edit, or
24
+ reject comments, commits, code, wiki edits, issues, and other contributions
25
+ that are not aligned to this Code of Conduct, or to ban temporarily or
26
+ permanently any contributor for other behaviors that they deem inappropriate,
27
+ threatening, offensive, or harmful.
28
+
29
+ By adopting this Code of Conduct, project maintainers commit themselves to
30
+ fairly and consistently applying these principles to every aspect of managing
31
+ this project. Project maintainers who do not follow or enforce the Code of
32
+ Conduct may be permanently removed from the project team.
33
+
34
+ This code of conduct applies both within project spaces and in public spaces
35
+ when an individual is representing the project or its community.
36
+
37
+ Instances of abusive, harassing, or otherwise unacceptable behavior may be
38
+ reported by contacting a project maintainer at jordinoguera83@gmail.com. All
39
+ complaints will be reviewed and investigated and will result in a response that
40
+ is deemed necessary and appropriate to the circumstances. Maintainers are
41
+ obligated to maintain confidentiality with regard to the reporter of an
42
+ incident.
43
+
44
+ This Code of Conduct is adapted from the [Contributor Covenant][homepage],
45
+ version 1.3.0, available at
46
+ [http://contributor-covenant.org/version/1/3/0/][version]
47
+
48
+ [homepage]: http://contributor-covenant.org
49
+ [version]: http://contributor-covenant.org/version/1/3/0/
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in fast_business_time.gemspec
4
+ gemspec
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2016 Spreemo, Inc.
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,90 @@
1
+ # FastBusinessTime
2
+
3
+ Time calculations based on business hours. Inspired by [business_time](https://github.com/bokmann/business_time) but with significantly better performance.
4
+
5
+ ## Installation
6
+
7
+ Add this line to your application's Gemfile:
8
+
9
+ ```ruby
10
+ gem 'fast_business_time'
11
+ ```
12
+
13
+ And then execute:
14
+
15
+ $ bundle
16
+
17
+ Or install it yourself as:
18
+
19
+ $ gem install fast_business_time
20
+
21
+ ## Usage
22
+
23
+ #### Initializiation
24
+
25
+ First initialize a calculator, a calculator needs a `schedule` and `holidays`:
26
+
27
+ ```
28
+ schedule = {
29
+ [:mon, :tue, :wed, :thu, :fri] => [[9 * 3600, 17 * 3600]]
30
+ }
31
+ holidays = [Date.new(2016, 1, 1), Date.new(2016, 12, 25)]
32
+ calculator = FastBusinessTime::Calculator.new(schedule: schedule, holidays: holidays)
33
+
34
+ ```
35
+
36
+ `schedule` is a hash whose keys are an array of weekdays and the values are an array of time intervals in seconds since the beginning of the day. So if we have a business that works on Monday 9:00-13:00, 14:00-17:30 and Tue-Fri 9:00-17:00 we would set schedule to:
37
+
38
+ ````
39
+ schedule = {
40
+ [:mon] => [[9 * 3600, 13 * 3600], [14 * 3600, 17 * 3600 + 30 * 60]],
41
+ [:tue, :wed, :thu, :fri] => [[9 * 3600, 17 * 3600]]]
42
+ }
43
+ ````
44
+
45
+ `holidays` is an array of dates, you could probably use the [holiday gem](https://github.com/holidays/holidays)
46
+
47
+ It's recommended to memoize the calculators, so you could do:
48
+
49
+ ````
50
+ class TimeCalculators
51
+ def self.ny
52
+ @ny ||= FastBusinessTime::Calculator.new(
53
+ schedule: [:mon, :tue, :wed, :thu, :fri] => [[9 * 3600, 17 * 3600]],
54
+ holidays: [Date.new(2016, 1, 1), Date.new(2016, 12, 25)]
55
+ )
56
+ end
57
+
58
+ def self.sf
59
+ @sf ||= FastBusinessTime::Calculator.new(
60
+ schedule: [:mon, :tue, :wed, :thu, :fri] => [[9 * 3600, 18 * 3600]],
61
+ holidays: [Date.new(2016, 1, 1), Date.new(2016, 12, 25)]
62
+ )
63
+ end
64
+ end
65
+ ````
66
+
67
+ #### Methods
68
+
69
+ * `calculator.seconds_between_times(time1, time2)`: (Integer) Working seconds between two times.
70
+ * `calculator.days_between_dates(date1, date2)`: (Integer) Number of work days between two dates (excluding edges).
71
+ * `calculator.seconds_since_beginning_of_workday(time)`: (Integer) Working seconds since workday started.
72
+ * `calculator.seconds_until_end_of_workday(time)`: (Integer) Working seconds until workday finishes.
73
+ * `calculator.holiday?(time_or_date)`: (Boolean) Whether time or date is a holiday.
74
+ * `calculator.add_days_to_date(days, date)`: (Date) Adds given work days to a date.
75
+
76
+
77
+ ## Development
78
+
79
+ After checking out the repo, run `bin/setup` to install dependencies. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
80
+
81
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
82
+
83
+ ## Contributing
84
+
85
+ Bug reports and pull requests are welcome on GitHub at https://github.com/spreemo/fast_business_time. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](http://contributor-covenant.org) code of conduct.
86
+
87
+
88
+ ## License
89
+
90
+ Copyright (c) 2016 Spreemo. The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
data/Rakefile ADDED
@@ -0,0 +1,2 @@
1
+ require "bundler/gem_tasks"
2
+ task :default => :spec
data/bin/console ADDED
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "fast_business_time"
5
+ require "pry"
6
+ Pry.start
data/bin/setup ADDED
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
@@ -0,0 +1,25 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'fast_business_time/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = 'fast_business_time'
8
+ spec.version = FastBusinessTime::VERSION
9
+ spec.authors = ['Jordi Noguera']
10
+ spec.email = ['jordinoguera83@gmail.com']
11
+
12
+ spec.summary = %q{Time calculations based on business hours}
13
+ spec.homepage = 'https://github.com/spreemo/fast_business_time'
14
+ spec.license = 'MIT'
15
+
16
+ spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
17
+ spec.bindir = 'exe'
18
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
19
+ spec.require_paths = ['lib']
20
+
21
+ spec.add_development_dependency 'bundler', '~> 1.12'
22
+ spec.add_development_dependency 'rake', '~> 10.0'
23
+ spec.add_development_dependency 'rspec'
24
+ spec.add_development_dependency 'pry'
25
+ end
@@ -0,0 +1,5 @@
1
+ require 'fast_business_time/version'
2
+ require 'fast_business_time/calculator'
3
+ require 'fast_business_time/work_day'
4
+ require 'fast_business_time/schedule'
5
+ require 'fast_business_time/holiday_collection'
@@ -0,0 +1,50 @@
1
+ module FastBusinessTime
2
+ class Calculator
3
+ def initialize(schedule:, holidays: [])
4
+ @schedule = Schedule.new(schedule)
5
+ @holidays = HolidayCollection.new(collection: holidays, schedule: @schedule)
6
+ end
7
+
8
+ def seconds_between_times(first_time, last_time)
9
+ return -1 * seconds_between_times(last_time, first_time) if last_time < first_time
10
+ schedule.seconds_in_date_range(first_time.to_date, last_time.to_date) -
11
+ holidays.seconds_in_date_range(first_time.to_date, last_time.to_date) -
12
+ seconds_since_beginning_of_workday(first_time) -
13
+ seconds_until_end_of_workday(last_time)
14
+ end
15
+
16
+ def seconds_since_beginning_of_workday(time)
17
+ return 0 if holiday?(time)
18
+ schedule.seconds_since_beginning_of_day(time)
19
+ end
20
+
21
+ def seconds_until_end_of_workday(time)
22
+ return 0 if holiday?(time)
23
+ schedule.seconds_until_end_of_day(time)
24
+ end
25
+
26
+ def days_between_dates(first_date, second_date)
27
+ return 0 if first_date == second_date
28
+ schedule.days_in_date_range(first_date, second_date - 1) -
29
+ holidays.days_in_date_range(first_date, second_date - 1)
30
+ end
31
+
32
+ def holiday?(time)
33
+ holidays.include?(time.to_date)
34
+ end
35
+
36
+ def add_days_to_date(days, date)
37
+ holiday_count = 0
38
+ loop do
39
+ last_date = schedule.add_days_to_date(days + holiday_count, date)
40
+ return last_date if holiday_count == holidays.days_in_date_range(date, last_date)
41
+ holiday_count = holidays.days_in_date_range(date, last_date)
42
+ end
43
+ end
44
+
45
+ private
46
+
47
+ attr_reader :holidays, :schedule
48
+ end
49
+ end
50
+
@@ -0,0 +1,61 @@
1
+ require 'set'
2
+
3
+ class FastBusinessTime::HolidayCollection
4
+ def initialize(collection:, schedule:)
5
+ @schedule = schedule
6
+ @dates = calculate_holidays_time(collection)
7
+ end
8
+
9
+ def days_in_date_range(first_date, second_date)
10
+ holidays_before(second_date + 1) - holidays_before(first_date)
11
+ end
12
+
13
+ def seconds_in_date_range(first_date, second_date)
14
+ holiday_seconds_before(second_date + 1) - holiday_seconds_before(first_date)
15
+ end
16
+
17
+ def include?(date)
18
+ get(date)[:holiday?]
19
+ end
20
+
21
+ private
22
+
23
+ attr_reader :schedule
24
+
25
+ def holidays_before(date)
26
+ get(date)[:holidays_before]
27
+ end
28
+
29
+ def holiday_seconds_before(date)
30
+ get(date)[:holiday_seconds_before]
31
+ end
32
+
33
+ def get(date)
34
+ @dates[date] || { holiday?: false, holidays_before: 0, holiday_seconds_before: 0 }
35
+ end
36
+
37
+ def calculate_holidays_time(collection)
38
+ return {} if collection.empty?
39
+
40
+ holidays_before = 0
41
+ holiday_seconds_before = 0
42
+
43
+ sorted_holidays = SortedSet.new(collection)
44
+ first_holiday = sorted_holidays.min
45
+ last_holiday = sorted_holidays.max
46
+
47
+ (first_holiday..last_holiday).each_with_object({}) do |date, hash|
48
+ holiday = sorted_holidays.include?(date)
49
+ working_seconds = schedule.seconds_per_wday(date.wday)
50
+ hash[date] = {
51
+ holiday?: holiday,
52
+ holidays_before: holidays_before,
53
+ holiday_seconds_before: holiday_seconds_before
54
+ }
55
+ if holiday && working_seconds > 0
56
+ holidays_before += 1
57
+ holiday_seconds_before += working_seconds
58
+ end
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,125 @@
1
+ class FastBusinessTime::Schedule
2
+ WDAYS = {
3
+ sun: 0,
4
+ mon: 1,
5
+ tue: 2,
6
+ wed: 3,
7
+ thu: 4,
8
+ fri: 5,
9
+ sat: 6
10
+ }
11
+
12
+ attr_reader :seconds_per_week
13
+
14
+ def initialize(times)
15
+ @times = unpack_times(times)
16
+ @seconds_per_week = @times.values.map(&:seconds).inject(&:+)
17
+ @days_per_week = @times.keys.count
18
+ @seconds_in_wday_ranges = calculate_seconds_in_wday_ranges
19
+ @days_in_wday_ranges = calculate_days_in_wday_ranges
20
+ @wdays_plus_days = calculate_wdays_plus_days
21
+ end
22
+
23
+ def seconds_per_wday(wday)
24
+ day = @times[wday]
25
+ return 0 unless day
26
+ day.seconds
27
+ end
28
+
29
+ def seconds_in_date_range(first_date, second_date)
30
+ number_of_days = (second_date - first_date + 1).to_i
31
+ full_weeks = number_of_days / 7
32
+ seconds = @seconds_per_week * full_weeks
33
+ return seconds if number_of_days % 7 == 0
34
+ seconds + @seconds_in_wday_ranges[[first_date.wday, second_date.wday]]
35
+ end
36
+
37
+ def days_in_date_range(first_date, second_date)
38
+ number_of_days = (second_date - first_date + 1).to_i
39
+ full_weeks = number_of_days / 7
40
+ work_days = @days_per_week * full_weeks
41
+ return work_days if number_of_days % 7 == 0
42
+ work_days + @days_in_wday_ranges[[first_date.wday, second_date.wday]]
43
+ end
44
+
45
+ def seconds_since_beginning_of_day(time)
46
+ day = @times[time.wday]
47
+ return 0 unless day
48
+ time_in_seconds = time_in_seconds(time)
49
+ day.times.map do |start, _end|
50
+ time_intersection([start, time_in_seconds], [start, _end])
51
+ end.inject(&:+)
52
+ end
53
+
54
+ def seconds_until_end_of_day(time)
55
+ day = @times[time.wday]
56
+ return 0 unless day
57
+ time_in_seconds = time_in_seconds(time)
58
+ day.times.map do |start, _end|
59
+ time_intersection([time_in_seconds, _end], [start, _end])
60
+ end.inject(&:+)
61
+ end
62
+
63
+ def add_days_to_date(days, date)
64
+ weeks = days / @days_per_week
65
+ rest = days % @days_per_week
66
+
67
+ date + 7 * weeks + @wdays_plus_days[[date.wday, rest]]
68
+ end
69
+
70
+ private
71
+
72
+ def unpack_times(times)
73
+ times.each_with_object({}) do |(days, day_times), hash|
74
+ days.each do |day|
75
+ hash[WDAYS[day]] = FastBusinessTime::WorkDay.new(day_times)
76
+ end
77
+ end
78
+ end
79
+
80
+ def calculate_seconds_in_wday_ranges
81
+ (0..6).each_with_object({}) do |first_wday, hash|
82
+ total_seconds = 0
83
+ (0..5).each do |offset|
84
+ second_wday = (first_wday + offset) % 7
85
+ total_seconds += seconds_per_wday(second_wday)
86
+ hash[[first_wday, second_wday]] = total_seconds
87
+ end
88
+ end
89
+ end
90
+
91
+ def calculate_days_in_wday_ranges
92
+ (0..6).each_with_object({}) do |first_wday, hash|
93
+ total_days = 0
94
+ (0..5).each do |offset|
95
+ second_wday = (first_wday + offset) % 7
96
+ total_days += 1 if @times.has_key?(second_wday)
97
+ hash[[first_wday, second_wday]] = total_days
98
+ end
99
+ end
100
+ end
101
+
102
+ def calculate_wdays_plus_days
103
+ (0..6).each_with_object({}) do |first_wday, hash|
104
+ calendar_days = 0
105
+ last_wday = first_wday
106
+ (0..@days_per_week - 1).each do |days|
107
+ while !@times.has_key?(last_wday)
108
+ calendar_days += 1
109
+ last_wday = (last_wday + 1) % 7
110
+ end
111
+ hash[[first_wday, days]] = calendar_days
112
+ calendar_days += 1
113
+ last_wday = (last_wday + 1) % 7
114
+ end
115
+ end
116
+ end
117
+
118
+ def time_intersection(segment1, segment2)
119
+ [[segment1[1], segment2[1]].min - [segment1[0], segment2[0]].max, 0].max
120
+ end
121
+
122
+ def time_in_seconds(time)
123
+ time.hour * 3600 + time.min * 60 + time.sec
124
+ end
125
+ end
@@ -0,0 +1,3 @@
1
+ module FastBusinessTime
2
+ VERSION = "0.1.0"
3
+ end
@@ -0,0 +1,10 @@
1
+ class FastBusinessTime::WorkDay
2
+ attr_reader :times, :seconds
3
+
4
+ def initialize(times)
5
+ @times = times
6
+ @seconds = times.map do |(first, last)|
7
+ last - first
8
+ end.inject(&:+)
9
+ end
10
+ end
metadata ADDED
@@ -0,0 +1,116 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: fast_business_time
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Jordi Noguera
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2016-12-01 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: bundler
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.12'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.12'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rake
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '10.0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '10.0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rspec
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: pry
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ description:
70
+ email:
71
+ - jordinoguera83@gmail.com
72
+ executables: []
73
+ extensions: []
74
+ extra_rdoc_files: []
75
+ files:
76
+ - ".gitignore"
77
+ - CODE_OF_CONDUCT.md
78
+ - Gemfile
79
+ - Gemfile.lock
80
+ - LICENSE
81
+ - README.md
82
+ - Rakefile
83
+ - bin/console
84
+ - bin/setup
85
+ - fast_business_time.gemspec
86
+ - lib/fast_business_time.rb
87
+ - lib/fast_business_time/calculator.rb
88
+ - lib/fast_business_time/holiday_collection.rb
89
+ - lib/fast_business_time/schedule.rb
90
+ - lib/fast_business_time/version.rb
91
+ - lib/fast_business_time/work_day.rb
92
+ homepage: https://github.com/spreemo/fast_business_time
93
+ licenses:
94
+ - MIT
95
+ metadata: {}
96
+ post_install_message:
97
+ rdoc_options: []
98
+ require_paths:
99
+ - lib
100
+ required_ruby_version: !ruby/object:Gem::Requirement
101
+ requirements:
102
+ - - ">="
103
+ - !ruby/object:Gem::Version
104
+ version: '0'
105
+ required_rubygems_version: !ruby/object:Gem::Requirement
106
+ requirements:
107
+ - - ">="
108
+ - !ruby/object:Gem::Version
109
+ version: '0'
110
+ requirements: []
111
+ rubyforge_project:
112
+ rubygems_version: 2.4.5.1
113
+ signing_key:
114
+ specification_version: 4
115
+ summary: Time calculations based on business hours
116
+ test_files: []