Идеальный метод для усечения строки с многоточием
Я уверен, что все мы видели многоточие в статусах 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, потому что это то, что мне больше всего интересно видеть.
14 ответов
Мне нравится идея, чтобы "тонкие" персонажи считались половиной персонажа. Простое и хорошее приближение.
Основная проблема с большинством многоточий, однако, заключается в том, что они разбивают слова в середине. Вот решение, учитывающее границы слов (но не углубляющееся в pixel-math и 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) + "...";
}
Тест алгоритма выглядит так:
Я в шоке, никто не упомянул Commons Lang StringUtils # abbreviate ().
Обновление: да, это не учитывает тонких символов, но я не согласен с этим, учитывая, что у всех разные настройки экранов и шрифтов, и большая часть людей, которые попадают сюда на эту страницу, вероятно, ищут поддерживаемую библиотеку, такую как выше.
Кажется, вы можете получить более точную геометрию из графического контекста 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://stackru.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();
}
});
}
}
Если вы говорите о веб-сайте - т.е. выводе 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 для полного текста, если он не будет очень длинным)
Надеюсь, это поможет. Это гораздо более простое решение: попытка путаницы вычислить размер текста и усечь его, и все такое. (конечно, если вы пишете приложение без веб-интерфейса, вам все равно может потребоваться это сделать)
У этого решения есть один недостаток: Firefox не поддерживает стиль многоточия. Раздражает, но я не думаю, что это критично - он по-прежнему правильно обрезает текст, так как это решается переполнением: скрыто, оно просто не отображает многоточие. Он работает во всех других браузерах (включая IE, вплоть до IE5.5!), Поэтому немного раздражает, что Firefox пока этого не делает. Надеемся, что новая версия Firefox скоро решит эту проблему.
[РЕДАКТИРОВАТЬ]
Люди все еще голосуют за этот ответ, поэтому я должен отредактировать его, чтобы отметить, что Firefox теперь поддерживает стиль многоточия. Функция была добавлена в Firefox 7. Если вы используете более раннюю версию (у FF3.6 и FF4 все еще есть пользователи), то вам не повезло, но большинство пользователей FF теперь в порядке. Здесь есть много подробностей: переполнение текста: многоточие в Firefox 4? (и FF5)
Для меня это было бы идеально -
public static String ellipsis(final String text, int length)
{
return text.substring(0, length - 3) + "...";
}
Я не стал бы беспокоиться о размере каждого символа, если бы не знал, где и каким шрифтом он будет отображаться. Многие шрифты являются шрифтами фиксированной ширины, где каждый символ имеет одинаковое измерение.
Даже если это шрифт переменной ширины, и если вы посчитаете 'i', 'l', чтобы взять половину ширины, то почему бы не подсчитать 'w' 'm', чтобы взять двойную ширину? Смесь таких символов в строке обычно усредняет влияние их размера, и я предпочел бы игнорировать такие детали. Мудрый выбор значения длины имел бы наибольшее значение.
Используя метод com.google.common.base.Ascii.truncate(CharSequence, int, String) в Guava:
Ascii.truncate("foobar", 7, "..."); // returns "foobar"
Ascii.truncate("foobar", 5, "..."); // returns "fo..."
Как насчет этого (чтобы получить строку из 50 символов):
text.replaceAll("(?<=^.{47}).*$", "...");
public static String getTruncated(String str, int maxSize){
int limit = maxSize - 3;
return (str.length() > maxSize) ? str.substring(0, limit) + "..." : str;
}
На мой взгляд, вы не можете получить хорошие результаты без пиксельной математики.
Таким образом, Java, вероятно, является неправильным концом для решения этой проблемы, когда вы находитесь в контексте веб-приложения (например, Facebook).
Я бы пошел на JavaScript. Поскольку Javascript не является моей основной областью интересов, я не могу судить, действительно ли это хорошее решение, но оно может дать вам указатель.
Если вы беспокоитесь о том, что многоточие скрывает только очень небольшое количество символов, почему бы просто не проверить это условие?
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;
}
Я бы пошел с чем-то похожим на стандартную модель, которая у вас есть. Я бы не стал беспокоиться о ширине символов - как сказал @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, length равно 30 и minNumberOfhiddenCharacters = 10, мы перейдем ко второй части внутреннего if и получим 27 символов + ..., чтобы получить 30. Это на самом деле то же самое как если бы мы вошли в первую часть цикла (это признак того, что у нас правильные граничные условия). Если бы длина текста была 36, мы получили бы 26 символов + многоточие, дающее нам 29 символов с 10 скрытыми.
Я спорил о том, сделает ли перестановка некоторой логики сравнения более понятной, но в итоге решил оставить все как есть. Вы можете найти это text.length() - minNumberOfhiddenCharacters < length-3
делает более очевидным, что вы делаете, хотя.
Большинство из этих решений не учитывают метрики шрифта, вот очень простое, но работающее решение для 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;
}
Для простых случаев я использовал для этого String.format.
Здесь я сокращаю до максимум 10 символов и добавляю многоточие:
String abbreviate(String longString) {
return String.format("%.10s...", longString);
}
Малоизвестный факт, что «точные» числа в шаблоне формата используются для усечения строк.
Добавьте свою собственную проверку длины, конечно, если вы хотите сделать многоточие условным. (Я сокращал JWT для ведения журнала, поэтому знаю , что он будет длиннее)
В качестве бонуса, если String уже короче, чем точность, заполнение не выполняется, оно просто оставляется как есть.
> System.out.println(abbreviate("This is a very long string"));
> System.out.println(abbreviate("Shorty"));
This is a ...
Shorty...
Вы также можете просто реализовать это так:
mb_strimwidth($string, 0, 120, '...')
Спасибо.