【问题标题】:Ideal method to truncate a string with ellipsis用省略号截断字符串的理想方法
【发布时间】:2011-04-05 13:58:33
【问题描述】:

我相信我们所有人都在 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,因为这是我最感兴趣的。

【问题讨论】:

  • 虽然我现在懒得提供真正的解决方案,但这里有一个改进“显示更多”链接的提示:将它们更改为“显示更多(xyz 附加字符)”。这样我就可以提前知道是否值得……

标签: java ellipsis


【解决方案1】:

我喜欢让“瘦”字符算作半个字符的想法。简单且很好的近似值。

然而,大多数省略号的主要问题是(恕我直言)它们在中间切词。这是一个考虑词边界的解决方案(但没有深入研究像素数学和 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) + "...";
}

算法测试如下:

【讨论】:

  • 您可能希望使用省略号字符 而不是三个句点,因为该行可能会在句点之间精确中断。对上面的代码进行此更改时,将所有出现的3 更改为1
  • 我猜它可能应该使用 BreakIterator 而不是寻找 ASCII 空间。
【解决方案2】:

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

更新:是的,它没有考虑纤细的字符,但考虑到每个人都有不同的屏幕和字体设置,并且大部分登陆此页面的人可能正在寻找一个像上面那样维护的库。

【讨论】:

  • 这不符合我的问题。
  • 我想是的。我错过了你的苗条人物参考,但我个人认为这很荒谬,并没有考虑到 i18n。它的 not ideal 方法,现在人们将复制并粘贴上面的代码,当有一个库已经以确定的方式执行此操作时......顺便说一句,你错过了t 因为“t”在我的屏幕上很小。
  • 感谢亚当的回答! StringUtils.abbreviate 非常适合我的用例。
  • 警告! - 您的链接已损坏
【解决方案3】:

您似乎可以从 Java 图形上下文的 FontMetrics 中获得更准确的几何图形。

附录:在处理这个问题时,区分模型和视图可能会有所帮助。模型是String,一个有限的 UTF-16 代码点序列,而视图是一系列字形,在某些设备上以某种字体呈现。

在 Java 的特殊情况下,可以使用SwingUtilities.layoutCompoundLabel() 来实现翻译。下面的例子截取BasicLabelUI中的布局调用来演示效果。在其他情况下也可以使用实用程序方法,但必须根据经验确定合适的FontMetrics

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 http://stackoverflow.com/questions/3597550 */
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();
            }
        });
    }
}

【讨论】:

  • 所以,如果我理解的话,您是在创建一个标签,设置字体,然后根据标签的渲染确定文本的长度,即让 Swing 为您计算省略号?因此,假设他们处理省略号本身不短于原始省略号的情况,这只有在您可以准确复制字体度量时才有效。
  • @Mr. Shiny and New:我认为这是一个公平的概要。 FontMetrics 和视图的几何图形定义了结果。请注意,layoutCompoundLabel()(间接)返回的(可能缩短的)String 包括省略号。
  • 这是一个很好的答案,但并没有真正回答这个问题。尽管 OP 没有明确指定上下文,但可以假设目标是构建一个文本缩短器,用于缩短文本以在网站上显示 sn-p。
  • @Avi:好点子!我添加了输出以显示layoutCompoundLabel()(间接)返回的省略文本。当然,如果必须猜测目的地的字体指标,FontMetrics 和任何一个都一样好。对于 Web 使用,@deadsven 引用的迭代 JavaScript 方法可能更好。
【解决方案4】:

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

text-overflow:ellipsis;

这并不像将样式添加到您的 CSS 那样简单,因为它会与其他 CSS 交互;例如,它要求元素具有溢出:隐藏;如果您希望将文本放在一行中,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 不支持省略号样式。烦人,但我认为这并不重要——它仍然会正确截断文本,因为这是由溢出处理的:隐藏,它只是不显示省略号。它确实适用于所有其他浏览器(包括 IE,一直到 IE5.5!),所以 Firefox 还没有做到这一点有点烦人。希望新版本的 Firefox 能尽快解决这个问题。

[编辑]
人们仍在对这个答案进行投票,所以我应该编辑它以注意 Firefox 现在确实支持省略号样式。该功能是在 Firefox 7 中添加的。如果您使用的是早期版本(FF3.6 和 FF4 仍然有一些用户),那么您就不走运了,但大多数 FF 用户现在都可以了。这里有更多详细信息:text-overflow:ellipsis in Firefox 4? (and FF5)

