Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Using LINQ on a List to return a subset list based on sequential dates

Tags:

c#

linq

I have a scenario where I have a list of objects with a datetime field in the object. I'm trying to find out if there is a way to use LINQ to group the list by sequential datetimes and return a subset of the list with the sequential datetimes as a range.

public virtual IList<LineItem> LineItems { get; set; }
...
public class LineItem
{
    public virtual string Status { get; set; }
    public virtual DateTime TransDate { get; set; }
    ...
}

So If I had 6 LineItems with Status = P for all and

TransDate = { 8/1/2011 , 8/2/2011 , 8/3/2011 , 8/5/2011 , 8/6/2011 , 8/9/2011 }

respectively, I'd like to return the following list:

{ (P, 8/1/2011-8/3/2011) , (P,8/5/2011-8/6/2011) , (P,8/9/2011) }

Any thoughts? I can do this with iterating through the list manually and checking the TransDate to see if it's sequential, I am just looking for a more elegant (preferably LINQ) way of doing it. Thanks!

like image 951
sgeddes Avatar asked Nov 30 '11 15:11

sgeddes


2 Answers

I would use a helper method like this:

private static IEnumerable<ICollection<T>> PartitionByPredicate<T>(
    this IEnumerable<T> seq, Func<T, T, bool> split)
{
    var buffer = new List<T>();

    foreach (var x in seq)
    {
        if (buffer.Any() && split(buffer.Last(), x))
        {
            yield return buffer;
            buffer = new List<T>();
        }

        buffer.Add(x);
    }

    if (buffer.Any())
        yield return buffer;
}

And then:

var sorted = LineItems.OrderBy(i => i.TransDate);
var split = sorted.PartitionByPredicate(
    (x, y) => (y.TransDate.Date - x.TransDate.Date).TotalDays > 1)

(edit: cleaned it up slightly, my first version was silly.)

like image 75
mqp Avatar answered Sep 29 '22 09:09

mqp


I suggest you go with an iterator block implementation, as suggested by @mquander.

But here's a fun, pure LINQ solution that will work (albeit inefficiently), assuming the dates are distinct and chronological:

var groups = from item in LineItems
             let startDate = item.TransDate
             group item by LineItems.Select(lineItem => lineItem.TransDate)
                                    .SkipWhile(endDate => endDate < startDate)                                        
                                    .TakeWhile((endDate, index) => 
                                                startDate.AddDays(index) == endDate)
                                    .Last();

//If required:
 var groupsAsLists = groups.Select(g => g.ToList()).ToList();

This works by choosing the last sequential date in any date-sequence as the key for that sequence.

like image 24
Ani Avatar answered Sep 29 '22 08:09

Ani