Я надеюсь на краткий способ выполнить следующее преобразование. Я хочу преобразовать лирику песни. Вход будет выглядеть примерно так:
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
Лирика, очевидно, будет неизвестна, но пустая строка отмечает подразделение между стихами во входе.
У меня есть несколько методов расширения, которые я всегда держу при себе, которые делают этот тип обработки очень простым. Решение в целом будет длиннее, чем другие, но это полезные методы, которые нужно иметь под рукой, и как только у вас есть методы расширения, ответ будет очень коротким и легко читаемым.
Во-первых, есть метод 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
Вероятно, есть более краткий способ сделать это, но вот одно решение, которое работает при правильном вводе:
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());
Очевидно, это довольно уродливо. Совсем не предложение для производственного кода.
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
*/
}
Примите ввод как одну большую строку. Затем определите количество строк в стихе.
Используйте .Split, чтобы получить массив строк, каждый элемент теперь является строкой. Затем переберите количество имеющихся строк и используйте конструктор строк, чтобы добавить SplitStrArray (i) и SplitStrArray (i + строки в стихе).
Думаю, это будет лучший подход. Я не говорю, что LINQ не так хорош, но глупо говорить: «У меня есть проблема, и я хочу использовать этот инструмент для ее решения».
«Мне нужно ввернуть шуруп в стену, но я хочу воспользоваться молотком». Если вы настроены решительно, вы, вероятно, найдете способ использовать молоток; но ИМХО, это не лучший вариант действий. Может быть, у кого-то еще будет действительно отличный пример LINQ, который упростит его, и я буду чувствовать себя глупо, разместив это ....
Попробуйте. 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();
}