2010-03-21 56 views
5

我希望有一個簡潔的方式來執行以下轉換。我想轉換歌詞。輸入將是這個樣子: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 

歌詞顯然是未知的,但空白線標誌着輸入中經文之間的區分。

回答

3

我有幾個擴展方法,我總是保持這種處理非常簡單。整體解決方案將比其他解決方案更長,但是這些解決方案是非常有用的方法,一旦你有了擴展方法,那麼答案非常簡短,易於閱讀。

首先,有一個壓縮方法,該方法序列任意數量的:

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

再有就是它做大致相同的事情的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 
+0

非常方便的ZIP方法! – Larsenal

0

將您的輸入視爲一個大字符串。然後確定一節經文中的行數。

使用.Split獲取一個字符串數組,每個項目現在是一條線。然後遍歷你所擁有的行數,然後使用stringbuilder來追加SplitStrArray(i)和SplitStrArray(一節中的i +行)。

我認爲這將是最好的方法。我並不是說LINQ不是很棒,但說'我有問題,我想用這個工具來解決它'似乎很愚蠢。

「我必須擰上牆 - 但我想用錘子」。如果你確定了,你可能會找到一種方法來使用錘子;但恕我直言,這不是最好的行動方針。也許別人會有一個非常棒的LINQ例子,這使得它非常容易,我會覺得發佈這個很愚蠢......

+0

是的,在程序上這樣做會很容易。由於這是非關鍵的「週末代碼」,我很好奇,是否有辦法在LINQ單線程中做到這一點。 – Larsenal

+0

這並不是說Linq不是一個好的工具,它只是你需要的特定轉換不是標準Linq庫的一部分。你需要一個'Split'方法和一個'Zip'方法,這兩個方法都不是標準的,但兩者都很容易編寫。 – Aaronaught

+3

將Zip添加到.NET 4中(http://msdn.microsoft.com/zh-cn/library/dd267698%28VS.100%29.aspx)。 –

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

顯然這很醜陋。完全沒有建議生產代碼。

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

它們並不是真的需要修剪,因爲我在我的例子中列出了所有的歌詞,如果你將它們滑動到邊緣作爲你的修剪不再需要,這取決於輸入,如果你使用線閱讀器我通常使用.Trim(),以確保我的字符串是「乾淨的」 –

+0

@Matthew感謝您的反饋。我最初試圖避免'Regex.Split'和似乎在使用普通的「Split」不用修剪它們時會出現空行,我將不得不重複我的步驟來重現並找出發生的事情。 –

+0

是否有可能您的空行有空格或製表符在裏面事故?這就是爲什麼我通常在檢查空之前使用.Trim()。幫助解決那些你無法「看到」的惱人的錯誤。 –

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

LINQ表達式中的變異狀態('++ x')不是很好的樣式,因爲它假定了某個處理順序。它可能在這裏工作,但是如果你在Split之後放了一個'.AsParallel()',它可能不起作用。 – Gabe

+0

有許多事情「不應該」完成,但事實上無論如何都是這樣做的,因爲它們是最簡單的方法。所有的例子都需要一個已知的處理順序,所以他們都會遇到與多線程的「魔術」版本有關的問題。我們是程序員和工程師必須瞭解和期望的東西。有時候必須作出犧牲。如果您遇到問題,請隨時創建您自己的示例。 –