【Kotlin】絵文字を含む Unicode 文字列の文字数をカウントする方法と文字ごとの構成要素

絵文字 は AndroidStudio 上で扱いづらいですよね。

例えば、


"😀😍"

という2つの絵文字は、


"\uD83E\uDD78" + "\uD83D\uDC3B\u200D\u2744\uFE0F"

と書く方が編集しやすいです。(しかし分かりづらい。)

絵文字をコピーして Android Studio 上でペーストすると、

\uXXXX\uXXXX

のような「UTF-16 エスケープシーケンス」に置き換わりますよね。

(しかし置き換わらない場合もある。)

👉 Python vs Kotlin Unicode Escape Sequence (エスケープシーケンス) の記述 hatena-bookmark

 

😀 文字を数える

検証するために文字列データを作ります。

絵文字も新旧バージョンのものを含めます。

15文字です。

比較的に新しい Unicode Emoji 15.1 の2つの絵文字は、今現在、まともに見たことがありません。

このデータの文字数をカウントするのは、


println(Regex("\\X").findAll(data).count())
// 15

というかんじでしょうか。

Unicode extended grapheme clusters are supported by the grapheme cluster matcher \X.

👉 2.2 Extended Grapheme Clusters and Character Classes with Strings hatena-bookmark
👉 Unicord support - Pattern  |  Android Developers hatena-bookmark

強く開発環境に影響されると思いますので、各バージョン安定版最新に更新して揃えておくべきでしょう。


android {
  // ...
  compileOptions {
    sourceCompatibility JavaVersion.VERSION_17
    targetCompatibility JavaVersion.VERSION_17
  }

  kotlinOptions {
    jvmTarget = JavaVersion.VERSION_17.toString()
  }
}

 

😀 文字の構成(要素)を確認する

人間が見ている文字は内部的には複数の要素から構成されています。

絵文字周りで開発している方にはどこかで見たようなすごく便利な表です。

文字列を渡せば簡単に確認できるようにメソッドにしておきます。

先ほどの1文字ごとに分割したあとにその内部を要素に分割して表示します。

data は、先述の15文字を使ってみました。


dump(data)

AndroidStudio のデバッグウィンドウやターミナルでも表示できない絵文字はありますが、内部的には問題なく処理できてるように見えます。

 

😀 まとめ

今回、新しくはっきり認識できたことは、

kotlin の String.length は、


その文字(列)の Char (/uXXXX) の数を表している。

ということ。コードポイントの数ではない。

Char は、コードポイントを 内部的な UTF-16サロゲートペア 分割されたあとの要素。


println("👩‍👧‍👦".length)
// 8

👉 kotlin/Char.kt at 924c28507067cbfbf78a6509ea89eabe496e34ca · JetBrains/kotlin · GitHub hatena-bookmark



👉 絵文字が意図しない白黒で表示される ➡️ - Unicode Variation Selector hatena-bookmark


【Python】Unicode / UTF-16 エスケープシーケンス文字列 から 文字 に変換する方法

みんな大好き絵文字「👍」で。


👉 GitHub Emoji Unicode Full Emoji List - shortcode | code point | escape-sequence hatena-bookmark

 

👍 Unicode エスケープシーケンス


s = '\\U0001f44d'  # r'\U0001f44d'

print(s)
# \U0001f44d

print(s.encode().decode('unicode-escape'))
# 👍

print(eval(f'"{s}"').encode().decode('unicode-escape'))
# ð

「Unicode エスケープシーケンス」から「文字」に変換するには、


'\\U0001f44d'.encode().decode('unicode-escape')

でいけます。

 

👍 UTF-16 エスケープシーケンス

同様に、


s = '\\ud83d\\udc4d'  # r'\ud83d\udc4d'

print(s)
# \ud83d\udc4d

print(s.encode('utf-16', 'surrogatepass').decode('utf-16'))
# \ud83d\udc4d

print(eval(f'"{s}"').encode('utf-16', 'surrogatepass').decode('utf-16'))
# 👍

「UTF-16 エスケープシーケンス」から「文字」に変換するには、


eval('"\\ud83d\\udc4d"').encode('utf-16', 'surrogatepass').decode('utf-16')

でいけます。

 

👍 まとめ

ややこしいです。他にいい方法ないかな。



【Unicode】UTF-16 サロゲートぺア と コードポイント の変換

UTF-16 は、Unicode の文字をコードポイントと呼ばれる整数値で表現するエンコーディング方式です。UTF-16 では、一部のUnicode 文字は16ビットでは表現できないため、サロゲートペアと呼ばれる特殊な方法で符号化されます。

