views:

80

answers:

3

So, I am trying to calculate the time between two dates that fits certain criteria (here: work / non-work) and I'm confused about the results as I can't find out why it's wrong.

But first, some code;

**Input Date A:** 2009-01-01 2:00 pm
**Input Date B:** 2009-01-02 9:00 am

So, as you can see, the total timespan (calculated e.g. by DateB.Substract(DateA)) is 19 hours.

I now want to calculate how many hours in this timespan are "non-work" hours, based on an average work day from 8am to 5pm - Result should be 15 hours (So, 19 - 15 = 4 hours total work time) (2:00 pm to 5:00 pm plus 8:00 am to 9:00 am)

But, following my code

DateTime _Temp = new DateTime(2009, 1, 1, 14, 0, 0);
DateTime _End = new DateTime(2009, 1, 2, 9, 0, 0);

int _WorkDayStart = 8;
int _WorkDayEnd = 17;

int _Return = 0;

while (_End > _Temp)
{
    if (_Temp.Hour <= _WorkDayStart || _Temp.Hour >= _WorkDayEnd)
    _Return++;
    _Temp = _Temp.AddHours(1);
}

the result is 16 hours (19 - 16 = 3 hours total work time) - I don't see where the mistake is, so there is one hour missing?! I refactored it on paper and it should work as intended... but doesn't :-/

Anyone sees the mistake?

+3  A: 

You're counting both ends as non-work, instead of just one. This line:

if (_Temp.Hour <= _WorkDayStart || _Temp.Hour >= _WorkDayEnd)

should probably be:

if (_Temp.Hour < _WorkDayStart || _Temp.Hour >= _WorkDayEnd)

You're effectively stepping through "start hours". So 8am itself should count as a work hour, because it's the start of a work hour, but 5pm won't because it's the start of a non-work hour.

I would also strongly advise you to either put braces around the body of the if statement or at least indent the following line. (I'd further advise you to use camelCase for local variables, but that's just a convention thing - I've never seen C# code written with that convention for local variables before now. You may want to read Microsoft's naming conventions document - it doesn't specify local variables, but they;re generally in camel case.)

Finally, I personally find it easier to read conditions where the "thing that's changing" is on the left - so I'd change your loop condition to:

while (_Temp < _End)

An alternative is to change it into a for loop. With all of these changes, the code would be:

DateTime start = new DateTime(2009, 1, 1, 14, 0, 0);
DateTime end = new DateTime(2009, 1, 2, 9, 0, 0);

int workDayStart = 8;
int workDayEnd = 17;

int nonWorkHours = 0;

for (DateTime time = start; time < end; time = time.AddHours(1))
{
    if (time.Hour < workDayStart || time.Hour >= workDayEnd)
    {
        nonWorkHours++;
    }
}

Finally, extract this into a method:

public static int CountNonWorkHours(DateTime start, DateTime end,
                                    int workDayStart, int workDayEnd)
{
    int nonWorkHours = 0;

    for (DateTime time = start; time < end; time = time.AddHours(1))
    {
        if (time.Hour < workDayStart || time.Hour >= workDayEnd)
        {
            nonWorkHours++;
        }
    }
    return nonWorkHours;
}

EDIT: Regarding konamiman's suggestion... yes, looping over each hour is very inefficient. However, it's relatively easy to get right. Unless you're going to be doing this a lot with long time periods, I'd use this fairly simple code. It would be very easy to end up with off-by-one errors in various situations if you tried to do a per-day version. While I don't like inefficient code, I don't mind it if it's not hurting me :)

Jon Skeet
Ok, makes sense... The following line is indented, just forgot it to do here, no worries ;-) Thing about naming is I somehow created my own by now as I didn't come across a good naming convenction for C# by now - as e.g. PCS (Pear Coding Standard) for PHP... But I'm willing to learn :)
ApoY2k
Ok, Jon, 121k not enough? Can't you let other users take some rep on the easy ones? :) Don't know how you do it, but you're amazing. You're faster than Chuck Norris.
tzup
Thanks for the long answer. But, you mentioned what I also thought the whole time along; this method will be used quite often - the date ranges are not that long, only in some single cases they are over several days or even weeks... But I can't see any chance how to avoid a loop as I need the result to be exact on hours :-/
ApoY2k
@ApoY2k: "Quite often" and "even weeks" need to be more precise really - a week is only 168 hours, which is hardly a long loop :) I suggest you benchmark this with real data to find out whether you need to be more efficient or not.
Jon Skeet
Okay, "even" probably was exaggerated ;) In a normal case, this method is called about 80 times, each with a date range of 1-3 days... I guess the loop should do
ApoY2k
@ApoY2k: I suspect you wouldn't even be able to time that amount of activity accurately ;)
Jon Skeet
+1  A: 

If you plan to reuse this code, I would refactor it to avoid the loop. You could just multiply the number of whole days by the labour hours per day, then treat the first and the last day of the interval as special cases.

Konamiman
No, that will loop round once too many times - it would count both the start and the end. Consider a period of 5-6, which is a single hour - you only want to go through the loop once.
Jon Skeet
Ok, then I will edit to keep just the other suggestion.
Konamiman
I've commented on your suggestion in my answer - basically it's the right approach if the code needs to be efficient, but it would take a lot longer to make sure it's right in all cases. (At least, it would for me.) It's really easy to miss corner cases in something like this, whereas the "loop over the hours" is inefficient but fairly simple. (Mind you, it will only work when the start/end times and work start/stop times are on the hour of course!)
Jon Skeet
A: 

You could also use this to avoid a loop

DateTime startDate = new DateTime(2009, 1, 1, 14, 0, 0);
            DateTime endDate = new DateTime(2009, 1, 2, 9, 0, 0);
            int startTime = 8;
            int endTime = 17;
            int ret =   ((endDate.Subtract(startDate).Days - 1) * 8) 
                        + (startDate.Hour >= startTime && startDate.Hour < endTime ? endTime - startDate.Hour : 0)
                        + (endDate.Hour > startTime && endDate.Hour <= endTime ? endDate.Hour - startTime : 0);
astander