web-dev-qa-db-ja.com

省略記号を使用して文字列を切り捨てる理想的な方法

私たちは皆、Facebookのステータス(または他の場所)でEllipsisを見て、[もっと見る]をクリックすると、たった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というタグが付いています。これが私が見たいと思っているものだからです。

56
Amy B

「細い」文字を半角として数えるというアイデアが好きです。シンプルで優れた近似。

しかし、ほとんどの楕円化の主な問題は、(imho)真ん中の単語のチョップです。 Wordの境界を考慮に入れたソリューションを次に示します(ただし、ピクセル演算や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) + "...";
}

アルゴリズムのテストは次のようになります。

enter image description here

76
aioobe

誰も言及されていないことにショックを受けています Commons Lang StringUtils#abbreviate()

更新:はい上記。

53
Adam Gent

Javaグラフィックコンテキストの FontMetrics ]からより正確なジオメトリを取得できるようです。

補遺:この問題に取り組む際に、モデルとビューを区別すると役立つ場合があります。モデルはUTF-16コードポイントの有限シーケンスであるStringです。一方、ビューは一連のグリフであり、デバイスによってはフォントで表示されます。

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 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();
            }
        });
    }
}
26
trashgod

Webサイト、つまりHTML/JS/CSSの出力について話している場合、純粋なCSSソリューションがあるため、これらすべてのソリューションを捨てることができます。

text-overflow:Ellipsis;

CSSにスタイルを追加するだけでは、他のCSSと相互作用するため、それほど単純ではありません。たとえば、要素にoverflow:hiddenが必要です。テキストを1行にしたい場合は、white-space:nowrap;も良い。

次のようなスタイルシートがあります。

.myelement {
  Word-wrap:normal;
  white-space:nowrap;
  overflow:hidden;
  -o-text-overflow:Ellipsis;
  text-overflow:Ellipsis;
  width: 120px;
}

JavaScript関数を実行してスタイルを変更するだけの「詳細」ボタンを使用することもできます。ビンゴでは、ボックスのサイズが変更され、テキスト全体が表示されます。 (ただし、私の場合は、非常に長くなる可能性がない限り、全文にhtml title属性を使用する傾向があります)

お役に立てば幸いです。それは混乱してテキストサイズを計算し、それを切り詰めようとする、はるかに簡単なソリューションです。 (もちろん、非Webベースのアプリを書いている場合でも、それを行う必要があるかもしれません)

このソリューションには欠点が1つあります。FirefoxはEllipsisスタイルをサポートしていません。迷惑ですが、クリティカルだとは思いません-overflow:hiddenで処理されるため、テキストは正しく切り捨てられますが、省略記号は表示されません。それは他のすべてのブラウザで動作します(IEを含む、IE5.5まで遡ります!)ので、Firefoxがまだ動作しないのは少し面倒です。 Firefoxの新しいバージョンがこの問題をすぐに解決することを願っています。

[編集]
人々はまだこの答えに投票しているので、FirefoxがEllipsisスタイルをサポートするようになりました。この機能はFirefox 7で追加されました。以前のバージョン(FF3.6とFF4にはまだ一部のユーザーがいる)を使用している場合は運が悪いですが、ほとんどのFFユーザーは大丈夫です。これについての詳細はこちらにあります: text-overflow:Ellipsis in Firefox 4?(およびFF5)

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

私にとってこれは理想的です-

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

どこでどのフォントで表示されるのかを本当に理解していない限り、すべての文字のサイズについて心配する必要はありません。多くのフォントは、すべての文字が同じサイズの固定幅フォントです。

可変幅フォントで、「i」、「l」をカウントして幅の半分を取得する場合でも、「w」、「m」をカウントして幅を2倍にするのはなぜですか?文字列にそのような文字を混在させると、一般にサイズの影響が平均化されます。そのような詳細を無視することをお勧めします。 「長さ」の値を賢明に選択することが最も重要です。

4
Gopi

これについては(50文字の文字列を取得するには):

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

私はあなたが持っている標準モデルに似たもので行くと思います。文字幅のことは気にしません-@Gopiが言ったように、おそらく最終的にすべてのバランスが取れるようになるでしょう。私がやりたいのは、「minNumberOfhiddenCharacters」のようなパラメータ(もう少し冗長なものかもしれません)があることです。次に、エリプシスチェックを実行すると、次のようになります。

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

これが意味することは、テキストの長さが35で、「長さ」が30で、非表示にする最小文字数が10の場合、文字列が完全に取得されるということです。非表示にする最小文字数が3の場合、これらの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の2番目の部分に入り、27文字+ ...を取得して30にします。これは実際には同じですループの最初の部分に入ったかのように(これは境界条件が正しいことを示す兆候です)。テキストの長さが36だった場合、26文字+省略記号が得られ、29文字が表示され、10が非表示になります。

比較ロジックの一部を再配置することでより直感的になるかどうかを議論していましたが、最終的にはそのままにすることにしました。 text.length() - minNumberOfhiddenCharacters < length-3を使用すると、何をしているのかがより明確になります。

3
Chris

Guavaの com.google.common.base.Ascii.truncate(CharSequence、int、String) メソッドの使用:

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

省略記号が非常に少数の文字を隠しているだけではないかと心配している場合は、単にその状態を確認してみませんか?

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;
}
3
davmac

私の目では、ピクセル演算なしでは良い結果を得ることはできません。

したがって、Javaは、おそらくWebアプリケーションのコンテキスト(facebookなど)にいるときにこの問題を解決するのに間違った目的です。

私はjavascriptに行きます。 Javascriptは私の主要な関心分野ではないので、 this が良い解決策であるかどうかを判断することはできませんが、指針を与えるかもしれません。

3
rompetroll

このソリューションのほとんどは、フォントメトリックを考慮に入れていません。ここでは、非常にシンプルですが、Javaスイングを長年使用している作業用ソリューションです。

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;
}
0
Beowolve