代码之家  ›  专栏  ›  技术社区  ›  Late Starter

将字符串拆分成最大长度的行的最佳方式,而不打断单词

  •  22
  • Late Starter  · 技术社区  · 11 年前

    如果可能的话,我想将字符串拆分为指定最大长度的行,而不拆分任何单词(如果有超过最大行长度的单词,则必须拆分)。

    一如既往,我敏锐地意识到字符串是不可变的,最好使用StringBuilder类。我见过一些例子,其中字符串被拆分成单词,然后使用StringBuilder类构建行,但下面的代码对我来说似乎“更整洁”。

    我在描述中提到了“最佳”,而不是“最有效”,因为我也对代码的“口才”感兴趣。这些字符串永远不会是巨大的,通常会分成两到三行,而且数千行也不会发生这种情况。

    下面的代码真的很糟糕吗?

    private static IEnumerable<string> SplitToLines(string stringToSplit, int maximumLineLength)
    {
        stringToSplit = stringToSplit.Trim();
        var lines = new List<string>();
    
        while (stringToSplit.Length > 0)
        {
            if (stringToSplit.Length <= maximumLineLength)
            {
                lines.Add(stringToSplit);
                break;
            }
    
            var indexOfLastSpaceInLine = stringToSplit.Substring(0, maximumLineLength).LastIndexOf(' ');
            lines.Add(stringToSplit.Substring(0, indexOfLastSpaceInLine >= 0 ? indexOfLastSpaceInLine : maximumLineLength).Trim());
            stringToSplit = stringToSplit.Substring(indexOfLastSpaceInLine >= 0 ? indexOfLastSpaceInLine + 1 : maximumLineLength);
        }
    
        return lines.ToArray();
    }
    
    7 回复  |  直到 11 年前
        1
  •  23
  •   3vts    4 年前

    即使这篇文章已经3年了,我也希望用 Regex 以实现相同的目的:

    如果您希望拆分字符串,然后使用要显示的文本,可以使用以下选项:

    public string SplitToLines(string stringToSplit, int maximumLineLength)
    {
        return Regex.Replace(stringToSplit, @"(.{1," + maximumLineLength +@"})(?:\s|$)", "$1\n");
    }
    

    另一方面,如果您需要一个集合,则可以使用此选项:

    public MatchCollection SplitToLines(string stringToSplit, int maximumLineLength)
    {
        return Regex.Matches(stringToSplit, @"(.{1," + maximumLineLength +@"})(?:\s|$)");
    }
    

    笔记

    记住导入正则表达式( using System.Text.RegularExpressions; )

    您可以对匹配使用字符串插值:
    $@"(.{{1,{maximumLineLength}}})(?:\s|$)"

    这个 MatchCollection 工作起来几乎像 Array

    示例与说明匹配 here

        2
  •  14
  •   Enigmativity    4 年前

    如何将此作为解决方案:

    IEnumerable<string> SplitToLines(string stringToSplit, int maximumLineLength)
    {
        var words = stringToSplit.Split(' ').Concat(new [] { "" });
        return
            words
                .Skip(1)
                .Aggregate(
                    words.Take(1).ToList(),
                    (a, w) =>
                    {
                        var last = a.Last();
                        while (last.Length > maximumLineLength)
                        {
                            a[a.Count() - 1] = last.Substring(0, maximumLineLength);
                            last = last.Substring(maximumLineLength);
                            a.Add(last);
                        }
                        var test = last + " " + w;
                        if (test.Length > maximumLineLength)
                        {
                            a.Add(w);
                        }
                        else
                        {
                            a[a.Count() - 1] = test;
                        }
                        return a;
                    });
    }
    

    我修改了这个,更喜欢这个:

    IEnumerable<string> SplitToLines(string stringToSplit, int maximumLineLength)
    {
        var words = stringToSplit.Split(' ');
        var line = words.First();
        foreach (var word in words.Skip(1))
        {
            var test = $"{line} {word}";
            if (test.Length > maximumLineLength)
            {
                yield return line;
                line = word;
            }
            else
            {
                line = test;
            }
        }
        yield return line;
    }
    
        3
  •  9
  •   rzslk    11 年前

    我不认为你的解决方案太糟糕。然而,我认为你应该把三元分解成一个if-else,因为你正在测试两次相同的条件。您的代码可能也有错误。根据您的描述,您似乎需要行<=maxLineLength,但代码计算最后一个单词后的空格,并在<=比较导致有效<修剪字符串的行为。

    这是我的解决方案。

    private static IEnumerable<string> SplitToLines(string stringToSplit, int maxLineLength)
        {
            string[] words = stringToSplit.Split(' ');
            StringBuilder line = new StringBuilder();
            foreach (string word in words)
            {
                if (word.Length + line.Length <= maxLineLength)
                {
                    line.Append(word + " ");
                }
                else
                {
                    if (line.Length > 0)
                    {
                        yield return line.ToString().Trim();
                        line.Clear();
                    }
                    string overflow = word;
                    while (overflow.Length > maxLineLength)
                    {
                        yield return overflow.Substring(0, maxLineLength);
                        overflow = overflow.Substring(maxLineLength);
                    }
                    line.Append(overflow + " ");
                }
            }
            yield return line.ToString().Trim();
        }
    

    它比您的解决方案稍长,但应该更简单。它还使用StringBuilder,因此对于大型字符串来说速度更快。我对20000个单词进行了基准测试,每个单词从1到11个字符,每行10个字符宽。我的方法在14毫秒内完成,而你的方法是1373毫秒。

        4
  •  2
  •   shenku    11 年前

    试试这个(未经测试)

        private static IEnumerable<string> SplitToLines(string value, int maximumLineLength)
        {
            var words = value.Split(' ');
            var line = new StringBuilder();
    
            foreach (var word in words)
            {
                if ((line.Length + word.Length) >= maximumLineLength)
                {
                    yield return line.ToString();
                    line = new StringBuilder();
                }
    
                line.AppendFormat("{0}{1}", (line.Length>0) ? " " : "", word);
            }
    
            yield return line.ToString();
        }
    
        5
  •  1
  •   JoeH    3 年前
    • 比公认答案快约6倍
    • 比Regex版本快1.5倍以上 处于释放模式 (取决于线路长度)
    • 可以选择是否在行末尾保留空格(正则表达式版本始终保留空格)
        static IEnumerable<string> SplitToLines(string stringToSplit, int maximumLineLength, bool removeSpace = true)
            {
                int start = 0;
                int end = 0;
                for (int i = 0; i < stringToSplit.Length; i++)
                {
                    char c = stringToSplit[i];
                    if (c == ' ' || c == '\n')
                    {
                        if (i - start > maximumLineLength)
                        {
                            string substring = stringToSplit.Substring(start, end - start); ;
                            start = removeSpace ? end + 1 : end; // + 1 to remove the space on the next line
                            yield return substring;
                        }
                        else
                            end = i;
                    }
                }
                yield return stringToSplit.Substring(start); // remember last line
            }
    

    以下是用于测试速度的示例代码(同样,在您自己的机器上运行并在Release模式下测试以获得准确的计时) https://dotnetfiddle.net/h5I1GC
    发布模式下我机器上的计时.Net 4.8

    Accepted Answer: 667ms
    Regex: 368ms
    My Version: 117ms
    
        6
  •  0
  •   Charlie Honea    6 年前

    我的要求是在30个字符限制之前的最后一个空格处换行。 我是这样做的。希望这对任何人都有帮助。

     private string LineBreakLongString(string input)
            {
                var outputString = string.Empty;
                var found = false;
                int pos = 0;
                int prev = 0;
                while (!found)
                    {
                        var p = input.IndexOf(' ', pos);
                        {
                            if (pos <= 30)
                            {
                                pos++;
                                if (p < 30) { prev = p; }
                            }
                            else
                            {
                                found = true;
                            }
                        }
                        outputString = input.Substring(0, prev) + System.Environment.NewLine + input.Substring(prev, input.Length - prev).Trim();
                    }
    
                return outputString;
            }
    
        7
  •  -1
  •   Vimal CK    3 年前

    一种使用递归方法和 ReadOnlySpan (已测试)

    public static void SplitToLines(ReadOnlySpan<char> stringToSplit, int index, ref List<string> values)
    {
       if (stringToSplit.IsEmpty || index < 1) return;
       var nextIndex = stringToSplit.IndexOf(' ');
       var slice = stringToSplit.Slice(0, nextIndex < 0 ? stringToSplit.Length : nextIndex);
    
       if (slice.Length <= index)
       {
          values.Add(slice.ToString());
          nextIndex++;
       }
       else
       {
          values.Add(slice.Slice(0, index).ToString());
          nextIndex = index;
       }
    
       if (stringToSplit.Length <= index) return;
       SplitToLines(stringToSplit.Slice(nextIndex), index, ref values);
    }