Группировка последовательных идентичных объектов: IEnumerable <T> к IEnumerable <IEnumerable <T>>

У меня есть межпокоящаяся проблема: Данный IEnumerable<string>, действительно ли возможно привести к последовательности IEnumerable<IEnumerable<string>> это группирует идентичные смежные строки в одной передаче?

Позвольте мне объяснить.

1. Основной иллюстративный образец:

Рассмотрение следующего IEnumerable<string> (псевдо представление):

{"a","b","b","b","c","c","d"}

Как добраться IEnumerable<IEnumerable<string>> это привело бы к чему-то вроде формы:

{ // IEnumerable<IEnumerable<string>>
    {"a"},         // IEnumerable<string>
    {"b","b","b"}, // IEnumerable<string>
    {"c","c"},     // IEnumerable<string>
    {"d"}          // IEnumerable<string>
}

Прототип метода был бы:

public IEnumerable<IEnumerable<string>> Group(IEnumerable<string> items)
{
    // todo
}

Но это могло также быть:

public void Group(IEnumerable<string> items, Action<IEnumerable<string>> action)
{
    // todo
}

... где action был бы назван для каждой подпоследовательности.

2. Более сложный образец

Хорошо, первая выборка очень проста, и только имеет целью ясно давать понять намерение высокого уровня.

Теперь предположите, что мы имеем дело с IEnumerable<Anything>, где Anything тип, определенный как это:

public class Anything
{
    public string Key {get;set;}
    public double Value {get;set;}
}

Мы теперь хотим генерировать подпоследовательности на основе Ключа, (сгруппируйте каждое последовательное Anything это имеет тот же ключ) для позже использования их для вычисления итогового значения группой:

public void Compute(IEnumerable<Anything> items)
{
    Console.WriteLine(items.Sum(i=>i.Value));
}

// then somewhere, assuming the Group method 
// that returns an IEnumerable<IEnumerable<Anything>> actually exists:
foreach(var subsequence in Group(allItems))
{
    Compute(subsequence);
}

3. Важные примечания

  • Только одно повторение по исходной последовательности
  • Никакие посреднические выделения наборов (мы можем принять миллионы объектов в исходной последовательности и миллионы consecutives объекты в каждой группе),
  • Хранение перечислителей и задержанного поведения при выполнении
  • Мы можем предположить, что получающиеся подпоследовательности будут выполнены с помощью итераций только однажды и будут выполнены с помощью итераций в порядке.

Действительно ли возможно, и как Вы записали бы это?

7
задан Timwi 24 September 2012 в 18:19
поделиться

4 ответа

Это то, что вы ищете?

  • Итерировать список только один раз.
  • Отложить исполнение.
  • Никаких промежуточных коллекций (другой мой пост не прошел по этому критерию).

Это решение полагается на состояние объекта, потому что трудно разделить состояние между двумя методами IEnumerable, которые используют yield (без параметров ref или out).

internal class Program
{
    static void Main(string[] args)
    {
        var result = new[] { "a", "b", "b", "b", "c", "c", "d" }.Partition();
        foreach (var r in result)
        {
            Console.WriteLine("Group".PadRight(16, '='));
            foreach (var s in r)
                Console.WriteLine(s);
        }
    }
}

internal static class PartitionExtension
{
    public static IEnumerable<IEnumerable<T>> Partition<T>(this IEnumerable<T> src)
    {
        var grouper = new DuplicateGrouper<T>();
        return grouper.GroupByDuplicate(src);
    }
}

internal class DuplicateGrouper<T>
{
    T CurrentKey;
    IEnumerator<T> Itr;
    bool More;

    public IEnumerable<IEnumerable<T>> GroupByDuplicate(IEnumerable<T> src)
    {
        using(Itr = src.GetEnumerator())
        {
            More = Itr.MoveNext();

            while (More)
                yield return GetDuplicates();
        }
    }

