将文本行包装到最大行宽的Java代码

33

在我独立开发(而且很差劲)之前,我想知道是否存在一些现有的Java代码可以将文本行包装到给定的最大宽度。理想情况下它会:

  • 保留原有的换行符
  • 在单词边界上分割超过最大长度的行
  • 通过插入连字符来分解长度超过最大行宽的单词

编辑: 这里没有 "像素",只有java.lang.String。 "最大宽度" 指的是一行上的字符数。


1
你是追求像素级精度的算法,还是近似算法就可以了? - aioobe
8个回答

36

1
它对我来说只有一个警告。它会剥离字符串前面的任何空格。 - jett

6

使用word-wrap库(可以在Maven Central上获得)。

以下是使用它的一种方法:

String text = "hi there how are you going?";
String wrapped = 
  WordWrap.from(text)
    .maxWidth(10)
    .insertHyphens(true) // true is the default
    .wrap();

输出结果为:

hi there
how are
you going?

该库保留行首空格,这是针对Apache commons-lang行为的一项抱怨。您还可以指定stringWidth函数,以获得在呈现文本时精确到像素级的结果。

该库具有良好的单元测试覆盖率(考虑从网络上复制和粘贴代码块时要牢记这一点!)。

Maven 依赖项为:

<dependency>
  <groupId>com.github.davidmoten</groupId>
  <artifactId>word-wrap</artifactId>
  <version>0.1.9</version>
</dependency>

请确保查找更新版本。

3
这是我的看法。
private static final String LINEBREAK = "\n"; // or "\r\n";

public static String wrap(String string, int lineLength) {
    StringBuilder b = new StringBuilder();
    for (String line : string.split(Pattern.quote(LINEBREAK))) {
        b.append(wrapLine(line, lineLength));
    }
    return b.toString();
}

private static String wrapLine(String line, int lineLength) {
    if (line.length() == 0) return LINEBREAK;
    if (line.length() <= lineLength) return line + LINEBREAK;
    String[] words = line.split(" ");
    StringBuilder allLines = new StringBuilder();
    StringBuilder trimmedLine = new StringBuilder();
    for (String word : words) {
        if (trimmedLine.length() + 1 + word.length() <= lineLength) {
            trimmedLine.append(word).append(" ");
        } else {
            allLines.append(trimmedLine).append(LINEBREAK);
            trimmedLine = new StringBuilder();
            trimmedLine.append(word).append(" ");
        }
    }
    if (trimmedLine.length() > 0) {
        allLines.append(trimmedLine);
    }
    allLines.append(linebreak);
    return allLines.toString();
}

此解决方案将两个空格转换为一个空格(因此与Apache commons WordUtils存在相同的错误,即@jett提出的问题)。


为什么在 return 之前要加上最后的 allLines.append(linebreak);?似乎会在不需要的地方添加额外的换行符。 - JeffThompson

2
我使用以下内容。这将保留所有水平空白,允许多字符换行和单词断开序列,并允许悬挂或其他标记的多字符缩进以分割单词。Javadoc旨在充分描述用法。
/**Wraps a source String into a series of lines having a maximum specified length.  The source is
 * wrapped at: spaces, horizontal tabs, system newLine characters, or a specified newLine character
 * sequence.  Existing newLine character sequences in the source string, whether they be the system
 * newLine or the specified newLine, are honored.  Existing whitespace (spaces and horizontal tabs)
 * is preserved.
 * <p>
 * When <tt>wrapLongWords</tt> is true, words having a length greater than the specified
 * <tt>lineLength</tt> will be broken, the specified <tt>longWordBreak</tt> terminator appended,
 * and a new line initiated with the text of the specified <tt>longWordLinePrefix</tt> string.  The
 * position of the break will be unceremoniously chosen such that <tt>ineLength</tt> is honored.
 * One use of <tt>longWordLinePrefix</tt> is to effect "hanging indents"  by specifying a series of
 * spaces for this parameter.  This parameter can contain the lineFeed character(s).  Although
 * <tt>longWordLinePrefix</tt> can contain the horizontal tab character, the results are not
 * guaranteed because no attempt is made to determine the quantity of character positions occupied by a
 * horizontal tab.</p>
 * <p>
 * Example usage:
 * <pre>
 * wrap( "  A very long word is Abracadabra in my book", 11, "\n", true, "-", "  ");</pre>
 * returns (note the effect of the single-character lineFeed):
 * <pre>
 *   A very
 * long word
 * is Abraca-
 *   dabra in
 * my book</pre>
 * Whereas, the following:
 * <pre>
 * wrap( "  A very long word is Abracadabra in my book", 11, null, true, null, "  ");</pre>
 * returns (due to the 2-character system linefeed):
 * <pre>
 *   A very
 * long
 * word is A
 *   bracada
 *   bra in
 * my book</pre></p>
 *
 * @param src  the String to be word wrapped, may be null
 * @param lineLength the maximum line length, including the length of <tt>newLineStr</tt> and, when
 *        applicable, <tt>longWordLinePrefix</tt>.  If the value is insufficient to accommodate
 *        these two parameters + 1 character, it will be increased accordingly.
 * @param newLineStr the string to insert for a new line, or <code>null</code> to use the value
 *        reported as the system line separator by the JVM
 * @param wrapLongWords  when <tt>false</tt>, words longer than <tt>wrapLength</t> will not be broken
 * @param longWordBreak string with which to precede <tt>newLineStr</tt> on each line of a broken word,
 *        excepting the last line, or <tt>null</tt> if this feature is not to be used
 * @param longWordLinePrefix string with which to prefix each line of a broken word, subsequent
 *        to the first line, or <tt>null</tt> if no prefix is to be used
 * @return a line with newlines inserted, or <code>null</code> if <tt>src</tt> is null
 */
public static String wrap( String src, int lineLength, String newLineStr, boolean wrapLongWords,
    String longWordBreak, String longWordLinePrefix ) {
  // Trivial case
  if ( src == null ) return null;

  if ( newLineStr == null )
    newLineStr = System.getProperty( "line.separator" );

  if ( longWordBreak == null )
    longWordBreak = "";

  if ( longWordLinePrefix == null )
    longWordLinePrefix = "";

  // Adjust maximum line length to accommodate the newLine string
  lineLength -= newLineStr.length();
  if ( lineLength < 1 )
    lineLength = 1;

  // Guard for long word break or prefix that would create an infinite loop
  if ( wrapLongWords && lineLength - longWordBreak.length() - longWordLinePrefix.length() < 1 )
    lineLength += longWordBreak.length() + longWordLinePrefix.length();

  int
      remaining = lineLength,
      breakLength = longWordBreak.length();

  Matcher m = Pattern.compile( ".+?[ \\t]|.+?(?:" + newLineStr + ")|.+?$" ).matcher( src );

  StringBuilder cache = new StringBuilder();

  while ( m.find() ) {
    String word = m.group();

    // Breakup long word
    while ( wrapLongWords && word.length() > lineLength ) {
      cache
          .append( word.substring( 0, remaining - breakLength ) )
          .append( longWordBreak )
          .append( newLineStr );
      word = longWordLinePrefix + word.substring( remaining - breakLength );
      remaining = lineLength;
      } // if

    // Linefeed if word exceeds remaining space
    if ( word.length() > remaining ) {
      cache
          .append( newLineStr )
          .append( word );
      remaining = lineLength;
      } // if

    // Word fits in remaining space
    else
      cache.append( word );

    remaining -= word.length();
    } // while

  return cache.toString();    
  } // wrap()

0

完整的Java包装代码: 它也可以打破长单词。

