英文:
Calculate business days skipping holidays and weekends
问题
我有一个问题,流程从一个初始日期开始,假设是2022年4月14日,我需要在这个日期上加上十天,但是我需要考虑周末和假期,所以如果在初始日期和最终日期之间有一个周末和一个假期,最终日期将是2022年4月27日。还需要考虑初始日期是否是周末或假期。
我的第一个方法是创建一个循环,检查从初始日期加上十天的每一天,对于每个星期六、星期日和假期,加一天,这样我将有十天加上三天,将这个结果加到我的初始日期上,最终计算出最终日期。
我的问题是,是否有其他解决方案或实现方式可以更高效?因为将来可能会有很多人使用这个方法。
英文:
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.
答案1
得分: 2
此外,不要忘记在添加累积的额外天数(即周末和假期)时,这些天数可能会覆盖新的周末和假期,因此您必须进行“递归”处理。
最简单的解决方案
最简单的解决方案可以从初始日期开始,逐天增加,并检查每一天是否是可跳过的(周末或假期)天还是非可跳过的天。如果不可跳过,则减少天数,并重复此过程,直到添加所需的天数为止。
代码如下:
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
}
测试代码如下:
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))
输出结果如下(在 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
更快的解决方案
更快的解决方案可以避免逐天增加的循环。
计算周末天数: 知道初始日期是星期几,以及要增加的天数,我们可以计算中间的周末天数。例如,如果我们要增加14天,那就是2个完整的星期,其中肯定包括4个周末天。如果我们要增加更多,例如16天,那也包括2个完整的星期(4个周末天),以及1或2天,我们可以轻松检查。
计算假期: 我们可以使用一个技巧,在一个按日期排序的切片中列出假期,这样我们可以轻松/快速地找到两个日期之间的天数。我们可以在排序的切片中进行二分搜索,以找到某个时间段的起始日期和结束日期,并且在这两个索引之间的元素数量就是该时间段内的假期天数。注意:落在周末的假期不应包含在此切片中(否则它们将被计算两次)。
让我们看看这个实现的样子:
// 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 // 完整的星期
// 如果有不完整的星期,考虑周末天数:
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)
})
// 在范围 [start..end] 内有 last - first 个假期
numHolidays := last - first
if last < len(holidays) && holidays[last].Equal(end) {
numHolidays++ // end 正好是一个假期
}
if numHolidays == 0 {
return end // 完成
}
// 我们需要添加 numHolidays,使用相同的“规则”:
return addDaysFast(end, numHolidays)
}
测试代码如下:
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))
输出结果如下(在 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
改进 addDaysFast()
还有一些方法可以改进 addDaysFast()
:
-
用于检查不完整星期的初始循环可以用算术计算来替代(参见示例)
-
递归可以用迭代解决方案来替代
-
另一种解决方案可以将周末天数列为假期,这样可以消除计算周末天数的第一部分(不能包含重复项)
英文:
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)
答案2
得分: 0
我会像这样计算工作日。不包括假期查询,它没有循环,并且具有O(1)的时间和空间复杂度:
-
计算范围开始日期和结束日期之间的整数天数。
-
将其除以7。商是范围内的整数周数;余数是剩余的整数天数。
-
范围内的工作日基数是范围内的整数周数乘以5,因为每个7天周期,无论何时开始,都包含2个周末日。
-
最后的剩余整数天数必须调整以去除任何周末日。这取决于剩余整数天数和范围结束日期的星期几。
-
假期查询留给读者作为练习,因为这太依赖于文化、地区和业务,无法解决。应该注意的是,这里的逻辑中有一个内置的假设,即“假期”不会发生在星期六或星期日。
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
}
英文:
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
}
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论