    IEnumerable<T> GetDuplicates()
    {
        CurrentKey = Itr.Current;
        while (More && CurrentKey.Equals(Itr.Current))
        {
            yield return Itr.Current;
            More = Itr.MoveNext();
        }
    }
}

Edit: Добавлен метод расширения для более чистого использования. Логика теста с фиксированным циклом, так что сначала оценивается «Больше».

Редактировать: Удалите перечислитель по завершении

5
ответ дан 6 December 2019 в 21:11
поделиться

Лучшее решение, отвечающее всем требованиям

Хорошо, откажитесь от моего предыдущего решения (я оставлю его ниже, только для справки). Вот гораздо лучший подход, который пришёл мне в голову после того, как я сделал свой первоначальный пост.

Напишите новый класс, который реализует IEnumerator и предоставляет несколько дополнительных свойств: IsValid и Предыдущий . Это все, что вам действительно нужно, чтобы разрешить весь беспорядок, связанный с необходимостью поддерживать состояние внутри блока итератора, используя yield .

Вот как я это сделал (как видите, довольно тривиально):

internal class ChipmunkEnumerator<T> : IEnumerator<T> {

    private readonly IEnumerator<T> _internal;
    private T _previous;
    private bool _isValid;

    public ChipmunkEnumerator(IEnumerator<T> e) {
        _internal = e;
        _isValid = false;
    }

    public bool IsValid {
        get { return _isValid; }
    }

    public T Previous {
        get { return _previous; }
    }

    public T Current {
        get { return _internal.Current; }
    }

    public bool MoveNext() {
        if (_isValid)
            _previous = _internal.Current;

        return (_isValid = _internal.MoveNext());
    }

    public void Dispose() {
        _internal.Dispose();
    }

    #region Explicit Interface Members

    object System.Collections.IEnumerator.Current {
        get { return Current; }
    }

    void System.Collections.IEnumerator.Reset() {
        _internal.Reset();
        _previous = default(T);
        _isValid = false;
    }

    #endregion

}

(Я назвал это ChipmunkEnumerator , потому что сохранение предыдущего значения напомнило мне о том, как у бурундуков есть мешочки на щеках, где они Держите орехи. Это действительно важно? Прекратите смеяться надо мной.)

Теперь использовать этот класс в методе расширения для обеспечения именно того поведения, которое вы хотите, не так уж сложно!

Обратите внимание, что ниже я определил GroupConsecutive , чтобы фактически возвращать IEnumerable > по той простой причине, что если они все равно сгруппированы по ключу, имеет смысл возвращать IGrouping , а не просто IEnumerable . Как оказалось, это все равно поможет нам позже ...

public static IEnumerable<IGrouping<TKey, T>> GroupConsecutive<T, TKey>(this IEnumerable<T> source, Func<T, TKey> keySelector)
    where TKey : IEquatable<TKey> {

    using (var e = new ChipmunkEnumerator<T>(source.GetEnumerator())) {
        if (!e.MoveNext())
            yield break;

        while (e.IsValid) {
            yield return e.GetNextDuplicateGroup(keySelector);
        }
    }
}

public static IEnumerable<IGrouping<T, T>> GroupConsecutive<T>(this IEnumerable<T> source)
    where T : IEquatable<T> {

    return source.GroupConsecutive(x => x);
}

private static IGrouping<TKey, T> GetNextDuplicateGroup<T, TKey>(this ChipmunkEnumerator<T> e, Func<T, TKey> keySelector)
    where TKey : IEquatable<TKey> {

    return new Grouping<TKey, T>(keySelector(e.Current), e.EnumerateNextDuplicateGroup(keySelector));
}

private static IEnumerable<T> EnumerateNextDuplicateGroup<T, TKey>(this ChipmunkEnumerator<T> e, Func<T, TKey> keySelector)
    where TKey : IEquatable<TKey> {

    do {
        yield return e.Current;

    } while (e.MoveNext() && keySelector(e.Previous).Equals(keySelector(e.Current)));
}