【讨论】:

  • 我也喜欢这个答案。可悲的是,我现在所在的地方的 CEO 使用 Firefox,当他看不到东西时会抱怨,甚至忽略了所有其他浏览器...... :( 但我希望 Firefox 能尽快支持这一点!
  • 是的,当你得到它的时候很烦人。我们已经采取了务实的方法,我们可以在 Firefox 中没有省略号,因为其余的功能都可以正常工作(即它被正确截断,阅读更多链接工作等)。你可以绕过它;可能有一个半透明的淡入白块覆盖文本元素的最后几个字符,因此如果文本确实覆盖了它,它似乎会淡出。它不是省略号,但它可能是一个合适的替代方案。
【解决方案5】:

对我来说这将是理想的 -

 public static String ellipsis(final String text, int length)
 {
     return text.substring(0, length - 3) + "...";
 }

我不会担心每个字符的大小,除非我真的知道它将在哪里以及以什么字体显示。许多字体是固定宽度的字体,其中每个字符都有相同的尺寸。

即使它是可变宽度字体,如果你算上'i','l'占一半宽度,那为什么不算'w''m'占两倍宽度呢?字符串中这些字符的混合通常会平均它们大小的影响,我宁愿忽略这些细节。明智地选择“长度”的值最重要。

【讨论】:

  • 在生产代码中使用了 OP 的算法(和一些派生算法)和这个算法,我可以说,至少在我的上下文(Android 开发)中,这一行更加一致。 OP 的方法在不同的文本块中差异很大。尚未探究其根本原因,仅报告我所看到的。
  • 这可能会引发 IndexOutOfBoundsException。您应该在使用子字符串之前测试字符串的长度。
  • 那是你的 sn-p 中的三个句号,不是省略号……
【解决方案6】:

使用 Guava 的com.google.common.base.Ascii.truncate(CharSequence, int, String) 方法:

Ascii.truncate("foobar", 7, "..."); // returns "foobar"
Ascii.truncate("foobar", 5, "..."); // returns "fo..."

【讨论】:

    【解决方案7】:

    这个怎么样(得到一个 50 个字符的字符串):

    text.replaceAll("(?<=^.{47}).*$", "...");
    

    【讨论】:

      【解决方案8】:
       public static String getTruncated(String str, int maxSize){
          int limit = maxSize - 3;
          return (str.length() > maxSize) ? str.substring(0, limit) + "..." : str;
       }
      

      【讨论】:

        【解决方案9】:

        如果您担心省略号仅隐藏极少数字符,为什么不检查该条件?

        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:我不得不反对; @deadsven 引用的方法似乎更精确,因为网络浏览器知道所选字体的度量。浏览器视图。
        • @trashgod:如果您想在客户端使用 Javascript 执行此操作,那么是的,@deadsven 的链接将提供解决方案。然而,由于各种原因,有时这种方法是不可接受的。
        【解决方案10】:

        我会选择类似于您拥有的标准模型的东西。我不会为字符宽度问题而烦恼-正如@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 个字符 + 省略号,给我们 29 个字符,其中 10 个隐藏。

        我在争论重新排列一些比较逻辑是否会使其更直观,但最终决定保持原样。您可能会发现 text.length() - minNumberOfhiddenCharacters &lt; length-3 使您所做的事情更加明显。

        【讨论】:

          【解决方案11】:

          在我看来,没有像素数学就无法获得好的结果。

          因此,当您在 Web 应用程序上下文(如 facebook)中时,Java 可能是解决此问题的错误方法。

          我会选择 JavaScript。由于 Javascript 不是我主要感兴趣的领域,我无法真正判断 this 是否是一个好的解决方案,但它可能会给你一个指导。

          【讨论】:

          【解决方案12】:

          大多数这种解决方案都没有考虑字体指标,这是一个非常简单但有效的 java swing 解决方案,我已经使用了多年。

          private String ellipsisText(String text, FontMetrics metrics, Graphics2D g2, int targetWidth) {
             String shortText = text;
             int activeIndex = text.length() - 1;
          
             Rectangle2D textBounds = metrics.getStringBounds(shortText, g2);
             while (textBounds.getWidth() > targetWidth) {
                shortText = text.substring(0, activeIndex--);
                textBounds = metrics.getStringBounds(shortText + "...", g2);
             }
             return activeIndex != text.length() - 1 ? shortText + "..." : text;
          }
          

          【讨论】:

            【解决方案13】:

            你也可以像这样简单地实现:

            mb_strimwidth($string, 0, 120, '...')
            

            谢谢。

            【讨论】:

              猜你喜欢
              • 2021-10-24
              • 1970-01-01
              • 2015-03-11
              • 2019-04-20
              • 2023-03-09
              • 1970-01-01
              • 2011-09-17
              • 2011-05-08
              相关资源
              最近更新 更多