2010-05-13 26 views
7

Tengo un problema sin interés: Dada una IEnumerable<string>, es posible producir una secuencia de IEnumerable<IEnumerable<string>> que los grupos idénticos cuerdas adyacentes en una sola pasada?La agrupación de artículos idénticos consecutivos: IEnumerable <T> a IEnumerable <IEnumerable <T>>

Déjame explicarte.

1. Ejemplo básico ilustrativa:

Teniendo en cuenta lo siguiente IEnumerable<string> (representación pseudo):

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

Cómo obtener un IEnumerable<IEnumerable<string>> que daría algo de la forma:

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

El prototipo de método sería:

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

Pero también podría ser:

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

... donde action serían llamados para cada subsecuencia.

2. Más complicada muestra

Ok, la primera muestra es muy simple, y sólo tiene como objetivo hacer el intento de alto nivel claro.

Ahora imaginemos que estamos tratando con IEnumerable<Anything>, donde Anything es un tipo definido de esta manera:

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

Ahora queremos generar las subsecuencias basada en la clave, (grupo todos los consecutiva Anything que tienen la misma clave) para su uso posterior con el fin de calcular el valor total por grupo:

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. notas importantes

  • Sólo una iteración sobre la secuencia original
  • No hay colecciones intermedias asignaciones (que pueden suponer millones de artículos en la secuencia original, y millones consecutivos artículos en cada grupo)
  • enumeradores Mantener y ejecución deferida comportamiento
  • Podemos suponer que las subsecuencias resultantes se iterarán solo una vez y se iterarán en orden.

¿Es posible, y cómo lo escribiría?

+2

im asumiendo en su respuesta muestra que quiere decir { "b", "b", "b"} –

+0

@ Josh: Buena captura - He arreglado el problema, gracias! –

+0

En su ejemplo complicado, Sum debe repetir la colección por segunda vez. ¿De qué sirve restringir "Grupo" a una iteración si el código de llamada repetirá los mismos elementos nuevamente? –

Respuesta

5

¿Esto es lo que estás buscando?

  • Iterar la lista una sola vez.
  • Defer ejecución.
  • Sin colecciones intermedias (mi otra publicación falló en este criterio).

Esta solución se basa en el estado del objeto porque es difícil compartir el estado entre dos métodos de IEnumerable que utilizan el rendimiento (sin ref o out params).

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

Editar: Método de extensión agregado para un uso más limpio. Se corrigió la lógica de prueba de bucle para que primero se evalúe "Más".

Editar: Desechar el empadronador cuando termine

+0

+1: se ve bien para mí – Jon

+0

Solución simple y correcta, ¡gracias! –

+0

+1: Bien hecho. –

2

Su segunda viñeta es la problemática.He aquí por qué:

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

aquí, que está tratando de iterar sobre el cuarto grupo y luego el primer grupo ... eso no es claramente sólo va a funcionar si todos los grupos se almacenan o se puede volver a leer la secuencia, ni de lo cual es ideal

Sospecho que usted quiere un enfoque más "reactivo" - No sé de improviso si Reactive Extensions hace lo que quiere (el requisito "consecutivo" es inusual) pero básicamente debe proporcionar algún tipo de acción para ser ejecutado en cada grupo ... de esa manera el método no tendrá que preocuparse por tener que devolverte algo que pueda usarse más adelante, una vez que haya terminado de leerse.

Avisadme si me gustaría tratar de encontrar una solución dentro de Rx, o si sería feliz con algo como:

void GroupConsecutive(IEnumerable<string> items, 
         Action<IEnumerable<string>> action) 
Solución
+1

Entiendo perfectamente lo que dices. Sin embargo, puede considerar que tengo control total sobre el código de llamada y que cada subsecuencia se repetirá solo una vez y en orden. "Proporcionar una acción para ejecutar en cada grupo": cómo pasar el grupo (como IEnumerable ) a la acción? –

+0

Ese es un muy buen punto. Creo que algo parecido a lo que el OP intenta hacer, en espíritu, sin embargo, * es * posible. Solo necesita comprender sus limitaciones, por ejemplo, que tratar de usar el valor resultante como cualquier otro 'IEnumerable' (por ejemplo, al invocar' ToList' en él) va a causar problemas. –

+0

@Romain: 'action (group);' donde por supuesto 'group es IEnumerable '. ¿Apagón momentáneo? – Jon

3

Mucho mejor que cumple todos los requisitos

OK, deseche mi solución anterior (la dejo abajo, solo para referencia). Este es un enfoque mucho mejor que se me ocurrió después de hacer mi publicación inicial.

Escriba una nueva clase que implemente IEnumerator<T> y proporcione algunas propiedades adicionales: IsValid y Previous. Esto es todo lo que necesita para resolver todo el problema con tener que mantener el estado dentro de un bloque iterador usando yield.

Así es como lo hice (bastante trivial, como se puede ver):

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 

} 

(me llamaron a este un ChipmunkEnumerator porque mantener el valor anterior me recordó lo ardillas tienen bolsas en las mejillas donde guardan ¿Realmente importa? Deje de burlarse de mí.)