(Для реализации этих методов я написал простой класс Grouping , который реализует IGrouping самым простым из возможных способов. Я опустил код, чтобы продолжать двигаться дальше ...)

Хорошо, проверьте это.Я думаю, что приведенный ниже пример кода довольно хорошо отражает нечто похожее на более реалистичный сценарий, который вы описали в своем обновленном вопросе.

var entries = new List<KeyValuePair<string, int>> {
    new KeyValuePair<string, int>( "Dan", 10 ),
    new KeyValuePair<string, int>( "Bill", 12 ),
    new KeyValuePair<string, int>( "Dan", 14 ),
    new KeyValuePair<string, int>( "Dan", 20 ),
    new KeyValuePair<string, int>( "John", 1 ),
    new KeyValuePair<string, int>( "John", 2 ),
    new KeyValuePair<string, int>( "Bill", 5 )
};

var dupeGroups = entries
    .GroupConsecutive(entry => entry.Key);

foreach (var dupeGroup in dupeGroups) {
    Console.WriteLine(
        "Key: {0} Sum: {1}",
        dupeGroup.Key.PadRight(5),
        dupeGroup.Select(entry => entry.Value).Sum()
    );
}

Вывод:

Key: Dan   Sum: 10
Key: Bill  Sum: 12
Key: Dan   Sum: 34
Key: John  Sum: 3
Key: Bill  Sum: 5

Обратите внимание, что это также решает проблему с моим исходным ответом на работу с объектами IEnumerator , которые были типами значений. (При таком подходе это не имеет значения.)

Если вы попробуете позвонить сюда ToList , все равно будет проблема, и вы сами это узнаете, если попробуете. Но учитывая, что вы включили отложенное выполнение в качестве требования , я сомневаюсь, что вы все равно будете это делать. Для foreach это работает.


Оригинальное, беспорядочное и несколько глупое решение

Что-то подсказывает мне, что я буду полностью опровергнут за то, что говорю это, но ...

Да , это возможно (я думаю). См. Ниже чертовски беспорядочное решение, которое я собрал вместе. (Улавливает исключение, чтобы узнать, когда он закончен, так что вы знаете , что это отличный дизайн!)

Теперь, точка зрения Джона о наличии очень реальной проблемы в случае, если вы попытаетесь это сделать, например , ToList , а затем доступ к значениям в результирующем списке по индексу, является полностью допустимым. Но если ваше только намерение состоит в том, чтобы иметь возможность перебирать IEnumerable , используя foreach - а вы только , сделав это в вашем собственном коде - тогда, я думаю, это может сработать для вас.

В любом случае, вот краткий пример того, как это работает:

var ints = new int[] { 1, 3, 3, 4, 4, 4, 5, 2, 3, 1, 6, 6, 6, 5, 7, 7, 8 };

var dupeGroups = ints.GroupConsecutiveDuplicates(EqualityComparer<int>.Default);

foreach (var dupeGroup in dupeGroups) {
    Console.WriteLine(
        "New dupe group: " +
        string.Join(", ", dupeGroup.Select(i => i.ToString()).ToArray())
    );
}

Вывод:

New dupe group: 1
New dupe group: 3, 3
New dupe group: 4, 4, 4
New dupe group: 5
New dupe group: 2
New dupe group: 3
New dupe group: 1
New dupe group: 6, 6, 6
New dupe group: 5
New dupe group: 7, 7
New dupe group: 8

А теперь (беспорядочный, как дерьмо) код:

Обратите внимание, что, поскольку этот подход требует передачи фактического перечислителя между несколькими разными методами, он не будет работать , если этот перечислитель является типом значения, поскольку вызовы MoveNext в одном методе влияют только на локальную копию.

