2010-08-30 28 views
47

Estoy seguro de que todos hemos visto elipsis en los estados de Facebook (o en otro lugar), y pulsado "Mostrar más" y solo hay otros 2 caracteres más o menos. Supongo que esto se debe a la programación perezosa, porque seguramente hay un método ideal.Método ideal para truncar una cadena con puntos suspensivos

La mía cuenta los caracteres delgados [iIl1] como "caracteres medios", pero esto no pasa por alto las elipsis "se ven tontas cuando apenas ocultan los caracteres".

¿Existe un método ideal? Aquí está el mío:

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

El lenguaje en realidad no importa, pero etiquetado como Java porque eso es lo que más me interesa ver.

+2

Mientras que soy demasiado perezoso para aportar una solución real ahora, he aquí una sugerencia para mejorar los enlaces 'mostrar más': cambiarlos a 'mostrar más (xyz caracteres adicionales)'. De esa forma sé de antemano si valdrá la pena ... –

Respuesta

68

Me gusta la idea de dejar que los personajes "delgados" cuenten como medio personaje. Simple y una buena aproximación.

El principal problema con la mayoría de las elipsis sin embargo, son (imho) que pican de palabras en el medio. Aquí hay una solución que toma en cuenta los límites de palabras (pero no se sumerge en pixel-math y 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) + "..."; 
} 

Una prueba del algoritmo tiene el siguiente aspecto:

enter image description here

+3

Me gusta mucho esta. –

+1

¡Esto también funciona muy bien en C#! Gracias! – noocyte

+1

Sin necesidad de modificaciones, funciona a la perfección. –

25

Parece que puede obtener una geometría más precisa desde el contexto gráfico de Java FontMetrics.

Adición: Al abordar este problema, puede ayudar a distinguir entre el modelo y la vista. El modelo es String, una secuencia finita de puntos de código UTF-16, mientras que la vista es una serie de glifos, representados en alguna fuente en algún dispositivo.

En el caso particular de Java, se puede usar SwingUtilities.layoutCompoundLabel() para efectuar la traducción. El siguiente ejemplo intercepta la llamada de diseño en BasicLabelUI para demostrar el efecto. Puede ser posible utilizar el método de utilidad en otros contextos, pero el FontMetrics apropiado debería determinarse empíricamente.

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(); 
      } 
     }); 
    } 
} 
+1

Entonces, si lo entiendo, está creando una etiqueta, estableciendo la fuente, y luego determinando por cuánto tiempo el texto está basado en la representación de la etiqueta, es decir, haciendo que Swing calcule la elipsis para ti? Por lo tanto, suponiendo que manejen la situación donde la elipsis en sí misma no es más corta que la original, esto solo funciona si puede duplicar exactamente las métricas de la fuente. –

+0

@Mr. Brillante y nuevo: creo que es una buena sinopsis. El 'FontMetrics' y la geometría de la vista definen el resultado. Tenga en cuenta que el (posiblemente acortado) 'String' devuelto (indirectamente) por' layoutCompoundLabel() 'incluye los puntos suspensivos. – trashgod

+0

Esta es una buena respuesta, pero realmente no responde la pregunta. Aunque el OP no especifica el contexto explícitamente, se puede suponer que el objetivo es crear un acortador de texto para acortar el texto para mostrar un fragmento en un sitio web. – Avi

3

Si usted está preocupado acerca de los puntos suspensivos sólo se esconde un muy pequeño número de caracteres, ¿por qué no echa para esa condición?

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

Exactamente. Dependiendo de dónde se vaya a mostrar el texto, probablemente no pueda determinar con precisión qué tan grande será de todos modos. Ciertamente, un navegador web tiene demasiadas variables: tamaño de fuente, familia de fuentes, hojas de estilo del usuario, ppp, etc. Luego, debe preocuparse de combinar caracteres, caracteres que no sean de impresión, etc. ¡Manténgalo simple! –

+0

@Mr.Brillante y nuevo: tengo que objetar; el enfoque citado por @deadsven parece ser más preciso, ya que el navegador web conoce las métricas de la fuente elegida. El navegador _es_ la vista. – trashgod

