imseng-business_time 0.6.2
Sign up to get free protection for your applications and to get access to all the features.
- data/LICENSE +20 -0
- data/README.rdoc +180 -0
- data/lib/business_time/business_days.rb +36 -0
- data/lib/business_time/business_hours.rb +70 -0
- data/lib/business_time/config.rb +89 -0
- data/lib/business_time/core_ext/date.rb +14 -0
- data/lib/business_time/core_ext/fixnum.rb +18 -0
- data/lib/business_time/core_ext/time.rb +122 -0
- data/lib/business_time/version.rb +3 -0
- data/lib/business_time.rb +15 -0
- data/lib/generators/business_time/config_generator.rb +21 -0
- data/rails_generators/business_time_config/business_time_config_generator.rb +11 -0
- data/rails_generators/business_time_config/templates/business_time.rb +7 -0
- data/rails_generators/business_time_config/templates/business_time.yml +13 -0
- metadata +160 -0
data/LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2009,2010,2011,2012 bokmann
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.rdoc
ADDED
@@ -0,0 +1,180 @@
|
|
1
|
+
= business_time
|
2
|
+
|
3
|
+
{<img src="https://secure.travis-ci.org/bokmann/business_time.png" />}[http://travis-ci.org/bokmann/business_time]
|
4
|
+
|
5
|
+
ActiveSupport gives us some great helpers so we can do things like:
|
6
|
+
|
7
|
+
5.days.ago
|
8
|
+
|
9
|
+
and
|
10
|
+
|
11
|
+
8.hours.from_now
|
12
|
+
|
13
|
+
as well as helpers to do that from any provided date or time.
|
14
|
+
|
15
|
+
I needed this, but taking into account business hours/days and holidays.
|
16
|
+
|
17
|
+
== Usage
|
18
|
+
* install the gem
|
19
|
+
|
20
|
+
gem install business_time
|
21
|
+
|
22
|
+
or
|
23
|
+
|
24
|
+
sudo gem install business_time
|
25
|
+
|
26
|
+
if you require sudo to install gems
|
27
|
+
|
28
|
+
* open up your console
|
29
|
+
|
30
|
+
# if in irb, add these lines:
|
31
|
+
|
32
|
+
require 'rubygems'
|
33
|
+
require 'active_support'
|
34
|
+
require 'business_time'
|
35
|
+
|
36
|
+
# try these examples, using the current time:
|
37
|
+
|
38
|
+
1.business_hour.from_now
|
39
|
+
4.business_hours.from_now
|
40
|
+
8.business_hours.from_now
|
41
|
+
|
42
|
+
1.business_hour.ago
|
43
|
+
4.business_hours.ago
|
44
|
+
8.business_hours.ago
|
45
|
+
|
46
|
+
1.business_day.from_now
|
47
|
+
4.business_days.from_now
|
48
|
+
8.business_days.from_now
|
49
|
+
|
50
|
+
1.business_day.ago
|
51
|
+
4.business_days.ago
|
52
|
+
8.business_days.ago
|
53
|
+
|
54
|
+
# and we can do it from any Date or Time object.
|
55
|
+
my_birthday = Date.parse("August 4th, 1969")
|
56
|
+
8.business_days.after(my_birthday)
|
57
|
+
8.business_days.before(my_birthday)
|
58
|
+
|
59
|
+
my_birthday = Time.parse("August 4th, 1969, 8:32 am")
|
60
|
+
8.business_days.after(my_birthday)
|
61
|
+
8.business_days.before(my_birthday)
|
62
|
+
|
63
|
+
|
64
|
+
# We can adjust the start and end time of our business hours
|
65
|
+
BusinessTime::Config.beginning_of_workday = "8:30 am"
|
66
|
+
BusinessTime::Config.end_of_workday = "5:30 pm"
|
67
|
+
|
68
|
+
# and we can add holidays that don't count as business days
|
69
|
+
# July 5 in 2010 is a monday that the U.S. takes off because our independence day falls on that Sunday.
|
70
|
+
three_day_weekend = Date.parse("July 5th, 2010")
|
71
|
+
BusinessTime::Config.holidays << three_day_weekend
|
72
|
+
friday_afternoon = Time.parse("July 2nd, 2010, 4:50 pm")
|
73
|
+
tuesday_morning = 1.business_hour.after(friday_afternoon)
|
74
|
+
|
75
|
+
# plus, we can change the work week:
|
76
|
+
# July 9th in 2010 is a Friday.
|
77
|
+
BusinessTime::Config.work_week = [:sun, :mon, :tue, :wed, :thu]
|
78
|
+
thursday_afternoon = Time.parse("July 8th, 2010, 4:50 pm")
|
79
|
+
sunday_morning = 1.business_hour.after(thursday_afternoon)
|
80
|
+
|
81
|
+
# you can also calculate business duration between two dates
|
82
|
+
friday = Date.parse("December 24, 2010")
|
83
|
+
monday = Date.parse("December 27, 2010")
|
84
|
+
friday.business_days_until(monday) #=> 1
|
85
|
+
|
86
|
+
# or you can calculate business duration between two Time objects
|
87
|
+
ticket_reported = Time.parse("February 3, 2012, 10:40 am")
|
88
|
+
ticket_resolved = Time.parse("February 4, 2012, 10:50 am")
|
89
|
+
ticket_reported.business_time_until(ticket_resolved) #=> 8.hours + 10.minutes
|
90
|
+
|
91
|
+
# note that counterintuitively, durations might not be quite what you expect when involving weekends.
|
92
|
+
# Consider the following example:
|
93
|
+
ticket_reported = Time.parse("February 3, 2012, 10:40 am")
|
94
|
+
ticket_resolved = Time.parse("February 4, 2012, 10:40 am")
|
95
|
+
ticket_reported.business_time_until(ticket_resolved) # will equal 6 hours and 20 minutes!
|
96
|
+
|
97
|
+
# why does this happen? Feb 4 2012 is a Saturday. That time will roll over to
|
98
|
+
# Monday, Feb 6th 2012, 9:00am. The business time between 10:40am friday and 9am monday is
|
99
|
+
# 6 hours and 20 minutes. From a quick inspection of the code, it looks like it should be 8 hours.
|
100
|
+
|
101
|
+
== Usage in Rails
|
102
|
+
The code above should work on a rails console without any issue. You will want to add a line something like:
|
103
|
+
|
104
|
+
config.gem "business_time"
|
105
|
+
|
106
|
+
to your environment.rb file. Or if you're using bundler, add this line to your Gemfile:
|
107
|
+
|
108
|
+
gem "business_time"
|
109
|
+
|
110
|
+
This gem also includes a generator so you can bootstrap some stuff in your environment:
|
111
|
+
|
112
|
+
./script/generate business_time_config
|
113
|
+
|
114
|
+
Or in Rails 3:
|
115
|
+
|
116
|
+
script/rails generate business_time:config
|
117
|
+
|
118
|
+
The generator will add a /config/business_time.yml and a /config/initializers/business_time.rb file that will cause the start of business day, the end of business day, and your holidays to be loaded from the yaml file. You might want to programatically load your holidays from a database table, but you will want to pay attention to how the initializer works - you will want to make sure that the initializer sets stuff up appropriately so rails instances on mongrels or passenger will have the appropriate data as they come up and down.
|
119
|
+
|
120
|
+
== Outside of Rails
|
121
|
+
This code does depend on ActiveSupport, but nothing else within rails. Even then, it would be pretty easy to break that dependency as well (but would add some code bloat and remove some clarity). Feel free to use it on any ruby project you'd like!
|
122
|
+
|
123
|
+
== Timezone support
|
124
|
+
This gem strives to be timezone-agnostic. Due to some complications in the handling of timezones in the built in Time class, and some complexities (bugs?) in the timeWithZone class, this was harder than expected... but here's the idea:
|
125
|
+
|
126
|
+
* When you configure the gem with something like 9:00am as the start time,
|
127
|
+
this is agnostic of time zone.
|
128
|
+
* When you are dealing with a Time or TimeWithZone class, the timezone is
|
129
|
+
preserved and the beginning and end of times for the business day are
|
130
|
+
referenced in that time zone.
|
131
|
+
|
132
|
+
This can lead to some wierd looking effects if, say, you are in the Eastern time zone but doing everything in UTC times... Your business day will appear to start and end at 9:00 and 5:00 UTC. If this seems perplexing to you, I can almost guarantee you are in over your head with timezones in other ways too, this is just the first place you encountered it. 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. I'm hoping Arild and I write some good blog entries on the subject at http://blog.codesherpas.com.
|
133
|
+
|
134
|
+
== Integration with the Holidays gem
|
135
|
+
|
136
|
+
Chris Wise wrote up a great article[http://murmurinfo.wordpress.com/2012/01/11/handling-holidays-and-business-hours/] on using the business_time gem with the holidays[https://github.com/HealthyWeb/holidays] gem. It boils down to this:
|
137
|
+
|
138
|
+
Holidays.between(Date.civil(2011, 1, 1), 2.years.from_now, :ca_on, :o bserved).map{|holiday| BusinessTime::Config.holidays << holiday[:date]}
|
139
|
+
|
140
|
+
|
141
|
+
== Contributors
|
142
|
+
* David Bock http://github.com/bokmann
|
143
|
+
* Enrico Bianco http://github.com/enricob
|
144
|
+
* Arild Shirazi http://github.com/ashirazi
|
145
|
+
* Piotr Jakubowski http://github.com/piotrj
|
146
|
+
* Glenn Vanderburg http://github.com/glv
|
147
|
+
|
148
|
+
(Special thanks for Arild on the complexities of dealing with TimeWithZone)
|
149
|
+
|
150
|
+
== Note on Patches/Pull Requests
|
151
|
+
|
152
|
+
* Fork the project.
|
153
|
+
* Make your feature addition or bug fix.
|
154
|
+
* Add tests for it. This is important so I don't break it in a
|
155
|
+
future version unintentionally.
|
156
|
+
* Commit, do not mess with rakefile, version, or history.
|
157
|
+
(if you want to have your own version, that is fine but bump version in a commit by itself I can ignore when I pull)
|
158
|
+
* Send me a pull request. Bonus points for topic branches.
|
159
|
+
|
160
|
+
== TODO
|
161
|
+
|
162
|
+
* Arild has pointed out that there may be some logical inconsistencies
|
163
|
+
regarding the beginning_of_workday and end_of workday times not actually
|
164
|
+
being considered inside of the workday. I'd like to make sure that they
|
165
|
+
work as if the beginning_of_workday is included and the end_of_workday is
|
166
|
+
not included, just like the '...' range operator in Ruby.
|
167
|
+
|
168
|
+
== NOT TODO
|
169
|
+
|
170
|
+
* I spent way too much time in my previous java-programmer life building frameworks that worshipped complexity,
|
171
|
+
always trying to give the developer-user ultimate flexibility at the expense of the 'surface area' of the api.
|
172
|
+
Never again - I will sooner limit functionality to 80% so that something stays usable and let people fork.
|
173
|
+
* While there have been requests to add 'business minutes' and even 'business seconds' to this gem, I won't
|
174
|
+
entertain a pullup request with such things. If you find it useful, great. Most users won't, and they don't
|
175
|
+
need the baggage.
|
176
|
+
|
177
|
+
|
178
|
+
== Copyright
|
179
|
+
|
180
|
+
Copyright (c) 2010,2011,2012 bokmann. See LICENSE for details.
|
@@ -0,0 +1,36 @@
|
|
1
|
+
module BusinessTime
|
2
|
+
|
3
|
+
class BusinessDays
|
4
|
+
def initialize(days)
|
5
|
+
@days = days
|
6
|
+
end
|
7
|
+
|
8
|
+
def after(time = Time.now)
|
9
|
+
time = Time.zone ? Time.zone.parse(time.to_s) : Time.parse(time.to_s)
|
10
|
+
days = @days
|
11
|
+
while days > 0 || !Time.workday?(time)
|
12
|
+
days -= 1 if Time.workday?(time)
|
13
|
+
time = time + 1.day
|
14
|
+
end
|
15
|
+
time
|
16
|
+
end
|
17
|
+
|
18
|
+
alias_method :from_now, :after
|
19
|
+
alias_method :since, :after
|
20
|
+
|
21
|
+
def before(time = Time.now)
|
22
|
+
time = Time.zone ? Time.zone.parse(time.to_s) : Time.parse(time.to_s)
|
23
|
+
days = @days
|
24
|
+
while days > 0 || !Time.workday?(time)
|
25
|
+
days -= 1 if Time.workday?(time)
|
26
|
+
time = time - 1.day
|
27
|
+
end
|
28
|
+
time
|
29
|
+
end
|
30
|
+
|
31
|
+
alias_method :ago, :before
|
32
|
+
alias_method :until, :before
|
33
|
+
|
34
|
+
end
|
35
|
+
|
36
|
+
end
|
@@ -0,0 +1,70 @@
|
|
1
|
+
module BusinessTime
|
2
|
+
|
3
|
+
class BusinessHours
|
4
|
+
def initialize(hours)
|
5
|
+
@hours = hours
|
6
|
+
end
|
7
|
+
|
8
|
+
def ago
|
9
|
+
Time.zone ? before(Time.zone.now) : before(Time.now)
|
10
|
+
end
|
11
|
+
|
12
|
+
def from_now
|
13
|
+
Time.zone ? after(Time.zone.now) : after(Time.now)
|
14
|
+
end
|
15
|
+
|
16
|
+
def after(time)
|
17
|
+
after_time = Time.roll_forward(time)
|
18
|
+
# Step through the hours, skipping over non-business hours
|
19
|
+
@hours.times do
|
20
|
+
after_time = after_time + 1.hour
|
21
|
+
|
22
|
+
# Ignore hours before opening and after closing
|
23
|
+
if (after_time > Time.end_of_workday(after_time))
|
24
|
+
after_time = after_time + off_hours
|
25
|
+
end
|
26
|
+
|
27
|
+
# Ignore weekends and holidays
|
28
|
+
while !Time.workday?(after_time)
|
29
|
+
after_time = after_time + 1.day
|
30
|
+
end
|
31
|
+
end
|
32
|
+
after_time
|
33
|
+
end
|
34
|
+
alias_method :since, :after
|
35
|
+
|
36
|
+
def before(time)
|
37
|
+
before_time = Time.roll_forward(time)
|
38
|
+
# Step through the hours, skipping over non-business hours
|
39
|
+
@hours.times do
|
40
|
+
before_time = before_time - 1.hour
|
41
|
+
|
42
|
+
# Ignore hours before opening and after closing
|
43
|
+
if (before_time < Time.beginning_of_workday(before_time))
|
44
|
+
before_time = before_time - off_hours
|
45
|
+
end
|
46
|
+
|
47
|
+
# Ignore weekends and holidays
|
48
|
+
while !Time.workday?(before_time)
|
49
|
+
before_time = before_time - 1.day
|
50
|
+
end
|
51
|
+
end
|
52
|
+
before_time
|
53
|
+
end
|
54
|
+
|
55
|
+
private
|
56
|
+
|
57
|
+
def off_hours
|
58
|
+
return @gap if @gap
|
59
|
+
if Time.zone
|
60
|
+
gap_end = Time.zone.parse(BusinessTime::Config.beginning_of_workday)
|
61
|
+
gap_begin = (Time.zone.parse(BusinessTime::Config.end_of_workday)-1.day)
|
62
|
+
else
|
63
|
+
gap_end = Time.parse(BusinessTime::Config.beginning_of_workday)
|
64
|
+
gap_begin = (Time.parse(BusinessTime::Config.end_of_workday) - 1.day)
|
65
|
+
end
|
66
|
+
@gap = gap_end - gap_begin
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
end
|
@@ -0,0 +1,89 @@
|
|
1
|
+
require 'active_support/core_ext'
|
2
|
+
|
3
|
+
module BusinessTime
|
4
|
+
|
5
|
+
# controls the behavior of this gem. You can change
|
6
|
+
# the beginning_of_workday, end_of_workday, and the list of holidays
|
7
|
+
# manually, or with a yaml file and the load method.
|
8
|
+
class Config
|
9
|
+
class << self
|
10
|
+
# You can set this yourself, either by the load method below, or
|
11
|
+
# by saying
|
12
|
+
# BusinessTime::Config.beginning_of_workday = "8:30 am"
|
13
|
+
# someplace in the initializers of your application.
|
14
|
+
attr_accessor :beginning_of_workday
|
15
|
+
|
16
|
+
# You can set this yourself, either by the load method below, or
|
17
|
+
# by saying
|
18
|
+
# BusinessTime::Config.end_of_workday = "5:30 pm"
|
19
|
+
# someplace in the initializers of your application.
|
20
|
+
attr_accessor :end_of_workday
|
21
|
+
|
22
|
+
# You can set this yourself, either by the load method below, or
|
23
|
+
# by saying
|
24
|
+
# BusinessTime::Config.work_week = [:sun, :mon, :tue, :wed, :thu]
|
25
|
+
# someplace in the initializers of your application.
|
26
|
+
attr_accessor :work_week
|
27
|
+
|
28
|
+
# You can set this yourself, either by the load method below, or
|
29
|
+
# by saying
|
30
|
+
# BusinessTime::Config.holidays << my_holiday_date_object
|
31
|
+
# someplace in the initializers of your application.
|
32
|
+
attr_accessor :holidays
|
33
|
+
|
34
|
+
end
|
35
|
+
|
36
|
+
def self.work_week=(days)
|
37
|
+
@work_week = days
|
38
|
+
@weekdays = nil
|
39
|
+
end
|
40
|
+
|
41
|
+
def self.weekdays
|
42
|
+
return @weekdays unless @weekdays.nil?
|
43
|
+
|
44
|
+
lowercase_day_names = ::Time::RFC2822_DAY_NAME.map(&:downcase)
|
45
|
+
|
46
|
+
@weekdays = work_week.each_with_object([]) do |day_name, days|
|
47
|
+
day_num = lowercase_day_names.find_index(day_name.to_s.downcase)
|
48
|
+
days << day_num unless day_num.nil?
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
def self.reset
|
53
|
+
self.holidays = []
|
54
|
+
self.beginning_of_workday = "9:00 am"
|
55
|
+
self.end_of_workday = "5:00 pm"
|
56
|
+
self.work_week = %w[mon tue wed thu fri]
|
57
|
+
@weekdays = nil
|
58
|
+
end
|
59
|
+
|
60
|
+
# loads the config data from a yaml file written as:
|
61
|
+
#
|
62
|
+
# business_time:
|
63
|
+
# beginning_od_workday: 8:30 am
|
64
|
+
# end_of_workday: 5:30 pm
|
65
|
+
# holidays:
|
66
|
+
# - Jan 1st, 2010
|
67
|
+
# - July 4th, 2010
|
68
|
+
# - Dec 25th, 2010
|
69
|
+
def self.load(file)
|
70
|
+
self.reset
|
71
|
+
data = YAML::load(file.respond_to?(:read) ? file : File.open(file))
|
72
|
+
config = (data["business_time"] || {})
|
73
|
+
|
74
|
+
# load each config variable from the file, if it's present and valid
|
75
|
+
config_vars = %w(beginning_of_workday end_of_workday work_week)
|
76
|
+
config_vars.each do |var|
|
77
|
+
send("#{var}=", config[var]) if config[var] && respond_to?("#{var}=")
|
78
|
+
end
|
79
|
+
|
80
|
+
(config["holidays"] || []).each do |holiday|
|
81
|
+
self.holidays << Date.parse(holiday)
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
#reset the first time we are loaded.
|
86
|
+
self.reset
|
87
|
+
end
|
88
|
+
|
89
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
# Add workday and weekday concepts to the Date class
|
2
|
+
class Date
|
3
|
+
def workday?
|
4
|
+
self.weekday? && !BusinessTime::Config.holidays.include?(self)
|
5
|
+
end
|
6
|
+
|
7
|
+
def weekday?
|
8
|
+
BusinessTime::Config.weekdays.include? self.wday
|
9
|
+
end
|
10
|
+
|
11
|
+
def business_days_until(to_date)
|
12
|
+
(self...to_date).select{ |day| day.workday? }.size
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
# hook into fixnum so we can say things like:
|
2
|
+
# 5.business_hours.from_now
|
3
|
+
# 7.business_days.ago
|
4
|
+
# 3.business_days.after(some_date)
|
5
|
+
# 4.business_hours.before(some_date_time)
|
6
|
+
class Fixnum
|
7
|
+
include BusinessTime
|
8
|
+
|
9
|
+
def business_hours
|
10
|
+
BusinessHours.new(self)
|
11
|
+
end
|
12
|
+
alias_method :business_hour, :business_hours
|
13
|
+
|
14
|
+
def business_days
|
15
|
+
BusinessDays.new(self)
|
16
|
+
end
|
17
|
+
alias_method :business_day, :business_days
|
18
|
+
end
|
@@ -0,0 +1,122 @@
|
|
1
|
+
# Add workday and weekday concepts to the Time class
|
2
|
+
class Time
|
3
|
+
class << self
|
4
|
+
|
5
|
+
# Gives the time at the end of the workday, assuming that this time falls on a
|
6
|
+
# workday.
|
7
|
+
# Note: It pretends that this day is a workday whether or not it really is a
|
8
|
+
# workday.
|
9
|
+
def end_of_workday(day)
|
10
|
+
format = "%B %d %Y #{BusinessTime::Config.end_of_workday}"
|
11
|
+
Time.zone ? Time.zone.parse(day.strftime(format)) :
|
12
|
+
Time.parse(day.strftime(format))
|
13
|
+
end
|
14
|
+
|
15
|
+
# Gives the time at the beginning of the workday, assuming that this time
|
16
|
+
# falls on a workday.
|
17
|
+
# Note: It pretends that this day is a workday whether or not it really is a
|
18
|
+
# workday.
|
19
|
+
def beginning_of_workday(day)
|
20
|
+
format = "%B %d %Y #{BusinessTime::Config.beginning_of_workday}"
|
21
|
+
Time.zone ? Time.zone.parse(day.strftime(format)) :
|
22
|
+
Time.parse(day.strftime(format))
|
23
|
+
end
|
24
|
+
|
25
|
+
# True if this time is on a workday (between 00:00:00 and 23:59:59), even if
|
26
|
+
# this time falls outside of normal business hours.
|
27
|
+
def workday?(day)
|
28
|
+
Time.weekday?(day) &&
|
29
|
+
!BusinessTime::Config.holidays.include?(day.to_date)
|
30
|
+
end
|
31
|
+
|
32
|
+
# True if this time falls on a weekday.
|
33
|
+
def weekday?(day)
|
34
|
+
BusinessTime::Config.weekdays.include? day.wday
|
35
|
+
end
|
36
|
+
|
37
|
+
def before_business_hours?(time)
|
38
|
+
time < beginning_of_workday(time)
|
39
|
+
end
|
40
|
+
|
41
|
+
def after_business_hours?(time)
|
42
|
+
time > end_of_workday(time)
|
43
|
+
end
|
44
|
+
|
45
|
+
# Rolls forward to the next beginning_of_workday
|
46
|
+
# when the time is outside of business hours
|
47
|
+
def roll_forward(time)
|
48
|
+
|
49
|
+
if (Time.before_business_hours?(time) || !Time.workday?(time))
|
50
|
+
next_business_time = Time.beginning_of_workday(time)
|
51
|
+
elsif Time.after_business_hours?(time)
|
52
|
+
next_business_time = Time.beginning_of_workday(time) + 1.day
|
53
|
+
else
|
54
|
+
next_business_time = time.clone
|
55
|
+
end
|
56
|
+
|
57
|
+
while !Time.workday?(next_business_time)
|
58
|
+
next_business_time += 1.day
|
59
|
+
end
|
60
|
+
|
61
|
+
next_business_time
|
62
|
+
end
|
63
|
+
|
64
|
+
# Rolls backwards to the previous end_of_workday when the time is outside
|
65
|
+
# of business hours
|
66
|
+
def roll_backward(time)
|
67
|
+
if (Time.before_business_hours?(time) || !Time.workday?(time))
|
68
|
+
prev_business_time = Time.end_of_workday(time) - 1.day
|
69
|
+
elsif Time.after_business_hours?(time)
|
70
|
+
prev_business_time = Time.end_of_workday(time)
|
71
|
+
else
|
72
|
+
prev_business_time = time.clone
|
73
|
+
end
|
74
|
+
|
75
|
+
while !Time.workday?(prev_business_time)
|
76
|
+
prev_business_time -= 1.day
|
77
|
+
end
|
78
|
+
|
79
|
+
prev_business_time
|
80
|
+
end
|
81
|
+
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
class Time
|
86
|
+
|
87
|
+
def business_time_until(to_time)
|
88
|
+
|
89
|
+
# Make sure that we will calculate time from A to B "clockwise"
|
90
|
+
direction = 1
|
91
|
+
if self < to_time
|
92
|
+
time_a = self
|
93
|
+
time_b = to_time
|
94
|
+
else
|
95
|
+
time_a = to_time
|
96
|
+
time_b = self
|
97
|
+
direction = -1
|
98
|
+
end
|
99
|
+
|
100
|
+
# Align both times to the closest business hours
|
101
|
+
time_a = Time::roll_forward(time_a)
|
102
|
+
time_b = Time::roll_forward(time_b)
|
103
|
+
|
104
|
+
# If same date, then calculate difference straight forward
|
105
|
+
if time_a.to_date == time_b.to_date
|
106
|
+
result = time_b - time_a
|
107
|
+
return result *= direction
|
108
|
+
end
|
109
|
+
|
110
|
+
# Both times are in different dates
|
111
|
+
result = Time.parse(time_a.strftime('%Y-%m-%d ') + BusinessTime::Config.end_of_workday) - time_a # First day
|
112
|
+
result += time_b - Time.parse(time_b.strftime('%Y-%m-%d ') + BusinessTime::Config.beginning_of_workday) # Last day
|
113
|
+
|
114
|
+
# All days in between
|
115
|
+
duration_of_working_day = Time.parse(BusinessTime::Config.end_of_workday) - Time.parse(BusinessTime::Config.beginning_of_workday)
|
116
|
+
result += (time_a.to_date.business_days_until(time_b.to_date) - 1) * duration_of_working_day
|
117
|
+
|
118
|
+
# Make sure that sign is correct
|
119
|
+
result *= direction
|
120
|
+
end
|
121
|
+
|
122
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
require 'thread'
|
2
|
+
require 'active_support'
|
3
|
+
require 'active_support/version'
|
4
|
+
if ActiveSupport::VERSION::MAJOR > 2
|
5
|
+
require 'active_support/time'
|
6
|
+
end
|
7
|
+
require 'time'
|
8
|
+
|
9
|
+
require 'business_time/config'
|
10
|
+
require 'business_time/business_hours'
|
11
|
+
require 'business_time/business_days'
|
12
|
+
require 'business_time/core_ext/date'
|
13
|
+
require 'business_time/core_ext/time'
|
14
|
+
require 'business_time/core_ext/fixnum'
|
15
|
+
|
@@ -0,0 +1,21 @@
|
|
1
|
+
module BusinessTime
|
2
|
+
module Generators
|
3
|
+
class ConfigGenerator < Rails::Generators::Base # :nodoc:
|
4
|
+
|
5
|
+
def self.gem_root
|
6
|
+
File.expand_path("../../../..", __FILE__)
|
7
|
+
end
|
8
|
+
|
9
|
+
def self.source_root
|
10
|
+
# Use the templates from the 2.3.x generator
|
11
|
+
File.join(gem_root, 'rails_generators', 'business_time_config', 'templates')
|
12
|
+
end
|
13
|
+
|
14
|
+
def generate
|
15
|
+
template 'business_time.rb', File.join('config', 'initializers', 'business_time.rb')
|
16
|
+
template 'business_time.yml', File.join('config', 'business_time.yml')
|
17
|
+
end
|
18
|
+
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,11 @@
|
|
1
|
+
# This generator simply drops the business_time.rb and business_time.yml file
|
2
|
+
# into the appropate places in a rails app to configure and initialize the
|
3
|
+
# data. Once generated, these files are yours to modify.
|
4
|
+
class BusinessTimeConfigGenerator < Rails::Generator::Base
|
5
|
+
def manifest
|
6
|
+
record do |m|
|
7
|
+
m.template('business_time.rb', "config/initializers/business_time.rb")
|
8
|
+
m.template('business_time.yml', "config/business_time.yml")
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end
|
@@ -0,0 +1,7 @@
|
|
1
|
+
BusinessTime::Config.load("#{Rails.root}/config/business_time.yml")
|
2
|
+
|
3
|
+
# or you can configure it manually: look at me! I'm Tim Ferris!
|
4
|
+
# BusinessTime.Config.beginning_of_workday = "10:00 am"
|
5
|
+
# BusinessTime.Config.end_of_workday = "11:30 am"
|
6
|
+
# BusinessTime.Config.holidays << Date.parse("August 4th, 2010")
|
7
|
+
|
metadata
ADDED
@@ -0,0 +1,160 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: imseng-business_time
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
prerelease:
|
5
|
+
version: 0.6.2
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- wortdar
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2013-06-13 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: activesupport
|
16
|
+
version_requirements: !ruby/object:Gem::Requirement
|
17
|
+
requirements:
|
18
|
+
- - '>='
|
19
|
+
- !ruby/object:Gem::Version
|
20
|
+
version: 2.0.0
|
21
|
+
none: false
|
22
|
+
requirement: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - '>='
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: 2.0.0
|
27
|
+
none: false
|
28
|
+
prerelease: false
|
29
|
+
type: :runtime
|
30
|
+
- !ruby/object:Gem::Dependency
|
31
|
+
name: tzinfo
|
32
|
+
version_requirements: !ruby/object:Gem::Requirement
|
33
|
+
requirements:
|
34
|
+
- - ~>
|
35
|
+
- !ruby/object:Gem::Version
|
36
|
+
version: 0.3.31
|
37
|
+
none: false
|
38
|
+
requirement: !ruby/object:Gem::Requirement
|
39
|
+
requirements:
|
40
|
+
- - ~>
|
41
|
+
- !ruby/object:Gem::Version
|
42
|
+
version: 0.3.31
|
43
|
+
none: false
|
44
|
+
prerelease: false
|
45
|
+
type: :runtime
|
46
|
+
- !ruby/object:Gem::Dependency
|
47
|
+
name: appraisal
|
48
|
+
version_requirements: !ruby/object:Gem::Requirement
|
49
|
+
requirements:
|
50
|
+
- - '>='
|
51
|
+
- !ruby/object:Gem::Version
|
52
|
+
version: '0'
|
53
|
+
none: false
|
54
|
+
requirement: !ruby/object:Gem::Requirement
|
55
|
+
requirements:
|
56
|
+
- - '>='
|
57
|
+
- !ruby/object:Gem::Version
|
58
|
+
version: '0'
|
59
|
+
none: false
|
60
|
+
prerelease: false
|
61
|
+
type: :development
|
62
|
+
- !ruby/object:Gem::Dependency
|
63
|
+
name: rake
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - '>='
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: 0.9.2
|
69
|
+
none: false
|
70
|
+
requirement: !ruby/object:Gem::Requirement
|
71
|
+
requirements:
|
72
|
+
- - '>='
|
73
|
+
- !ruby/object:Gem::Version
|
74
|
+
version: 0.9.2
|
75
|
+
none: false
|
76
|
+
prerelease: false
|
77
|
+
type: :development
|
78
|
+
- !ruby/object:Gem::Dependency
|
79
|
+
name: shoulda
|
80
|
+
version_requirements: !ruby/object:Gem::Requirement
|
81
|
+
requirements:
|
82
|
+
- - '>='
|
83
|
+
- !ruby/object:Gem::Version
|
84
|
+
version: '0'
|
85
|
+
none: false
|
86
|
+
requirement: !ruby/object:Gem::Requirement
|
87
|
+
requirements:
|
88
|
+
- - '>='
|
89
|
+
- !ruby/object:Gem::Version
|
90
|
+
version: '0'
|
91
|
+
none: false
|
92
|
+
prerelease: false
|
93
|
+
type: :development
|
94
|
+
- !ruby/object:Gem::Dependency
|
95
|
+
name: rdoc
|
96
|
+
version_requirements: !ruby/object:Gem::Requirement
|
97
|
+
requirements:
|
98
|
+
- - '>='
|
99
|
+
- !ruby/object:Gem::Version
|
100
|
+
version: '0'
|
101
|
+
none: false
|
102
|
+
requirement: !ruby/object:Gem::Requirement
|
103
|
+
requirements:
|
104
|
+
- - '>='
|
105
|
+
- !ruby/object:Gem::Version
|
106
|
+
version: '0'
|
107
|
+
none: false
|
108
|
+
prerelease: false
|
109
|
+
type: :development
|
110
|
+
description: Have you ever wanted to do things like "6.business_days.from_now" and have weekends and holidays taken into account? Now you can.
|
111
|
+
email: darrin.wortlehock@db.com
|
112
|
+
executables: []
|
113
|
+
extensions: []
|
114
|
+
extra_rdoc_files: []
|
115
|
+
files:
|
116
|
+
- LICENSE
|
117
|
+
- README.rdoc
|
118
|
+
- lib/business_time.rb
|
119
|
+
- lib/business_time/business_days.rb
|
120
|
+
- lib/business_time/business_hours.rb
|
121
|
+
- lib/business_time/config.rb
|
122
|
+
- lib/business_time/core_ext/date.rb
|
123
|
+
- lib/business_time/core_ext/fixnum.rb
|
124
|
+
- lib/business_time/core_ext/time.rb
|
125
|
+
- lib/business_time/version.rb
|
126
|
+
- lib/generators/business_time/config_generator.rb
|
127
|
+
- rails_generators/business_time_config/business_time_config_generator.rb
|
128
|
+
- rails_generators/business_time_config/templates/business_time.rb
|
129
|
+
- rails_generators/business_time_config/templates/business_time.yml
|
130
|
+
homepage: http://github.com/imseng/business_time
|
131
|
+
licenses: []
|
132
|
+
post_install_message:
|
133
|
+
rdoc_options: []
|
134
|
+
require_paths:
|
135
|
+
- lib
|
136
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
137
|
+
requirements:
|
138
|
+
- - '>='
|
139
|
+
- !ruby/object:Gem::Version
|
140
|
+
segments:
|
141
|
+
- 0
|
142
|
+
hash: 2
|
143
|
+
version: '0'
|
144
|
+
none: false
|
145
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
146
|
+
requirements:
|
147
|
+
- - '>='
|
148
|
+
- !ruby/object:Gem::Version
|
149
|
+
segments:
|
150
|
+
- 0
|
151
|
+
hash: 2
|
152
|
+
version: '0'
|
153
|
+
none: false
|
154
|
+
requirements: []
|
155
|
+
rubyforge_project:
|
156
|
+
rubygems_version: 1.8.24
|
157
|
+
signing_key:
|
158
|
+
specification_version: 3
|
159
|
+
summary: Support for doing time math in business hours and days
|
160
|
+
test_files: []
|