Как считать графемные кластеры или "воспринимаемые" символы эмодзи в 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