1. 概述

在这个教程中,我们将学习如何自动在给定字符数后包裹句子。因此,我们的程序将返回一个带有换行的新字符串。

2. 通用算法

让我们考虑以下句子:Baeldung 是一个提供深入教程和文章的流行网站,涵盖各种编程和软件开发主题,主要关注 Java 和相关技术

我们希望每 n 个字符插入一行,其中 n 表示字符数。让我们看看如何实现这一点:

String wrapStringCharacterWise(String input, int n) {      
    StringBuilder stringBuilder = new StringBuilder(input);
    int index = 0;
    while(stringBuilder.length() > index + n) {
        index = stringBuilder.lastIndexOf(" ", index + n);    
        stringBuilder.replace(index, index + 1, "\n");
        index++; 
    }
    return stringBuilder.toString();
}

n=20 为例,理解我们的示例代码:

  • 我们首先找到距离 20 个字符处的最近的空格:在这个例子中,单词 apopular 之间。
  • 然后我们用换行符替换这个空格。
  • 接着,我们从下一个单词的开头开始,例如我们的例子中是 popular

当剩余的句子少于 20 个字符时,我们就停止算法。我们自然地通过一个 for 循环 实现此算法。此外,为了方便,我们内部使用了 StringBuilder,并将输入参数化:

我们可以编写一个 单元测试 来验证我们的方法对示例的预期结果:

@Test
void givenStringWithMoreThanNCharacters_whenWrapStringCharacterWise_thenCorrectlyWrapped() {
    String input = "Baeldung is a popular website that provides in-depth tutorials and articles on various programming and software development topics, primarily focused on Java and related technologies.";
    assertEquals("Baeldung is a\npopular website that\nprovides in-depth\ntutorials and\narticles on various\nprogramming and\nsoftware development\ntopics, primarily\nfocused on Java and\nrelated\ntechnologies.", wrapper.wrapStringCharacterWise(input, 20));
}

3. 边界情况

目前,我们编写了一个非常简单的代码。在实际应用中,可能需要处理一些边界情况。在这篇文章中,我们将探讨其中两个。

3.1. 单词长度超过字符限制

首先,如果一个单词太大,无法包裹怎么办?为了简单起见,**在这种情况下,我们可以抛出一个 IllegalArgumentException**。在循环的每次迭代中,我们需要检查给定长度前确实存在空格:

String wrapStringCharacterWise(String input, int n) {      
    StringBuilder stringBuilder = new StringBuilder(input);
    int index = 0;
    while(stringBuilder.length() > index + n) {
        index = stringBuilder.lastIndexOf(" ", index + n);
        if (index == -1) {
            throw new IllegalArgumentException("impossible to slice " + stringBuilder.substring(0, n));
        }       
        stringBuilder.replace(index, index + 1, "\n");
        index++; 
    }
    return stringBuilder.toString();
}

同样,我们可以编写一个简单的 JUnit 测试来验证:

@Test
void givenStringWithATooLongWord_whenWrapStringCharacterWise_thenThrows() {
    String input = "The word straightforward has more than 10 characters";
    assertThrows(IllegalArgumentException.class, () -> wrapper.wrapStringCharacterWise(input, 10));
}

3.2. 原始输入已有换行符

另一个边缘情况是,输入字符串中已经包含换行符。目前,如果我们向句子中的 Baeldung 后添加一个换行符,它会被同样包裹。但听起来更直观的是在现有换行符之后开始包裹。

为此,我们在算法的每次迭代中都会搜索最后一个换行符;如果存在,我们将移动光标并跳过包裹部分:

String wrapStringCharacterWise(String input, int n) {      
    StringBuilder stringBuilder = new StringBuilder(input);
    int index = 0;
    while(stringBuilder.length() > index + n) {
        int lastLineReturn = stringBuilder.lastIndexOf("\n", index + n);
        if (lastLineReturn > index) {
            index = lastLineReturn;
        } else {
            index = stringBuilder.lastIndexOf(" ", index + n);
            if (index == -1) {
                throw new IllegalArgumentException("impossible to slice " + stringBuilder.substring(0, n));
            }       
            stringBuilder.replace(index, index + 1, "\n");
            index++;
        }    
    }
    return stringBuilder.toString();
}

