2010-05-13 6 views
7

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

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

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

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

+2

IM предполагая в вашем образце ответа вы имеете в виду { «б», «б», «б»} –

+0

@Josh: Хороший улов - я исправил вопрос, спасибо! –

+0

В вашем сложном примере Sum должен повторить сборку во второй раз. Какой смысл ограничивать «Группировку» на одну итерацию, если вызывающий код снова будет повторять те же элементы? –

ответ

5

Это вы что искали?

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

Это решение зависит от состояния объекта, потому что сложно разделить состояние между двумя методами 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: Утилизировать перечислителем когда закончил

+0

+1: хорошо выглядит для меня – Jon

+0

Простое и правильное решение, спасибо! –

+0

+1: Красиво сделано. –

2

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

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) 
+1

Я прекрасно понимаю, что вы говорите. Однако вы можете считать, что я полностью контролирую код вызова и что каждая подпоследовательность будет повторяться только один раз и в порядке. «Предоставление действия для каждой группы» - Как передать группу (как IEnumerable ) в действие? –

+0

Это очень хороший момент. Я думаю, что что-то похожее на то, что ОП пытается сделать, по духу, но возможно. Ему просто нужно понять его ограничения, например, что попытка использовать полученное значение так же, как любое другое 'IEnumerable' (например, вызывая' ToList' на нем) вызовет проблемы. –

+0

@Romain: 'action (group);' где, конечно, 'группа IEnumerable '. Моментальное затемнение? – Jon

3

Way лучшее решение, которое отвечает всем требованиям

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

Создайте новый класс, который реализует 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 он работает.


Оригинал, Грязное, и в некоторой степени Stupid решение

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

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

Теперь точку Джона о там быть очень реальная проблема в том случае, если вы пытаетесь сделать, например, ToList, и затем получить доступ к значениям в результирующем списке по индексу, является полностью допустимым. Но если ваш только намерение здесь, чтобы быть в состоянии петли над IEnumerable<T> с помощью 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 

А теперь для (грязный, как дерьмо) Код:

Примечание что, поскольку этот подход требует прохождения фактического перечислителя вокруг betw een несколько разных методов, это не будет работать, если этот перечислитель является типом значения, поскольку вызовы 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; 
    } 
} 
+0

Эй, @ Дань, молодец. Это одно правильное решение. Спасибо! –

+0

+1 для улучшения использования Я имел такое же представление о 'IsValid' и' предыдущем'. Ваше решение немного лучше, чем у меня с точки зрения использования, но использует тот же подход. – dss539

+0

@ dss539: Ницца, похоже, что великие мысли думают одинаково;) Лично мне нравится идея 'IEnumerator ', которая предоставляет свойства 'Предыдущая' и' IsValid', независимо от какой-либо конкретной проблемы, поскольку я чувствую, что это может полезны и в других сценариях. Но ваш подход, безусловно, более краток! –

2

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

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.

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

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

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

+0

Интересный подход, но вы повторяете весь список дважды. Вы разбиваете итерацию на куски, но каждый предмет сравнивается дважды (1 для Take, затем 1 для Skip). Кроме того, это исключает значения по умолчанию как часть набора данных (например, нулевые строки или целочисленное значение 0). Тем не менее, это довольно круто, и у меня нет лучшего подхода. – dss539

+1

@dss: Любое решение, очевидно, должно повторять один раз над сборником, чтобы разделить его (вот что делает здесь «SkipWhile'). Вторая итерация происходит только тогда, когда * you * перебирает результаты, которые предоставляет этот метод (только * then * выполняется 'TakeWhile'). Я в этом не прав? Что касается типов значений: как я упоминаю, это лучшее, что можно сделать, если вы хотите их поддержать. :-) – Jon

+0

Спасибо, что ответил Джон! Это решение кажется правильным, но есть небольшая проблема, хотя в отношении первого ограничения: используя TakeWhile, тогда SkipWhile делает вас итерацией ** дважды ** над каждой группой, поэтому вы повторяете сбор дважды. –

Смежные вопросы