工作日计算算法优化






3.90/5 (16投票s)
2005年6月10日
5分钟阅读

190419

1299
实现了工作日和年龄计算的库,并且还实现了C#中的DateDiff函数。
引言
我的队友问我,如何计算两个给定日期之间的工作日天数?我的第一反应是,从开始日期循环到结束日期。但过了一段时间,我意识到这个解决方案并不优化。所以我开始开发我自己的优化算法。
在本文中,我们将讨论该算法,并查看其在SQL Server 2000上的实现以及一些C#代码。
让我们先从算法开始。以下是计算中涉及的关键因素。
因素
- 每周有多少个工作日?
这取决于公司/国家政策。在美国,通常是每周5天,而在印度和许多其他国家,许多公司每周工作6天。我们的算法需要考虑到这一点。
- 有多少个假期?
第二个因素是指定期间内的假期数量。这同样取决于公司政策。我们可以将这些详细信息保存在一个表中。这也很简单。
基本算法
- 将时间跨度以周为单位计算。称之为W。
- 从周数中减去第一周。W = W - 1
- 将周数乘以每周的工作日数。称之为D。
- 找出指定时间段内的假期。称之为H。
- 计算第一周的天数。称之为SD。
- 计算最后一天的天数。称之为ED。
- 将所有天数相加。BD = D + SD + ED – H。
很简单吧…!!!
我们知道每周的工作日数,并且由于模式每周重复,所以我们首先计算完整的周数。我们应该从周数中减去一。一旦我们得到周数,我们就可以乘以工作日数,从而得到一个粗略的工作日数。
好的,我们得到了一个粗略的工作日数,例如D。我称之为粗略数字,因为它不是精确的答案,因为我们在第一步中减去了一周。
现在,我们将找出开始周有多少天。例如,如果开始日期是星期三,那么开始周的天数是3天(如果每周工作日数为5天)或4天(如果每周工作日数为6天)。
同样,找出最后一周有多少天。最后,找出在此期间有多少个假期。这将是对Holiday表的简单聚合查询。
一旦我们掌握了所有数字,根据上述表达式,我们就可以得出我们想要的答案。希望这能消除您的所有疑虑。所以让我们开始实现这个算法。
首先,我们将尝试使用T-SQL在SQL Server上实现它。
SQL Server 实现
SQL Server 有许多日期函数,其中两个是DATEDIFF
和DATEPART
。在这次实现中它们会很有用。
您可以将此算法实现为过程或函数。首选的是函数,但在这里我将把它实现为一个过程,以便我们可以使用print
命令。一切就绪后,您可以通过删除print
命令并使用return
将其转换为过程。
解决方案
Create procedure SpBusinessDays (@dtStartDate datetime, @dtEndDate datetime,
@indDaysInWeek int)
as
begin
declare
@intWeeks int
,@indDays int
,@intSdays int
,@intEdays int
-- Find the number of weeks between the dates. Subtract 1
-- since we do not want to count the current week.
select @intWeeks = datediff( week, @dtStartDate, @dtEndDate) - 1
print 'week'
print @intWeeks
-- calculate the number of days in these compelete weeks.
select @indDays = @intWeeks * @indDaysInWeek
print 'Est. Days'
print @indDays
-- Get the number of days in the starting week.
if @indDaysInWeek = 5
-- If Saturday, Sunday is holiday
if datepart( dw, @dtStartDate) = 7
select @intSdays = 7 - datepart( dw, @dtStartDate)
else
select @intSdays = 7 - datepart( dw, @dtStartDate) - 1
else
-- If Sunday is only <st1:place>Holiday</st1:place>
select @intSdays = 7 - datepart( dw, @dtStartDate)
print 'Starting Days'
print @intSdays
-- Calculate the days in the last week.
if @indDaysInWeek = 5
if datepart( dw, @dtEndDate) = 7
select @intEdays = datepart( dw, @dtEndDate) - 2
else
select @intEdays = datepart( dw, @dtEndDate) - 1
else
select @intEdays = datepart( dw, @dtEndDate) - 1
print 'End Days'
print @intEdays
-- Sum everything together.
select @indDays = @indDays + @intSdays + @intEdays
print 'Ans'
print @indDays
end
注意:开始日期是排他的。
如果注意到这里,如果工作日数为6,我们无需担心任何事情,只需计算天数即可。如果工作日数为5,那么我们必须处理星期六。
就是这样。很简单。
C# 实现
好的,但是,如果您在表示层上进行计算,存储过程或“函数”将无法工作。因此,现在我们将在C#中实现相同的算法。
这是C#中的实现
/// <summary>
/// Calulates Business Days within the given range of days.
/// Start date and End date inclusive.
/// </summary>
/// <param name="startDate">Datetime object
/// containing Starting Date</param>
/// <param name="EndDate">Datetime object containing
/// End Date</param>
/// <param name="NoOfDayWeek">integer denoting No of Business
/// Day in a week</param>
/// <param name="DayType"> DayType=0 for Business Day and
/// DayType=1 for WeekEnds </param>
/// <returns></returns>
public static double CalculateBDay(
DateTime startDate,
DateTime EndDate,
int NoOfDayWeek, /* No of Working Day per week*/
int DayType
)
{
double iWeek, iDays, isDays, ieDays;
//* Find the number of weeks between the dates. Subtract 1 */
// since we do not want to count the current week. * /
iWeek =DateDiff("ww",startDate,EndDate)-1 ;
iDays = iWeek * NoOfDayWeek;
//
if( NoOfDayWeek == 5)
{
//-- If Saturday, Sunday is holiday
if ( startDate.DayOfWeek == DayOfWeek.Saturday )
isDays = 7 -(int) startDate.DayOfWeek;
else
isDays = 7 - (int)startDate.DayOfWeek - 1;
}
else
{
//-- If Sunday is only <st1:place>Holiday</st1:place>
isDays = 7 - (int)startDate.DayOfWeek;
}
//-- Calculate the days in the last week. These are not included in the
//-- week calculation. Since we are starting with the end date, we only
//-- remove the Sunday (datepart=1) from the number of days. If the end
//-- date is Saturday, correct for this.
if( NoOfDayWeek == 5)
{
if( EndDate.DayOfWeek == DayOfWeek.Saturday )
ieDays = (int)EndDate.DayOfWeek - 2;
else
ieDays = (int)EndDate.DayOfWeek - 1;
}
else
{
ieDays = (int)EndDate.DayOfWeek - 1 ;
}
//-- Sum everything together.
iDays = iDays + isDays + ieDays;
if(DayType ==0)
return iDays;
else
return T.Days - iDays;
}
附属产品
DateDiff
函数在我处理这个问题时,我还遇到了C#中
DateTime
函数的另一个问题。我了解到C#没有VB.NET中存在的DateDiff
这样的重要函数。所以我把它包含在同一个库中。正如Tim McCurdy所说,我们可以将Microsoft.VisualBasic.dll添加到我们的项目中,并使用VB团队实现的DateDiff
函数,但我注意到许多人不喜欢将C#代码与VB.NET代码混合的想法,尽管在技术上这是完全可以的。计算周、月或年的第二个问题是,它们并不简单。您不能通过TimeSpan.Totaldays
/ 7 来获得周数。规则规定,周数等于给定期间内您跨越周边界的次数。为了解决这个问题,我添加了一个名为GetWeeks
的新函数。/// <summary> /// Calculate weeks between starting date and ending date /// </summary> /// <param name="stdate"></param> /// <param name="eddate"></param> /// <returns></returns> public static int GetWeeks(DateTime stdate, DateTime eddate ) { TimeSpan t= eddate - stdate; int iDays; if( t.Days < 7) { if(stdate.DayOfWeek > eddate.DayOfWeek) return 1; //It is accross the week else return 0; // same week } else { iDays = t.Days -7 +(int) stdate.DayOfWeek ; int i=0; int k=0; for(i=1;k<iDays ;i++) { k+=7; } if(i>1 && eddate.DayOfWeek != DayOfWeek.Sunday ) i-=1; return i; } } /// <summary> /// Mimic the Implementation of DateDiff function of VB.Net. /// Note : Number of Year/Month is calculated /// as how many times you have crossed the boundry. /// e.g. if you say starting date is 29/01/2005 /// and 01/02/2005 the year will be 0,month will be 1. /// /// </summary> /// <param name="datePart">specifies on which part /// of the date to calculate the difference </param> /// <param name="startDate">Datetime object containing /// the beginning date for the calculation</param> /// <param name="endDate">Datetime object containing /// the ending date for the calculation</param> /// <returns></returns> public static double DateDiff(string datePart, DateTime startDate, DateTime endDate) { //Get the difference in terms of TimeSpan TimeSpan T; T = endDate - startDate; //Get the difference in terms of Month and Year. int sMonth, eMonth, sYear, eYear; sMonth = startDate.Month; eMonth = endDate.Month; sYear = startDate.Year; eYear = endDate.Year; double Months,Years=0; Months = eMonth - sMonth; Years = eYear - sYear; Months = Months + ( Years*12); switch(datePart.ToUpper()) { case "WW": case "DW": return (double)GetWeeks(startDate,endDate); case "MM": return Months; case "YY": case "YYYY": return Years; case "QQ": case "QQQQ": //Difference in Terms of Quater return Math.Ceiling((double)T.Days/90.0); case "MI": case "N": return T.TotalMinutes ; case "HH": return T.TotalHours ; case "SS": return T.TotalSeconds; case "MS": return T.TotalMilliseconds; case "DD": default: return T.Days; } }
- 年龄计算
与工作日相比,这是一个简单的计算。我添加了一个函数,可以计算指定日期的年龄,以年、月和日为单位。
/// <summary> /// Calculate Age on given date. /// Calculates as Years, Months and Days. /// </summary> /// <param name="DOB">Datetime object /// containing DOB value</param> /// <param name="OnDate">Datetime object containing given /// date, for which we need to calculate the age</param> /// <returns></returns> public static string Age(DateTime DOB, DateTime OnDate) { //Get the difference in terms of Month and Year. int sMonth, eMonth, sYear, eYear; double Months, Years; sMonth = DOB.Month; eMonth = OnDate.Month; sYear = DOB.Year; eYear = OnDate.Year; // calculate Year if( eMonth >= sMonth) Years = eYear - sYear; else Years = eYear - sYear -1; //calculate Months if( eMonth >= sMonth) Months = eMonth - sMonth; else if ( OnDate.Day > DOB.Day) Months = (12-sMonth)+eMonth-1; else Months = (12-sMonth)+eMonth-2; double tDays=0; //calculate Days if( eMonth != sMonth && OnDate.Day != DOB.Day ) { if(OnDate.Day > DOB.Day) tDays = DateTime.DaysInMonth(OnDate.Year, OnDate.Month) - DOB.Day; else tDays = DateTime.DaysInMonth(OnDate.Year, OnDate.Month-1) - DOB.Day + OnDate.Day ; } string strAge = Years+"/"+Months+"/"+tDays; return strAge; }
摘要
如果您注意到算法,我也提到了假期,但我没有在上述任何代码中实现它,但可以通过一个简单的查询来处理。所以我把它留给您。如果您喜欢这段代码,或者它在您的项目中很有用,请告诉我。您可以通过Gaurang.Desai@gmail.com给我写信。
历史
- 修正了C#代码中给定期间的周数计算的bug。2005年8月17日。
- 2005年6月10日首次发布。