business_time 0.7.3 → 0.9.3
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/LICENSE +1 -1
- data/README.rdoc +58 -29
- data/lib/business_time.rb +4 -2
- data/lib/business_time/business_days.rb +51 -22
- data/lib/business_time/business_hours.rb +31 -7
- data/lib/business_time/config.rb +60 -13
- data/lib/business_time/core_ext/active_support/time_with_zone.rb +1 -0
- data/lib/business_time/core_ext/date.rb +53 -6
- data/lib/business_time/core_ext/{fixnum.rb → integer.rb} +2 -2
- data/lib/business_time/core_ext/time.rb +1 -0
- data/lib/business_time/parsed_time.rb +34 -0
- data/lib/business_time/time_extensions.rb +74 -21
- data/lib/business_time/version.rb +1 -1
- metadata +24 -43
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 5fe356eb4f4e620e3f3db2aa9242df1c63213500
|
4
|
+
data.tar.gz: d1f662a4f576662e3f6d140970d5b2191016fda8
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 86434d31fbf26aa3372510db883266643431a449fa2980cee43e9d34198c140a560d0606d41911aa0a47e030b373eeba0442f38dad07c36a5b5113a811c97213
|
7
|
+
data.tar.gz: 5c1ae7866418a21538030ba2129698eaafb2016b5d715cd2c13e31e1739b8732ff625af6f87e48e98bbe697ea06005d74c35bfab7c8610b306f7ba7c9d0121b5
|
data/LICENSE
CHANGED
data/README.rdoc
CHANGED
@@ -15,11 +15,11 @@ as well as helpers to do that from any provided date or time.
|
|
15
15
|
I needed this, but taking into account business hours/days and holidays.
|
16
16
|
|
17
17
|
== Usage
|
18
|
-
|
18
|
+
=== install the gem
|
19
19
|
|
20
20
|
gem install business_time
|
21
21
|
|
22
|
-
|
22
|
+
=== open up your console
|
23
23
|
|
24
24
|
# if in irb, add these lines:
|
25
25
|
|
@@ -43,7 +43,11 @@ I needed this, but taking into account business hours/days and holidays.
|
|
43
43
|
4.business_days.ago
|
44
44
|
8.business_days.ago
|
45
45
|
|
46
|
-
|
46
|
+
Date.today.workday?
|
47
|
+
Date.parse("2015-12-09").workday?
|
48
|
+
Date.parse("2015-12-12").workday?
|
49
|
+
|
50
|
+
And we can do it from any Date or Time object.
|
47
51
|
my_birthday = Date.parse("August 4th, 1969")
|
48
52
|
8.business_days.after(my_birthday)
|
49
53
|
8.business_days.before(my_birthday)
|
@@ -53,24 +57,24 @@ I needed this, but taking into account business hours/days and holidays.
|
|
53
57
|
8.business_days.before(my_birthday)
|
54
58
|
|
55
59
|
|
56
|
-
|
60
|
+
We can adjust the start and end time of our business hours
|
57
61
|
BusinessTime::Config.beginning_of_workday = "8:30 am"
|
58
62
|
BusinessTime::Config.end_of_workday = "5:30 pm"
|
59
63
|
|
60
|
-
|
61
|
-
|
64
|
+
and we can add holidays that don't count as business days
|
65
|
+
July 5 in 2010 is a monday that the U.S. takes off because our independence day falls on that Sunday.
|
62
66
|
three_day_weekend = Date.parse("July 5th, 2010")
|
63
67
|
BusinessTime::Config.holidays << three_day_weekend
|
64
68
|
friday_afternoon = Time.parse("July 2nd, 2010, 4:50 pm")
|
65
69
|
tuesday_morning = 1.business_hour.after(friday_afternoon)
|
66
70
|
|
67
|
-
|
71
|
+
plus, we can change the work week:
|
68
72
|
# July 9th in 2010 is a Friday.
|
69
73
|
BusinessTime::Config.work_week = [:sun, :mon, :tue, :wed, :thu]
|
70
74
|
thursday_afternoon = Time.parse("July 8th, 2010, 4:50 pm")
|
71
75
|
sunday_morning = 1.business_hour.after(thursday_afternoon)
|
72
76
|
|
73
|
-
|
77
|
+
As alternative we also can change the business hours for each work day:
|
74
78
|
BusinessTime::Config.work_hours = {
|
75
79
|
:mon=>["9:00","17:00"],
|
76
80
|
:fri=>["9:00","17:00"],
|
@@ -80,26 +84,46 @@ I needed this, but taking into account business hours/days and holidays.
|
|
80
84
|
monday = Time.parse("December 27, 2010 11:00")
|
81
85
|
working_hours = friday.business_time_until(monday) # 9.hours
|
82
86
|
|
83
|
-
|
87
|
+
You can also calculate business duration between two dates
|
84
88
|
friday = Date.parse("December 24, 2010")
|
85
89
|
monday = Date.parse("December 27, 2010")
|
86
90
|
friday.business_days_until(monday) #=> 1
|
87
91
|
|
88
|
-
|
92
|
+
Or you can calculate business duration between two Time objects
|
89
93
|
ticket_reported = Time.parse("February 3, 2012, 10:40 am")
|
90
94
|
ticket_resolved = Time.parse("February 4, 2012, 10:50 am")
|
91
95
|
ticket_reported.business_time_until(ticket_resolved) #=> 8.hours + 10.minutes
|
92
96
|
|
93
|
-
|
94
|
-
|
97
|
+
You can also determine if a given time is within business hours
|
98
|
+
Time.parse("February 3, 2012, 10:00 am").during_business_hours?
|
99
|
+
|
100
|
+
Note that counterintuitively, durations might not be quite what you expect when involving weekends.
|
101
|
+
Consider the following example:
|
95
102
|
ticket_reported = Time.parse("February 3, 2012, 10:40 am")
|
96
103
|
ticket_resolved = Time.parse("February 4, 2012, 10:40 am")
|
97
104
|
ticket_reported.business_time_until(ticket_resolved) # will equal 6 hours and 20 minutes!
|
98
105
|
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
106
|
+
Why does this happen? Feb 4 2012 is a Saturday. That time will roll over to
|
107
|
+
Monday, Feb 6th 2012, 9:00am. The business time between 10:40am friday and 9am monday is
|
108
|
+
6 hours and 20 minutes. From a quick inspection of the code, it looks like it should be 8 hours.
|
109
|
+
|
110
|
+
Or you can calculate business dates between two dates
|
111
|
+
monday = Date.parse("December 20, 2010")
|
112
|
+
wednesday = Date.parse("December 22, 2010")
|
113
|
+
monday.business_dates_until(wednesday) #=> [Mon, 20 Dec 2010, Tue, 21 Dec 2010]
|
114
|
+
|
115
|
+
You can get the first workday after a time or return itself if it is a workday
|
116
|
+
saturday = Time.parse("Sat Aug 9, 18:00:00, 2014")
|
117
|
+
monday = Time.parse("Mon Aug 11, 18:00:00, 2014")
|
118
|
+
Time.first_business_day(saturday) #=> "Mon Aug 11, 18:00:00, 2014"
|
119
|
+
Time.first_business_day(monday) #=> "Mon Aug 11, 18:00:00, 2014"
|
120
|
+
|
121
|
+
# similar to Time#first_business_day Time#previous_business_day only cares about
|
122
|
+
# workdays:
|
123
|
+
saturday = Time.parse("Sat Aug 9, 18:00:00, 2014")
|
124
|
+
monday = Time.parse("Mon Aug 11, 18:00:00, 2014")
|
125
|
+
Time.previous_business_day(saturday) #=> "Fri Aug 8, 18:00:00, 2014"
|
126
|
+
Time.previous_business_day(monday) #=> "Mon Aug 11, 18:00:00, 2014"
|
103
127
|
== Rails generator
|
104
128
|
|
105
129
|
rails generate business_time:config
|
@@ -123,12 +147,11 @@ and some complexities (bugs?) in the timeWithZone class, this was harder than ex
|
|
123
147
|
preserved and the beginning and end of times for the business day are
|
124
148
|
referenced in that time zone.
|
125
149
|
|
126
|
-
This can lead to some
|
150
|
+
This can lead to some weird looking effects if, say, you are in the Eastern time zone but doing everything in UTC times...
|
127
151
|
Your business day will appear to start and end at 9:00 and 5:00 UTC.
|
128
152
|
If this seems perplexing to you, I can almost guarantee you are in over your head with timezones in other ways too,
|
129
153
|
this is just the first place you encountered it.
|
130
154
|
Timezone relative date handling gets more and more complicated every time you look at it and takes a long time before it starts to seem simple again.
|
131
|
-
I'm hoping Arild and I write some good blog entries on the subject at http://blog.codesherpas.com.
|
132
155
|
|
133
156
|
== Integration with the Holidays gem
|
134
157
|
|
@@ -141,16 +164,6 @@ I'm hoping Arild and I write some good blog entries on the subject at http://blo
|
|
141
164
|
# BusinessTime::Config.holidays << holiday[:date].next_week if !holiday[:date].weekday?
|
142
165
|
end
|
143
166
|
|
144
|
-
== Releases
|
145
|
-
|
146
|
-
0.7.1 - fixing a multithreaded issue, upgrading some dependencies, loosening the dependency on TZInfo
|
147
|
-
|
148
|
-
0.7.0 - major maintenance upgrade on the process of constructing the gem, testing the gem, and updating dependencies.
|
149
|
-
the api has not changed.
|
150
|
-
|
151
|
-
|
152
|
-
0.6.2 - rchady pointed out that issue #14 didn't appear to be released. This fixes that, as well as confirms that all tests run as expected on Ruby 2.0.0p195
|
153
|
-
|
154
167
|
== Contributors
|
155
168
|
* David Bock http://github.com/bokmann
|
156
169
|
* Enrico Bianco http://github.com/enricob
|
@@ -158,6 +171,8 @@ I'm hoping Arild and I write some good blog entries on the subject at http://blo
|
|
158
171
|
* Piotr Jakubowski http://github.com/piotrj
|
159
172
|
* Glenn Vanderburg http://github.com/glv
|
160
173
|
* Michael Grosser http://github.com/grosser
|
174
|
+
* Michael Curtis http://github.com/mcurtis
|
175
|
+
* Brian Ewins http://github.com/bazzargh
|
161
176
|
|
162
177
|
(Special thanks for Arild on the complexities of dealing with TimeWithZone)
|
163
178
|
|
@@ -189,6 +204,20 @@ I'm hoping Arild and I write some good blog entries on the subject at http://blo
|
|
189
204
|
need the baggage.
|
190
205
|
|
191
206
|
|
207
|
+
== A note on stability and change
|
208
|
+
|
209
|
+
Sometimes people ask me why this gem doesn't release more often. My opinions on that are best discussed in person in a friendly discussion, but I'll attempt some of that here.
|
210
|
+
|
211
|
+
First, a big part of the reason is that the projects I do use this gem on are happy with it's current functionality. It is 'suitable for the purpose' for which I released it, and as such, maintenance I do on this gem is a gift to the community.
|
212
|
+
|
213
|
+
Second, out of the ~1.3 million downloads (according to rubygems.org), the number of real 'issues' with this gem have been minimal. Most of the issues that are opened are really people with slightly different requirements than I have regarding whether 'off hours' work counts as the previous or the next business day, a disagreement on the semantics of days vs. hours, etc. I take care to try to explain these choices in the open issues, but to my mind, they aren't true issues if it's just a difference of opinion. Even so, I'll gladly accept pull requests that resolve this difference of opinion as a configuration option... just don't expect me to do your job for you. I've already given you 90% of what you need.
|
214
|
+
|
215
|
+
Third, a business time gem is, well, relevant to businesses. Many businesses don't move quickly. My government clients move even more slowly. Stability is favored in these environments.
|
216
|
+
|
217
|
+
Fourth, new features can wait. To the person that adds them they can be mission critical, but with modern packaging processes, they can use their version without waiting for their changes to be included in the upstream version. Their changes don't break your code.
|
218
|
+
|
219
|
+
I'm proud of the work in this gem; the stability is a big part of that. This gem has lived longer than many others that have attempted to do the same thing. I expect it to be here chugging away when Ruby has become the next COBOL.
|
220
|
+
|
192
221
|
== Copyright
|
193
222
|
|
194
|
-
Copyright (c) 2010
|
223
|
+
Copyright (c) 2010-2017 bokmann. See LICENSE for details.
|
data/lib/business_time.rb
CHANGED
@@ -4,12 +4,14 @@ require 'active_support/time'
|
|
4
4
|
require 'time'
|
5
5
|
require 'yaml'
|
6
6
|
|
7
|
+
require 'business_time/parsed_time'
|
8
|
+
require 'business_time/version'
|
7
9
|
require 'business_time/config'
|
8
10
|
require 'business_time/business_hours'
|
9
11
|
require 'business_time/business_days'
|
10
|
-
require 'business_time/core_ext/
|
11
|
-
require 'business_time/core_ext/fixnum'
|
12
|
+
require 'business_time/core_ext/integer'
|
12
13
|
|
13
14
|
require 'business_time/time_extensions'
|
15
|
+
require 'business_time/core_ext/date'
|
14
16
|
require 'business_time/core_ext/time'
|
15
17
|
require 'business_time/core_ext/active_support/time_with_zone'
|
@@ -1,38 +1,67 @@
|
|
1
1
|
require 'active_support/time'
|
2
2
|
|
3
3
|
module BusinessTime
|
4
|
-
|
5
4
|
class BusinessDays
|
5
|
+
include Comparable
|
6
|
+
attr_reader :days
|
7
|
+
|
6
8
|
def initialize(days)
|
7
9
|
@days = days
|
8
10
|
end
|
9
11
|
|
10
|
-
def
|
11
|
-
|
12
|
-
|
13
|
-
while days > 0 || !Time.workday?(time)
|
14
|
-
days -= 1 if Time.workday?(time)
|
15
|
-
time = time + 1.day
|
12
|
+
def <=>(other)
|
13
|
+
if other.class != self.class
|
14
|
+
raise ArgumentError.new("#{self.class} can't be compared with #{other.class}")
|
16
15
|
end
|
17
|
-
|
16
|
+
self.days <=> other.days
|
17
|
+
end
|
18
|
+
|
19
|
+
def after(time = Time.current)
|
20
|
+
non_negative_days? ? calculate_after(time, @days) : calculate_before(time, -@days)
|
18
21
|
end
|
19
|
-
|
22
|
+
|
20
23
|
alias_method :from_now, :after
|
21
24
|
alias_method :since, :after
|
22
|
-
|
23
|
-
def before(time = Time.
|
24
|
-
|
25
|
-
days = @days
|
26
|
-
while days > 0 || !Time.workday?(time)
|
27
|
-
days -= 1 if Time.workday?(time)
|
28
|
-
time = time - 1.day
|
29
|
-
end
|
30
|
-
time
|
25
|
+
|
26
|
+
def before(time = Time.current)
|
27
|
+
non_negative_days? ? calculate_before(time, @days) : calculate_after(time, -@days)
|
31
28
|
end
|
32
|
-
|
29
|
+
|
33
30
|
alias_method :ago, :before
|
34
31
|
alias_method :until, :before
|
35
|
-
|
36
|
-
|
37
|
-
|
32
|
+
|
33
|
+
private
|
34
|
+
|
35
|
+
def non_negative_days?
|
36
|
+
@days >= 0
|
37
|
+
end
|
38
|
+
|
39
|
+
def calculate_after(time, days)
|
40
|
+
while days > 0 || !time.workday?
|
41
|
+
days -= 1 if time.workday?
|
42
|
+
time += 1.day
|
43
|
+
end
|
44
|
+
# If we have a Time or DateTime object, we can roll_forward to the
|
45
|
+
# beginning of the next business day
|
46
|
+
if time.is_a?(Time) || time.is_a?(DateTime)
|
47
|
+
time = Time.roll_forward(time) unless time.during_business_hours?
|
48
|
+
end
|
49
|
+
time
|
50
|
+
end
|
51
|
+
|
52
|
+
def calculate_before(time, days)
|
53
|
+
while days > 0 || !time.workday?
|
54
|
+
days -= 1 if time.workday?
|
55
|
+
time -= 1.day
|
56
|
+
end
|
57
|
+
# If we have a Time or DateTime object, we can roll_backward to the
|
58
|
+
# beginning of the previous business day
|
59
|
+
if time.is_a?(Time) || time.is_a?(DateTime)
|
60
|
+
unless time.during_business_hours?
|
61
|
+
time = Time.beginning_of_workday(Time.roll_backward(time))
|
62
|
+
end
|
63
|
+
end
|
64
|
+
time
|
65
|
+
end
|
66
|
+
end
|
38
67
|
end
|
@@ -1,10 +1,20 @@
|
|
1
1
|
module BusinessTime
|
2
2
|
|
3
3
|
class BusinessHours
|
4
|
+
include Comparable
|
5
|
+
attr_reader :hours
|
6
|
+
|
4
7
|
def initialize(hours)
|
5
8
|
@hours = hours
|
6
9
|
end
|
7
10
|
|
11
|
+
def <=>(other)
|
12
|
+
if other.class != self.class
|
13
|
+
raise ArgumentError.new("#{self.class.to_s} can't be compared with #{other.class.to_s}")
|
14
|
+
end
|
15
|
+
self.hours <=> other.hours
|
16
|
+
end
|
17
|
+
|
8
18
|
def ago
|
9
19
|
Time.zone ? before(Time.zone.now) : before(Time.now)
|
10
20
|
end
|
@@ -14,9 +24,24 @@ module BusinessTime
|
|
14
24
|
end
|
15
25
|
|
16
26
|
def after(time)
|
27
|
+
non_negative_hours? ? calculate_after(time, @hours) : calculate_before(time, -@hours)
|
28
|
+
end
|
29
|
+
alias_method :since, :after
|
30
|
+
|
31
|
+
def before(time)
|
32
|
+
non_negative_hours? ? calculate_before(time, @hours) : calculate_after(time, -@hours)
|
33
|
+
end
|
34
|
+
|
35
|
+
private
|
36
|
+
|
37
|
+
def non_negative_hours?
|
38
|
+
@hours >= 0
|
39
|
+
end
|
40
|
+
|
41
|
+
def calculate_after(time, hours)
|
17
42
|
after_time = Time.roll_forward(time)
|
18
43
|
# Step through the hours, skipping over non-business hours
|
19
|
-
|
44
|
+
hours.times do
|
20
45
|
after_time = after_time + 1.hour
|
21
46
|
|
22
47
|
if after_time.hour == 0 && after_time.min == 0 && after_time.sec == 0
|
@@ -28,18 +53,17 @@ module BusinessTime
|
|
28
53
|
end
|
29
54
|
|
30
55
|
# Ignore weekends and holidays
|
31
|
-
while !
|
56
|
+
while !after_time.workday?
|
32
57
|
after_time = after_time + 1.day
|
33
58
|
end
|
34
59
|
end
|
35
60
|
after_time
|
36
61
|
end
|
37
|
-
alias_method :since, :after
|
38
62
|
|
39
|
-
def
|
63
|
+
def calculate_before(time, hours)
|
40
64
|
before_time = Time.roll_backward(time)
|
41
65
|
# Step through the hours, skipping over non-business hours
|
42
|
-
|
66
|
+
hours.times do
|
43
67
|
before_time = before_time - 1.hour
|
44
68
|
|
45
69
|
if before_time.hour == 0 && before_time.min == 0 && before_time.sec == 0
|
@@ -50,13 +74,13 @@ module BusinessTime
|
|
50
74
|
|
51
75
|
# Due to the 23:59:59 end-of-workday exception
|
52
76
|
time_roll_backward = Time.roll_backward(before_time)
|
53
|
-
time_roll_backward += 1.second if time_roll_backward.
|
77
|
+
time_roll_backward += 1.second if time_roll_backward.iso8601 =~ /23:59:59/
|
54
78
|
|
55
79
|
before_time = time_roll_backward - delta
|
56
80
|
end
|
57
81
|
|
58
82
|
# Ignore weekends and holidays
|
59
|
-
while !
|
83
|
+
while !before_time.workday?
|
60
84
|
before_time = before_time - 1.day
|
61
85
|
end
|
62
86
|
end
|
data/lib/business_time/config.rb
CHANGED
@@ -7,30 +7,73 @@ module BusinessTime
|
|
7
7
|
# manually, or with a yaml file and the load method.
|
8
8
|
class Config
|
9
9
|
DEFAULT_CONFIG = {
|
10
|
-
holidays:
|
11
|
-
beginning_of_workday: '9:00 am',
|
12
|
-
end_of_workday: '5:00 pm',
|
10
|
+
holidays: SortedSet.new,
|
11
|
+
beginning_of_workday: ParsedTime.parse('9:00 am'),
|
12
|
+
end_of_workday: ParsedTime.parse('5:00 pm'),
|
13
13
|
work_week: %w(mon tue wed thu fri),
|
14
14
|
work_hours: {},
|
15
15
|
work_hours_total: {},
|
16
16
|
_weekdays: nil,
|
17
|
+
fiscal_month_offset: 10,
|
17
18
|
}
|
18
19
|
|
19
20
|
class << self
|
21
|
+
def beginning_of_workday=(time)
|
22
|
+
config[:beginning_of_workday] = ParsedTime.parse(time)
|
23
|
+
end
|
24
|
+
|
25
|
+
def end_of_workday=(time)
|
26
|
+
config[:end_of_workday] = ParsedTime.parse(time)
|
27
|
+
end
|
28
|
+
|
29
|
+
def work_hours=(work_hours)
|
30
|
+
work_hours.each_with_object(config[:work_hours] = {}) do |(day, hours), c|
|
31
|
+
c[day] = hours.map do |time|
|
32
|
+
ParsedTime.parse(time)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
20
37
|
private
|
21
38
|
|
22
39
|
def config
|
40
|
+
return local_config if local_config?
|
23
41
|
Thread.main[:business_time_config] ||= default_config
|
24
42
|
end
|
25
43
|
|
26
44
|
def config=(config)
|
45
|
+
return self.local_config = config if local_config?
|
27
46
|
Thread.main[:business_time_config] = config
|
28
47
|
end
|
29
48
|
|
49
|
+
def local_config
|
50
|
+
local_config_stack.last
|
51
|
+
end
|
52
|
+
|
53
|
+
def local_config=(config)
|
54
|
+
local_config_stack.last.replace(config)
|
55
|
+
end
|
56
|
+
|
57
|
+
def local_config_stack
|
58
|
+
Thread.current[:business_time_local_config] ||= []
|
59
|
+
end
|
60
|
+
|
61
|
+
def local_config?
|
62
|
+
!local_config_stack.empty?
|
63
|
+
end
|
64
|
+
|
30
65
|
def threadsafe_cattr_accessor(name)
|
66
|
+
threadsafe_cattr_reader(name)
|
67
|
+
threadsafe_cattr_setter(name)
|
68
|
+
end
|
69
|
+
|
70
|
+
def threadsafe_cattr_reader(name)
|
31
71
|
define_singleton_method name do
|
32
72
|
config[name]
|
33
73
|
end
|
74
|
+
end
|
75
|
+
|
76
|
+
def threadsafe_cattr_setter(name)
|
34
77
|
define_singleton_method "#{name}=" do |value|
|
35
78
|
config[name] = value
|
36
79
|
end
|
@@ -41,13 +84,13 @@ module BusinessTime
|
|
41
84
|
# by saying
|
42
85
|
# BusinessTime::Config.beginning_of_workday = "8:30 am"
|
43
86
|
# someplace in the initializers of your application.
|
44
|
-
|
87
|
+
threadsafe_cattr_reader :beginning_of_workday
|
45
88
|
|
46
89
|
# You can set this yourself, either by the load method below, or
|
47
90
|
# by saying
|
48
91
|
# BusinessTime::Config.end_of_workday = "5:30 pm"
|
49
92
|
# someplace in the initializers of your application.
|
50
|
-
|
93
|
+
threadsafe_cattr_reader :end_of_workday
|
51
94
|
|
52
95
|
# You can set this yourself, either by the load method below, or
|
53
96
|
# by saying
|
@@ -65,18 +108,20 @@ module BusinessTime
|
|
65
108
|
# and end_of_workday. Keys will be added ad weekdays.
|
66
109
|
# Example:
|
67
110
|
# {:mon => ["9:00","17:00"],:tue => ["9:00","17:00"].....}
|
68
|
-
|
111
|
+
threadsafe_cattr_reader :work_hours
|
69
112
|
|
70
113
|
# total work hours for a day. Never set, always calculated.
|
71
114
|
threadsafe_cattr_accessor :work_hours_total
|
72
115
|
|
73
116
|
threadsafe_cattr_accessor :_weekdays # internal
|
74
117
|
|
118
|
+
threadsafe_cattr_accessor :fiscal_month_offset
|
119
|
+
|
75
120
|
class << self
|
76
121
|
def end_of_workday(day=nil)
|
77
122
|
if day
|
78
123
|
wday = work_hours[int_to_wday(day.wday)]
|
79
|
-
wday ? (wday.last
|
124
|
+
wday ? (wday.last == ParsedTime.new(0, 0) ? ParsedTime.new(23, 59, 59) : wday.last) : config[:end_of_workday]
|
80
125
|
else
|
81
126
|
config[:end_of_workday]
|
82
127
|
end
|
@@ -99,10 +144,11 @@ module BusinessTime
|
|
99
144
|
def weekdays
|
100
145
|
return _weekdays unless _weekdays.nil?
|
101
146
|
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
147
|
+
days = (!work_hours.empty? ? work_hours.keys : work_week).map do |day_name|
|
148
|
+
wday_to_int(day_name)
|
149
|
+
end.compact
|
150
|
+
|
151
|
+
self._weekdays = SortedSet.new(days)
|
106
152
|
end
|
107
153
|
|
108
154
|
# loads the config data from a yaml file written as:
|
@@ -131,11 +177,11 @@ module BusinessTime
|
|
131
177
|
end
|
132
178
|
|
133
179
|
def with(config)
|
134
|
-
|
180
|
+
local_config_stack.push(config().dup)
|
135
181
|
config.each { |k,v| send("#{k}=", v) } # calculations are done on setting
|
136
182
|
yield
|
137
183
|
ensure
|
138
|
-
|
184
|
+
local_config_stack.pop
|
139
185
|
end
|
140
186
|
|
141
187
|
def default_config
|
@@ -154,6 +200,7 @@ module BusinessTime
|
|
154
200
|
end
|
155
201
|
|
156
202
|
def reset
|
203
|
+
local_config_stack.clear
|
157
204
|
self.config = default_config
|
158
205
|
end
|
159
206
|
|
@@ -1,14 +1,61 @@
|
|
1
1
|
# Add workday and weekday concepts to the Date class
|
2
2
|
class Date
|
3
|
-
|
4
|
-
|
3
|
+
include BusinessTime::TimeExtensions
|
4
|
+
|
5
|
+
def business_days_until(to_date, inclusive = false)
|
6
|
+
business_dates_until(to_date, inclusive).size
|
7
|
+
end
|
8
|
+
|
9
|
+
def business_dates_until(to_date, inclusive = false)
|
10
|
+
if inclusive
|
11
|
+
(self..to_date).select(&:workday?)
|
12
|
+
else
|
13
|
+
(self...to_date).select(&:workday?)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
# Adapted from:
|
18
|
+
# https://github.com/activewarehouse/activewarehouse/blob/master/lib/active_warehouse/core_ext/time/calculations.rb
|
19
|
+
|
20
|
+
def week
|
21
|
+
cyw = ((yday - 1) / 7) + 1
|
22
|
+
cyw = 52 if cyw == 53
|
23
|
+
cyw
|
24
|
+
end
|
25
|
+
|
26
|
+
def quarter
|
27
|
+
((month - 1) / 3) + 1
|
28
|
+
end
|
29
|
+
|
30
|
+
def fiscal_month_offset
|
31
|
+
BusinessTime::Config.fiscal_month_offset
|
32
|
+
end
|
33
|
+
|
34
|
+
def fiscal_year_week
|
35
|
+
fyw = ((fiscal_year_yday - 1) / 7) + 1
|
36
|
+
fyw = 52 if fyw == 53
|
37
|
+
fyw
|
38
|
+
end
|
39
|
+
|
40
|
+
def fiscal_year_month
|
41
|
+
shifted_month = month - (fiscal_month_offset - 1)
|
42
|
+
shifted_month += 12 if shifted_month <= 0
|
43
|
+
shifted_month
|
44
|
+
end
|
45
|
+
|
46
|
+
def fiscal_year_quarter
|
47
|
+
((fiscal_year_month - 1) / 3) + 1
|
5
48
|
end
|
6
49
|
|
7
|
-
def
|
8
|
-
|
50
|
+
def fiscal_year
|
51
|
+
month >= fiscal_month_offset ? year + 1 : year
|
9
52
|
end
|
10
53
|
|
11
|
-
def
|
12
|
-
|
54
|
+
def fiscal_year_yday
|
55
|
+
offset_days = 0
|
56
|
+
1.upto(fiscal_month_offset - 1) { |m| offset_days += ::Time.days_in_month(m, year) }
|
57
|
+
shifted_year_day = yday - offset_days
|
58
|
+
shifted_year_day += 365 if shifted_year_day <= 0
|
59
|
+
shifted_year_day
|
13
60
|
end
|
14
61
|
end
|
@@ -1,9 +1,9 @@
|
|
1
|
-
# hook into
|
1
|
+
# hook into Integer so we can say things like:
|
2
2
|
# 5.business_hours.from_now
|
3
3
|
# 7.business_days.ago
|
4
4
|
# 3.business_days.after(some_date)
|
5
5
|
# 4.business_hours.before(some_date_time)
|
6
|
-
class
|
6
|
+
class Integer
|
7
7
|
def business_hours
|
8
8
|
BusinessTime::BusinessHours.new(self)
|
9
9
|
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
module BusinessTime
|
2
|
+
class ParsedTime
|
3
|
+
include Comparable
|
4
|
+
|
5
|
+
attr_reader :hour, :min, :sec
|
6
|
+
|
7
|
+
def initialize(hour, min = 0, sec = 0)
|
8
|
+
@hour = hour
|
9
|
+
@min = min
|
10
|
+
@sec = sec
|
11
|
+
end
|
12
|
+
|
13
|
+
def self.parse(time_or_string)
|
14
|
+
if time_or_string.is_a?(String)
|
15
|
+
time = Time.parse(time_or_string)
|
16
|
+
else
|
17
|
+
time = time_or_string
|
18
|
+
end
|
19
|
+
new(time.hour, time.min, time.sec)
|
20
|
+
end
|
21
|
+
|
22
|
+
def to_s
|
23
|
+
"#{hour}:#{min}:#{sec}"
|
24
|
+
end
|
25
|
+
|
26
|
+
def -(other)
|
27
|
+
(hour - other.hour) * 3600 + (min - other.min) * 60 + sec - other.sec
|
28
|
+
end
|
29
|
+
|
30
|
+
def <=>(other)
|
31
|
+
[hour, min, sec] <=> [other.hour, other.min, other.sec]
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -1,7 +1,14 @@
|
|
1
1
|
module BusinessTime
|
2
2
|
module TimeExtensions
|
3
|
-
|
4
|
-
|
3
|
+
# True if this time is on a workday (between 00:00:00 and 23:59:59), even if
|
4
|
+
# this time falls outside of normal business hours.
|
5
|
+
def workday?
|
6
|
+
weekday? && !BusinessTime::Config.holidays.include?(to_date)
|
7
|
+
end
|
8
|
+
|
9
|
+
# True if this time falls on a weekday.
|
10
|
+
def weekday?
|
11
|
+
BusinessTime::Config.weekdays.include?(wday)
|
5
12
|
end
|
6
13
|
|
7
14
|
module ClassMethods
|
@@ -10,7 +17,7 @@ module BusinessTime
|
|
10
17
|
# Note: It pretends that this day is a workday whether or not it really is a
|
11
18
|
# workday.
|
12
19
|
def end_of_workday(day)
|
13
|
-
end_of_workday =
|
20
|
+
end_of_workday = BusinessTime::Config.end_of_workday(day)
|
14
21
|
change_business_time(day,end_of_workday.hour,end_of_workday.min,end_of_workday.sec)
|
15
22
|
end
|
16
23
|
|
@@ -19,20 +26,21 @@ module BusinessTime
|
|
19
26
|
# Note: It pretends that this day is a workday whether or not it really is a
|
20
27
|
# workday.
|
21
28
|
def beginning_of_workday(day)
|
22
|
-
beginning_of_workday =
|
29
|
+
beginning_of_workday = BusinessTime::Config.beginning_of_workday(day)
|
23
30
|
change_business_time(day,beginning_of_workday.hour,beginning_of_workday.min,beginning_of_workday.sec)
|
24
31
|
end
|
25
32
|
|
26
33
|
# True if this time is on a workday (between 00:00:00 and 23:59:59), even if
|
27
34
|
# this time falls outside of normal business hours.
|
28
35
|
def workday?(day)
|
29
|
-
Time.
|
30
|
-
|
36
|
+
ActiveSupport::Deprecation.warn("`Time.workday?(time)` is deprecated. Please use `time.workday?`")
|
37
|
+
day.workday?
|
31
38
|
end
|
32
39
|
|
33
40
|
# True if this time falls on a weekday.
|
34
41
|
def weekday?(day)
|
35
|
-
|
42
|
+
ActiveSupport::Deprecation.warn("`Time.weekday?(time)` is deprecated. Please use `time.weekday?`")
|
43
|
+
day.weekday?
|
36
44
|
end
|
37
45
|
|
38
46
|
def before_business_hours?(time)
|
@@ -47,7 +55,7 @@ module BusinessTime
|
|
47
55
|
# when the time is outside of business hours
|
48
56
|
def roll_forward(time)
|
49
57
|
|
50
|
-
if Time.before_business_hours?(time) || !
|
58
|
+
if Time.before_business_hours?(time) || !time.workday?
|
51
59
|
next_business_time = Time.beginning_of_workday(time)
|
52
60
|
elsif Time.after_business_hours?(time) || Time.end_of_workday(time) == time
|
53
61
|
next_business_time = Time.beginning_of_workday(time + 1.day)
|
@@ -55,17 +63,27 @@ module BusinessTime
|
|
55
63
|
next_business_time = time.clone
|
56
64
|
end
|
57
65
|
|
58
|
-
while !
|
66
|
+
while !next_business_time.workday?
|
59
67
|
next_business_time = Time.beginning_of_workday(next_business_time + 1.day)
|
60
68
|
end
|
61
69
|
|
62
70
|
next_business_time
|
63
71
|
end
|
64
72
|
|
73
|
+
# Returns the time parameter itself if it is a business day
|
74
|
+
# or else returns the next business day
|
75
|
+
def first_business_day(time)
|
76
|
+
while !time.workday?
|
77
|
+
time = time + 1.day
|
78
|
+
end
|
79
|
+
|
80
|
+
time
|
81
|
+
end
|
82
|
+
|
65
83
|
# Rolls backwards to the previous end_of_workday when the time is outside
|
66
84
|
# of business hours
|
67
85
|
def roll_backward(time)
|
68
|
-
prev_business_time = if (Time.before_business_hours?(time) || !
|
86
|
+
prev_business_time = if (Time.before_business_hours?(time) || !time.workday?)
|
69
87
|
Time.end_of_workday(time - 1.day)
|
70
88
|
elsif Time.after_business_hours?(time)
|
71
89
|
Time.end_of_workday(time)
|
@@ -73,30 +91,40 @@ module BusinessTime
|
|
73
91
|
time.clone
|
74
92
|
end
|
75
93
|
|
76
|
-
while !
|
94
|
+
while !prev_business_time.workday?
|
77
95
|
prev_business_time = Time.end_of_workday(prev_business_time - 1.day)
|
78
96
|
end
|
79
97
|
|
80
98
|
prev_business_time
|
81
99
|
end
|
82
100
|
|
101
|
+
# Returns the time parameter itself if it is a business day
|
102
|
+
# or else returns the previous business day
|
103
|
+
def previous_business_day(time)
|
104
|
+
while !time.workday?
|
105
|
+
time = time - 1.day
|
106
|
+
end
|
107
|
+
|
108
|
+
time
|
109
|
+
end
|
110
|
+
|
83
111
|
def work_hours_total(day)
|
84
|
-
return 0 unless
|
112
|
+
return 0 unless day.workday?
|
85
113
|
|
86
114
|
day = day.strftime('%a').downcase.to_sym
|
87
115
|
|
88
116
|
if hours = BusinessTime::Config.work_hours[day]
|
89
117
|
BusinessTime::Config.work_hours_total[day] ||= begin
|
90
118
|
hours_last = hours.last
|
91
|
-
if hours_last ==
|
92
|
-
(
|
119
|
+
if hours_last == ParsedTime.new(0, 0)
|
120
|
+
(ParsedTime.new(23, 59) - hours.first) + 1.minute
|
93
121
|
else
|
94
|
-
|
122
|
+
hours_last - hours.first
|
95
123
|
end
|
96
124
|
end
|
97
125
|
else
|
98
126
|
BusinessTime::Config.work_hours_total[:default] ||= begin
|
99
|
-
|
127
|
+
BusinessTime::Config.end_of_workday - BusinessTime::Config.beginning_of_workday
|
100
128
|
end
|
101
129
|
end
|
102
130
|
end
|
@@ -104,11 +132,7 @@ module BusinessTime
|
|
104
132
|
private
|
105
133
|
|
106
134
|
def change_business_time time, hour, min=0, sec=0
|
107
|
-
|
108
|
-
time.in_time_zone(Time.zone).change(:hour => hour, :min => min, :sec => sec)
|
109
|
-
else
|
110
|
-
time.change(:hour => hour, :min => min, :sec => sec)
|
111
|
-
end
|
135
|
+
time.change(:hour => hour, :min => min, :sec => sec)
|
112
136
|
end
|
113
137
|
end
|
114
138
|
|
@@ -141,5 +165,34 @@ module BusinessTime
|
|
141
165
|
first_day + days_in_between + last_day
|
142
166
|
end * direction
|
143
167
|
end
|
168
|
+
|
169
|
+
def during_business_hours?
|
170
|
+
self.workday? && self.to_i.between?(Time.beginning_of_workday(self).to_i, Time.end_of_workday(self).to_i)
|
171
|
+
end
|
172
|
+
|
173
|
+
def consecutive_workdays
|
174
|
+
workday? ? consecutive_days { |date| date.workday? } : []
|
175
|
+
end
|
176
|
+
|
177
|
+
def consecutive_non_working_days
|
178
|
+
!workday? ? consecutive_days { |date| !date.workday? } : []
|
179
|
+
end
|
180
|
+
|
181
|
+
private
|
182
|
+
|
183
|
+
def consecutive_days
|
184
|
+
days = []
|
185
|
+
date = self + 1.day
|
186
|
+
while yield(date)
|
187
|
+
days << date
|
188
|
+
date += 1.day
|
189
|
+
end
|
190
|
+
date = self - 1.day
|
191
|
+
while yield(date)
|
192
|
+
days << date
|
193
|
+
date -= 1.day
|
194
|
+
end
|
195
|
+
(days << self).sort
|
196
|
+
end
|
144
197
|
end
|
145
198
|
end
|
metadata
CHANGED
@@ -1,115 +1,102 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: business_time
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
5
|
-
prerelease:
|
4
|
+
version: 0.9.3
|
6
5
|
platform: ruby
|
7
6
|
authors:
|
8
7
|
- bokmann
|
9
8
|
autorequire:
|
10
9
|
bindir: bin
|
11
10
|
cert_chain: []
|
12
|
-
date:
|
11
|
+
date: 2017-11-04 00:00:00.000000000 Z
|
13
12
|
dependencies:
|
14
13
|
- !ruby/object:Gem::Dependency
|
15
14
|
name: activesupport
|
16
15
|
requirement: !ruby/object:Gem::Requirement
|
17
|
-
none: false
|
18
16
|
requirements:
|
19
|
-
- -
|
17
|
+
- - ">="
|
20
18
|
- !ruby/object:Gem::Version
|
21
|
-
version: 3.
|
19
|
+
version: 3.2.0
|
22
20
|
type: :runtime
|
23
21
|
prerelease: false
|
24
22
|
version_requirements: !ruby/object:Gem::Requirement
|
25
|
-
none: false
|
26
23
|
requirements:
|
27
|
-
- -
|
24
|
+
- - ">="
|
28
25
|
- !ruby/object:Gem::Version
|
29
|
-
version: 3.
|
26
|
+
version: 3.2.0
|
30
27
|
- !ruby/object:Gem::Dependency
|
31
28
|
name: tzinfo
|
32
29
|
requirement: !ruby/object:Gem::Requirement
|
33
|
-
none: false
|
34
30
|
requirements:
|
35
|
-
- -
|
31
|
+
- - ">="
|
36
32
|
- !ruby/object:Gem::Version
|
37
33
|
version: '0'
|
38
34
|
type: :runtime
|
39
35
|
prerelease: false
|
40
36
|
version_requirements: !ruby/object:Gem::Requirement
|
41
|
-
none: false
|
42
37
|
requirements:
|
43
|
-
- -
|
38
|
+
- - ">="
|
44
39
|
- !ruby/object:Gem::Version
|
45
40
|
version: '0'
|
46
41
|
- !ruby/object:Gem::Dependency
|
47
42
|
name: rake
|
48
43
|
requirement: !ruby/object:Gem::Requirement
|
49
|
-
none: false
|
50
44
|
requirements:
|
51
|
-
- -
|
45
|
+
- - ">="
|
52
46
|
- !ruby/object:Gem::Version
|
53
47
|
version: '0'
|
54
48
|
type: :development
|
55
49
|
prerelease: false
|
56
50
|
version_requirements: !ruby/object:Gem::Requirement
|
57
|
-
none: false
|
58
51
|
requirements:
|
59
|
-
- -
|
52
|
+
- - ">="
|
60
53
|
- !ruby/object:Gem::Version
|
61
54
|
version: '0'
|
62
55
|
- !ruby/object:Gem::Dependency
|
63
56
|
name: rdoc
|
64
57
|
requirement: !ruby/object:Gem::Requirement
|
65
|
-
none: false
|
66
58
|
requirements:
|
67
|
-
- -
|
59
|
+
- - ">="
|
68
60
|
- !ruby/object:Gem::Version
|
69
61
|
version: '0'
|
70
62
|
type: :development
|
71
63
|
prerelease: false
|
72
64
|
version_requirements: !ruby/object:Gem::Requirement
|
73
|
-
none: false
|
74
65
|
requirements:
|
75
|
-
- -
|
66
|
+
- - ">="
|
76
67
|
- !ruby/object:Gem::Version
|
77
68
|
version: '0'
|
78
69
|
- !ruby/object:Gem::Dependency
|
79
70
|
name: minitest
|
80
71
|
requirement: !ruby/object:Gem::Requirement
|
81
|
-
none: false
|
82
72
|
requirements:
|
83
|
-
- -
|
73
|
+
- - ">="
|
84
74
|
- !ruby/object:Gem::Version
|
85
75
|
version: '0'
|
86
76
|
type: :development
|
87
77
|
prerelease: false
|
88
78
|
version_requirements: !ruby/object:Gem::Requirement
|
89
|
-
none: false
|
90
79
|
requirements:
|
91
|
-
- -
|
80
|
+
- - ">="
|
92
81
|
- !ruby/object:Gem::Version
|
93
82
|
version: '0'
|
94
83
|
- !ruby/object:Gem::Dependency
|
95
84
|
name: minitest-rg
|
96
85
|
requirement: !ruby/object:Gem::Requirement
|
97
|
-
none: false
|
98
86
|
requirements:
|
99
|
-
- -
|
87
|
+
- - ">="
|
100
88
|
- !ruby/object:Gem::Version
|
101
89
|
version: '0'
|
102
90
|
type: :development
|
103
91
|
prerelease: false
|
104
92
|
version_requirements: !ruby/object:Gem::Requirement
|
105
|
-
none: false
|
106
93
|
requirements:
|
107
|
-
- -
|
94
|
+
- - ">="
|
108
95
|
- !ruby/object:Gem::Version
|
109
96
|
version: '0'
|
110
97
|
description: Have you ever wanted to do things like "6.business_days.from_now" and
|
111
98
|
have weekends and holidays taken into account? Now you can.
|
112
|
-
email: dbock@
|
99
|
+
email: dbock@javaguy.org
|
113
100
|
executables: []
|
114
101
|
extensions: []
|
115
102
|
extra_rdoc_files: []
|
@@ -122,8 +109,9 @@ files:
|
|
122
109
|
- lib/business_time/config.rb
|
123
110
|
- lib/business_time/core_ext/active_support/time_with_zone.rb
|
124
111
|
- lib/business_time/core_ext/date.rb
|
125
|
-
- lib/business_time/core_ext/
|
112
|
+
- lib/business_time/core_ext/integer.rb
|
126
113
|
- lib/business_time/core_ext/time.rb
|
114
|
+
- lib/business_time/parsed_time.rb
|
127
115
|
- lib/business_time/time_extensions.rb
|
128
116
|
- lib/business_time/version.rb
|
129
117
|
- lib/generators/business_time/config_generator.rb
|
@@ -133,32 +121,25 @@ files:
|
|
133
121
|
homepage: https://github.com/bokmann/business_time
|
134
122
|
licenses:
|
135
123
|
- MIT
|
124
|
+
metadata: {}
|
136
125
|
post_install_message:
|
137
126
|
rdoc_options: []
|
138
127
|
require_paths:
|
139
128
|
- lib
|
140
129
|
required_ruby_version: !ruby/object:Gem::Requirement
|
141
|
-
none: false
|
142
130
|
requirements:
|
143
|
-
- -
|
131
|
+
- - ">="
|
144
132
|
- !ruby/object:Gem::Version
|
145
133
|
version: '0'
|
146
|
-
segments:
|
147
|
-
- 0
|
148
|
-
hash: -1720097037897540308
|
149
134
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
150
|
-
none: false
|
151
135
|
requirements:
|
152
|
-
- -
|
136
|
+
- - ">="
|
153
137
|
- !ruby/object:Gem::Version
|
154
138
|
version: '0'
|
155
|
-
segments:
|
156
|
-
- 0
|
157
|
-
hash: -1720097037897540308
|
158
139
|
requirements: []
|
159
140
|
rubyforge_project:
|
160
|
-
rubygems_version:
|
141
|
+
rubygems_version: 2.6.13
|
161
142
|
signing_key:
|
162
|
-
specification_version:
|
143
|
+
specification_version: 4
|
163
144
|
summary: Support for doing time math in business hours and days
|
164
145
|
test_files: []
|