最好的方法是将字符串分成最大长度的行,而不中断单词

本文关键字:单词 中断 方法 字符串 | 更新日期: 2023-09-27 17:49:45

我想把一个字符串分成指定最大长度的行,如果可能的话,不拆分任何单词(如果有一个单词超过了最大行长度,那么它将不得不被拆分)。

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

我在描述中提到了"最佳"而不是"最有效",因为我对代码的"口才"也很感兴趣。字符串永远不会太大,通常分成2行或3行,也不会发生数千行。

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

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

最好的方法是将字符串分成最大长度的行,而不中断单词

即使这篇文章是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|$)");
}
指出

记得导入regex (using System.Text.RegularExpressions;)

你可以在匹配中使用字符串插值:
$@"(.{{1,{maximumLineLength}}})(?:'s|$)"

MatchCollection几乎和Array一样工作

在这里匹配示例与解释

如何作为一个解决方案:

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

我认为你的解决方案还不错。然而,我认为你应该把你的三进制分解成if else,因为你要测试同样的条件两次。您的代码也可能有bug。根据您的描述,似乎您希望行<= 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,因此对于大字符串来说速度要快得多。我对20,000个从1到11个字符不等的单词进行了基准测试,每个单词分成10个字符宽的行。我的方法在14ms内完成,而你的方法在1373ms内完成。

Try this(未经测试)

    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();
    }
  • ~比接受的答案快6倍
  • 在释放模式(取决于行长)
  • 比Regex版本快1.5倍以上
  • 可选保留行尾空格或不保留行尾空格(regex版本总是保留空格)
    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
        }

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

Accepted Answer: 667ms
Regex: 368ms
My Version: 117ms

我的要求是在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;
        }

使用递归方法和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);
}