Calculate business days skipping holidays and weekends

Issue

i have this problem, the flow start with a initial date, let say 2022-04-14 and i have to add to this date ten days, but i have to considerate weekends and holidays, so perhaps if between the initial date and the final date we have one weekend and one holiday the final date it will be 2022-04-27. Also is necessary considerate if the initial date start in a weekend or a holiday.
This the problem.

My first approach is create a loop thats check every day between the initial day plus ten days and for every Saturday, Sunday and holidays sum one day, so i will have ten days plus three, this result will be added to my initial date to finally calculate the final date.

My question is, if there is another solution or implementation thats can be more efficient? cuz this maybe in the future it will be used for a lot of people.

Solution

Also don’t forget when adding the accumulated extra days (being weekends and holidays), those might cover new weekends and holidays, so you have to do this "recursively".

Simplest solution

The simplest solution could start from the initial date, increment it by a day, and check each if it’s a skippable (weekend or holiday) day or not. If not, decrement the number of days, and repeat until you added as many as needed.

This is how it could look like:

func addDays(start time.Time, days int) (end time.Time) {
    for end = start; days > 0; {
        end = end.AddDate(0, 0, 1)
        if !skippable(end) {
            days--
        }
    }
    return end
}

func skippable(day time.Time) bool {
    if wd := day.Weekday(); wd == time.Saturday || wd == time.Sunday {
        return true
    }
    if isHoliday(day) {
        return true
    }
    return false
}

func isHoliday(day time.Time) bool {
    return false // TODO
}

Testing it:

d := time.Date(2022, time.April, 14, 0, 0, 0, 0, time.UTC)
fmt.Println(addDays(d, 0))
fmt.Println(addDays(d, 1))
fmt.Println(addDays(d, 10))

Which outputs (try it on the Go Playground):

2022-04-14 00:00:00 +0000 UTC
2022-04-15 00:00:00 +0000 UTC
2022-04-28 00:00:00 +0000 UTC

Faster solution

A faster solution can avoid the loop to step day by day.

Calculating weekend days: Knowing what day the initial date is, and knowing how many days you want to step, we can calculate the number of weekend days in between. E.g. if we have to step 14 days, that’s 2 full weeks, that surely includes exactly 4 weekend days. If we have to step a little more, e.g. 16 days, that also includes 2 full weeks (4 weekend days), and optionally 1 or 2 more days which we can easily check.

Calculating holidays: We may use a trick to list the holidays in a sorted slice (sorted by date), so we can easily / quickly find the number of days between 2 dates. We can binary search in a sorted slice for the start and end date of some period, and the number of holidays in a period is the number of elements between these 2 indices. Note: holidays falling on weekends must not be included in this slice (else they would be accounted twice).

Let’s see how this implementation looks like:

// holidays is a sorted list of holidays
var holidays = []time.Time{
    time.Date(2022, time.April, 15, 0, 0, 0, 0, time.UTC),
}

func addDaysFast(start time.Time, days int) (end time.Time) {
    weekendDays := days / 7 * 2 // Full weeks
    // Account for weekends if there's fraction week:
    for day, fraction := start.AddDate(0, 0, 1), days%7; fraction > 0; day, fraction = day.AddDate(0, 0, 1), fraction-1 {
        if wd := day.Weekday(); wd == time.Saturday || wd == time.Sunday {
            weekendDays++
        }
    }

    end = start.AddDate(0, 0, days+weekendDays)

    first := sort.Search(len(holidays), func(i int) bool {
        return !holidays[i].Before(start)
    })
    last := sort.Search(len(holidays), func(i int) bool {
        return !holidays[i].Before(end)
    })

    // There are last - first holidays  in the range [start..end]
    numHolidays := last - first
    if last < len(holidays) && holidays[last].Equal(end) {
        numHolidays++ // end is exactly a holiday
    }

    if numHolidays == 0 {
        return end // We're done
    }

    // We have to add numHolidays, using the same "rules" above:
    return addDaysFast(end, numHolidays)
}

Testing it:

d := time.Date(2022, time.April, 14, 0, 0, 0, 0, time.UTC)
fmt.Println(addDaysFast(d, 0))
fmt.Println(addDaysFast(d, 1))
fmt.Println(addDaysFast(d, 10))

Output (try it on the Go Playground):

2022-04-14 00:00:00 +0000 UTC
2022-04-18 00:00:00 +0000 UTC
2022-04-29 00:00:00 +0000 UTC

Improving addDaysFast()

There are still ways to improve addDaysFast():

  • the initial loop to check for weekend days in the fraction week could be substituted with an arithmetic calculation (see example)
  • the recursion could be substituted with an iterative solution
  • an alternative solution could list weekend days as holidays, so the first part to calculate weekend days could be eliminated (duplicates must not be included)

Answered By – icza

Answer Checked By – Candace Johnson (GoLangFix Volunteer)

Leave a Reply

Your email address will not be published.