Идеальный метод для усечения строки с многоточием - PullRequest
56 голосов
/ 30 августа 2010

Я уверен, что все мы видели многоточие в статусах 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, потому что это то, что мне больше всего интересно видеть.

Ответы [ 12 ]

76 голосов
/ 07 сентября 2010

Мне нравится идея, чтобы "тонкие" персонажи считались половиной персонажа.Простое и хорошее приближение.

Основная проблема с большинством многоточий заключается в том, что (imho) они отбивают слова в середине .Вот решение, учитывающее границы слов (но не углубляющееся в 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) + "...";
}

Тест алгоритма выглядит следующим образом:

enter image description here

51 голосов
/ 01 сентября 2012

Я в шоке, никто не упомянул Commons Lang StringUtils # abbreviate () .

Обновление: да, это не учитывает тонких символов, ноЯ не согласен с этим, учитывая, что у всех разные экраны и настройки шрифтов, и большая часть людей, которые попадают сюда на эту страницу, вероятно, ищут поддерживаемую библиотеку, подобную приведенной выше.

26 голосов
/ 30 августа 2010

Похоже, вы можете получить более точную геометрию из FontMetrics.

графического контекста Java.

Приложение: При подходе к этой проблеме может помочь различить модель и вид. Модель представляет собой String, конечную последовательность кодовых точек UTF-16, в то время как представление представляет собой серию глифов, отображаемых некоторым шрифтом на некотором устройстве.

В конкретном случае 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();
            }
        });
    }
}
10 голосов
/ 07 сентября 2010

Если вы говорите о веб-сайте - т.е. выводе 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 решит эту проблему в ближайшее время.

[EDIT] Люди все еще голосуют за этот ответ, поэтому я должен отредактировать его, чтобы отметить, что Firefox теперь поддерживает стиль многоточия.Функция была добавлена ​​в Firefox 7. Если вы используете более раннюю версию (у FF3.6 и FF4 все еще есть пользователи), то вам не повезло, но большинство пользователей FF теперь в порядке.Здесь есть много подробностей: переполнение текста: многоточие в Firefox 4?(и FF5)

4 голосов
/ 26 марта 2015

Как насчет этого (получить строку из 50 символов):

text.replaceAll("(?<=^.{47}).*$", "...");
4 голосов
/ 31 мая 2012
 public static String getTruncated(String str, int maxSize){
    int limit = maxSize - 3;
    return (str.length() > maxSize) ? str.substring(0, limit) + "..." : str;
 }
4 голосов
/ 02 сентября 2010

Для меня это было бы идеально -

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

Я бы не стал беспокоиться о размере каждого символа, если бы не знал, где и каким шрифтом он будет отображаться. Многие шрифты являются шрифтами фиксированной ширины, где каждый символ имеет одинаковое измерение.

Даже если это шрифт переменной ширины, и если вы посчитаете 'i', 'l', чтобы взять половину ширины, то почему бы не считать 'w' 'm', чтобы взять двойную ширину? Смесь таких символов в строке обычно усредняет влияние их размера, и я предпочел бы игнорировать такие детали. Мудрый выбор значения «длина» будет иметь наибольшее значение.

3 голосов
/ 03 сентября 2010

На мой взгляд, вы не можете получить хорошие результаты без пиксельной математики.

Таким образом, Java, вероятно, является неправильным концом для решения этой проблемы, когда вы находитесь в контексте веб-приложения (например, Facebook).

Я бы пошел на JavaScript.Поскольку Javascript не является моей основной областью интересов, я не могу судить, является ли это хорошим решением, но оно может дать вам указатель.

3 голосов
/ 02 сентября 2010

Я бы пошел с чем-то похожим на стандартную модель, которая у вас есть.Я бы не стал беспокоиться о ширине символов - как сказал @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, длина равна 30 и minNumberOfhiddenCharacters = 10, мы перейдем ко второй части внутреннего if и получим 27символов + ... чтобы получить 30. Это на самом деле так же, как если бы мы вошли в первую часть цикла (что является признаком того, что у нас правильные граничные условия).Если бы длина текста была 36, мы получили бы 26 символов + многоточие, дающее нам 29 символов со скрытыми 10.

Я спорил, сделает ли перестановка некоторой логики сравнения более интуитивной, но в конце концов решилоставь как есть.Вы можете обнаружить, что text.length() - minNumberOfhiddenCharacters < length-3 делает более очевидным, что вы делаете, хотя.

3 голосов
/ 02 сентября 2010

Если вас волнует, что многоточие скрывает только очень небольшое количество символов, почему бы просто не проверить это условие?

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;
}
...