Группировка последовательных идентичных элементов: от IEnumerable <T>до IEnumerable <IEnumerable <T>> - PullRequest
7 голосов
/ 13 мая 2010

У меня есть интересная проблема: учитывая 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. Важные замечания

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

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

Ответы [ 4 ]

5 голосов
/ 14 мая 2010

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

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

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

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

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

Редактировать: после завершения работы с перечислителем

3 голосов
/ 13 мая 2010

Way Better Solution, отвечающий всем требованиям

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

Напишите новый класс, который реализует IEnumerator<T> и предоставляет несколько дополнительных свойств: IsValid и Previous. Это все, что вам действительно нужно, чтобы разрешить весь беспорядок с необходимостью поддерживать состояние внутри блока итератора, используя 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<TKey, T>> по той простой причине, что, если они в любом случае сгруппированы по ключу, имеет смысл возвращать IGrouping<TKey, T>, а не просто IEnumerable<T> , Оказывается, это все равно поможет нам позже ...

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

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

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<T> объектами, которые были типами значений. (При таком подходе это не имеет значения.)

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


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

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

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

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

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

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;
    }
}
2 голосов
/ 13 мая 2010

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

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 голосов
/ 13 мая 2010

Ваша вторая пуля проблемная. И вот почему:

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)
...