web-dev-qa-db-fra.com

Méthode idéale pour tronquer une chaîne avec des points de suspension

Je suis sûr que nous avons tous vu les statuts d'Ellipsis sur Facebook (ou ailleurs), cliqué sur "Afficher plus" et qu'il n'y a que 2 autres caractères environ. Je suppose que cela est dû à une programmation paresseuse, car il existe sûrement une méthode idéale.

Mine considère les caractères minuscules [iIl1] comme des "demi-caractères", mais cela ne contourne pas les Ellipsis de paraître ridicules lorsqu'ils cachent à peine des caractères.

Y a-t-il une méthode idéale? Voici le mien:

/**
 * 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;
}

La langue n'a pas vraiment d'importance, mais elle s'appelle Java car c'est ce que je suis surtout intéressé à voir.

51
Amy B

J'aime l'idée de laisser les personnages "minces" compter pour un demi-personnage. Simple et une bonne approximation.

Le problème principal avec la plupart des ellipses est cependant (à mon humble avis) que ils découpent les mots au milieu Voici une solution qui prend en compte les limites de Word (mais ne plonge pas dans pixel-math et l'API Swing).

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) + "...";
}

Un test de l'algorithme ressemble à ceci:

enter image description here

73
aioobe

Je suis choqué, personne n'a été mentionné Commons Lang StringUtils # abbreviate () .

Mise à jour: oui, les caractères minces ne sont pas pris en compte, mais je ne suis pas d'accord avec le fait que tout le monde a des écrans et des configurations de polices différents et qu'une grande partie des personnes qui atterrissent ici sur cette page cherchent probablement une bibliothèque maintenue comme ce qui précède.

45
Adam Gent

Il semble que vous pourriez obtenir une géométrie plus précise à partir de la variable FontMetrics du contexte graphique Java.

Addendum: En abordant ce problème, il peut être utile de faire la distinction entre le modèle et la vue. Le modèle est une String, une séquence finie de points de code UTF-16, tandis que la vue est une série de glyphes, restitués dans une police de caractères sur un périphérique.

Dans le cas particulier de Java, on peut utiliser SwingUtilities.layoutCompoundLabel() pour effectuer la traduction. L'exemple ci-dessous intercepte l'appel de présentation dans BasicLabelUI pour illustrer l'effet. Il est peut-être possible d’utiliser la méthode d’utilité dans d’autres contextes, mais il faudra déterminer empiriquement la FontMetrics appropriée.

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

Si vous parlez d'un site Web - c'est-à-dire une sortie HTML/JS/CSS, vous pouvez jeter toutes ces solutions, car il existe une solution CSS pure.

text-overflow:Ellipsis;

Ce n’est pas aussi simple que d’ajouter ce style à votre CSS, car il s’agence avec d’autres CSS; Par exemple, il faut que l'élément ait un débordement: hidden; et si vous voulez que votre texte soit sur une seule ligne, white-space:nowrap; est bon aussi.

J'ai une feuille de style qui ressemble à ceci:

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

Vous pouvez même avoir un bouton "lire plus" qui exécute simplement une fonction javascript pour changer les styles, et bingo, la case sera redimensionnée et le texte intégral sera visible. (dans mon cas, j’ai tendance à utiliser l’attribut titre HTML pour le texte intégral, sauf s’il risque d’être très long)

J'espère que cela pourra aider. C'est une solution beaucoup plus simple qui consiste à essayer de calculer la taille du texte et de le tronquer, et ainsi de suite. (bien sûr, si vous écrivez une application non basée sur le Web, vous devrez peut-être quand même le faire)

Cette solution présente un inconvénient: Firefox ne prend pas en charge le style Ellipsis. Ennuyeux, mais je ne pense pas que cela soit critique - Il tronque toujours le texte correctement, comme cela est traité par un débordement: masqué, il n’affiche tout simplement pas les Ellipsis. Cela fonctionne dans tous les autres navigateurs (y compris IE, jusqu'à IE5.5!), Donc il est un peu gênant que Firefox ne le fasse pas encore. Espérons qu'une nouvelle version de Firefox résoudra ce problème bientôt.

[MODIFIER]
Les gens votent toujours sur cette réponse, je devrais donc l'éditer pour noter que Firefox prend désormais en charge le style Ellipsis. La fonctionnalité a été ajoutée à Firefox 7. Si vous utilisez une version antérieure (FF3.6 et FF4 ont encore des utilisateurs), vous n’avez pas de chance, mais la plupart des utilisateurs de FF vont bien. Il y a beaucoup plus de détails à ce sujet ici: text-overflow: Ellipsis in Firefox 4? (et 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

Pour moi ce serait l'idéal -

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

Je ne voudrais pas m'inquiéter de la taille de chaque caractère à moins que je ne sache vraiment où et dans quelle police il va être affiché. De nombreuses polices sont des polices à largeur fixe où chaque caractère a la même dimension.

Même si c'est une police à largeur variable, et si vous comptez «i», «l» prend la moitié de la largeur, alors pourquoi ne pas compter «w» «m» pour prendre le double de la largeur? Un mélange de tels caractères dans une chaîne sera généralement une moyenne de l'effet de leur taille, et je préférerais ignorer de tels détails. Choisir judicieusement la valeur de 'longueur' importerait le plus. 

4
Gopi

Que diriez-vous de cela (pour obtenir une chaîne de 50 caractères):

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

J'irais avec quelque chose de similaire au modèle standard que vous avez. Je ne voudrais pas m'embêter avec la question de la largeur des caractères - comme @Gopi l'a dit, cela va probablement disparaître à la fin. Ce que je ferais de nouveau, c’est d’avoir un autre paramètre appelé quelque chose comme "minNumberOfhiddenCharacters" (peut-être un peu moins détaillé). Puis, quand je ferais la vérification des Ellipsis, je ferais quelque chose comme:

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

Cela signifie que si votre texte a une longueur de 35, votre "longueur" est de 30 et que votre nombre minimal de caractères à masquer est de 10, vous obtiendrez alors votre chaîne en entier. Si votre nombre minimal de caractères à masquer était 3, vous obtiendrez les Ellipsis au lieu de ces trois caractères.

La principale chose à prendre en compte est que j'ai subverti le sens de "longueur" afin qu'il ne soit plus une longueur maximale. La longueur de la chaîne en sortie peut maintenant être comprise entre 30 caractères (lorsque la longueur du texte est> 40) et 40 caractères (lorsque la longueur du texte est de 40 caractères). Effectivement, notre longueur maximale devient longueur + minNumberOfhiddenCharacters. La chaîne pourrait bien sûr être inférieure à 30 caractères lorsque la chaîne d'origine est inférieure à 30, mais il s'agit d'un cas ennuyeux que nous devrions ignorer.

Si vous voulez que la longueur soit un maximum absolu, vous voudriez quelque chose de plus semblable à:

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

Ainsi, dans cet exemple, si text.length () vaut 37, length vaut 30 et minNumberOfhiddenCharacters = 10, nous passerons à la deuxième partie de l'if interne et obtiendrons 27 caractères + ... pour en faire 30. comme si nous étions entrés dans la première partie de la boucle (ce qui est un signe que nous avons nos conditions aux limites) Si la longueur du texte était de 36, nous aurions 26 caractères + l'Ellipsis nous donnant 29 caractères dont 10 cachés.

Je débattais de la question de savoir si la réorganisation d'une partie de la logique de comparaison rendrait la chose plus intuitive, mais j'ai finalement décidé de la laisser telle quelle. Vous trouverez peut-être que text.length() - minNumberOfhiddenCharacters < length-3 rend plus évident ce que vous faites.

3
Chris

Si vous craignez que les Ellipsis ne cachent qu'un très petit nombre de caractères, pourquoi ne pas simplement vérifier cette condition?

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

À mes yeux, vous ne pouvez pas obtenir de bons résultats sans le calcul en pixels.

Ainsi, Java n’est probablement pas le bon bout pour résoudre ce problème lorsque vous vous trouvez dans un contexte d’application Web (comme Facebook).

J'irais pour javascript. Puisque javascript n'est pas mon domaine d'intérêt principal, je ne peux pas vraiment juger si ceci est une bonne solution, mais cela pourrait vous donner un pointeur.

3
rompetroll

Utilisation de la méthode com.google.common.base.Ascii.truncate (CharSequence, int, String) method:

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

La plupart de ces solutions ne prennent pas en compte les métriques de police. Voici une solution très simple mais efficace pour Java Swing que j'utilise depuis des années.

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