在Java中将字符串拆分为相等长度的子字符串,同时保持单词边界

10

如何将一个字符串拆分成具有最大字符长度的相等部分,同时保持单词边界?

比如说,如果我想将字符串"hello world"拆分为最大7个字符的相等子字符串,则应该返回:

"hello "

"world"

但是我的当前实现返回

"hello w"

"orld   "

我正在使用从Split string to equal length substrings in Java获取的以下代码来将输入字符串分成相等的部分。

public static List<String> splitEqually(String text, int size) {
    // Give the list the right capacity to start with. You could use an array
    // instead if you wanted.
    List<String> ret = new ArrayList<String>((text.length() + size - 1) / size);

    for (int start = 0; start < text.length(); start += size) {
        ret.add(text.substring(start, Math.min(text.length(), start + size)));
    }
    return ret;
}

在将字符串分成子字符串时,是否可以保持单词边界?

更具体地说,我需要字符串分割算法考虑由空格提供的单词边界,而不仅仅依靠字符长度进行字符串分割,尽管字符长度也需要考虑,但更像是字符的最大范围,而不是硬编码的字符长度。


1
你能否再提供一个更多字数的输入/输出示例吗? - jeojavi
确保例子中的字符串 "need for speed hot pursuit" 在最大字符范围为16的情况下进行分割。我需要根据单词边界来拆分字符串,所以输出应该是 "need for speed " 和 "hot pursuit",但是目前我使用的实现方式导致得到的结果是 "need for speed h" 和 "ot pursuit "。 - Nav
那么规则是在最大字符范围内或之前的空格处分割?如果第一个单词比字符范围长怎么办?你会在中间分割吗?例如:长度为7的“reallylongwordisfirst and here are several regular words”,你期望得到:“reallylongwordisfirst”、“and”、“here”、“are”、“several”、“regular”、“words”吗? - mdewitt
我有一个最大长度为4000个字符的限制。我想知道是否有一个由4000个字符组成的单词,但无论如何,这是针对Android文本转语音引擎而言的。如果不考虑单词边界,它会影响单词的发音,另一方面,它也有一次可以接受的最大字符范围...所以我希望现在你能看到我的困境了。 - Nav
换句话说,我们可以假设所需子字符串的最大长度始终大于最长单词的长度。 - Pshemo
显示剩余3条评论
2个回答

16
如果我理解您的问题正确,那么这段代码应该可以满足您的需求(但它假定maxLenght等于或大于最长单词)。
String data = "Hello there, my name is not importnant right now."
        + " I am just simple sentecne used to test few things.";
int maxLenght = 10;
Pattern p = Pattern.compile("\\G\\s*(.{1,"+maxLenght+"})(?=\\s|$)", Pattern.DOTALL);
Matcher m = p.matcher(data);
while (m.find())
    System.out.println(m.group(1));

输出:

Hello
there, my
name is
not
importnant
right now.
I am just
simple
sentecne
used to
test few
things.

"\\G\\s*(.{1,"+maxLenght+"})(?=\\s|$)" 正则表达式的简短(或非简短)解释:

(让我们记得在Java中,\不仅在正则表达式中是特殊字符,在字符串字面量中也是特殊字符,所以要使用预定义的字符集,例如\d,我们需要将其写为"\\d",因为我们还需要转义字符串字面量中的那个\

  • \G - 表示锚点,代表先前匹配的结束位置,或者如果还没有匹配(当我们刚开始搜索时),则表示字符串的开头(与^相同)。
  • \s* - 表示零个或多个空格(\s表示空格,*表示“零个或多个”)
  • (.{1,"+maxLenght+"}) - 让我们将其分解成更多部分(在运行时,:maxLenght将保持类似10这样的数值,因此正则表达式将把它视为.{1,10}
    • . 表示任何字符(默认情况下,它可以表示任何字符,除了像\n\r这样的行分隔符,但是由于Pattern.DOTALL标志,现在它可以表示任何字符-如果您想从新的一行开始拆分每个句子,则可以摆脱此方法参数,因为它的开头将在任何情况下在新行中打印
    • {1,10} - 这是限定词,允许先前描述的元素出现1至10次(默认情况下会尝试找到最大匹配重复次数),
    • .{1,10} - 因此,根据我们刚才说的内容,它简单地表示“1至10个任意字符”
    • () - 括号创建,结构允许我们保留匹配的特定部分(在这里,我们在\\s*之后添加了括号,因为我们只想使用空格后的部分)
  • (?=\\s|$) - 是向前查看机制,将确保由.{1,10}匹配的文本之后有:

    • 空格(\\s

      或者(用|表示)

    • 它之后是字符串的结尾$

因此,由于.{1,10},我们可以匹配最多10个字符。但是,由于(?=\\s|$)之后,我们要求由.{1,10}匹配的最后一个字符不是未完成的单词的一部分(在它之后必须有空格或字符串结尾)。


2
我很快会解释这个正则表达式,现在先测试一下它是否符合你的要求,如果可以的话请告诉我它是否有效。 - Pshemo
谢谢,它完美地运行了...只有一个问题,我已经能够将字符串的各个组放入列表中...但是是否有一种方法可以指定数组列表的大小..我听说当数组列表预先初始化为长度值时速度更快。 - Nav
2
如果您想获得更好的性能,您应该避免调整ArrayList的大小(创建比当前数组大两倍的新数组,其中将存储当前元素和新元素,这可能是大型数组的昂贵过程)。为了避免这种情况,您可以使用大于预期元素数量的大小初始化列表,因此可以使用类似new ArrayList((int) (1.5 * text.length()) / size)的方式进行初始化。 - Pshemo
我将text.length()/size的结果乘以一个系数,因为数组需要额外的空间来存储那些不能在单个标记中使用的字符,比如在“not important”这个词中,“important”需要被放置在单独的标记中,因为它太长了。 - Pshemo
这很酷 - 我总是忘记 \\G。不过s/Lenght/Length/gi - Boris the Spider

3

非正则表达式解决方案,以防有些人更喜欢不使用正则表达式:

private String justify(String s, int limit) {
    StringBuilder justifiedText = new StringBuilder();
    StringBuilder justifiedLine = new StringBuilder();
    String[] words = s.split(" ");
    for (int i = 0; i < words.length; i++) {
        justifiedLine.append(words[i]).append(" ");
        if (i+1 == words.length || justifiedLine.length() + words[i+1].length() > limit) {
            justifiedLine.deleteCharAt(justifiedLine.length() - 1);
            justifiedText.append(justifiedLine.toString()).append(System.lineSeparator());
            justifiedLine = new StringBuilder();
        }
    }
    return justifiedText.toString();
}

测试:

String text = "Long sentence with spaces, and punctuation too. And supercalifragilisticexpialidocious words. No carriage returns, tho -- since it would seem weird to count the words in a new line as part of the previous paragraph's length.";
System.out.println(justify(text, 15));

输出:

Long sentence
with spaces,
and punctuation
too. And
supercalifragilisticexpialidocious
words. No
carriage
returns, tho --
since it would
seem weird to
count the words
in a new line
as part of the
previous
paragraph's
length.

它考虑到长度超过设定限制的单词,因此不会跳过它们(与正则表达式版本不同,后者在找到supercalifragilisticexpialidosus时只停止处理)。
PS:关于所有输入单词都预期短于设定限制的评论是在我想出这个解决方案之后做出的;)

如果整个字符串不包含空格,这个解决方案同样有效。它只是将字符串分割。 - Amio.io

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