Combining consecutive dates into ranges

后端 未结 2 466
悲哀的现实
悲哀的现实 2020-12-18 12:44

I have a List of objects

public class sample
{
 public DateTime Date;
 public string content;
}

I want to be able to create a list of new o

相关标签:
2条回答
  • 2020-12-18 13:02

    Here's a non-Linq way to do it:

    List<sampleWithIntervals> groups = new List<sampleWithIntervals>();  
    sampleWithIntervals curGroup = null;
    
    foreach(sample s in samples.OrderBy(sa => sa.content).ThenBy(sa => sa.Date))
    {
        if(curGroup == null || // first group
            s.Date != curGroup.endDate.AddDays(1) ||
            s.content != curGroup.content   // new group
          ) 
        {
            curGroup = new sampleWithIntervals() {startDate = s.Date, endDate = s.Date, content = s.content};
            groups.Add(curGroup);
        }
        else
        {
            // add to current group
            curGroup.endDate = s.Date;
        }
    }
    

    You can do this with Linq using a trick that groups the items by the date minus the index to group consecutive items:

    samples.OrderBy(s => s.content)   
           .ThenBy(s => s.Date)
           // select each item with its index
           .Select ((s, i) => new {sample = s, index = i})  
           // group by date miuns index to group consecutive items
           .GroupBy(si => new {date = si.sample.Date.AddDays(-si.index), content = si.sample.content})  
           // get the min, max, and content of each group
           .Select(g => new sampleWithIntervals() {
                            startDate = g.Min(s => s.sample.Date), 
                            endDate = g.Max(s => s.sample.Date), 
                            content = g.First().sample.content
                            })
    
    0 讨论(0)
  • 2020-12-18 13:25

    I have this SplitBy extension method where you can specify a delimiter predicate with which the collection will be split, just like string.Split.

    public static IEnumerable<IEnumerable<T>> SplitBy<T>(this IEnumerable<T> source, 
                                                         Func<T, bool> delimiterPredicate,
                                                         bool includeEmptyEntries = false, 
                                                         bool includeSeparator = false)
    {
        var l = new List<T>();
        foreach (var x in source)
        {
            if (!delimiterPredicate(x))
                l.Add(x);
            else
            {
                if (includeEmptyEntries || l.Count != 0)
                {
                    if (includeSeparator)
                        l.Add(x);
    
                    yield return l;
                }
    
                l = new List<T>();
            }
        }
        if (l.Count != 0 || includeEmptyEntries)
            yield return l;
    }
    

    So now splitting is easy if you can specify a consecutive streak delimiter. For that you can order the collection and zip with adjacent items, so now the difference in dates in the two resultant columns can act as delimiter.

    var ordered = samples.OrderBy(x => x.content).ThenBy(x => x.Date).ToArray();
    var result = ordered.Zip(ordered.Skip(1).Append(new sample()), (start, end) => new { start, end })
                        .SplitBy(x => x.end.Date - x.start.Date != TimeSpan.FromDays(1), true, true)
                        .Select(x => x.Select(p => p.start).ToArray())
                        .Where(x => x.Any())
                        .Select(x => new sampleWithIntervals
                        {
                            content = x.First().content,
                            startDate = x.First().Date,
                            endDate = x.Last().Date
                        });
    

    new sample() is a dummy instance used to get the Zip correctly. The Append method is to append items to a IEnumerable<> sequence, and it goes like this:

    public static IEnumerable<T> Append<T>(this IEnumerable<T> source, params T[] items)
    {
        return source.Concat(items);
    }
    

    Note: this doesn't preserve the initial order. If you want the original order, select the index initially and form an anonymous class right away (Select((x, i) => new { x, i })) and at the last stage sort on the basis of index before selecting appropriate type.

    0 讨论(0)
提交回复
热议问题