Как считать графемные кластеры или "воспринимаемые" символы эмодзи в Java

Я рассчитываю подсчитать количество воспринимаемых символов смайликов в предоставленной строке Java. В настоящее время я использую библиотеку emoji4j, но она не работает для таких графических кластеров:

призвание EmojiUtil.getLength("‍‍‍") возвращается 4 вместо 1 и аналогично зовет EmojiUtil.getLength("‍‍‍") возвращается 5 вместо 2,

Есть ли какие-либо API или методы на String в Java, которые облегчают подсчет графических кластеров?

Я охотился вокруг, но по понятным причинам codePoints() метод на String включает в себя не только видимые смайлики, но и столяры нулевой ширины.

Я также попытался это с помощью BreakIterator:

public static int getLength(String emoji) {
    BreakIterator it = BreakIterator.getCharacterInstance();
    it.setText(emoji);
    int emojiCount = 0;
    while (it.next() != BreakIterator.DONE) {
        emojiCount++;
    }
    return emojiCount;
}

Но, похоже, ведет себя идентично codePoints() метод, возвращающий 8 за что-то вроде "‍‍‍",

3 ответа

Решение

Я закончил тем, что использовал библиотеку ICU, которая работала намного лучше. Никаких изменений (кроме операторов import) не потребовалось от моего исходного кодового блока, поскольку он просто обеспечивает другую реализацию BreakIterator,

Спустя более чем шесть лет после того, как был задан этот вопрос, появилось усовершенствование для правильной обработки кластеров графем вStringнаконец был реализован в Java 20, выпущенной несколько недель назад. См. .

JDK-8291660 Поддержка Grapheme в BreakIteratorВ API класса BreakIterator изменений нет , но его базовый код теперь корректно обрабатывает кластер графем как единую единицу, а не как несколько символов.

Вот пример приложения, использующий метод и данные, указанные в вопросе, без каких-либо изменений:

      import java.nio.charset.Charset;
import java.text.BreakIterator;

public class Main {

    public static void main(String[] args) throws java.io.UnsupportedEncodingException {
        System.out.println("System.getProperty(\"java.version\"): " + System.getProperty("java.version"));
        System.out.println("Charset.defaultCharset():" + Charset.defaultCharset());
        Main.printStringInfo("‍‍‍");
        Main.printStringInfo("‍‍‍");
    }

    static void printStringInfo(String s) {
        System.out.print("\nCode points for the String " + s + ":");
        s.codePoints().mapToObj(Integer::toHexString).forEach(x -> System.out.print(x + " "));
        System.out.println("\nThe length of the String " + s + " using String.length() is " + s.length());
        System.out.println("The length of the String " + s + " using BreakIterator is " + Main.getLength(s));
    }

    // Returns the correct number of perceived characters in a String.
    // Requires JDK 20+ to work correctly.
    // Earlier Java releases will incorrectly just count the code points instead.
    // JDK-8291660 "Grapheme support in BreakIterator" (https://bugs.openjdk.org/browse/JDK-8291660) refers.
    public static int getLength(String emoji) {
        BreakIterator it = BreakIterator.getCharacterInstance();
        it.setText(emoji);
        int count = 0;
        while (it.next() != BreakIterator.DONE) {
            count++;
        }
        return count;
    }
}

Вот результат, показывающий правильное количество графем (1 и 2) при использовании JDK 20:

      C:\Java\jdk-20\bin\java.exe -javaagent:C:\Users\johndoe\AppData\Local\JetBrains\Toolbox\apps\IDEA-U\ch-0\232.5150.116\lib\idea_rt.jar=53642:C:\Users\johndoe\AppData\Local\JetBrains\Toolbox\apps\IDEA-U\ch-0\232.5150.116\bin -Dfile.encoding=UTF-8 -Dsun.stdout.encoding=UTF-8 -Dsun.stderr.encoding=UTF-8 -classpath D:\II2023.1\Graphemes\out\production\Graphemes Main
System.getProperty("java.version"): 20-ea
Charset.defaultCharset():UTF-8

