Запрос LINQ - агрегация данных (группа смежных)

17

Возьмем класс под названием Cls :

public class Cls
{
    public int SequenceNumber { get; set; }
    public int Value { get; set; }
}

Теперь давайте заполним некоторую коллекцию следующими элементами:

Sequence
Number      Value
========    =====
1           9
2           9
3           15
4           15
5           15
6           30
7           9

Что мне нужно сделать, это перечислить номера последовательностей и проверить, имеет ли следующий элемент то же значение. Если да, значения агрегируются, и поэтому желаемый результат выглядит следующим образом:

Sequence    Sequence
Number      Number
From        To          Value
========    ========    =====
1           2           9
3           5           15
6           6           30
7           7           9

Как выполнить эту операцию с помощью запроса LINQ?

    
задан Dariusz Woźniak 14.02.2013 в 17:16
источник
  • Я полагаю, вам понадобится использовать стандартный для каждого цикла, интересный вопрос, хотя и хорошо поставить +1 –  RobJohnson 14.02.2013 в 17:20
  • Очень интересный вопрос, но я почему-то сомневаюсь, что версия LINQ будет намного читабельнее, чем версия цикла foreach. Я надеюсь, что ответ здесь может доказать мне иначе. –  l46kok 14.02.2013 в 17:26
  • Вы можете группировать по значению, а затем искать сгруппированные коллекции для непрерывных последовательностей, затем разделять их и сортировать по «от», но я думаю, что я согласен, что императивная версия будет не менее читаема в данном конкретном случае. –  Honza Brestan 14.02.2013 в 17:29
  • См. также: stackoverflow.com/q/7064157/21727 –  mbeckish 14.02.2013 в 18:35
  • См. ту же проблему в CodeGolf: codegolf.stackexchange.com/questions/10797/... –  Dariusz Woźniak 17.03.2013 в 19:11

7 ответов

14

Вы можете использовать GroupBy Linq в модифицированной версии, которая группируется только в том случае, если два элемента смежны, тогда это легко:

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

Результат:

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

Это метод расширения для группировки смежных элементов:

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);
    }
}

и используемый класс:

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;
    }
}
    
ответ дан Tim Schmelter 14.02.2013 в 17:32
  • +1 Отличный ответ, это много кода, хотя, я думаю, я бы просто использовал регулярный цикл для каждого цикла и создавал новую коллекцию таким образом –  RobJohnson 14.02.2013 в 17:36
  • Много кода? Это полностью повторное, обобщенное решение. Не так много, учитывая это. Фантастический ответ и новый инструмент для инструментария. +1 –  Pete 14.02.2013 в 17:39
  • Исходным источником для этого кода является blogs.msdn.microsoft.com/ericwhite/2008/04/20/... –  Quails4Eva 26.09.2016 в 12:54
  • @ Quails4Eva: я должен признать, что я действительно не знаю, где был исходный источник, я уверен, что это был не я, но я тоже не знаю этого блога. Автор говорит: «Этот подход был предложен одним из архитекторов LINQ год или два назад», поэтому он тоже не настоящий автор. –  Tim Schmelter 26.09.2016 в 13:02
  • @Tim Schmelter Это справедливая точка. Я бы это интерпретировал, поскольку они предложили подход, и он написал код, или, по крайней мере, версия архива LINQ не является общедоступной. Несмотря ни на что, я не слишком беспокоюсь об этом, мне просто показалось странным найти идентичные решения на расстоянии 5 лет, поэтому я добавил ссылку на более раннюю версию. –  Quails4Eva 26.09.2016 в 13:44
3

Вы можете использовать этот запрос linq

Демо

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)
            };
    
ответ дан Aducci 14.02.2013 в 18:17
3

MoreLinq предоставляет эту функциональность из коробки

Он называется GroupAdjacent и реализуется как метод расширения на IEnumerable :

  

Группирует смежные элементы последовательности в соответствии с заданной функцией выбора ключа.

enumerable.GroupAdjacent(e => e.Key)

Существует даже Nuget «исходный» пакет , который содержит только этот метод, если вы не хотите вносить дополнительный пакет Binary Nuget .

Метод возвращает IEnumerable<IGrouping<TKey, TValue>> , поэтому его вывод может обрабатываться таким же образом, как и выход из GroupBy .

    
ответ дан theDmi 24.04.2015 в 08:12
  • Я думаю, это должно быть отмечено как правильный ответ. Я лично предпочитаю добавлять пакет nuget для копирования / вставки. Кроме того, стоит понять, что еще есть в MoreLinq, которого я отсутствовал. –  Mike S. 08.10.2016 в 17:38
2

Вы можете сделать это следующим образом:

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);
}

Идея состоит в том, чтобы использовать Aggregate творчески: начиная со списка, состоящего из одного Run , изучите содержимое списка, который у нас есть на каждом этапе агрегации (оператор if в лямбда). В зависимости от последнего значения либо продолжите старый запуск, либо запустите новый.

Вот демон на идее .

    
ответ дан dasblinkenlight 14.02.2013 в 17:32
  • IMHO лучше использовать цикл foreach, когда в лямбда есть такой код. –  juharr 14.02.2013 в 17:38
  • @juharr Это не просто количество кода, это факт, что он вызывает побочные эффекты и в зависимости от этих побочных эффектов. Когда важные части любого вызова LINQ вызывают побочные эффекты, обычно это означает, что часть должна быть только в foreach. –  Servy 14.02.2013 в 17:42
  • @Servy Я согласен - я бы не использовал LINQ для запуска обнаружения именно по причине побочных эффектов. Я рассматриваю это как ответ на вопрос о головоломке LINQ, потому что OP запросил LINQ явно. –  dasblinkenlight 14.02.2013 в 18:49
2

Я смог выполнить это, создав собственный метод расширения.

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);
  }
}

Вот реализация:

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);
  }
}

И ожидаемый результат:

From  To    Value
===== ===== =====
1     2     9
3     5     15
6     6     30
7     7     9
    
ответ дан Joshua 14.02.2013 в 17:38
2

Вот реализация без каких-либо вспомогательных методов:

var grp = 0;
var results =
from i
in
input.Zip(
    input.Skip(1).Concat(new [] {input.Last ()}),
    (n1, n2) => Tuple.Create(
        n1, (n2.Value == n1.Value) ? grp : grp++
    )
)
group i by i.Item2 into gp
select new {SequenceNumFrom = gp.Min(x => x.Item1.SequenceNumber),SequenceNumTo = gp.Max(x => x.Item1.SequenceNumber), Value = gp.Min(x => x.Item1.Value)};

Идея такова:

  • Следите за своим собственным индикатором группировки, grp.
  • Присоедините каждый элемент коллекции к следующему элементу коллекции (через Skip (1) и Zip).
  • Если значения совпадают, они находятся в одной группе; в противном случае приращение grp сигнализирует о начале следующей группы.
ответ дан mbeckish 14.02.2013 в 18:24
1

Неверная темная магия следует. Императивная версия кажется, что в этом случае было бы легче.

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();
    
ответ дан Timothy Shields 14.02.2013 в 17:51