将长字符串分成每行60个字符但不要断开单词。

5

一定有更好的方法来实现这个功能。我只想将长字符串分成60个字符的行,但不要打断单词。因此,它不必添加到60个字符,只需少于60个字符即可。

下面的代码是我拥有的,并且它可以工作,但我认为还有更好的方法。有人有建议吗?

修改为使用StringBuilder并解决了删除重复单词的问题。 另外,不想使用正则表达式,因为我认为那比我现在拥有的效率低。

public static List<String> FormatMe(String Message)
{
    Int32 MAX_WIDTH = 60;
    List<String> Line = new List<String>();
    String[] Words;

    Message = Message.Trim();
    Words = Message.Split(" ".ToCharArray());

    StringBuilder s = new StringBuilder();
    foreach (String Word in Words)
    {
        s.Append(Word + " ");
        if (s.Length > MAX_WIDTH)
        {
            s.Replace(Word, "", 0, s.Length - Word.Length);
            Line.Add(s.ToString().Trim());
            s = new StringBuilder(Word + " ");
        }
    }

    if (s.Length > 0)
        Line.Add(s.ToString().Trim());

    return Line;
}

谢谢


你是在寻找更高效的算法还是更优秀的程序员来阅读这个友好的方法? - Nate
你为什么不使用泛型? - Nate
1
  1. 你的代码并没有按照你的期望工作。s.Replace(Word,"") 不仅会替换最后一个匹配项,也会替换字符串中任何部分匹配的 Word。
  2. s+=...会创建太多临时字符串对象,因为在C#中字符串是不可变的。尝试使用stringbuilder或string.Join()方法。
- Chansik Im
我正在寻找更高效的东西。 Chansik Im,谢谢你指出这一点。我已经修复了它。而且决定顺便使用字符串构建器。 - Tigran
我该如何使用泛型来优化这个呢? - Tigran
7个回答

7

这是另一个(现在已测试过)示例,非常类似于Keith的方法

static void Main(string[] args)
{
    const Int32 MAX_WIDTH = 60;

    int offset = 0;
    string text = Regex.Replace(File.ReadAllText("oneline.txt"), @"\s{2,}", " ");
    List<string> lines = new List<string>();
    while (offset < text.Length)
    {
        int index = text.LastIndexOf(" ", 
                         Math.Min(text.Length, offset + MAX_WIDTH));
        string line = text.Substring(offset,
            (index - offset <= 0 ? text.Length : index) - offset );
        offset += line.Length + 1;
        lines.Add(line);
    }
}

我使用手动替换所有换行符为" "的方式在这个文件上运行了它。

String.LastIndexOf(string value, int startIndex)。如何确保text.Substring(offset, text.LastIndexOf(" ", MAX_WIDTH))返回的字符串少于60个字符? - Chansik Im
@Rubens,更改并不能保证返回的字符串少于60个字符,因为您没有更改“text”。假设您有10K个以“ ”结尾的字符串“text”,text.LastIndexOf(“ ”,offset + MAX_WIDTH)将返回最后一个“ ”的索引,而不是在60个字符限制内。 - Chansik Im
@Chansik Im,我已经测试过了,请再看一下;谢谢。 - Rubens Farias
@Chansik,不客气!感谢你的所有反馈! - Rubens Farias
你最新的代码仍然存在问题,如果字符串中有一个单词的字符数超过了MAX_WIDTH。例如,尝试将MAX_WIDTH设置为10,然后使用此字符串“下面的代码片段说明了上面讨论的解决方案”。最后一行的长度将超过MAX_WIDTH。 - dalmate
显示剩余3条评论

1

另一个...

public static string SplitLongWords(string text, int maxWordLength)
{
    var reg = new Regex(@"\S{" + (maxWordLength + 1) + ",}");
    bool replaced;
    do
    {
        replaced = false;
        text = reg.Replace(text, (m) =>
        {
            replaced = true;
            return m.Value.Insert(maxWordLength, " ");                    
        });
    } while (replaced);

    return text;
}

欢迎来到stackoverflow!为了提高帖子的准确性,最好为示例代码提供一个简短的描述 :) - Picrofo Software

1

试试这个:

const Int32 MAX_WIDTH = 60;

string text = "...";
List<string> lines = new List<string>();
StringBuilder line = new StringBuilder();
foreach(Match word in Regex.Matches(text, @"\S+", RegexOptions.ECMAScript))
{
    if (word.Value.Length + line.Length + 1 > MAX_WIDTH)
    {
        lines.Add(line.ToString());
        line.Length = 0;
    }
    line.Append(String.Format("{0} ", word.Value));
}

if (line.Length > 0)
    line.Append(word.Value);

请注意查看:如何使用正则表达式添加换行符?

1

