2010-03-21 11 views
5

Estoy esperando una forma concisa para realizar la siguiente transformación. Quiero transformar las letras de las canciones. La entrada será algo como esto:Tricky transformación de cadena (con suerte) en LINQ

Verse 1 lyrics line 1 
Verse 1 lyrics line 2 
Verse 1 lyrics line 3 
Verse 1 lyrics line 4 

Verse 2 lyrics line 1 
Verse 2 lyrics line 2 
Verse 2 lyrics line 3 
Verse 2 lyrics line 4 

Y quiero transformarlos para la primera línea de cada verso se agrupan como en:

Verse 1 lyrics line 1 
Verse 2 lyrics line 1 

Verse 1 lyrics line 2 
Verse 2 lyrics line 2 

Verse 1 lyrics line 3 
Verse 2 lyrics line 3 

Verse 1 lyrics line 4 
Verse 2 lyrics line 4 

Letras, obviamente, será desconocida, pero el espacio en blanco línea marca una división entre los versos en la entrada.

Respuesta

3

Tengo unos pocos métodos de extensión que siempre guardo que hacen que este tipo de procesamiento sea muy simple. La solución en su totalidad va a ser más larga que otras, pero estos son métodos útiles para tener cerca, y una vez que tenga los métodos de extensión en su lugar, la respuesta es muy corta y fácil de leer.

En primer lugar, hay un método cremallera que lleva un número arbitrario de secuencias:

public static class EnumerableExtensions 
{ 
    public static IEnumerable<T> Zip<T>(
     this IEnumerable<IEnumerable<T>> sequences, 
     Func<IEnumerable<T>, T> aggregate) 
    { 
     var enumerators = sequences.Select(s => s.GetEnumerator()).ToArray(); 
     try 
     { 
      while (enumerators.All(e => e.MoveNext())) 
      { 

       var items = enumerators.Select(e => e.Current); 
       yield return aggregate(items); 
      } 
     } 
     finally 
     { 
      foreach (var enumerator in enumerators) 
      { 
       enumerator.Dispose(); 
      } 
     } 
    } 
} 

Luego hay un método de Split, que hace más o menos lo mismo a un IEnumerable<T> que string.Split hace en una cadena:

public static IEnumerable<IEnumerable<T>> Split<T>(this IEnumerable<T> items, 
    Predicate<T> splitCondition) 
{ 
    using (IEnumerator<T> enumerator = items.GetEnumerator()) 
    { 
     while (enumerator.MoveNext()) 
     { 
      yield return GetNextItems(enumerator, splitCondition).ToArray(); 
     } 
    } 
} 

private static IEnumerable<T> GetNextItems<T>(IEnumerator<T> enumerator, 
    Predicate<T> stopCondition) 
{ 
    do 
    { 
     T item = enumerator.Current; 
     if (stopCondition(item)) 
     { 
      yield break; 
     } 
     yield return item; 
    } while (enumerator.MoveNext()); 
} 

una vez que tenga estas extensiones en su lugar, la solución del problema de canto lírico es un pedazo de la torta:

string lyrics = ... 
var verseGroups = lyrics 
    .Split(new[] { Environment.NewLine }, StringSplitOptions.None) 
    .Select(s => s.Trim()) // Optional, if there might be whitespace 
    .Split(s => string.IsNullOrEmpty(s)) 
    .Zip(seq => string.Join(Environment.NewLine, seq.ToArray())) 
    .Select(s => s + Environment.NewLine); // Optional, add space between groups 
+0

¡Método ZIP muy útil! – Larsenal

0

Tome su entrada como una cadena grande. Luego determina la cantidad de líneas en un verso.

Use .Split para obtener una matriz de cadenas, cada elemento ahora es una línea. A continuación, recorra el número de líneas que tiene y use stringbuilder para anexar SplitStrArray (i) y SplitStrArray (líneas i + en un verso).

Creo que será el mejor enfoque. No digo que LINQ no sea increíble, pero parece una tontería decir: 'Tengo un problema y quiero usar esta herramienta para resolverlo'.

"Tengo que clavar un tornillo en la pared, pero quiero usar un martillo". Si está decidido, probablemente encontrará la forma de usar el martillo; pero en mi humilde opinión, ese no es el mejor curso de acción. Tal vez alguien más tenga un ejemplo LINQ realmente increíble que lo haga muy fácil y me sentiré tonto por publicarlo ...

+0

Sí, hacer esto procesalmente esto sería fácil. Como este es un "código de fin de semana" no crítico, tenía curiosidad de saber si habría alguna manera de hacerlo en un LINQ one-liner. – Larsenal

+0

No es que Linq no sea una buena herramienta para esto, sino que las transformaciones particulares que necesita no son parte de la biblioteca estándar de Linq. Necesita un método 'Split 'y un método' Zip', ninguno de los cuales es estándar, pero ambos son fáciles de escribir. – Aaronaught

+3