サロゲートペアは、2つの16ビットの値(上位サロゲートと下位サロゲート)を組み合わせて1つの Unicode 文字を表現します。上位サロゲートは 0xD800-0xDBFF の範囲にあり、下位サロゲートは 0xDC00-0xDFFF の範囲にあります。

Wikipedia で調べると数式があります。

【Unicode】UTF-16 サロゲートぺア と コードポイント の変換
👉 Unicode - Wikipedia hatena-bookmark


code-point <-> surrogate-pair

これに合わせて Python コードにしてみました。


def cp_to_sp(cp):
    cp -= 0x10000
    hsg = cp // 0x400 + 0xD800
    lsg = cp % 0x400 + 0xDC00
    return hsg, lsg


def sp_to_cp(hsg, lsg):
    cp = 0x10000 + (hsg - 0xD800) * 0x400 + (lsg - 0xDC00)
    return cp

この関数を、みんな大好き絵文字「👍」で試してみます。


👉 GitHub Emoji Unicode Full Emoji List - shortcode | code point | escape-sequence hatena-bookmark

表にあるデータから、コードポイントとサロゲートペアのそれぞれの値を使います。


cp = 0x1f44d
hsg, lsg = cp_to_sp(cp)
print(hex(hsg), hex(lsg))
# 0xd83d 0xdc4d

hsg = 0xd83d
lsg = 0xdc4d
cp = sp_to_cp(hsg, lsg)
print(hex(cp))
# 0x1f44d

うまくいってます。

 

👍 まとめ

Unicode まわりは CJK のおかげでか、ややこしいことになってます。

UTF-16などのエンコード処理を扱う場合、コード内では「コードポイント」を中心にを進めると幸せになることがよくあります。


👉 Python vs Kotlin Unicode Escape Sequence (エスケープシーケンス) の記述 hatena-bookmark


Python vs Kotlin Unicode Escape Sequence (エスケープシーケンス) の記述

あると便利な気がして Python スクリプトで作成しましたが。

👉 GitHub Emoji Unicode Full Emoji List - shortcode | code point | escape-sequence hatena-bookmark

AndroidStudio や IDEA Intellij では \uXXXX\uXXXX のような絵文字などの「Unicode Escape Sequence (エスケープシーケンス)」の記述をエディタからできますよね。


// Kotlin
println("Hello, world!")
println("\ud83d\udca4")

実行すると意図した絵文字が表示されます。

しかし、Python では、実行ができません。


# Python
print("Hello, world!")
print("\ud83d\udca4")

エラーメッセージは以下。

UnicodeEncodeError: 'utf-8' codec can't encode characters in position 0-1: surrogates not allowed

2つのエスケープシーケンスでそれぞれ試してみます。


💤
\U0001f4a4
\ud83d\udca4


// Kotlin    
    
// Illegal escape: '\U'
// println("\U0001f4a4")

// OK    
println("\ud83d\udca4")


# Python

# OK
print("\U0001f4a4")

# UnicodeEncodeError:
# 'utf-8' codec can't encode characters in position 0-1: surrogates not allowed
print("\ud83d\udca4")

 

😄 まとめ

エスケープシーケンスをコードに記述する場合、


Kotlin:
"\uXXXX\uXXXX"

Python:
"\UXXXXXXXX"

を使うと良い。

それぞれ、桁数(X)は、4桁、8桁で固定。
足りなければ、0 でパディング。



絵文字が意図しない白黒で表示される ➡️ - Unicode Variation Selector

一部の絵文字が白黒の記号と表示される場合があります。

カラーの絵文字と思いきや白黒。


Text("\u2B06\u27A1\u2B07\u2B05\u2195")

直接コードに絵文字埋め込むと意図通りに表示される。


// ブラウザ上でもうまく表示されないものがある。
Text("⬆️➡️⬇️⬅️↕️")

コードはわかりやすく Android Jetpack Compose にしています。

IDEのエディター上からコード内に直接絵文字を書くことができる感じもありますが、編集時にイラッとすること多いので直接書きたくありません。

これは、デバイスやOSやアプリが使用しているフォントだけに依るものなのでしょうか。

私の端末では、白黒とカラー両方の矢印を見たような気がする。

 

➡️ 調べてみる

この記事。

👉 Emoji displayed as monochrome symbol? 🤔 The Unicode variation selector hatena-bookmark

まとめると以下。

一部の記号は、意図しない白黒、またはカラー絵文字として表示される。

Unicode Variation Selector を使うとある程度制御できる。

Unicode Variation Selector を使用しない場合はシステム次第。

絵文字ピッカーには Unicode Variation Selector が含まれないものもある。

Mac の絵文字ピッカーや Emojipedia は Unicode Variation Selector が含まれています。

Unicode Variation Selector には  U+FE0E (VS15)と U+FE0F(VS16) が定義されている。