再次,我们可以用我们的示例测试我们的代码:

@Test
void givenStringWithLineReturns_whenWrapStringCharacterWise_thenWrappedAccordingly() {
    String input = "Baeldung\nis a popular website that provides in-depth tutorials and articles on various programming and software development topics, primarily focused on Java and related technologies.";
    assertEquals("Baeldung\nis a popular\nwebsite that\nprovides in-depth\ntutorials and\narticles on various\nprogramming and\nsoftware development\ntopics, primarily\nfocused on Java and\nrelated\ntechnologies.", wrapper.wrapStringCharacterWise(input, 20));
}

4. Apache WordUtils 的 wrap() 方法

我们可以使用 Apache WordUtils 的 wrap() 方法来实现所需的行为。 首先,让我们添加最新的 Apache*commons-text* 依赖:

<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-text</artifactId>
    <version>1.10.0</version>
</dependency>

与我们的代码相比,wrap() 方法的主要区别在于它默认使用平台无关的System 的换行符

@Test
void givenStringWithMoreThanNCharacters_whenWrap_thenCorrectlyWrapped() {
    String input = "Baeldung is a popular website that provides in-depth tutorials and articles on various programming and software development topics, primarily focused on Java and related technologies.";
    assertEquals("Baeldung is a" + System.lineSeparator() + "popular website that" + System.lineSeparator() + "provides in-depth" + System.lineSeparator() + "tutorials and" + System.lineSeparator() + "articles on various" + System.lineSeparator() + "programming and" + System.lineSeparator() + "software development" + System.lineSeparator() + "topics, primarily" + System.lineSeparator() + "focused on Java and" + System.lineSeparator() + "related" + System.lineSeparator() + "technologies.", WordUtils.wrap(input, 20));
}

默认情况下,wrap() 方法接受长单词但不进行包裹:

@Test
void givenStringWithATooLongWord_whenWrap_thenLongWordIsNotWrapped() {
    String input = "The word straightforward has more than 10 characters";
    assertEquals("The word" + System.lineSeparator() + "straightforward" + System.lineSeparator() + "has more" + System.lineSeparator() + "than 10" + System.lineSeparator() + "characters", WordUtils.wrap(input, 10));
}

最后但并非最不重要的一点,这个库忽略了我们的另一个边缘情况:

@Test
void givenStringWithLineReturns_whenWrap_thenWrappedLikeThereWasNone() {
    String input = "Baeldung" + System.lineSeparator() + "is a popular website that provides in-depth tutorials and articles on various programming and software development topics, primarily focused on Java and related technologies.";
    assertEquals("Baeldung" + System.lineSeparator() + "is a" + System.lineSeparator() + "popular website that" + System.lineSeparator() + "provides in-depth" + System.lineSeparator() + "tutorials and" + System.lineSeparator() + "articles on various" + System.lineSeparator() + "programming and" + System.lineSeparator() + "software development" + System.lineSeparator() + "topics, primarily" + System.lineSeparator() + "focused on Java and" + System.lineSeparator() + "related" + System.lineSeparator() + "technologies.", WordUtils.wrap(input, 20));
}

总之,我们可以查看方法的重载签名:

static String wrap(final String str, int wrapLength, String newLineStr, final boolean wrapLongWords, String wrapOn)

我们注意到额外的参数:

  • newLineStr:用于插入新行的不同字符。
  • wrapLongWords:一个布尔值,决定是否包裹长单词。
  • wrapOn:可以使用任何正则表达式代替空格。

5. 总结

在这篇文章中,我们看到了在给定字符数后包裹字符串的算法。我们实现了它,并添加了对几个边缘情况的支持。

最后,我们意识到 Apache WordUtilswrap() 方法非常灵活,大多数情况下应该足够使用。然而,如果我们不能使用外部依赖或需要特定行为,我们可以使用自己的实现。

一如既往,代码可以在 GitHub 上找到。