tags:

views:

109

answers:

3

Simple question. I have an ordered collection of dates. They are UK dates btw

01/01/10
01/02/10
01/03/10
01/04/10
02/04/10
03/04/10
04/04/10

And I want to convert this into a collection of date ranges

01/01/10 -> 01/01/10
01/02/10 -> 01/02/10
01/03/10 -> 01/03/10
01/04/10 -> 04/04/10

Just to clarify, I'm trying to convert any consecutive dates into a range. so the first 3 dates are stand alone and the last 4 get converted into a range 1st of April to 4th of April.

Now I can do this using loops but it's not very elegant. Does any one have any solutions out there that are?

Thanks

+3  A: 

Given that you want to determine ranges of consecutive date ranges, I think your only option is, as you say a loop. You can do it in a single pass though, and put it in an extension method so it'll operate on any IList<DateTime>, for example:

// purely an example, chances are this will have actual, y'know logic in live
public class DateRange
{
    private List<DateTime> dates = new List<DateTime>();

    public void Add(DateTime date)
    {
        this.dates.Add(date);
    }

    public IEnumerable<DateTime> Dates
    {
        get { return this.dates; }
    }
}

public static IEnumerable<DateRange> GetRanges(this IList<DateTime> dates)
{
    List<DateRange> ranges = new List<DateRange>();
    DateRange currentRange = null;

    // this presumes a list of dates ordered by day, if not then the list will need sorting first
    for( int i = 0; i < dates.Count; ++i )
    {
        var currentDate = dates[i];
        if( i == 0 || dates[i - 1] != currentDate.AddDays(-1))
        {
            // it's either the first date or the current date isn't consecutive to the previous so a new range is needed
            currentRange = new DateRange();
            ranges.Add(currentRange);
        }

        currentRange.Add(currentDate);
    }

    return ranges;
}

You could also make it even more generic by passing in an IEnumerable<DateTime>:

public static IEnumerable<DateRange> GetRanges(this IEnumerable<DateTime> dates)
{
    List<DateRange> ranges = new List<DateRange>();
    DateRange currentRange = null;
    DateTime? previousDate = null;

    // this presumes a list of dates ordered by day, if not then the list will need sorting first
    foreach( var currentDate in dates )
    {
        if( previousDate == null || previousDate.Value != currentDate.AddDays(-1) )
        {
            // it's either the first date or the current date isn't consecutive to the previous so a new range is needed
            currentRange = new DateRange();
            ranges.Add(currentRange);
        }

        currentRange.Add(currentDate);
        previousDate = currentDate;
    }

    return ranges;
}
Dave
Although this would solve the problem for this case. I'm not wanting the dates grouped by month, rather to work out the date ranges where the dates are consecutive.
mjmcloug
Ahh, I've edited my answer accordingly.
Dave
AM I right in thinking that this needs the dates to be ordered? Looks like it will work but wondering whether it is worth putting a sort in the beginning (possibly with a bool parameter to tell it whether it needs to sort or not) so it can cope with unordered date ranges.
Chris
According to the original question the dates are already ordered, but you're right, if not then they'd need to be.
Dave
No LINQ and no lambda expressions! That's good already. If the function could take IEnumerable<DateTime> (which is sufficient for the task) and didn't add default-created ranges, that carry no information about the date, it would be great :)
Maciej Hehl
Not sure what you mean by default-created ranges. If there are no dates then it creates an empty list, yes, but that's in keeping with the patterns of other extension methods that return collections. If you do a Where and there's no results you get an enumeration with no contents rather than a null reference. Or am I misunderstanding?
Dave
I'm talking about the fact, that you create a new `DateRange` and add it to `ranges` without associating it with any date.
Maciej Hehl
Ok sorry. I got it. It's a reference type :) `currentRange` still refers to the same range, the reference to which was added to `ranges`. To me it's really not obvious. Is it me?
Maciej Hehl
Ahh, I see what you mean now. Yeah, it's another class somewhere (presumably with a range of DateTimes or somesuch), so each iteration you'll be adding to the same range unless a change occurs meaning you need to create a new one. I could add a brief sample class, might make things a little clearer.
Dave
A: 
        var stringDates = new List<string> {"01/09/10", "31/08/10", "01/01/10"};

        var dates = stringDates.ConvertAll(DateTime.Parse);
        dates.Sort();

        var lastDateInSequence = new DateTime();
        var firstDateInSequence = new DateTime();  

        foreach (var range in dates.GroupBy(d => { if ((d - lastDateInSequence).TotalDays != 1)   
                                                       firstDateInSequence = d;  
                                                   lastDateInSequence = d;
                                                   return firstDateInSequence;  
        }))
        {
            var sb = new StringBuilder();
            sb.Append(range.First().ToShortDateString());
            sb.Append(" => ");
            sb.Append(range.Last().ToShortDateString());
            Console.WriteLine(sb.ToString());
        }
SKINDER
A: 
dates.Aggregate(new List<DateRange>(), (acc, dt) =>
                                       {
                                         if (acc.Count > 0 && acc.Last().d2 == dt.AddDays(-1))
                                           acc[acc.Count - 1].d2 = dt;
                                         else
                                           acc.Add(new DateRange(dt, dt));
                                         return acc;
                                       }
    );

where DateRange is a class like this:

class DateRange
{
  public DateTime d1, d2;

  public DateRange(DateTime d1, DateTime d2)
  {
    this.d1 = d1;
    this.d2 = d2;
  }
}
onof