Zip se está agregando a .NET 4 (http://msdn.microsoft.com/en-us/library/dd267698%28VS.100%29.aspx). –

1

Probablemente haya una forma más concisa de hacer esto, pero aquí hay una solución que funciona entrada válida:

 var output = String.Join("\r\n\r\n", // join it all in the end 
     Regex.Split(input, "\r\n\r\n") // split on blank lines 
      .Select(v => Regex.Split(v, "\r\n")) // now split lines in each verse 
      .SelectMany(vl => vl.Select((lyrics, i) => new { Line = i, Lyrics = lyrics })) // flatten things out, but attach line number 
      .GroupBy(b => b.Line).Select(c => new { Key = c.Key, Value = c }) // group by line number 
      .Select(e => String.Join("\r\n", e.Value.Select(f => f.Lyrics).ToArray())).ToArray()); 

Obviamente esto es bastante feo. Nada en absoluto una sugerencia para el código de producción.

0

Dale una oportunidad. Regex.Split se utiliza para evitar las entradas en blanco adicionalesString.Split se puede utilizar para determinar dónde se produce la primera línea en blanco con la ayuda del método Array.FindIndex. Esto indica la cantidad de versículos disponibles entre cada línea en blanco (dado que el formato es consistente por supuesto). A continuación, filtramos las líneas en blanco y determinamos el índice de cada línea y las agrupamos por el módulo del índice mencionado anteriormente.

string input = @"Verse 1 lyrics line 1 
Verse 1 lyrics line 2 
Verse 1 lyrics line 3 
Verse 1 lyrics line 4 
Verse 1 lyrics line 5 

Verse 2 lyrics line 1 
Verse 2 lyrics line 2 
Verse 2 lyrics line 3 
Verse 2 lyrics line 4 
Verse 2 lyrics line 5 

Verse 3 lyrics line 1 
Verse 3 lyrics line 2 
Verse 3 lyrics line 3 
Verse 3 lyrics line 4 
Verse 3 lyrics line 5 
"; 

// commented original Regex.Split approach 
//var split = Regex.Split(input, Environment.NewLine); 
var split = input.Split(new[] { Environment.NewLine }, StringSplitOptions.None); 
// find first blank line to determine # of verses 
int index = Array.FindIndex(split, s => s == ""); 
var result = split.Where(s => s != "") 
        .Select((s, i) => new { Value = s, Index = i }) 
        .GroupBy(item => item.Index % index); 

foreach (var group in result) 
{ 
    foreach (var item in group) 
    { 
     Console.WriteLine(item.Value); 
    }   
    Console.WriteLine(); 
} 
+0

No lo hacen realmente Necesito recortarlos, los recorté porque alineé todas las letras en mi ejemplo. Si los desliza hacia el borde ya que el recorte ya no es necesario. Esto dependerá de la entrada. Si usa un lector de línea de un archivo de texto de nuevo no sería un problema. Por lo general uso .Trim() de todos modos para asegurarme de que mis cadenas están "limpias" –

+0

@Matthew gracias por los comentarios. Inicialmente estaba tratando de evitar el 'Regex.Split' y parecía tener líneas en blanco cuando usaba el 'Split' normal sin ajustarlos. Tendré que volver sobre mis pasos para reproducir eso y descubrir qué sucedió. –

+0

¿Sería posible que tu línea vacía tuviera un espacio o una pestaña? en ella por ¿accidente? Es por eso que normalmente uso .Trim() antes de verificar si está vacío. Ayuda a sortear esos molestos insectos que no puedes "ver". –

1

LINQ es tan dulce ... Me encanta.

static void Main(string[] args) 
{ 
    var lyrics = @"Verse 1 lyrics line 1 
        Verse 1 lyrics line 2 
        Verse 1 lyrics line 3 
        Verse 1 lyrics line 4 

        Verse 2 lyrics line 1 
        Verse 2 lyrics line 2 
        Verse 2 lyrics line 3 
        Verse 2 lyrics line 4"; 
    var x = 0; 
    var indexed = from lyric in lyrics.Split(new[] { Environment.NewLine }, 
              StringSplitOptions.None) 
        let line = lyric.Trim() 
        let indx = line == string.Empty ? x = 0: ++x 
        where line != string.Empty 
        group line by indx; 

    foreach (var trans in indexed) 
    { 
     foreach (var item in trans) 
      Console.WriteLine(item); 
     Console.WriteLine(); 
    } 
    /* 
     Verse 1 lyrics line 1 
     Verse 2 lyrics line 1 

     Verse 1 lyrics line 2 
     Verse 2 lyrics line 2 

     Verse 1 lyrics line 3 
     Verse 2 lyrics line 3 

     Verse 1 lyrics line 4 
     Verse 2 lyrics line 4 
    */ 
} 
+5

El estado de mutación ('++ x') dentro de una expresión LINQ no es un buen estilo porque asume un cierto orden de procesamiento. Puede funcionar aquí, pero puede no funcionar si pones un '.AsParallel()' después de la división, por ejemplo. – Gabe

+0

Hay muchas cosas que "no deberían" hacerse, pero de hecho se hacen de todos modos porque son la forma más fácil de hacerlo. Todos los ejemplos requerirán un orden de procesamiento conocido, por lo que todos tendrán problemas con las versiones "mágicas" de multi-threading. Hay cosas que somos programadores e ingenieros que debemos entender y esperar. Algunas veces los sacrificios deben hacerse. Siéntete libre de crear tu propio ejemplo si tienes un problema con el mío. –