计算跳过假期和周末的工作日
Calculate business days skipping holidays and weekends
我有这个问题,流程从初始日期开始,假设是 2022-04-14,我必须将此日期添加十天,但我必须考虑周末和假期,所以也许如果介于初始日期和最终日期我们有一个周末和一个假期最终日期是 2022-04-27。如果初次约会是在周末或节假日开始的,也是必要的考虑。
这就是问题。
我的第一个方法是创建一个循环,每天检查初始日加十天和每个星期六、星期日和节假日总和一天,所以我将有十天加三天,这个结果将被添加到我的初始日期最终计算出最终日期。
我的问题是,是否有其他解决方案或实施方式可以更有效?因为这可能在未来会被很多人使用。
另外不要忘记在添加累积的额外天数(周末和节假日)时,这些天数可能会涵盖新的周末和节假日,因此您必须“递归”执行此操作。
最简单的解决方案
最简单的解决方案可以从初始日期开始,增加一天,然后检查每一天是否是可跳过的(周末或节假日)。如果不是,请减少天数,然后重复直到添加所需的天数。
这是它的样子:
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 天,我们可以轻松查看。
计算假期:我们可能会使用一个技巧在排序的切片中列出假期(按日期排序),因此我们可以轻松/快速地找到 2 之间的天数日期。我们可以在一个排序的切片中对某个时间段的开始和结束日期进行二分查找,一个时间段中的假期数就是这两个索引之间的元素数。注意:周末节假日不得包含在此切片中(否则它们将被计算两次)。
让我们看看这个实现的样子:
// 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)
}
正在测试:
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()
:
- 检查小数周中周末天数的初始循环可以用算术计算代替(参见 example)
- 递归可以用迭代解决方案代替
- 另一种解决方案可以将周末列为假期,因此可以删除计算周末天数的第一部分(不得包含重复项)
我会像这样计算工作日。不包括假期查找,它没有循环并且具有 O(1) 时间和 space 复杂度:
计算范围的开始日期和结束日期之间的天数
将其除以 7。商是范围内的周数;余数是剩余的小数周,以整天计算。
范围内的工作日基数是范围内的周数乘以 5,因为每 7 天的时间段,无论何时开始,都包含 2 个周末。
最后的小数周,在整天中,必须调整以删除任何周末。这是小数周中的天数和范围结束日期是星期几的函数。
假期查找留作 reader 的练习,因为这太过文化、区域设置和 business-dependent 无法解决。应该注意这里的逻辑中有一个 in-built 假设,即“假期”不会出现在周六或周日。
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
}
我有这个问题,流程从初始日期开始,假设是 2022-04-14,我必须将此日期添加十天,但我必须考虑周末和假期,所以也许如果介于初始日期和最终日期我们有一个周末和一个假期最终日期是 2022-04-27。如果初次约会是在周末或节假日开始的,也是必要的考虑。
这就是问题。
我的第一个方法是创建一个循环,每天检查初始日加十天和每个星期六、星期日和节假日总和一天,所以我将有十天加三天,这个结果将被添加到我的初始日期最终计算出最终日期。
我的问题是,是否有其他解决方案或实施方式可以更有效?因为这可能在未来会被很多人使用。
另外不要忘记在添加累积的额外天数(周末和节假日)时,这些天数可能会涵盖新的周末和节假日,因此您必须“递归”执行此操作。
最简单的解决方案
最简单的解决方案可以从初始日期开始,增加一天,然后检查每一天是否是可跳过的(周末或节假日)。如果不是,请减少天数,然后重复直到添加所需的天数。
这是它的样子:
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 天,我们可以轻松查看。
计算假期:我们可能会使用一个技巧在排序的切片中列出假期(按日期排序),因此我们可以轻松/快速地找到 2 之间的天数日期。我们可以在一个排序的切片中对某个时间段的开始和结束日期进行二分查找,一个时间段中的假期数就是这两个索引之间的元素数。注意:周末节假日不得包含在此切片中(否则它们将被计算两次)。
让我们看看这个实现的样子:
// 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)
}
正在测试:
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()
:
- 检查小数周中周末天数的初始循环可以用算术计算代替(参见 example)
- 递归可以用迭代解决方案代替
- 另一种解决方案可以将周末列为假期,因此可以删除计算周末天数的第一部分(不得包含重复项)
我会像这样计算工作日。不包括假期查找,它没有循环并且具有 O(1) 时间和 space 复杂度:
计算范围的开始日期和结束日期之间的天数
将其除以 7。商是范围内的周数;余数是剩余的小数周,以整天计算。
范围内的工作日基数是范围内的周数乘以 5,因为每 7 天的时间段,无论何时开始,都包含 2 个周末。
最后的小数周,在整天中,必须调整以删除任何周末。这是小数周中的天数和范围结束日期是星期几的函数。
假期查找留作 reader 的练习,因为这太过文化、区域设置和 business-dependent 无法解决。应该注意这里的逻辑中有一个 in-built 假设,即“假期”不会出现在周六或周日。
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
}