+0

@trashgod: si desea hacer esto en el lado del cliente, en Javascript, entonces sí, el enlace de @ devenven proporcionará una solución. Sin embargo, a veces ese enfoque no es aceptable por una variedad de razones. –

4

Para mí esto sería ideal -

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

yo no preocuparse por el tamaño de cada personaje a menos que realmente saben dónde y en qué fuente se va a visualizar. Muchas fuentes son fuentes de ancho fijo donde cada personaje tiene la misma dimensión.

Incluso si es una fuente de ancho variable, y si cuentas 'i', 'l' para tomar la mitad del ancho, entonces ¿por qué no contar 'w' 'm' para tomar el doble de ancho? Una combinación de tales personajes en una cuerda generalmente promediará el efecto de su tamaño, y preferiría ignorar tales detalles. Elegir el valor de 'longitud' sabiamente sería lo más importante.

+0

Habiendo utilizado el algoritmo de OP (y algunas derivaciones) y este en el código de producción, puedo decir que, al menos en mi contexto (desarrollo de Android), esta única línea es MUCHO más consistente. El enfoque de OP varió drásticamente en diferentes bloques de texto. No he explorado la causa raíz de por qué eso fue, simplemente informando lo que vi. –

+0

Esto podría arrojar una excepción IndexOutOfBoundsException. Debe probar la longitud de la cadena antes de usar la subcadena. –

+0

Y eso son tres paradas completas en su fragmento, _no_ una elipsis ... – conny

3

Iría con algo similar al modelo estándar que tiene. No me molestaría con el tema de los anchos de caracteres, ya que @Gopi dijo que es probable que todo salga bien al final. Lo que haría es nuevo es tener otro parámetro llamado algo así como "minNumberOfhiddenCharacters" (quizás un poco menos detallado). Luego, cuando el cheque doign puntos suspensivos que me gustaría hacer algo como:

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

Lo que esto significa es que si la longitud de su texto es 35, su "longitud" es 30 y el número de minutos de caracteres para ocultar es 10, entonces obtendrías tu cuerda completa Si su número mínimo de caracteres para ocultar era 3, obtendría puntos suspensivos en lugar de esos tres caracteres.

Lo más importante a tener en cuenta es que he subvertido el significado de "longitud" para que ya no sea una longitud máxima. La longitud de la cadena de salida ahora puede ser desde 30 caracteres (cuando la longitud del texto es> 40) a 40 caracteres (cuando la longitud del texto es de 40 caracteres). Efectivamente, nuestra longitud máxima se convierte en length + minNumberOfhiddenCharacters. Por supuesto, la cadena podría tener menos de 30 caracteres cuando la cuerda original sea inferior a 30, pero este es un caso aburrido que debemos ignorar.

Si quieres longitud a un máximo duro y rápido entonces lo que quiere algo más como:

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

lo tanto, en este ejemplo, si text.length() es 37, la longitud es de 30 y minNumberOfhiddenCharacters = 10 luego entraremos en la segunda parte del if interno y obtendremos 27 caracteres + ... para hacer 30. Esto es realmente lo mismo que si hubiésemos entrado en la primera parte del ciclo (que es un signo que tienen nuestras condiciones de frontera correctas). Si la longitud del texto fuera 36 obtendríamos 26 caracteres + los puntos suspensivos que nos da 29 caracteres con 10 ocultos.

Estaba debatiendo si reorganizar parte de la lógica de comparación lo haría más intuitivo pero al final decidió dejarlo tal como está. Puede encontrar que text.length() - minNumberOfhiddenCharacters < length-3 hace que sea más obvio lo que está haciendo.

3

En mi opinión, no se pueden obtener buenos resultados sin el cálculo de píxeles.

Por lo tanto, Java es probablemente el extremo equivocado para solucionar este problema cuando se encuentra en un contexto de aplicación web (como facebook).

I'd go to javascript. Como Javascript no es mi principal campo de interés, realmente no puedo juzgar si this es una buena solución, pero podría darle un puntero.

+0