VS15 を追加すると白黒テキスト、VS16 はカラー絵文字が表示されます。

ここに挙げられている、Macの絵文字ピッカー (Control + Command + Space) でみると以下。右クリックで文字情報見れます。


⬆️
up arrow
Unicode: U+2B06 U+FE0F, UTF-8: E2 AC 86 EF B8 8F

⬇️
down arrow
Unicode: U+2B07 U+FE0F, UTF-8: E2 AC 87 EF B8 8F

➡️
right arrow
Unicode: U+27A1 U+FE0F, UTF-8: E2 9E A1 EF B8 8F

⬅️
left arrow
Unicode: U+2B05 U+FE0F, UTF-8: E2 AC 85 EF B8 8F

↕️
up-down arrow
Unicode: U+2195 U+FE0F, UTF-8: E2 86 95 EF B8 8F


⬆︎
UPWARDS BLACK ARROW
Unicode: U+2B06 U+FE0E, UTF-8: E2 AC 86 EF B8 8E

⬇︎
DOWNWARDS BLACK ARROW
Unicode: U+2B07 U+FE0E, UTF-8: E2 AC 87 EF B8 8E

➡︎
BLACK RIGHTWARDS ARROW
Unicode: U+27A1 U+FE0E, UTF-8: E2 9E A1 EF B8 8E

⬅︎
LEFTWARDS BLACK ARROW
Unicode: U+2B05 U+FE0E, UTF-8: E2 AC 85 EF B8 8E

⬍
UP DOWN BLACK ARROW
Unicode: U+2B0D, UTF-8: E2 AC 8D

同様に、以下からコピーした絵文字も Variation Selector きちんと入ってます。

👉 📙 Emojipedia — 😃 Home of Emoji Meanings 💁👌🎍😍 hatena-bookmark

入ってないツールや絵文字サイトもありますので注意です。

 

➡️ 試してみる

String がどうなってるか拡張関数を作っておきます。



これを使って詳細見てみます。


"⬆➡⬇⬅↕".printUnicodeEscapeSequences()

"⬆︎➡︎⬇︎⬅︎↕︎".printUnicodeEscapeSequences()

"⬆️➡️⬇️⬅️↕️".printUnicodeEscapeSequences()

"⬆️".printUnicodeEscapeSequences()

"➡️".printUnicodeEscapeSequences()

"⬇️".printUnicodeEscapeSequences()

"⬅️".printUnicodeEscapeSequences()

"↕️".printUnicodeEscapeSequences()

結果。


⬆➡⬇⬅↕
\u2B06\u27A1\u2B07\u2B05\u2195

⬆︎➡︎⬇︎⬅︎↕︎
\u2B06\uFE0E\u27A1\uFE0E\u2B07\uFE0E\u2B05\uFE0E\u2195\uFE0E

⬆️➡️⬇️⬅️↕️
\u2B06\uFE0F\u27A1\uFE0F\u2B07\uFE0F\u2B05\uFE0F\u2195\uFE0F

⬆️
\u2B06\uFE0F

➡️
\u27A1\uFE0F

⬇️
\u2B07\uFE0F

⬅️
\u2B05\uFE0F

↕️
\u2195\uFE0F

 

➡️ まとめ

異体字セレクタ (いたいじセレクタ、英: Variation Selectors) は、Unicode および ISO/IEC 10646 (UCS) における、文字の字体をより詳細に指定するためのセレクタ (選択子) である。

👉 異体字セレクタ - Wikipedia hatena-bookmark

矢印の場合カラー絵文字をある程度強制したい場合の Kotlin 文字列 Unicode 記述は、

Unicode Variation Selector\uFE0F (VS16) を文字シンボルの後につける。」


⬆️➡️⬇️⬅️↕️
\u2B06\uFE0F\u27A1\uFE0F\u2B07\uFE0F\u2B05\uFE0F\u2195\uFE0F

Jetpack Compose で書くと以下。


Text("\u2B06\uFE0F\u27A1\uFE0F\u2B07\uFE0F\u2B05\uFE0F\u2195\uFE0F")

逆に、白黒にしたい場合は、\uFE0E (VS15) をつければよい。

別に、layout.xml だろうが strings.xml だろうが同じ。

あと、ちなみに、いつも話題に上がる国旗とか。


🇯🇵
\uD83C\uDDEF\uD83C\uDDF5

👨‍👩‍👧‍👧
\uD83D\uDC68\u200D\uD83D\uDC69\u200D\uD83D\uDC67\u200D\uD83D\uDC67

なげえ。

👉 【Kotlin】絵文字を含む Unicode 文字列の文字数をカウントする方法と文字ごとの構成要素 hatena-bookmark