Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to calculate next, previous business day in Rails?

How to calculate next and previous business days in Rails?

like image 988
Subba Rao Avatar asked Sep 10 '09 17:09

Subba Rao


4 Answers

date = Date.today
date.next_weekday
date.prev_weekday
like image 60
Bill Lipa Avatar answered Nov 04 '22 14:11

Bill Lipa


With the holidays-gem you can also check, if there is a public holiday. If you do so, you must define the region you need. The holidays-gem allows also to use subregions (e.g. us-va...)

An example code with German (de) and US-american (us) holidays.

require 'holidays'
require 'holidays/us'
require 'holidays/de'
require 'holidays/core_extensions/date'
class Date
  include Holidays::CoreExtensions::Date #provide Date#holiday?

  def next_business_day(region=:any)
    skip_weekends_and_holidays(1,region)
  end    

  def previous_business_day(region=:any)
    skip_weekends_and_holidays(-1,region)
  end

  def skip_weekends_and_holidays(inc, region = :any)
    date = self + inc
    while (date.wday == 6 or date.holiday?(region) ) do
      date += inc
    end   
    date
  end
end

Get attention: skip_weekends_and_holidays does not increment business days. If you increment 5 days from a Monday, you end on a Monday (unless this Monday is no holiday). If there was a holiday during the 5 days, there is additional increment.

Some test code:

