1

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.

icza
  • 389,944
  • 63
  • 907
  • 827
Mtac
  • 61
  • 5
  • I don't see any other way to do this. Although depending on how you implement it there could be a huge performance differences. – TheHippo Apr 25 '22 at 18:48

2 Answers2

2

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)
icza
  • 389,944
  • 63
  • 907
  • 827
0

I would compute business days something like this. Exclusive of the holiday lookup, it has no loops and has O(1) time and space complexity:

  • Compute the whole number of days between the start and end date of the range

  • Divide that by 7. The quotient is the whole number of weeks in the range; the remainder is the fractional week left over, in whole days.

  • The base number of business days in the range is the whole number of weeks in the range multiplied by 5, as every 7 day period, regardless of when it started, contains 2 weekend days.

  • The fractional week at the end, in whole days, must be adjusted to remove any weekend days. That is a function of the whole number of days in the fractional week and the day of the week of the ending date of the range.

  • Holiday lookup is left as an exercise for the reader, as that is far too culture-, locale-, and business-dependent to address. One should note that there is an in-built assumption in the logic here that "holidays" do not occur on Saturday or Sunday.

func BusinessDays(start time.Time, end time.Time) int {
    from := toDate(start)
    thru := toDate(end)
    weeks, days := delta(from, thru)

    adjustedDays := adjustDays(days, thru.Weekday())

    businessDays := ( ( weeks * 5) + adjustedDays ) - holidaysInRange(from, thru)

    return businessDays

}

func toDate(t time.Time) time.Time {
    y, m, d := t.Date()
    adjusted := time.Date(y, m, d, 0, 0, 0, 0, t.Location())
    return adjusted
}

func holidaysInRange(from, thru time.Time) (cnt int) {
    // TODO: Actual implementation left as an exercise for the reader
    return cnt
}

func delta(from, thru time.Time) (weeks, days int) {
    const seconds_per_day = 86400
    totalDays := (thru.Unix() - from.Unix()) / seconds_per_day

    weeks = int(totalDays / 7)
    days = int(totalDays % 7)
    return weeks, days
}

func adjustDays(days int, lastDay time.Weekday) int {
    adjusted := days
    switch days {
    case 1:
        switch lastDay {
        case time.Saturday:
        case time.Sunday:
            adjusted -= 1
        }
    case 2:
        switch lastDay {
        case time.Sunday:
            adjusted -= 2
        case time.Saturday:
        case time.Monday:
            adjusted -= 1
        }
    case 3:
        switch lastDay {
        case time.Sunday:
        case time.Monday:
            adjusted -= 2
        case time.Tuesday:
        case time.Saturday:
            adjusted -= 1
        }
    case 4:
        switch lastDay {
        case time.Sunday:
        case time.Monday:
        case time.Tuesday:
            adjusted -= 2
        case time.Wednesday:
        case time.Saturday:
            adjusted -= 1
        }
    case 5:
        switch lastDay {
        case time.Sunday:
        case time.Monday:
        case time.Tuesday:
        case time.Wednesday:
            adjusted -= 2
        case time.Thursday:
        case time.Saturday:
            adjusted -= 1
        }
    case 6:
        switch lastDay {
        case time.Sunday:
        case time.Monday:
        case time.Tuesday:
        case time.Wednesday:
        case time.Thursday:
            adjusted -= 2
        case time.Friday:
        case time.Saturday:
            adjusted -= 1
        }
    }

    return adjusted
}
Nicholas Carey
  • 71,308
  • 16
  • 93
  • 135