public static IEnumerable<IEnumerable<T>> GroupConsecutiveDuplicates<T>(this IEnumerable<T> source, IEqualityComparer<T> comparer) {
    using (var e = source.GetEnumerator()) {
        if (e.GetType().IsValueType)
            throw new ArgumentException(
                "This method will not work on a value type enumerator."
            );

        // get the ball rolling
        if (!e.MoveNext()) {
            yield break;
        }

        IEnumerable<T> nextDuplicateGroup;

        while (e.FindMoreDuplicates(comparer, out nextDuplicateGroup)) {
            yield return nextDuplicateGroup;
        }
    }
}

private static bool FindMoreDuplicates<T>(this IEnumerator<T> enumerator, IEqualityComparer<T> comparer, out IEnumerable<T> duplicates) {
    duplicates = enumerator.GetMoreDuplicates(comparer);

    return duplicates != null;
}

private static IEnumerable<T> GetMoreDuplicates<T>(this IEnumerator<T> enumerator, IEqualityComparer<T> comparer) {
    try {
        if (enumerator.Current != null)
            return enumerator.GetMoreDuplicatesInner(comparer);
        else
            return null;

    } catch (InvalidOperationException) {
        return null;
    }
}

private static IEnumerable<T> GetMoreDuplicatesInner<T>(this IEnumerator<T> enumerator, IEqualityComparer<T> comparer) {
    while (enumerator.Current != null) {
        var current = enumerator.Current;
        yield return current;

        if (!enumerator.MoveNext())
            break;

        if (!comparer.Equals(current, enumerator.Current))
            break;
    }
}
3
ответ дан 6 December 2019 в 21:11
поделиться

Вторая проблема - проблема. Вот почему:

var groups = CallMagicGetGroupsMethod().ToList();
foreach (string x in groups[3])
{
    ...
}
foreach (string x in groups[0])
{
    ...
}

Здесь он пытается перебрать четвертую группу, а затем первую группу ... это явно будет работать, только если все группы буферизованы или , он может перечитать последовательность, ни то, ни другое. из которых идеально.

Я подозреваю, что вам нужен более «реактивный» подход - я не знаю наверняка, делают ли Reactive Extensions то, что вы хотите («последовательное» требование необычно), но вы должны в основном предоставить своего рода действие, которое будет выполнено для каждой группы ... таким образом, методу не нужно будет беспокоиться о том, чтобы вернуть вам что-то, что можно было бы использовать позже, после того, как он уже закончил чтение.

Дайте мне знать, если вы хотите, чтобы я попытался найти решение в Rx, или вам понравится что-то вроде:

void GroupConsecutive(IEnumerable<string> items,
                      Action<IEnumerable<string>> action)
2
ответ дан 6 December 2019 в 21:11
поделиться

Вот решение, которое, как мне кажется, удовлетворяет вашим требованиям, работает с любым типом элементов данных и является довольно коротким и читабельным:

public static IEnumerable<IEnumerable<T>> Partition<T>(this IEnumerable<T> list)
{
    var current = list.FirstOrDefault();

    while (!Equals(current, default(T))) {
        var cur = current;
        Func<T, bool> equalsCurrent = item => item.Equals(cur);
        yield return list.TakeWhile(equalsCurrent);
        list = list.SkipWhile(equalsCurrent);
        current = list.FirstOrDefault();
    }
}

Примечания:

  1. Отложенное выполнение есть (и TakeWhile, и SkipWhile делают это).
  2. Я думаю, что при этом итерация по всей коллекции выполняется только один раз (с SkipWhile); при обработке возвращаемых IEnumerables итерация по коллекции выполняется еще раз, но само разбиение выполняется только один раз.
  3. Если вам не важны типы значений, вы можете добавить ограничение и изменить условие while на проверку на null.

Если я в чем-то ошибаюсь, мне будут особенно интересны комментарии, указывающие на ошибки!

Очень важное замечание:

Это решение не позволит вам перечислять созданные перечислимые в любом порядке, кроме того, в котором оно их предоставляет. Однако, я думаю, что автор оригинального сообщения довольно ясно дал понять в комментариях, что это не проблема.

2
ответ дан 6 December 2019 в 21:11
поделиться
Другие вопросы по тегам:

Похожие вопросы: