使用省略号截断字符串的最佳方法

63

我相信我们大家都看到了Facebook状态(或其他地方)上的省略号,并点击“显示更多”,结果只会出现另外2个字符左右。我猜这是因为程序设计懒惰造成的,因为肯定有一种理想的方法。

我的方法是将占用空间较小的字符[iIl1] 计算为“半个字符”,但是当省略号隐藏了很少的字符时,这种方法并不能解决省略号看起来很奇怪的问题。

是否存在一种理想的方法?以下是我的方法:

/**
 * Return a string with a maximum length of <code>length</code> characters.
 * If there are more than <code>length</code> characters, then string ends with an ellipsis ("...").
 *
 * @param text
 * @param length
 * @return
 */
public static String ellipsis(final String text, int length)
{
    // The letters [iIl1] are slim enough to only count as half a character.
    length += Math.ceil(text.replaceAll("[^iIl]", "").length() / 2.0d);

    if (text.length() > length)
    {
        return text.substring(0, length - 3) + "...";
    }

    return text;
}

语言并不重要,但标记为Java,因为那是我最感兴趣的。


3
虽然我现在懒得提供一个真正的解决方案,但是这里有一个提示可以改善“显示更多”链接:将它们改为“显示更多(xyz个额外字符)”。这样我就可以提前知道它是否值得去看了... - Sean Patrick Floyd
14个回答

86

我喜欢将“瘦”字符视为半个字符的想法。简单且是一个很好的近似。

大多数省略文本的主要问题(依我之见)是它们会在单词中间截断。这里有一个考虑到单词边界的解决方案(但不涉及像素计算和Swing-API)。

private final static String NON_THIN = "[^iIl1\\.,']";

private static int textWidth(String str) {
    return (int) (str.length() - str.replaceAll(NON_THIN, "").length() / 2);
}

public static String ellipsize(String text, int max) {

    if (textWidth(text) <= max)
        return text;

    // Start by chopping off at the word before max
    // This is an over-approximation due to thin-characters...
    int end = text.lastIndexOf(' ', max - 3);

    // Just one long word. Chop it off.
    if (end == -1)
        return text.substring(0, max-3) + "...";

    // Step forward as long as textWidth allows.
    int newEnd = end;
    do {
        end = newEnd;
        newEnd = text.indexOf(' ', end + 1);

        // No more spaces.
        if (newEnd == -1)
            newEnd = text.length();

    } while (textWidth(text.substring(0, newEnd) + "...") < max);

    return text.substring(0, end) + "...";
}

算法的测试看起来像这样:

在此输入图片描述


2
您可能希望使用省略号字符 而不是三个点,因为该行可能会在点之间精确断开。在对上述代码进行此更改时,请将所有出现的 3 更改为 1 - Paul Lammertsma
我猜它可能应该使用BreakIterator而不是寻找ASCII空格。 - Hakanai

82

我很震惊,竟然没有人提到 Commons Lang StringUtils#abbreviate()

更新:是的,它没有考虑细字体,但我不同意这一点,因为每个人的屏幕和字体设置都不同,而且很多来到本页面的人可能正在寻找像上面提到的那样维护良好的库。


4
这并不能回答我的问题。 - Amy B
4
我猜是这样。我错过了你提到的“slim characters”的参考,但个人认为这种方法很荒谬,没有考虑到国际化(i18n)。它并不是理想的方法,现在人们将会复制粘贴以上的代码,而已经有一个库可以以确定的方式完成这个任务... 顺便说一句,你错过了“t”,因为在我的屏幕上“t”很细。 - Adam Gent

30

看起来你可以从Java图形上下文的FontMetrics中获得更准确的几何信息。

补充说明:在解决这个问题时,区分模型和视图可能会有所帮助。模型是一个String,是一系列UTF-16代码点,而视图是一系列字形,在某个设备上使用某种字体呈现。

在Java的特定情况下,可以使用SwingUtilities.layoutCompoundLabel()来实现翻译。下面的示例拦截了BasicLabelUI中的布局调用,以演示效果。可能可以在其他上下文中使用实用程序方法,但必须经验性地确定相应的FontMetrics

alt text

import java.awt.Color;
import java.awt.EventQueue;
import java.awt.Font;
import java.awt.FontMetrics;
import java.awt.GridLayout;
import java.awt.Rectangle;
import java.awt.event.ComponentAdapter;
import java.awt.event.ComponentEvent;
import javax.swing.BorderFactory;
import javax.swing.Icon;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.border.EmptyBorder;
import javax.swing.border.LineBorder;
import javax.swing.plaf.basic.BasicLabelUI;

/** @see https://dev59.com/iXA65IYBdhLWcg3w2iip */
public class LayoutTest extends JPanel {

    private static final String text =
        "A damsel with a dulcimer in a vision once I saw.";
    private final JLabel sizeLabel = new JLabel();
    private final JLabel textLabel = new JLabel(text);
    private final MyLabelUI myUI = new MyLabelUI();

    public LayoutTest() {
        super(new GridLayout(0, 1));
        this.setBorder(BorderFactory.createCompoundBorder(
            new LineBorder(Color.blue), new EmptyBorder(5, 5, 5, 5)));
        textLabel.setUI(myUI);
        textLabel.setFont(new Font("Serif", Font.ITALIC, 24));
        this.add(sizeLabel);
        this.add(textLabel);
        this.addComponentListener(new ComponentAdapter() {

            @Override
            public void componentResized(ComponentEvent e) {
                sizeLabel.setText(
                    "Before: " + myUI.before + " after: " + myUI.after);
            }
        });
    }

    private static class MyLabelUI extends BasicLabelUI {

        int before, after;

        @Override
        protected String layoutCL(
            JLabel label, FontMetrics fontMetrics, String text, Icon icon,
            Rectangle viewR, Rectangle iconR, Rectangle textR) {
            before = text.length();
            String s = super.layoutCL(
                label, fontMetrics, text, icon, viewR, iconR, textR);
            after = s.length();
            System.out.println(s);
            return s;
        }
    }

    private void display() {
        JFrame f = new JFrame("LayoutTest");
        f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        f.add(this);
        f.pack();
        f.setLocationRelativeTo(null);
        f.setVisible(true);
    }

    public static void main(String[] args) {
        EventQueue.invokeLater(new Runnable() {

            @Override
            public void run() {
                new LayoutTest().display();
            }
        });
    }
}

1
所以,如果我理解正确,您正在创建一个标签,设置字体,然后根据标签的渲染确定文本的长度,即让Swing为您计算省略号?因此,假设它们处理省略号本身不比原始文本短的情况,只有在您可以完全复制字体度量时才有效。 - Mr. Shiny and New 安宇
@Mr. Shiny and New:我认为这是一个公正的概述。FontMetrics和视图的几何形状定义了结果。请注意,由layoutCompoundLabel()(可能被缩短)间接返回的String包括省略号。 - trashgod
这是一个不错的回答,但并没有真正回答问题。虽然OP没有明确指定上下文,但可以假设目标是构建一个文本缩短器,用于在网站上显示片段。 - Avi
1
@Avi:好观点!我已经添加了输出,以显示由layoutCompoundLabel()(间接)返回的省略文本。当然,如果必须猜测目标字体度量,则FontMetrics与任何其他字体度量一样好。对于Web使用,@deadsven引用的迭代JavaScript方法可能更优越。 - trashgod

11

如果你在讨论一个网站 - 即输出HTML/JS/CSS,那么你可以放弃所有这些解决方案,因为有一个纯CSS的解决方案。

text-overflow:ellipsis;

这并不像你只需将该样式添加到CSS中那么简单,因为它会影响其他CSS;例如,它需要元素具有overflow:hidden;,如果您希望文本在单行上,则white-space:nowrap;也很好。

我有一个这样的样式表:

.myelement {
  word-wrap:normal;
  white-space:nowrap;
  overflow:hidden;
  -o-text-overflow:ellipsis;
  text-overflow:ellipsis;
  width: 120px;
}
你甚至可以有一个“阅读更多”按钮,只需运行JavaScript函数来更改样式,然后盒子将重新调整大小,完整文本将可见。(在我的情况下,我倾向于使用HTML标题属性来显示完整文本,除非它可能会变得非常长)希望这可以帮助你。这是一个比尝试计算文本大小并截断它等复杂解决方案简单得多的方法。(当然,如果你正在编写非基于web的应用程序,你可能仍需要这样做)。这种解决方案有一个缺点:Firefox不支持省略号样式。这很烦人,但我不认为它是关键的——它确实正确地截断了文��,因为它被overflow:hidden处理,只是没有显示省略号。它在所有其他浏览器中都有效(包括IE,一直到IE5.5!),所以很烦人Firefox还没有做到这一点。希望火狐的新版本能够解决这个问题。[编辑]人们仍然在对这个答案投票,所以我应该编辑一下说明Firefox现在支持省略号样式了。该功能已经添加到Firefox 7中。如果你使用的是早期版本(FF3.6和FF4仍有一些用户),那么你就没有机会了,但大多数Firefox用户现在都可以使用。关于这个问题有更多的细节可以查看这里:text-overflow:ellipsis in Firefox 4? (and FF5)

是的,当你遇到这种情况时很烦人。 我们采取了务实的方法,在Firefox中我们可以不使用省略号,因为其余功能都正常工作(即正确截断,阅读更多链接有效等)。 你可以绕过它进行黑客攻击;也许可以使用半透明的淡入白色块覆盖文本元素的最后几个字符,这样如果文本确实覆盖它,它就会呈现淡出效果。虽然不是省略号,但可能是一个合适的替代方案。 - Spudley

6
对我来说,这将是理想的 -
 public static String ellipsis(final String text, int length)
 {
     return text.substring(0, length - 3) + "...";
 }

我不会担心每个字符的大小,除非我真的知道它将在哪里以及以什么字体显示。许多字体都是等宽字体,其中每个字符具有相同的尺寸。
即使它是变宽字体,如果您将“i”、“l”计为一半宽度,那么为什么不将“w”、“m”计为双倍宽度?字符串中这些字符的混合通常会平均其大小的影响,我更喜欢忽略此类细节。明智地选择“长度”的值最重要。

在生产代码中,我使用了OP算法(和一些派生算法)以及这个算法,至少在我的上下文(Android开发)中,我可以说,这一行代码要一致得多。 OP的方法在不同的文本块之间变化很大。我还没有探索为什么会这样,只是报告我看到的情况。 - Dave Sims
2
在使用substring之前,您应该测试字符串的长度,否则可能会引发IndexOutOfBoundsException异常。 - Jared Rummler
1
这在你的代码片段中是三个句点,_不是_省略号… - conny
这总是返回省略号,即使文本不需要被截断。这不是提问者所要求的。 - Some Guy

6

5
这样做如何(获取一个长度为50个字符的字符串):
text.replaceAll("(?<=^.{47}).*$", "...");

4
 public static String getTruncated(String str, int maxSize){
    int limit = maxSize - 3;
    return (str.length() > maxSize) ? str.substring(0, limit) + "..." : str;
 }

3
如果你担心省略号只隐藏了极少数字符,为什么不检查这种情况呢?
public static String ellipsis(final String text, int length)
{
    // The letters [iIl1] are slim enough to only count as half a character.
    length += Math.ceil(text.replaceAll("[^iIl]", "").length() / 2.0d);

    if (text.length() > length + 20)
    {
        return text.substring(0, length - 3) + "...";
    }

    return text;
}

没错。根据文本将要显示的位置,你可能无法准确确定它的大小。当然,网页浏览器有太多变量:字体大小、字体系列、用户样式表、dpi等等。然后你需要担心组合字符、非打印字符等等。保持简单! - Mr. Shiny and New 安宇
@Mr. Shiny and New:我必须反对;@deadsven提到的方法似乎更精确,因为Web浏览器知道所选字体的度量。浏览器就是视图。 - trashgod
@trashgod:如果你想在客户端使用Javascript实现这个功能,那么@deadsven提供的链接可以解决问题。然而,由于各种原因,有时候这种方法并不可行。 - Mr. Shiny and New 安宇

3

我建议你采用与你所拥有的标准模型类似的东西。不必担心字符宽度的问题——正如@Gopi所说,这可能最终会平衡。我想做的新事情是再加一个参数,比如叫做“minNumberOfhiddenCharacters”(可能要简洁一些)。然后在进行省略号检查时,我会做以下操作:

if (text.length() > length+minNumberOfhiddenCharacters)
{
    return text.substring(0, length - 3) + "...";
}

这意味着,如果您的文本长度为35,您的“长度”为30,您要隐藏的最小字符数为10,则您将获得完整的字符串。如果您要隐藏的最小字符数为3,则会显示省略号而不是这三个字符。
需要注意的主要事项是,我扭曲了“长度”的含义,使其不再是最大长度。输出字符串的长度现在可以是从30个字符(当文本长度> 40时)到40个字符(当文本长度为40个字符长时)的任何值。实际上,我们的最大长度变成了length + minNumberOfhiddenCharacters。当原始字符串少于30个字符时,该字符串可能比30个字符更短,但这是一个无聊的情况,我们应该忽略它。
如果您想让长度成为硬性的最大值,则需要类似以下内容的东西:
if (text.length() > length)
{
    if (text.length() - length < minNumberOfhiddenCharacters-3)
    {
        return text.substring(0, text.length() - minNumberOfhiddenCharacters) + "...";
    }
    else
    {
        return text.substring(0, length - 3) + "...";
    }
}

如果text.length()是37,长度是30,minNumberOfhiddenCharacters=10,那么我们将进入内部if的第二部分,并获取27个字符+...以使长度为30。这实际上与我们进入循环的第一部分相同(这表明我们已经正确设置了边界条件)。如果文本长度为36,则我们将得到26个字符+省略号,从而获得10个隐藏的29个字符。
我正在考虑重新排列某些比较逻辑是否会使其更直观,但最终决定保持不变。但你可能会发现,text.length() - minNumberOfhiddenCharacters < length-3 让你更清楚地知道自己在做什么。

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