Introduction
The attached code contains logic to do two things with respect to calculation of business hours:
- How much business time elapsed (in minutes) between two dates
- What is the next business date and time after x minutes
Background
We had these requirements in our project so we had to start. The first check was to find if others already wrote similar logic (don't reinvent the wheel?) and got many but they were not suited well to our requirements. So this code was born and it is not only my effort but there has been a major contribution by Michael and Kathir, my teammates.
Using the Code
You can look at the complete code by downloading the source code. It has three projects:
- Console - client
- Class library - a tiny library that contains the logic
- UnitTest library - a testing project contains 36 test methods to make sure our code works in any given scenario. This is the place you should add if any scenario has been missed but you want to test (and fix the code if that failed)
The class which does the work is Calculation
. You must pass the Holidays and open hours (office time) to use this class. While calculating the business time, the code considers only the office hours and work days. All others will not be part of the calculation.
The only constructor is as follows:
public Calculation(IEnumerable<datetime> holidays, OpenHours openHours)
{
_holidays = dateListToStringList(holidays);
_openHours = openHours;
}
getElapsedMinutes
This method returns how much business time is left between two dates.
public double getElapsedMinutes(DateTime startDate, DateTime endDate)
{
if (_openHours.StartHour == 0 || _openHours.EndHour == 0)
throw new InvalidOperationException
("Open hours cannot be started with zero hours or ended with zero hours");
int hour = startDate.Hour;
int minute = startDate.Minute;
if (hour == 0 && minute == 0)
{
startDate = DateTime.Parse(string.Format("{0} {1}:{2}",
startDate.ToString(DateFormat), _openHours.StartHour, _openHours.StartMinute));
}
hour = endDate.Hour;
minute = endDate.Minute;
if (hour == 0 && minute == 0)
{
endDate = DateTime.Parse(string.Format("{0} {1}:{2}",
endDate.ToString(DateFormat), _openHours.EndHour, _openHours.EndMinute));
}
startDate = nextOpenDay(startDate);
endDate = prevOpenDay(endDate);
if (startDate > endDate)
return 0;
if (startDate.ToString(DateFormat).Equals(endDate.ToString(DateFormat)))
{
if (!isWorkingDay(startDate))
return 0;
if (startDate.DayOfWeek == DayOfWeek.Saturday || startDate.DayOfWeek == DayOfWeek.Sunday ||
_holidays.Contains(startDate.ToString(DateFormat)))
return 0;
if (isDateBeforeOpenHours(startDate))
{
startDate = getStartOfDay(startDate);
}
if (isDateAfterOpenHours(endDate))
{
endDate = getEndOfDay(endDate);
}
var endminutes = (endDate.Hour * 60) + endDate.Minute;
var startminutes = (startDate.Hour * 60) + startDate.Minute;
return endminutes - startminutes;
}
var endOfDay = getEndOfDay(startDate);
var startOfDay = getStartOfDay(endDate);
var usedMinutesinEndDate = endDate.Subtract(startOfDay).TotalMinutes;
var usedMinutesinStartDate = endOfDay.Subtract(startDate).TotalMinutes;
var tempStartDate = startDate.AddDays(1);
var workingHoursInMinutes = (_openHours.EndHour - _openHours.StartHour) * 60;
var totalUsedMinutes = usedMinutesinEndDate + usedMinutesinStartDate;
for (DateTime day = tempStartDate.Date; day < endDate.Date; day = day.AddDays(1.0))
{
if (isWorkingDay(day))
{
totalUsedMinutes += workingHoursInMinutes;
}
}
return totalUsedMinutes;
}
Testing It
This is tested with 20 test methods (or 20 scenarios) to ensure the code gets the result as expected. The test methods are named as three parts as Roy Osherove suggested in his book The art of Unit Testing. That is:
[methodUnderTest]_[Scenario]_[Expected]
Example:
[TestMethod]
public void getElapsedMinutes_SameDateBut2HoursDiffernt_120()
{
var calculator = new Calculation(new List<DateTime>(), new OpenHours("08:00;16:00"));
var startDate = DateTime.Parse("2015-04-07 08:00");
var endDate = DateTime.Parse("2015-04-07 10:00");
var result = calculator.getElapsedMinutes(startDate, endDate);
Assert.AreEqual(120, result);
}
What we test above is to find how much time has passed from 8 to 10 in the same day. And the result should be 120 minutes.
The scenario:
- No holidays
- Office time is from 08:00 to 16:00
- Start and End date is the same
- Start hour is 08:00
- End hour is 10:00
The complete list of methods is here. You can add any missed scenario by yourself into the code and test.
add
This method returns the next business date and time after x minutes. This is useful in scenarios like finding the deadline of a task.
public DateTime add(DateTime date, int minutes)
{
if (_openHours != null)
{
if (_openHours.StartHour == 0 || _openHours.EndHour == 0)
throw new InvalidOperationException
("Open hours cannot be started with zero hours or ended with zero hours");
date = nextOpenDay(date);
var endOfDay = getEndOfDay(date);
var minutesLeft = (int)endOfDay.Subtract(date).TotalMinutes;
if (minutesLeft < minutes)
{
date = nextOpenDay(endOfDay.AddMinutes(1));
date = nextOpenDay(date);
minutes -= minutesLeft;
}
var workingHoursInMinutes = (_openHours.EndHour - _openHours.StartHour) * 60;
while (minutes > workingHoursInMinutes)
{
date = getStartOfDay(date.AddDays(1));
date = nextOpenDay(date);
minutes -= workingHoursInMinutes;
}
}
return date.AddMinutes(minutes);
}
Testing It
This is tested with 16 test methods (or 16 scenarios) to ensure the code gets the result as expected.
Example:
[TestMethod]
public void add_StartTimeIsSaturday_addFromMonday()
{
var calculator = getEmptyCalculator();
var saturdayStartDate = DateTime.Parse("2013-01-05 10:00");
var result = calculator.add(saturdayStartDate, 60);
Assert.AreEqual(DateTime.Parse("2013-01-07 09:00"), result);
}
What we test above is to find when is the next business date and time after 60 minutes. In this case, we add 60 minutes on Saturday and the expected result is one hour after the office start hour on Monday.
The complete list of methods is here. You can add any missed scenario by yourself into the code and test.
Points of Interest
What I think is the test methods are interesting here because we can know instantly how our code will work in real time and it is easy to fix if anything goes wrong in real time. Just add a test method with that failed scenario and fix the code.
Finally
Please download the source code and explore yourself (you need Visual Studio 12 to open it, but it does work in older versions too - but copy the files manually.)