Code points for the String ‍‍‍:1f469 200d 1f469 200d 1f466 200d 1f466 
The length of the String ‍‍‍ using String.length() is 11
The length of the String ‍‍‍ using BreakIterator is 1

Code points for the String ‍‍‍:1f47b 1f469 200d 1f469 200d 1f466 200d 1f466 
The length of the String ‍‍‍ using String.length() is 13
The length of the String ‍‍‍ using BreakIterator is 2

Process finished with exit code 0

А вот результат идентичного кода, показывающий неправильное количество графем (7 и 8) при использовании JDK 17:

      C:\Java\jdk-17.0.2\bin\java.exe -javaagent:C:\Users\johndoe\AppData\Local\JetBrains\Toolbox\apps\IDEA-U\ch-0\232.5150.116\lib\idea_rt.jar=53775:C:\Users\johndoe\AppData\Local\JetBrains\Toolbox\apps\IDEA-U\ch-0\232.5150.116\bin -Dfile.encoding=UTF-8 -classpath D:\II2023.1\Graphemes\out\production\Graphemes Main
System.getProperty("java.version"): 17.0.2
Charset.defaultCharset():UTF-8

Code points for the String ‍‍‍:1f469 200d 1f469 200d 1f466 200d 1f466 
The length of the String ‍‍‍ using String.length() is 11
The length of the String ‍‍‍ using BreakIterator is 7

Code points for the String ‍‍‍:1f47b 1f469 200d 1f469 200d 1f466 200d 1f466 
The length of the String ‍‍‍ using String.length() is 13
The length of the String ‍‍‍ using BreakIterator is 8

Process finished with exit code 0

Я тестировал это в IntellijIDEA 2023.1.1 Preview , используя Oracle OpenJDK версии 20.0.1 и Oracle OpenJDK версии 17.0.2.

JDK 15 представил поддержку расширенных кластеров графем вjava.util.regexпакет (примечание к выпуску ). Тем временемjava.txt.BreakIteratorеще не обновлен (рабочий статус ).

Для тех, кто хочет самодельное решение, этот код работает:

      /** Returns the number of grapheme clusters within `text` between positions
  * `start` and `end`.  Omits any partial cluster at the end of the span.
  */
int columnarSpan( String text, int start, int end ) {
    return columnarSpan( text, start, end, /*wholeOnly*/true ); }


/** @param wholeOnly Whether to omit any partial cluster at the end
  *   of the span.  Iff `true` and `end` bisects the final cluster,
  *   then the final cluster is omitted from the count.
  */
int columnarSpan( final String text, final int start, final int end,
      final boolean wholeOnly ) {
    graphemeMatcher.reset( text ).region( start, end );
    int count = 0;
    while( graphemeMatcher.find() ) ++count;
    if( wholeOnly  &&  count > 0  &&  end < text.length() ) {
        final int countNext = columnarSpan( text, start, end + 1, false );
        if( countNext == count ) --count; } /* The character at `end` bisects
          the final cluster, which therefore lies partly outside the span.
          Therefore exclude it from the count. */
    return count; }


final Matcher graphemeMatcher = graphemePattern.matcher( "" );


/** The pattern of a grapheme cluster.
  */
static final Pattern graphemePattern = Pattern.compile( "\\X" ); } /*
  The alternative of `java.txt.BreakIterator`, is apparently outdated.
  https://bugs.openjdk.java.net/browse/JDK-8174266
  https://bugs.openjdk.java.net/browse/JDK-8243579 */

Назовите это так:

      String emoji = "‍‍‍";
int count = columnarSpan( emoji, 0, /*end*/emoji.length() );
System.out.println( count );

⇒ 2

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

      int count = columnarSpan( emoji, 0, /*end*/emoji.length() - 1 );
System.out.println( count );

⇒ 1

Как правило, это поведение, которое вам нужно, чтобы напечатать строку текста с указателем символа, расположенным под ней (например, '^'), указывающий на кластер символа по данному индексу. Чтобы избежать этого поведения (указывая после cluster ), вызовите базовый метод следующим образом.

      int count = columnarSpan( emoji, 0, /*end*/emoji.length() - 1, false );
System.out.println( count );

⇒ 2
Другие вопросы по тегам