views:

405

answers:

9

Hi All,

I need to determine duration between two DateTimes in minutes.

However, there is a slight twist:

  • exclude weekends
  • only count minutes which are between 7:00AM and 7:00PM. for example: [09/30/2010 6:39:00 PM] - [09/30/2010 7:39:00 PM] = 21 Minutes

I'm just having a hard time coming up with a decent way to do it and would appreciate if anyone can suggest something.

Thanks.


Edit:

I ended up going with dtb's solution. There is only one special case which needed to be taken care of: if end time is after 7:00PM, count the minutes from 7:00AM to the actual end time.

This is how I modified it:

var minutes = from day in start.DaysInRangeUntil(end)
                where !day.IsWeekendDay()
                let st = Helpers.Max(day.AddHours(7), start)
                let en = (day.DayOfYear == end.DayOfYear ? 
                            end :
                            Helpers.Min(day.AddHours(19), end)
                            )
                select (en - st).TotalMinutes;

Again, thanks for the help.

+5  A: 

Take your start time, get the amount of minutes to the end of that day (Ie the 7pm).

Then from the 7am the next day, count the amount of days to the final day (excluding any time into the end day).

Calculate how many (If any) weekends have passed. (For every weekends reduce the count of days by 2).

Do some simple math from there to get the total minutes for the count of days.

Add the extra time on the final day and the start days extra time.

Psytronic
I agree with this one - it's not a simple one-step calculation, but it's clean, and the code should be pretty readable.
Joe Enos
I'd whip up some code but a) That ruins the fun for the OP, and b) My VS is on the fritz.
Psytronic
+16  A: 

You can, of course, use LINQ:

DateTime a = new DateTime(2010, 10, 30, 21, 58, 29);
DateTime b = a + new TimeSpan(12, 5, 54, 24, 623);

var minutes = from day in a.DaysInRangeUntil(b)
              where !day.IsWeekendDay()
              let start = Max(day.AddHours( 7), a)
              let end   = Min(day.AddHours(19), b)
              select (end - start).TotalMinutes;

var result = minutes.Sum();

// result == 6292.89

(Note: You probably need to check for a lot of corner cases which I completely ignored.)

Helper methods:

static IEnumerable<DateTime> DaysInRangeUntil(this DateTime start, DateTime end)
{
    return Enumerable.Range(0, 1 + (int)(end.Date - start.Date).TotalDays)
                     .Select(dt => start.Date.AddDays(dt));
}

static bool IsWeekendDay(this DateTime dt)
{
    return dt.DayOfWeek == DayOfWeek.Saturday
        || dt.DayOfWeek == DayOfWeek.Sunday;
}

static DateTime Max(DateTime a, DateTime b)
{
    return new DateTime(Math.Max(a.Ticks, b.Ticks));
}

static DateTime Min(DateTime a, DateTime b)
{
    return new DateTime(Math.Min(a.Ticks, b.Ticks));
}
dtb
+1, I still cringe a little bit when I look at this code but i applaud the creative use of LINQ.
JaredPar
Might I suggest an `IsWeekend` extension?
Ron Warholic
First, I would like to say this is indeed a very compact way to handle this problem. LINQ is pretty slick. My issue is that you are iterating over every single day between start and end. If this was a 5 year period, you would be performing a lot of unnecessary calculation. Don't get me wrong, today's computers are super powerful and could handle something like this easily. But it is easy to compute total hours in a 7 day period => 5 * 12 = 60. Computing the remaining part is tricky, but will never require iterating over more than 7 days.
dana
@dana: It's always easier to optimize a correct solution than to correct an optimized solution. And I'm not even sure if my comparatively readable solution is correct in its current form (corner cases, time zones, DST, etc.)
dtb
@dtb, true but optimising from iterating to straight math is generally such a rewrite that you lose any prove of correctness you made along the way. If iterating is going to be fine long-term then fine, also if you needed a "just works damnit!" here while you dealt with frying bigger fish, but if you were already thinking it was likely to need optimising then this is one case where I'd go straight to the alternative and not consider it premature.
Jon Hanna
to address dana and Jon, my data does not span over long periods of time. At the most, the difference between start and end times is a week. 95% of the times, it is 2-3 days.
nsr81
+4  A: 

