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)