Хитрое строковое преобразование (надо надеяться), в 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

И я хочу преобразовать их так, первая строка каждого стиха группируется как в:

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
задан Larsenal 21 March 2010 в 03:37
поделиться

5 ответов

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

Во-первых, есть метод Zip, который принимает произвольное количество последовательностей:

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

Затем есть метод Split, который делает примерно то же самое с IEnumerable , что строка. выполняет со строкой:

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

После того, как у вас есть эти расширения, решение проблемы песни и текста становится простым:

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
3
ответ дан 14 December 2019 в 19:09
поделиться

Вероятно, есть более краткий способ сделать это, но вот одно решение, которое работает при правильном вводе:

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

Очевидно, это довольно уродливо. Совсем не предложение для производственного кода.

1
ответ дан 14 December 2019 в 19:09
поделиться

LINQ такой милый ... Мне он просто нравится.

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
     */
}
1
ответ дан 14 December 2019 в 19:09
поделиться

Примите ввод как одну большую строку. Затем определите количество строк в стихе.

Используйте .Split, чтобы получить массив строк, каждый элемент теперь является строкой. Затем переберите количество имеющихся строк и используйте конструктор строк, чтобы добавить SplitStrArray (i) и SplitStrArray (i + строки в стихе).

Думаю, это будет лучший подход. Я не говорю, что LINQ не так хорош, но глупо говорить: «У меня есть проблема, и я хочу использовать этот инструмент для ее решения».

«Мне нужно ввернуть шуруп в стену, но я хочу воспользоваться молотком». Если вы настроены решительно, вы, вероятно, найдете способ использовать молоток; но ИМХО, это не лучший вариант действий. Может быть, у кого-то еще будет действительно отличный пример LINQ, который упростит его, и я буду чувствовать себя глупо, разместив это ....

0
ответ дан 14 December 2019 в 19:09
поделиться

Попробуйте. Regex.Split используется для предотвращения лишних пустых записей String.Split может использоваться для определения места первой пустой строки с помощью Array.FindIndex ] метод. Это указывает количество стихов, доступных между каждой пустой строкой (при условии, что формат, конечно, согласован). Затем мы отфильтровываем пустые строки, определяем индекс каждой строки и группируем их по модулю вышеупомянутого индекса.

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
ответ дан 14 December 2019 в 19:09
поделиться
Другие вопросы по тегам:

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