在正则表达式内部,匹配评估器函数(匿名方法)执行繁重的工作,并将新的行大小存储到 StringBuilder 中。我们不使用 Regex.Replace 方法的返回值,因为我们仅仅使用它的匹配评估器函数来实现从正则表达式调用内部进行换行的特性 - 只是为了好玩,因为我认为这很酷。

using System;
using System.Text;
using System.Text.RegularExpressions;

strInput 是你想要转换的行。

int MAX_LEN = 60;
StringBuilder sb = new StringBuilder();
int bmark = 0; //bookmark position

Regex.Replace(strInput, @".*?\b\w+\b.*?", 
    delegate(Match m) {
        if (m.Index - bmark + m.Length + m.NextMatch().Length > MAX_LEN 
                || m.Index == bmark && m.Length >= MAX_LEN) {
            sb.Append(strInput.Substring(bmark, m.Index - bmark + m.Length).Trim() + Environment.NewLine);
            bmark = m.Index + m.Length;
        } return null;
    }, RegexOptions.Singleline);

if (bmark != strInput.Length) // last portion
    sb.Append(strInput.Substring(bmark));

string strModified = sb.ToString(); // get the real string from builder

值得注意的是,在Match Evaluator中if表达式的第二个条件m.Index == bmark && m.Length >= MAX_LEN是作为一个异常条件,以防有一个单词超过60个字符(或超过设置的最大长度)- 这里不会对其进行拆分,只会将其存储在一行上 - 我想你可能需要在现实世界中为该条件创建第二个公式来连字它或做些其他处理。


正则表达式可能需要微调以正确处理标点符号,但我已经完成了这个示例并继续前进 - 它是灵活的,并提供了另一种实现换行的要点 - 匿名方法内核心逻辑仅有几行,比作者发布的核心逻辑更短,因此我认为这是一个可行的答案。它周围的所有支持内容使其变得更大。 - John K
如果你想将每个文本行捕获到集合元素或数组元素中,你可以很容易地用List<string>.Add(..)替换StringBuilder.Append(..)。 - John K

0

我尝试了原始解决方案,发现它并没有完全起作用。我稍微修改了一下使其可以运行。它现在对我有效,并解决了我之前遇到的问题。谢谢。 Jim。

public static List<String> FormatMe(String message)
    {
        int maxLength = 10;
        List<String> Line = new List<String>();
        String[] words;

        message = message.Trim();
        words = message.Split(" ".ToCharArray());

        StringBuilder sentence = new StringBuilder();
        foreach (String word in words)
        {
            if((sentence.Length + word.Length) <= maxLength)
            {
                sentence.Append(word + " ");

            }
            else
            {
                Line.Add(sentence.ToString().Trim());
                sentence = new StringBuilder(word + " ");
            }
        }

        if (sentence.Length > 0)
            Line.Add(sentence.ToString().Trim());

        return Line;
    }

    private void btnSplitText_Click(object sender, EventArgs e)
    {
        List<String> Line = new List<string>();
        string message = "The quick brown fox jumps over the lazy dog.";
        Line = FormatMe(message);
    }

0
List<string> lines = new List<string>();
while (message.Length > 60) {
  int idx = message.LastIndexOf(' ', 60);
  lines.Add(message.Substring(0, idx));
  message = message.Substring(idx + 1, message.Length - (idx + 1));
}
lines.Add(message);

你可能需要进行一些修改来处理多个空格、长度超过60个字符的单词等问题...


message = message.Substring(idx + 1, message.Length - (idx + 1)) <-- 可能会创建太多临时字符串,尤其是原始消息很大的情况下。虽然不太可能发生,但是想象一下原始字符串大小为86KB,足以分配到大对象堆中,并且您仅通过减去约60个字符来创建临时字符串。 是的,这太极端了 :) - Chansik Im
一切都取决于Substring是否实际上进行了复制,或者它只是保留了对父字符串的指针(我不知道C#在这方面的做法)。Rubens的答案通过保留起始索引而避免了这个问题,而不是显式地进行子字符串操作。 - Keith Randall
它返回一个新的字符串,但是引用与原始字符串相同的char[]数组。 - Keith Randall
因此,无论所选子字符串的大小如何,子字符串在O(1)时间内运行并使用O(1)空间。 - Keith Randall
很高兴知道Java处理方式不同。感谢您分享您的知识。不幸的是,目前看来C#实现String.Substring()的方式并非如此。请参见以下SO讨论@https://dev59.com/qUjSa4cB1Zd3GeqPEEg1 - Chansik Im
显示剩余3条评论

0

我会先保存原字符串的长度。然后,倒着开始,只需减去,因为我很可能通过从最后一个单词开始并向后移动而更快地下降到60以下,而不是构建字符串。

一旦我知道字符串的长度,就可以使用 StringBuilder 来构建新字符串。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接