public class Test {

public static void main(String[] args) {
    String string = "Comments, notes or other information about the packages will print here. Your comments go here.";
    
    List<String> list = wrapLine(string, 12);
    
    for (String str : list)
        System.out.println(str);
}

/**
 * Wrap your string.
 * @param line - String line which need to be wrapped.
 * @param wrapLength - Wrapping threshold. Must be greater than 1.
 * @return
 */
private static List<String> wrapLine(String line, int wrapLength) {
    List<String> resultList = new ArrayList<String>();

    if (line == null || line.length() == 0) {
        resultList.add("");
        return resultList;
    }
    if (line.length() <= wrapLength) {
        resultList.add(line);
        return resultList;
    }

    String[] words = line.split(" ");

    for (String word : words) {
        if (resultList.size() == 0) {
            addNewWord(resultList, word, wrapLength);
        } else {
            String lastLine = resultList.get(resultList.size() - 1);

            if (lastLine.length() + word.length() < wrapLength) {
                lastLine = lastLine + word + " ";
                resultList.set(resultList.size() - 1, lastLine);
            } else if (lastLine.length() + word.length() == wrapLength) {
                lastLine = lastLine + word;
                resultList.set(resultList.size() - 1, lastLine);
            } else {
                if (isThereMuchSpace(lastLine, wrapLength)) {
                    breakLongWord(resultList, word, wrapLength, lastLine.length());
                } else {
                    addNewWord(resultList, word, wrapLength);
                }
            }
        }
    }

    return resultList;
}

private static void addNewWord(List<String> resultList, String word, int wrapLength) {
    if (word.length() < wrapLength) {
        resultList.add(word + " ");
    } else if (word.length() == wrapLength) {
        resultList.add(word);
    } else {
        breakLongWord(resultList, word, wrapLength, 0);
    }
}

private static void breakLongWord(List<String> resultList, String word, int wrapLength, int offset) {
    String part = word.substring(0, (wrapLength - offset) - 1);
    if (offset > 1) {
        String lastLine = resultList.get(resultList.size() - 1);
        lastLine = lastLine + part + "-";
        resultList.set(resultList.size() - 1, lastLine);
    } else {
        resultList.add(part + "-");
    }

    String nextPart = word.substring((wrapLength - offset) - 1, word.length());
    if (nextPart.length() > wrapLength)
        breakLongWord(resultList, nextPart, wrapLength, 0);
    else if (nextPart.length() == wrapLength)
        resultList.add(nextPart);
    else
        resultList.add(nextPart + " ");
}

private static boolean isThereMuchSpace(String line, double lineLength) {
    double expectedPercent = (lineLength * 65) / 100;
    if (line.length() <= expectedPercent)
        return true;
    return false;
}

}


0
public static List<String> stringBreak(String string, int maxChar) {

    List<String> subLines = new ArrayList<String>();

    int length = string.length();
    int start = 0;
    int end = maxChar;
    if (length > maxChar) {

        int noOfLines = (length / maxChar) + 1;

        int endOfStr[] = new int[noOfLines];

        for (int f = 0; f < noOfLines - 1; f++) {

            int end1 = maxChar;

            endOfStr[f] = end;

            if (string.charAt(end - 1) != ' ') {

                if (string.charAt(end - 2) == ' ') {

                    subLines.add(string.substring(start, end - 1));
                    start = end - 1;
                    end = end - 1 + end1;

                } else if (string.charAt(end - 2) != ' '
                        && string.charAt(end) == ' ') {

                    subLines.add(string.substring(start, end));
                    start = end;
                    end = end + end1;

                } else if (string.charAt(end - 2) != ' ') {

                    subLines.add(string.substring(start, end) + "-");
                    start = end;
                    end = end + end1;

                } else if (string.charAt(end + 2) == ' ') {
                    System.out.println("m here ............");
                    int lastSpaceIndex = string.substring(start, end)
                            .lastIndexOf("");
                    subLines.add(string.substring(start, lastSpaceIndex));

                    start = lastSpaceIndex;
                    end = lastSpaceIndex + end1;
                }

            } else {

                subLines.add(string.substring(start, end));
                start = end;
                end = end + end1;
            }

        }

        subLines.add(string.substring(endOfStr[noOfLines - 2], length));

    }

    return subLines;
}

17
读这段代码片段时,我的大脑抛出了一个IndexOutOfBoundsException异常。 :( - Mike Clark

0
你想做的事情只有在使用固定宽度字体显示结果时才能起作用。否则,每行中的字符数将不同。如果这对你来说没问题,我会说你的情况相当罕见(特别是考虑到连字号),所以我怀疑你会找到现成的解决方案。

0
如果您正在尝试格式化某种文档,还有旧的Unix roff(或runoff)命令系列。您只需要插入格式化命令,让roff来完成繁重的工作即可。

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