Método ideal para truncar una cadena con puntos suspensivos

Estoy seguro de que todos hemos visto puntos suspensivos en los estados de Facebook (o en otro lugar), y hemos hecho clic en “Mostrar más” y solo hay otros 2 caracteres más o menos. Supongo que esto se debe a la progtwigción perezosa, porque seguramente hay un método ideal.

La mía cuenta los caracteres delgados [iIl1] como “medio caracteres”, pero esto no pasa por alto las elipsis “se ven tontas cuando ocultan casi ningún personaje”.

¿Hay un método ideal? Aquí esta el mio:

 /** * Return a string with a maximum length of length characters. * If there are more than length 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.

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

Sin embargo, el problema principal con la mayoría de los elipsis es (imho) que cortan 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 se ve así:

enter image description here

Estoy sorprendido de que nadie haya mencionado Commons Lang StringUtils # abreviatura () .

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 fonts y una gran parte de las personas que aterrizan aquí en esta página probablemente busquen una biblioteca mantenida como lo anterior.

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

Adición: Al abordar este problema, puede ayudar a distinguir entre el modelo y la vista. El modelo es una 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 los FontMetrics apropiados deberían determinarse empíricamente.

texto alternativo

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

Si está hablando de un sitio web, es decir, sacando HTML / JS / CSS, puede descartar 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; es bueno también

Tengo una hoja de estilo que se ve así:

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

Incluso puede tener un botón “leer más” que simplemente ejecuta una función de JavaScript para cambiar los estilos, y el bingo, la caja se volverá a clasificar según el tamaño y el texto completo será visible. (en mi caso, aunque 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ás escribiendo una aplicación que no está basada en la web, es posible que aún necesites hacer eso)

Hay una desventaja para 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.

[EDITAR]
La gente aún está votando sobre esta respuesta, por lo que debería editarla para señalar que Firefox ahora admite 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. Aquí hay muchos más detalles sobre esto: text-overflow: puntos suspensivos en Firefox 4? (y FF5)

Para mí esto sería ideal –

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

No me preocuparía el tamaño de cada personaje a menos que realmente sepa dónde y en qué tipo de letra se va a mostrar. Muchas fonts son fonts 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.

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

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

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

Si le preocupan las elipsis y solo esconde una cantidad muy pequeña de personajes, ¿por qué no solo busca 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; } 

Me gustaría ir con algo similar al modelo estándar que tienes. 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 realice la verificación de puntos suspensivos, haré algo como:

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

Lo que esto significará es que si la longitud de su texto es 35, su “longitud” es 30 y su número mínimo de caracteres para ocultar es 10, entonces obtendrá su secuencia 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 que la duración sea un máximo difícil y rápido, entonces querrás 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) + "..."; } } 

Entonces en este ejemplo si text.length () es 37, length es 30 y minNumberOfhiddenCharacters = 10, entonces entraremos a la segunda parte del if interno y obtendremos 27 caracteres + ... para hacer 30. Esto es realmente lo mismo como si hubiésemos entrado en la primera parte del ciclo (que es un signo de que tenemos 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.

En mi opinión, no se pueden obtener buenos resultados sin las matemáticas de píxeles.

Por lo tanto, es probable que Java sea el final equivocado para solucionar este problema cuando se encuentre en un contexto de aplicación web (como facebook).

Yo buscaría javascript. Dado que Javascript no es mi principal campo de interés, realmente no puedo juzgar si esta es una buena solución, pero podría darle un puntero.

Usando el método com.google.common.base.Ascii.truncate (CharSequence, int, String) de Guava:

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