+1 de acuerdo. No puedo comentar el JavaScript tampoco, pero resalta la necesidad de abordar el problema en la vista. Como comentario adicional, usaría "..." http://www.fileformat.info/info/unicode/char/2026/index.htm – trashgod

10

Si estamos hablando de un sitio web - es decir, la salida de HTML/JS/CSS, se puede tirar todas estas soluciones porque hay una solución pura de CSS.

text-overflow:ellipsis; 

No es tan simple como simplemente agregar ese estilo a su CSS, porque interfiere con otros CSS; por ejemplo, requiere que el elemento tenga desbordamiento: oculto; y si quiere su texto en una sola línea, white-space:nowrap; también es bueno.

que tienen una hoja de estilo que se parece a esto:

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

Usted puede incluso tener un botón de "leer más" que simplemente se ejecuta una función javascript para cambiar los estilos, y el bingo, la caja será cambiar el tamaño y el texto completo será visible.(en mi caso, tiendo a usar el atributo de título html para el texto completo, a menos que sea muy largo)

Espero que ayude. Es una solución mucho más simple que tratar de complicar el cálculo del tamaño del texto y truncarlo, y todo eso. (por supuesto, si está escribiendo una aplicación que no está basada en la web, puede que necesite hacerlo)

Hay una desventaja en esta solución: Firefox no es compatible con el estilo de puntos suspensivos. Es molesto, pero no creo que sea crítico. Aún trunca el texto correctamente, ya que se trata de desbordamiento: oculto, simplemente no muestra los puntos suspensivos. Funciona en todos los otros navegadores (incluido IE, todo el camino de regreso a IE5.5!), Por lo que es un poco molesto que Firefox aún no lo haga. Esperemos que una nueva versión de Firefox resuelva este problema pronto.

[EDIT]
La gente sigue votando en esta respuesta, por lo que deben editar con señalar que Firefox no soporta ahora el estilo de puntos suspensivos. La característica se agregó en Firefox 7. Si está usando una versión anterior (FF3.6 y FF4 aún tienen algunos usuarios), entonces no tiene suerte, pero la mayoría de los usuarios de FF ahora están de acuerdo. Hay muchos más detalles sobre esto aquí: text-overflow:ellipsis in Firefox 4? (and FF5)

+0

Me gusta esta respuesta también. Tristemente, el CEO de donde estoy ahora solo usa Firefox y se queja cuando no puede ver las cosas correctamente, incluso ignorando a todos los otros navegadores ... :(¡Pero espero que Firefox lo admita pronto! –

+0

Sí, es molesto cuando obtienes eso. Hemos adoptado el enfoque pragmático de que podemos vivir sin las elipsis en Firefox, dado que el resto de la funcionalidad funciona bien (es decir, se trunca correctamente, el enlace de leer más funciona, etc.). Podría hackear alrededor, tal vez tener un bloque semitransparente de fundido a blanco que cubre los últimos caracteres de su elemento de texto, por lo que si el texto lo cubre, parece desvanecerse. No es una elipsis, pero puede ser una alternativa adecuada. – Spudley

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

Me sorprende que nadie ha mencionado Commons Lang StringUtils#abbreviate().

Actualización: sí, no tiene en cuenta los caracteres delgados, pero no estoy de acuerdo con eso teniendo en cuenta que todos tienen diferentes pantallas y configuración de fuentes y una gran parte de las personas que aterrizan en esta página probablemente estén buscando para una biblioteca mantenida como la anterior.

+1

Eso no hace lo que mi pregunta hace. –

+4

Supongo que sí. Extrañé la referencia de los personajes delgados, pero personalmente creo que es ridículo y no tiene en cuenta ** i18n **. ts ** no ** el método ** ideal ** y ahora la gente va a copiar y pegar el código anterior cuando hay una biblioteca que ya lo hace de una manera determinística ... Por cierto, faltó t porque "t" es delgado en mi pantalla. –

+1

¡Gracias la respuesta Adam! StringUtils.abbreviate funcionó bien para mi caso de uso. –

4

¿Qué tal esto (para obtener una cadena de 50 caracteres):

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