[
  Date.new(2012,6,8), #Friday
  Date.new(2012,6,10), #Monday
  Date.new(2012,6,9), #Sunday
  Date.new(2012,12,24), #Christmas eve
  Date.new(2012,12,26), #After Christmas 
].each{|t|
  %w{us de}.each{|region|
    puts "====#{region}======"
    puts "Today: #{Date::DAYNAMES[t.wday]} #{Date::MONTHNAMES[t.mon]} #{t.day}"
    nextday = t.next_business_day(region)
    puts "Next B-day: #{Date::MONTHNAMES[nextday.mon]} #{nextday.day} - #{Date::DAYNAMES[nextday.wday]}"
    previousday = t.previous_business_day(region)
    puts "Previous B-day: #{Date::MONTHNAMES[previousday.mon]} #{previousday.day} - #{Date::DAYNAMES[previousday.wday]}"
  }

An extract from result (christmas eve):

====us======
Today: Monday December 24
Next B-day: December 26 - Wednesday
Previous B-day: December 23 - Sunday

Germany has two free days (25+26.12):

====de======
Today: Monday December 24
Next B-day: December 27 - Thursday
Previous B-day: December 23 - Sunday

Update: I made another version to determine multiple business days:

require 'holidays'
require 'holidays/us'
require 'holidays/core_extensions/date'
#~ require 'holidays/de'
class Date
  include Holidays::CoreExtensions::Date #provide Date#holiday?
  def next_business_day(region=:any)
    next_business_days(1,region)
  end    

  def next_business_days(inc, region=:any)
    date = self
    inc.times{
      date = date.next
      while (date.wday == 6 or date.holiday?(region) ) do
        date = date.next
      end
    }
    date
  end    

  def previous_business_day(region=:any)
    previous_business_days(1,region)
  end

  def previous_business_days(inc, region=:any)
    date = self
    inc.times{
      date = date.prev_day
      while (date.wday == 6 or date.holiday?(region) ) do
        date = date.prev_day
      end
    }
    date
  end    


end

My test code:

require 'test/unit'
class BDay_Test < Test::Unit::TestCase
  def test_2012_06_08_us()
    date = Date.new(2012, 6, 8)
    assert_equal( Date.new(2012, 06, 10), date.next_business_day('us'))
    assert_equal( Date.new(2012, 06,  7), date.previous_business_day('us'))

    assert_equal( Date.new(2012, 06, 17), date.next_business_days(7, 'us'))
    assert_equal( Date.new(2012, 05, 31), date.previous_business_day(7, 'us'))
  end
  def test_2012_06_08_de()
    date = Date.new(2012, 6, 8)
    assert_equal( Date.new(2012, 06, 10), date.next_business_day('de'))
    assert_equal( Date.new(2012, 06,  7), date.previous_business_day('de'))

    assert_equal( Date.new(2012, 06, 17), date.next_business_days(7, 'de'))
    assert_equal( Date.new(2012, 05, 31), date.previous_business_day(7, 'de'))
  end
  def test_2012_06_10_us()
    date = Date.new(2012, 6, 10)
    assert_equal( Date.new(2012, 06, 11), date.next_business_day('us'))
    assert_equal( Date.new(2012, 06,  8), date.previous_business_day('us'))

    assert_equal( Date.new(2012, 06, 18), date.next_business_days(7, 'us'))
    assert_equal( Date.new(2012, 06,  1), date.previous_business_day(7, 'us'))
  end
  def test_2012_06_10_de()
    date = Date.new(2012, 6, 10)
    assert_equal( Date.new(2012, 06, 11), date.next_business_day('de'))
    assert_equal( Date.new(2012, 06,  8), date.previous_business_day('de'))

    assert_equal( Date.new(2012, 06, 18), date.next_business_days(7, 'de'))
    assert_equal( Date.new(2012, 06,  1), date.previous_business_day(7, 'de'))
  end
  def test_2012_06_09_us()
    date = Date.new(2012, 6, 9)
    assert_equal( Date.new(2012, 06, 10), date.next_business_day('us'))
    assert_equal( Date.new(2012, 06,  8), date.previous_business_day('us'))

    assert_equal( Date.new(2012, 06, 17), date.next_business_days(7, 'us'))
    assert_equal( Date.new(2012, 06,  1), date.previous_business_day(7, 'us'))
  end
  def test_2012_06_09_de()
    date = Date.new(2012, 6, 9)
    assert_equal( Date.new(2012, 06, 10), date.next_business_day('de'))
    assert_equal( Date.new(2012, 06,  8), date.previous_business_day('de'))

    assert_equal( Date.new(2012, 06, 17), date.next_business_days(7, 'de'))
    assert_equal( Date.new(2012, 06,  1), date.previous_business_day(7, 'de'))
  end
  def test_2012_12_24_us()
    date = Date.new(2012, 12, 24)
    assert_equal( Date.new(2012, 12, 26), date.next_business_day('us'))
    assert_equal( Date.new(2012, 12, 23), date.previous_business_day('us'))

    assert_equal( Date.new(2013, 01,  3), date.next_business_days(7, 'us'))
    assert_equal( Date.new(2012, 12, 16), date.previous_business_day(7, 'us'))
  end
  def test_2012_12_24_de()
    date = Date.new(2012, 12, 24)
    assert_equal( Date.new(2012, 12, 27), date.next_business_day('de'))
    assert_equal( Date.new(2012, 12, 23), date.previous_business_day('de'))

    assert_equal( Date.new(2013, 01,  4), date.next_business_days(7, 'de'))
    assert_equal( Date.new(2012, 12, 16), date.previous_business_day(7, 'de'))
  end
  def test_2012_12_26_us()
    date = Date.new(2012, 12, 26)
    assert_equal( Date.new(2012, 12, 27), date.next_business_day('us'))
    assert_equal( Date.new(2012, 12, 24), date.previous_business_day('us'))

    assert_equal( Date.new(2013, 01,  4), date.next_business_days(7, 'us'))
    assert_equal( Date.new(2012, 12, 17), date.previous_business_day(7, 'us'))
  end
  def test_2012_12_26_de()
    date = Date.new(2012, 12, 26)
    assert_equal( Date.new(2012, 12, 27), date.next_business_day('de'))
    assert_equal( Date.new(2012, 12, 24), date.previous_business_day('de'))

    assert_equal( Date.new(2013, 01,  4), date.next_business_days(7, 'de'))
    assert_equal( Date.new(2012, 12, 17), date.previous_business_day(7, 'de'))
  end

end    

See test_2012_12_24_us() and date.next_business_days(7,... You end in 2013, each holiday in the period is respected.

like image 31
knut Avatar answered Nov 04 '22 15:11

knut


As far as I understand, this is what you are looking for? (tested it)

require 'date'
def next_business_day(date)
  skip_weekends(date, 1)
end    

def previous_business_day(date)
  skip_weekends(date, -1)
end

def skip_weekends(date, inc = 1)
  date += inc
  while date.wday == 0 || date.wday == 6
    date += inc
  end   
  date
end

You can test it as follows:

begin
  t = Date.new(2009,9,11) #Friday, today
  puts "Today: #{Date::DAYNAMES[t.wday]} #{Date::MONTHNAMES[t.mon]} #{t.day}"
  nextday = next_business_day(t)
  puts "Next B-day: #{Date::MONTHNAMES[nextday.mon]} #{nextday.day}"
  previousday = previous_business_day(nextday)
  puts "back to previous: #{Date::MONTHNAMES[previousday.mon]} #{previousday.day}"
  yesterday = previous_business_day(previousday)
  puts "yesterday: #{Date::MONTHNAMES[yesterday.mon]} #{yesterday.day}"  
end  
like image 22
Felix Ogg Avatar answered Nov 04 '22 16:11

Felix Ogg


You may need to calculate business days in the future starting from a Saturday or Sunday. 1 business day after a Monday is the Tuesday, 1 business day from a Sunday should also be Tuesday - the starting weekend day should be ignored. The following achieves this:

class Date

  def business_days_future(inc)
    date = skip_weekend
    inc.times do
      date = date + 1
      date = date.skip_weekend
    end
    date
  end

  # If date is a saturday or sunday, advance to the following monday
  def skip_weekend
    if wday == 0
      self + 1
    elsif wday == 6
      self + 2
    else
      self
    end
  end

end
like image 3
DavidNorth Avatar answered Nov 04 '22 16:11

DavidNorth