Подтвердить что ты не робот

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

Возьмем класс под названием 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?

4b9b3361

Ответ 1

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

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

Ответ 2

Вы можете использовать этот запрос 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)
            };

Ответ 3

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

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

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

enumerable.GroupAdjacent(e => e.Key)

Существует даже Nuget "исходный" пакет, который содержит только этот метод, если вы не хотите втягивать дополнительный бинарный пакет Nuget.

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

Ответ 4

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

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 в лямбда). В зависимости от последнего значения либо продолжите старый запуск, либо запустите новый.

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

Ответ 5

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

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

Ответ 6

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

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 сигнализирует о начале следующей группы.

Ответ 7

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

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