Try the following DiffRange function.

public static DateTime DayStart(DateTime date)
{
    return date.Date.AddHours(7);
}

public static DateTime DayEnd(DateTime date)
{
    return date.Date.AddHours(19);
}

public static TimeSpan DiffSingleDay(DateTime start, DateTime end)
{
    if ( start.Date != end.Date ) {
        throw new ArgumentException();
    }

    if (start.DayOfWeek == DayOfWeek.Saturday || start.DayOfWeek == DayOfWeek.Sunday )
    {
        return TimeSpan.Zero;
    }

    start = start >= DayStart(start) ? start : DayStart(start);
    end = end <= DayEnd(end) ? end : DayEnd(end);
    return end - start;
}

public static TimeSpan DiffRange(DateTime start, DateTime end)
{
    if (start.Date == end.Date)
    {
        return DiffSingleDay(start, end);
    }

    var firstDay = DiffSingleDay(start, DayEnd(start));
    var lastDay = DiffSingleDay(DayStart(end), end);

    var middle = TimeSpan.Zero;
    var current = start.AddDays(1);
    while (current.Date != end.Date)
    {
        middle = middle + DiffSingleDay(current.Date, DayEnd(current.Date));
        current = current.AddDays(1);
    }

    return firstDay + lastDay + middle;
}
JaredPar
This produces a vastly different result than my solution. Can you spot what's wrong with my code?
dtb
@dtb, looks like my issue. I added 14 instead of 19. Will correct
JaredPar
Oh cool. Your corrected version now returns the same result for my example.
dtb
Now you just have to account for DST :-D
LBushkin
@LBushkin, I surrender then ;)
JaredPar
This is good except you are iterating over all days. For date ranges that cover several weeks, it is easy to compute the # of hours in a complete week as I did in my solution.
dana
@JaredPar I know you're joking but... You actually don't have to account for DST as all jurisdictions that have DST implement the switch during one of the periods that aren't counted. Since the implementation of DateTime doesn't have full timezone information, then the offset between local and UTC will be considered the same in both cases (often a nuisance, but a saver here). Provided that nobody enters a UTC time for one and a local for the other (which would be crazy) DST isn't an issue.
Jon Hanna
+1  A: 

I'm sure there's something I missed.

  TimeSpan CalcBusinessTime(DateTime a, DateTime b)
  {
     if (a > b)
     {
        DateTime tmp = a;
        a = b;
        b = tmp;
     }

     if (a.TimeOfDay < new TimeSpan(7, 0, 0))
        a = new DateTime(a.Year, a.Month, a.Day, 7, 0, 0);
     if (b.TimeOfDay > new TimeSpan(19, 0, 0))
        b = new DateTime(b.Year, b.Month, b.Day, 19, 0, 0);

     TimeSpan sum = new TimeSpan();
     TimeSpan fullDay = new TimeSpan(12, 0, 0);
     while (a < b)
     {
        if (a.DayOfWeek != DayOfWeek.Saturday && a.DayOfWeek != DayOfWeek.Sunday)
        {
           sum += (b - a < fullDay) ? b - a : fullDay;
        }
        a = a.AddDays(1);
     }

     return sum;
  } 
Jeremiah Nunn
@dtb, Thanks, hopefully bug free now. Maybe I'll run some tests
Jeremiah Nunn
+1  A: 

It was a pretty hard question. For a basic, in a straightforward approach, I have written the below code:

DateTime start = new DateTime(2010, 01, 01, 21, 00, 00);
DateTime end = new DateTime(2010, 10, 01, 14, 00, 00);

// Shift start date's hour to 7 and same for end date
// These will be added after doing calculation:
double startAdjustmentMinutes = (start - start.Date.AddHours(7)).TotalMinutes;
double endAdjustmentMinutes = (end - end.Date.AddHours(7)).TotalMinutes;

// We can do some basic
// mathematical calculation to find weekdays count:
// divide by 7 multiply by 5 gives complete weeks weekdays
// and adding remainder gives the all weekdays:
int weekdaysCount = (((int)((end.Date - start.Date).Days / 7) * 5) 
          + ((end.Date - start.Date).Days % 7));
