/business_time

Support for doing time math in business hours and days

Primary LanguageRubyMIT LicenseMIT

business_time

<img src=“https://secure.travis-ci.org/bokmann/business_time.png” />

ActiveSupport gives us some great helpers so we can do things like:

5.days.ago

and

8.hours.from_now

as well as helpers to do that from any provided date or time.

I needed this, but taking into account business hours/days and holidays.

Usage

install the gem

gem install business_time

open up your console

# if in irb, add these lines:

require 'business_time'

# try these examples, using the current time:

1.business_hour.from_now
4.business_hours.from_now
8.business_hours.from_now

1.business_hour.ago
4.business_hours.ago
8.business_hours.ago

1.business_day.from_now
4.business_days.from_now
8.business_days.from_now

1.business_day.ago
4.business_days.ago
8.business_days.ago

Date.today.workday?
Date.parse("2015-12-09").workday?
Date.parse("2015-12-12").workday?

And we can do it from any Date or Time object.

my_birthday = Date.parse("August 4th, 1969")
8.business_days.after(my_birthday)
8.business_days.before(my_birthday)

my_birthday = Time.parse("August 4th, 1969, 8:32 am")
8.business_days.after(my_birthday)
8.business_days.before(my_birthday)

We can adjust the start and end time of our business hours

BusinessTime::Config.beginning_of_workday = "8:30 am"
BusinessTime::Config.end_of_workday = "5:30 pm"

and we can add holidays that don’t count as business days July 5 in 2010 is a monday that the U.S. takes off because our independence day falls on that Sunday.

three_day_weekend = Date.parse("July 5th, 2010")
BusinessTime::Config.holidays << three_day_weekend
friday_afternoon = Time.parse("July 2nd, 2010, 4:50 pm")
tuesday_morning = 1.business_hour.after(friday_afternoon)

plus, we can change the work week:

# July 9th in 2010 is a Friday.
BusinessTime::Config.work_week = [:sun, :mon, :tue, :wed, :thu]
thursday_afternoon = Time.parse("July 8th, 2010, 4:50 pm")
sunday_morning = 1.business_hour.after(thursday_afternoon)

As alternative we also can change the business hours for each work day:

BusinessTime::Config.work_hours = {
  :mon=>["9:00","17:00"],
  :fri=>["9:00","17:00"],
  :sat=>["10:00","15:00"]
}
friday = Time.parse("December 24, 2010 15:00")
monday = Time.parse("December 27, 2010 11:00")
working_hours = friday.business_time_until(monday) # 9.hours

You can also calculate business duration between two dates

friday = Date.parse("December 24, 2010")
monday = Date.parse("December 27, 2010")
friday.business_days_until(monday) #=> 1

Or you can calculate business duration between two Time objects

ticket_reported = Time.parse("February 3, 2012, 10:40 am")
ticket_resolved = Time.parse("February 4, 2012, 10:50 am")
ticket_reported.business_time_until(ticket_resolved) #=> 8.hours + 10.minutes

You can also determine if a given time is within business hours

Time.parse("February 3, 2012, 10:00 am").during_business_hours?

Note that counterintuitively, durations might not be quite what you expect when involving weekends. Consider the following example:

ticket_reported = Time.parse("February 3, 2012, 10:40 am")
ticket_resolved = Time.parse("February 4, 2012, 10:40 am")
ticket_reported.business_time_until(ticket_resolved) # will equal 6 hours and 20 minutes!

Why does this happen? Feb 4 2012 is a Saturday. That time will roll over to Monday, Feb 6th 2012, 9:00am. The business time between 10:40am friday and 9am monday is 6 hours and 20 minutes. From a quick inspection of the code, it looks like it should be 8 hours.

Or you can calculate business dates between two dates

monday = Date.parse("December 20, 2010")
wednesday = Date.parse("December 22, 2010")
monday.business_dates_until(wednesday) #=> [Mon, 20 Dec 2010, Tue, 21 Dec 2010]

You can get the first workday after a time or return itself if it is a workday

saturday = Time.parse("Sat Aug 9, 18:00:00, 2014")
monday = Time.parse("Mon Aug 11, 18:00:00, 2014")
Time.first_business_day(saturday) #=> "Mon Aug 11, 18:00:00, 2014"
Time.first_business_day(monday) #=> "Mon Aug 11, 18:00:00, 2014"

# similar to Time#first_business_day Time#previous_business_day only cares about
# workdays:
saturday = Time.parse("Sat Aug 9, 18:00:00, 2014")
monday = Time.parse("Mon Aug 11, 18:00:00, 2014")
Time.previous_business_day(saturday) #=> "Fri Aug 8, 18:00:00, 2014"
Time.previous_business_day(monday) #=> "Mon Aug 11, 18:00:00, 2014"

Rails generator

rails generate business_time:config

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.

Timezone support

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:

  • When you configure the gem with something like 9:00am as the start time, this is agnostic of time zone.

  • When you are dealing with a Time or TimeWithZone class, the timezone is preserved and the beginning and end of times for the business day are referenced in that time zone.

This can lead to some weird 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.

Integration with the Holidays gem

Chris Wise wrote up a great article on using the business_time gem with the holidays gem. It boils down to this:

Holidays.between(Date.civil(2013, 1, 1), 2.years.from_now, :ca_on, :observed).map do |holiday|
  BusinessTime::Config.holidays << holiday[:date]
  # Implement long weekends if they apply to the region, eg:
  # BusinessTime::Config.holidays << holiday[:date].next_week if !holiday[:date].weekday?
end

Releases

0.7.6 - Fixed a defect where timezone was not preserved when dealing with beginning_or_workday and end_of_workday.

Thanks bazzargh.

0.7.2 - 0.7.5 - many pull requests from contributors, and a changelog was not adequately kept. I regret that.

git blame is your friend for diagnosing these dark days.

0.7.1 - fixing a multithreaded issue, upgrading some dependencies, loosening the dependency on TZInfo

0.7.0 - major maintenance upgrade on the process of constructing the gem, testing the gem, and updating dependencies.

the api has not changed.

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

Contributors

* David Bock  http://github.com/bokmann
* Enrico Bianco  http://github.com/enricob
* Arild Shirazi  http://github.com/ashirazi
* Piotr Jakubowski http://github.com/piotrj
* Glenn Vanderburg http://github.com/glv
* Michael Grosser http://github.com/grosser
* Michael Curtis http://github.com/mcurtis
* Brian Ewins http://github.com/bazzargh

(Special thanks for Arild on the complexities of dealing with TimeWithZone)

Note on Patches/Pull Requests

  • Fork the project.

  • Make your feature addition or bug fix.

  • Add tests for it. This is important so I don’t break it in a future version unintentionally.

  • Commit, do not mess with rakefile, version, or history. (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)

  • Send me a pull request. Bonus points for topic branches.

TODO

  • Arild has pointed out that there may be some logical inconsistencies regarding the beginning_of_workday and end_of workday times not actually being considered inside of the workday. I’d like to make sure that they work as if the beginning_of_workday is included and the end_of_workday is not included, just like the ‘…’ range operator in Ruby.

NOT TODO

  • I spent way too much time in my previous java-programmer life building frameworks that worshipped complexity, always trying to give the developer-user ultimate flexibility at the expense of the ‘surface area’ of the api. Never again - I will sooner limit functionality to 80% so that something stays usable and let people fork.

  • While there have been requests to add ‘business minutes’ and even ‘business seconds’ to this gem, I won’t entertain a pull request with such things. If you find it useful, great. Most users won’t, and they don’t need the baggage.

Copyright © 2010-2016 bokmann. See LICENSE for details.