LINQ query — Data aggregation (Group Adjacent)

前端 未结 7 1967
失恋的感觉
失恋的感觉 2020-12-03 03:09

Let\'s take a class called Cls:

public class Cls
{
    public int SequenceNumber { get; set; }
    public int Value { get; set; }
}
相关标签:
7条回答
  • 2020-12-03 03:45

    MoreLinq provides this functionality out of the box

    It's called GroupAdjacent and is implemented as extension method on IEnumerable:

    Groups the adjacent elements of a sequence according to a specified key selector function.

    enumerable.GroupAdjacent(e => e.Key)
    

    There is even a Nuget "source" package that contains only that method, if you don't want to pull in an additional binary Nuget package.

    The method returns an IEnumerable<IGrouping<TKey, TValue>>, so its output can be processed in the same way output from GroupBy would be.

    0 讨论(0)
  • 2020-12-03 03:46

    Untested dark magic follows. The imperative version seems like it would be easier in this case.

    IEnumerable<Cls> data = ...;
    var query = data
        .GroupBy(x => x.Value)
        .Select(g => new
        {
            Value = g.Key,
            Sequences = g
                .OrderBy(x => x.SequenceNumber)
                .Select((x,i) => new
                {
                    x.SequenceNumber,
                    OffsetSequenceNumber = x.SequenceNumber - i
                })
                .GroupBy(x => x.OffsetSequenceNumber)
                .Select(g => g
                    .Select(x => x.SequenceNumber)
                    .OrderBy(x => x)
                    .ToList())
                .ToList()
        })
        .SelectMany(x => x.Sequences
            .Select(s => new { First = s.First(), Last = s.Last(), x.Value }))
        .OrderBy(x => x.First)
        .ToList();
    
    0 讨论(0)
  • 2020-12-03 03:48

    I was able to accomplish it by creating a custom extension method.

    static class Extensions {
      internal static IEnumerable<Tuple<int, int, int>> GroupAdj(this IEnumerable<Cls> enumerable) {
        Cls start = null;
        Cls end = null;
        int value = Int32.MinValue;
    
        foreach (Cls cls in enumerable) {
          if (start == null) {
            start = cls;
            end = cls;
            continue;
          }
    
          if (start.Value == cls.Value) {
            end = cls;
            continue;
          }
    
          yield return Tuple.Create(start.SequenceNumber, end.SequenceNumber, start.Value);
          start = cls;
          end = cls;
        }
    
        yield return Tuple.Create(start.SequenceNumber, end.SequenceNumber, start.Value);
      }
    }
    

    Here's the implementation:

    static void Main() {
      List<Cls> items = new List<Cls> {
        new Cls { SequenceNumber = 1, Value = 9 },
        new Cls { SequenceNumber = 2, Value = 9 },
        new Cls { SequenceNumber = 3, Value = 15 },
        new Cls { SequenceNumber = 4, Value = 15 },
        new Cls { SequenceNumber = 5, Value = 15 },
        new Cls { SequenceNumber = 6, Value = 30 },
        new Cls { SequenceNumber = 7, Value = 9 }
      };
    
      Console.WriteLine("From  To    Value");
      Console.WriteLine("===== ===== =====");
      foreach (var item in items.OrderBy(i => i.SequenceNumber).GroupAdj()) {
        Console.WriteLine("{0,-5} {1,-5} {2,-5}", item.Item1, item.Item2, item.Item3);
      }
    }
    

    And the expected output:

    From  To    Value
    ===== ===== =====
    1     2     9
    3     5     15
    6     6     30
    7     7     9
    
    0 讨论(0)
  • 2020-12-03 03:50

    You can use this linq query

    Demo

    var values = (new[] { 9, 9, 15, 15, 15, 30, 9 }).Select((x, i) => new { x, i });
    
    var query = from v in values
                let firstNonValue = values.Where(v2 => v2.i >= v.i && v2.x != v.x).FirstOrDefault()
                let grouping = firstNonValue == null ? int.MaxValue : firstNonValue.i
                group v by grouping into v
                select new
                {
                  From = v.Min(y => y.i) + 1,
                  To = v.Max(y => y.i) + 1,
                  Value = v.Min(y => y.x)
                };
    
    0 讨论(0)
  • 2020-12-03 04:00

    You can use Linq's GroupBy in a modified version which groups only if the two items are adjacent, then it's easy as:

    var result = classes
        .GroupAdjacent(c => c.Value)
        .Select(g => new { 
            SequenceNumFrom = g.Min(c => c.SequenceNumber),
            SequenceNumTo = g.Max(c => c.SequenceNumber),  
            Value = g.Key
        });
    
    foreach (var x in result)
        Console.WriteLine("SequenceNumFrom:{0} SequenceNumTo:{1} Value:{2}", x.SequenceNumFrom, x.SequenceNumTo, x.Value);
    

    DEMO

    Result:

    SequenceNumFrom:1  SequenceNumTo:2  Value:9
    SequenceNumFrom:3  SequenceNumTo:5  Value:15
    SequenceNumFrom:6  SequenceNumTo:6  Value:30
    SequenceNumFrom:7  SequenceNumTo:7  Value:9
    

    This is the extension method to to group adjacent items:

    public static IEnumerable<IGrouping<TKey, TSource>> GroupAdjacent<TSource, TKey>(
            this IEnumerable<TSource> source,
            Func<TSource, TKey> keySelector)
        {
            TKey last = default(TKey);
            bool haveLast = false;
            List<TSource> list = new List<TSource>();
            foreach (TSource s in source)
            {
                TKey k = keySelector(s);
                if (haveLast)
                {
                    if (!k.Equals(last))
                    {
                        yield return new GroupOfAdjacent<TSource, TKey>(list, last);
                        list = new List<TSource>();
                        list.Add(s);
                        last = k;
                    }
                    else
                    {
                        list.Add(s);
                        last = k;
                    }
                }
                else
                {
                    list.Add(s);
                    last = k;
                    haveLast = true;
                }
            }
            if (haveLast)
                yield return new GroupOfAdjacent<TSource, TKey>(list, last);
        }
    }
    

    and the class used:

    public class GroupOfAdjacent<TSource, TKey> : IEnumerable<TSource>, IGrouping<TKey, TSource>
    {
        public TKey Key { get; set; }
        private List<TSource> GroupList { get; set; }
        System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
        {
            return ((System.Collections.Generic.IEnumerable<TSource>)this).GetEnumerator();
        }
        System.Collections.Generic.IEnumerator<TSource> System.Collections.Generic.IEnumerable<TSource>.GetEnumerator()
        {
            foreach (var s in GroupList)
                yield return s;
        }
        public GroupOfAdjacent(List<TSource> source, TKey key)
        {
            GroupList = source;
            Key = key;
        }
    }
    
    0 讨论(0)
  • 2020-12-03 04:02

    You can do it like this:

    var all = new [] {
        new Cls(1, 9)
    ,   new Cls(2, 9)
    ,   new Cls(3, 15)
    ,   new Cls(4, 15)
    ,   new Cls(5, 15)
    ,   new Cls(6, 30)
    ,   new Cls(7, 9)
    };
    var f = all.First();
    var res = all.Skip(1).Aggregate(
        new List<Run> {new Run {From = f.SequenceNumber, To = f.SequenceNumber, Value = f.Value} }
    ,   (p, v) => {
        if (v.Value == p.Last().Value) {
            p.Last().To = v.SequenceNumber;
        } else {
            p.Add(new Run {From = v.SequenceNumber, To = v.SequenceNumber, Value = v.Value});
        }
        return p;
    });
    foreach (var r in res) {
        Console.WriteLine("{0} - {1} : {2}", r.From, r.To, r.Value);
    }
    

    The idea is to use Aggregate creatively: starting with a list consisting of a single Run, examine the content of the list we've got so far at each stage of aggregation (the if statement in the lambda). Depending on the last value, either continue the old run, or start a new one.

    Here is a demo on ideone.

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