// so we can multiply it by minutes between 7am to 7 pm
int minutes = weekdaysCount * (12 * 60);

// after adding adjustment we have the result:
int result = minutes + startAdjustmentMinutes + endAdjustmentMinutes;

I know this not seem programmatically beautiful but I don't know if it is good to iterate through days and hours between start and end.

Musa Hafalır
+1  A: 
static int WorkPeriodMinuteDifference(DateTime start, DateTime end)
{
    //easier to only have to work in one direction.
    if(start > end)
        return WorkPeriodMinuteDifference(end, start);
    //if weekend, move to start of next Monday.
    while((int)start.DayOfWeek % 6 == 0)
        start = start.Add(new TimeSpan(1, 0, 0, 0)).Date;
    while((int)end.DayOfWeek % 6 == 0)
        end = end.Add(new TimeSpan(1, 0, 0, 0)).Date;
    //Move up to 07:00 or down to 19:00
    if(start.TimeOfDay.Hours < 7)
        start = new DateTime(start.Year, start.Month, start.Day, 7, 0, 0);
    else if(start.TimeOfDay.Hours > 19)
        start = new DateTime(start.Year, start.Month, start.Day, 19, 0, 0);
    if(end.TimeOfDay.Hours < 7)
        end = new DateTime(end.Year, end.Month, end.Day, 7, 0, 0);
    else if(end.TimeOfDay.Hours > 19)
        end = new DateTime(end.Year, end.Month, end.Day, 19, 0, 0);

    TimeSpan difference = end - start;

    int weeks = difference.Days / 7;
    int weekDays = difference.Days % 7;
    if(end.DayOfWeek < start.DayOfWeek)
        weekDays -= 2;

    return (weeks * 5 * 12 * 60) + (weekDays * 12 * 60) + difference.Hours * 60 + difference.Minutes
}
Jon Hanna
+1  A: 

My implementation :) The idea is to quickly compute the total weeks, and walk the remaining week day by day...

public TimeSpan Compute(DateTime start, DateTime end)
{
    // constant start / end times per day
    TimeSpan sevenAM = TimeSpan.FromHours(7);
    TimeSpan sevenPM = TimeSpan.FromHours(19);

    if( start >= end )
    {
        throw new Exception("invalid date range");
    }

    // total # of weeks
    int completeWeeks = ((int)(end - start).TotalDays) / 7;

    // starting total
    TimeSpan total = TimeSpan.FromHours(completeWeeks * 12 * 5);

    // adjust the start date to be exactly "completeWeeks" past its original start
    start = start.AddDays(completeWeeks * 7);

    // walk days from the adjusted start to end (at most 7), accumulating time as we can...
    for(
        // start at midnight
        DateTime dt = start.Date;

        // continue while there is time left
        dt < end;

        // increment 1 day at a time
        dt = dt.AddDays(1)
    )
    {
        // ignore weekend
        if( (dt.DayOfWeek == DayOfWeek.Saturday) ||
             (dt.DayOfWeek == DayOfWeek.Sunday) )
        {
            continue;
        }

        // get the start/end time for each day...
        // typically 7am / 7pm unless we are at the start / end date
        TimeSpan dtStartTime = ((dt == start.Date) && (start.TimeOfDay > sevenAM)) ?
            start.TimeOfDay : sevenAM;
        TimeSpan dtEndTime = ((dt == end.Date) && (end.TimeOfDay < sevenPM)) ?
            end.TimeOfDay : sevenPM;

        if( dtStartTime < dtEndTime )
        {
            total = total.Add(dtEndTime - dtStartTime);
        }
    }

    return total;
}
dana
A: 

I won't write any code, but having a DateTime you can tell the Day of the week, thus you know how many weekends are in your range, so you can tell how many minutes are in a weekend.

So it wouldn't be so hard... of course there must be an optimal one line solution... but i think you can get around with this one.

I forgot to mention that you also know the minutes btween 7:00 pm and 7:00 am so all you have to do is substract the right amount of minutes to the time difference you get.

sebastian
A: 

Use TimeSpan.TotalMinutes, subtract non-business days, subtract superfluous hours.

ZeroElement