Ahora, utilizar esta clase en un método de extensión para proporcionar exactamente el comportamiento que desea no es tan difícil.

en cuenta que a continuación he definido GroupConsecutive vuelven en realidad a un IEnumerable<IGrouping<TKey, T>> por la sencilla razón de que, si estas se agrupan por la clave de todos modos, tiene sentido para devolver un IGrouping<TKey, T> en lugar de sólo una IEnumerable<T>. Como resultado, esto va a ayudarnos a salir adelante de todos modos ...

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

(Para poner en práctica estos métodos, escribí una sencilla clase que implementa Grouping<TKey, T>IGrouping<TKey, T> de la manera más sencilla posible. He omitido el código sólo para seguir avanzando ...)

OK, échale un vistazo. Creo que el siguiente ejemplo de código captura muy bien algo parecido al escenario más realista que describiste en tu pregunta actualizada.

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

Salida:

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

cuenta de esto también corrige el problema con mi respuesta original de tratar con IEnumerator<T> objetos que eran los tipos de valor. (Con este enfoque, no importa.)

Todavía hay un problema si intenta llamar al ToList aquí, ya que lo averiguará si lo intenta. Pero teniendo en cuenta que incluyó la ejecución diferida como un requisito , dudo que lo haría de todos modos. Para un foreach, funciona.


solución original, sucio, y algo estúpida

Algo me dice que voy a ser totalmente refutada por decir esto, pero ...

, es posible (Creo). Vea a continuación una solución desordenada que arrojé. (Detecta una excepción a conocer cuando esté terminado, por lo que sabe es un gran diseño!)

Ahora, el punto de Jon acerca de que hay un problema muy real en el caso de que se intenta hacer, por ejemplo, ToList, y luego acceder a los valores en la lista resultante por índice, es totalmente válido. Pero si su única intención aquí es ser capaz de bucle sobre un IEnumerable<T> utilizando un foreach - y usted es solamente hacer esto en su propio código - entonces, bueno, creo que esto podría funcionar para usted .

De todos modos, aquí hay un rápido ejemplo de como funciona:

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

Salida:

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 

Y ahora para el (desordenada como basura) Código:

Nota que dado que este enfoque requiere pasar el enumerador real alrededor de una pocos métodos diferentes, no funcionarán si ese enumerador es un tipo de valor, ya que las llamadas a MoveNext en un método solo afectan a una copia local.

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

Hola @Dan, bien hecho. Esa es una solución correcta. ¡Gracias! –

+0

+1 para un uso mejorado Tuve la misma idea de 'IsValid' y' Previous'. Tu solución es un poco más bonita que la mía desde el punto de vista del uso, pero usa el mismo enfoque. – dss539

+0

@ dss539: Bueno, parece que las mentes geniales piensan igual;) Personalmente, me gusta la idea de tener un 'IEnumerator ' que proporcione propiedades 'Previous' y' IsValid', independientemente de cualquier problema específico, ya que creo que podría resultar útil en otros escenarios también. ¡Pero su enfoque es ciertamente más conciso! –

2

he aquí una solución que creo que satisface sus necesidades, funciona con cualquier tipo de elemento de datos, y es bastante corto y fácil de leer:

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

Notas:

  1. diferido la ejecución está allí (ambos TakeWhile y SkipWhile lo hacen).
  2. Creo que esto itera sobre toda la colección solo una vez (con SkipWhile); itera sobre la colección una vez más cuando procesa los IEnumerables devueltos, pero la partición misma itera solo una vez.
  3. Si no le importan los tipos de valores, puede agregar una restricción y cambiar la condición while a una prueba para null.

Si estoy de alguna manera equivocado, ¡me interesarían especialmente los comentarios que señalan los errores!

muy importante Aparte:

Esta solución no le permiten enumerar las enumerables producidos en cualquier orden distinto del que les proporciona en Sin embargo, creo que el cartel original ha sido bastante claro. en comentarios que esto no es un problema.

+0

Enfoque interesante, pero repite la lista completa dos veces. Se divide la iteración en fragmentos, pero cada elemento se compara dos veces (1 para Take, luego 1 para Skip). Además, esto excluye los valores predeterminados como parte del conjunto de datos (por ejemplo, cadenas nulas o valor entero 0). Aún así, esto es genial y no tengo un mejor enfoque. – dss539

+1

@dss: Bueno, cualquier solución obviamente necesita iterar una vez sobre la colección para particionarla (esto es lo que hace 'SkipWhile' aquí). La segunda iteración solo ocurre cuando * you * itera sobre los resultados que proporciona este método (solo * then * es 'TakeWhile' ejecutado). ¿Estoy equivocado en esto? En cuanto a los tipos de valores: como menciono, esto es lo mejor que se puede hacer si quieres apoyarlos. :-) – Jon

+0

¡Gracias por responder a Jon! Esta solución parece correcta, pero hay un pequeño problema con respecto a la primera restricción: al utilizar TakeWhile, SkipWhile lo hace iterar ** dos veces ** sobre cada grupo, por lo que itera la colección dos veces. –

Cuestiones relacionadas