LINQ中棘手的字符串转换(希望如此)

时间:2010-03-21 03:37:18

标签: c# linq string

我希望以简洁的方式执行以下转换。我想改变歌词。输入看起来像这样:

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 个答案:

答案 0 :(得分:3)

我总是保留一些扩展方法,这使得这种处理非常简单。整个解决方案将比其他解决方案更长,但这些方法是有用的方法,一旦你有扩展方法,那么答案非常简短易读。

首先,有一个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<T> string.Split对字符串的作用大致相同:

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

答案 1 :(得分:1)

可能有一种更简洁的方法可以做到这一点,但这里有一个解决方案可以提供有效的输入:

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

显然这很难看。完全不是生产代码的建议。

答案 2 :(得分:1)

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
     */
}

答案 3 :(得分:0)

将您的输入作为一个大字符串。然后确定一节经文中的行数。

使用.Split获取字符串数组,每个项目现在都是一行。然后遍历你拥有的行数,并使用stringbuilder附加SplitStrArray(i)和SplitStrArray(i +中的行+)。

我认为这将是最好的方法。我不是说LINQ不是很棒,但似乎很愚蠢地说,“我有一个问题,我想用这个工具来解决它”。

“我必须把螺丝钉在墙上 - 但我想用锤子”。如果你有决心,你可能会找到一种方法来使用锤子;但恕我直言,这不是最好的行动方案。也许别人会有一个非常棒的LINQ例子让它变得非常简单,我会觉得发布这个很傻....

答案 4 :(得分:0)

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