なでしこ3で絵文字が入った文章の文字列処理をしようとしたら、なかなかうまくいかなくてがんばる話。
正規表現でサロゲートペア文字の判定をしたり、異体字セレクタの判定をしたりします。
発端
なでしこ1はShift_JISなので、絵文字どころかUnicodeの文字が使えなくて時々悲しいのですが、なでしこ3はばっちり使えます。やったね!
ところが、絵文字の入った文章で文字列処理しようとしたら、いろいろうまく行かなかったのです。
たとえば・・・
「こんにちは😀」の文字数を言う。
なでしこさんは、6
と教えてくださいます。あってますね!
「こんにちは😀寒いですね💧」で「😀」が何文字目かを言う。
6
と仰せです。まあ、当然ですよね。
がっ!
「こんにちは😀寒いですね💧」で「寒」が何文字目かを言う。
なでしこさんは、8
と仰るのです・・・
6文字目の次が8!
次に・・・
「寒いですね💧」の1だけ文字右部分を言う。
なんだコレ。
ちなみに見えない目でようく見ると、ちっちゃな四角の中に「DCA7」と書いてありますよ。
Qiitaでは表示できないみたいですが。
サロゲートペア?
Unicode(UTF-16)は1文字を2バイトで表現しています。
なでしこさんも、JavaScriptに変換されて実行されるので、内部的にはUTF-16だということです。
ところがUnicodeで扱う文字がどんどん増えて、とうてい65535文字では対応できないぃ~って事になったので、文字数を拡張するために一部の文字を4バイトで表現する事にしたそうです。つまり、一文字で二文字分消費している文字があるってことですね。そして、絵文字もそこに含まれているために、6文字目の次が8文字目となる事態が発生するわけなのでした。なんと~~~。
そんなわけで、「💧」は、ATOKさんの文字パレットを見てみると、U+1F4A7(D83D DCA7)となっていますね。おぉ「DCA7」!
上位サロゲートがu+D83D、下位サロゲートがu+DCA7、二つ合わせて「💧」になる、とゆうことでした。
しかし「文字右部分」命令は、このサロゲートペアには対応しておらず、2バイトを一文字として文字列の右から取得する命令だから、一文字が半分なっちゃって、化けたように表示されるんですね。
サロゲートペア文字の判定
正規表現を使えば判定できそうですね。
前に、漢字か判定するヤツを作ったからそれと同様にして、上位サロゲートが「\uD800-\uDBFF」、下位サロゲートが「\uDC00-\uDFFF」だそうなので、こんな感じで判定できます。
//上位でも下位でもヒットする。
●サロゲート判定(Sが|Sの|Sを)
Sを「^[\uD800-\uDBFF\uDC00-\uDFFF]」で正規表現マッチ。
もし、それがNULLならば、いいえで戻る。
違えば、はいで戻る。
ここまで。
●上位サロゲート判定(Sが|Sの|Sを)
Sを「^[\uD800-\uDBFF]」で正規表現マッチ。
もし、それがNULLならば、いいえで戻る。
違えば、はいで戻る。
ここまで。
●下位サロゲート判定(Sが|Sの|Sを)
Sを「^[\uDC00-\uDFFF]」で正規表現マッチ。
もし、それがNULLならば、いいえで戻る。
違えば、はいで戻る。
ここまで。
そして、判定がはいだったら抜き出す数を+1するようにすれば良さそげです。
テスト=「こんにちは😀寒いですね💧」
数=6。# 抜き出す文字数。
結果=テストの1から数を文字抜き出し。
結果を表示。# あれっ💧
もし、((結果の1だけ文字右部分)の上位サロゲート判定)=はいならば、:
結果2=テストの1から数+1を文字抜き出し。
結果2を表示。# こんにちは😀
できました!
これで、1文字が分割されて化けちゃうのは防げるようになりました☆
サロゲートペア文字の個数を数える
でも、途中に絵文字が入ってた場合は? 文字数はどうなりますか??
動物=「ねこ🐱うさ🐰いぬ🐶はむ🐹」
動物の文字数を表示。#12
動物の1から12を文字抜き出しを表示。#ねこ🐱うさ🐰いぬ🐶
文字数はあってるんです。
でも、文字抜き出しは、分かってましたが3つの絵文字が消費した3文字分が抜けてしまいました。
お試ししてみたところ、文字数
と文字列分解
はサロゲートペアに影響されず一文字は一文字として取得されるようです。これはどうやらじゃばすくりぷと的にArray.from
を使っているからのようだね。しらんけど。
それ以外は、件の如し。
元のJavascriptの命令の仕様だからしょうがないね! しらんけど。
とりあえず、その文字列に何個サロゲートペア文字があるか数えて、抜き出す文字数にプラスすればいいんですかね。
●サロゲートペア文字個数(Sの)
Sを「[\uD800-\uDBFF\uDC00-\uDFFF]」で正規表現マッチ。
もし、それ=NULLならば、0で戻る。
違えば、それの要素数/2で戻る。
ここまで。
動物=「ねこ🐱うさ🐰いぬ🐶はむ🐹」
動物の文字数を表示。#12
数=それ。
結果=動物の1から数を文字抜き出し。
結果を表示。# ねこ🐱うさ🐰いぬ🐶 足りない!
もし、(結果のサロゲートペア文字個数)>0ならば、:
結果=動物の1から数+それを文字抜き出し。
結果を表示。# あっ
もし、((結果の1だけ文字右部分)の上位サロゲート判定)=はいならば、:
結果=動物の1から数+(結果のサロゲートペア文字個数)+1を文字抜き出し。
結果を表示。# ねこ🐱うさ🐰いぬ🐶はむ🐹
できました☆
実際には、最初に抜き出した時にも絵文字が分割されていないかチェックする必要がありそうですね。
文字列分解でできそう
うーん、だけど文字列分解がサロゲートペアに影響されずできるんなら、一文字ずつ配列操作したほうが早そうな?
動物=「ねこ🐱うさ🐰いぬ🐶はむ🐹」
動物の文字数を表示。#12
数=それ。
動物配列=動物を文字列分解。
動物配列の0から数を配列取り出す。
それを空で配列結合して表示。# ねこ🐱うさ🐰いぬ🐶はむ🐹
できました!
文字列操作は1スタートですが配列は0スタートなのでそこだけ注意です。
右部分も、
テスト=「寒いですね💧」
テスト配列=テストを文字列分解。
末尾=(テスト配列の要素数)-1
テスト配列の末尾から1を配列取り出して表示。# 💧
できました!
こっちのほうが圧倒的にカンタンそう?
いやいや、それでも判定したり数えたりはできたほうが何かとイイですよ。タブン。
そしてやっぱり・・・
サロゲートペアだけじゃなかった絵文字の罠!
異体字セレクタ
たとえば、「❤️」。よく使いますよね~❤️
「❤️」の文字数を表示。
とやったら、2
になるんです。
文字列分解したら、「❤」の後になにやら見えない文字が入っていますよ。
ナニコレ。
ハート=「❤️」を文字列分解。
ASC(ハート[1])を16進数変換して表示。
「fe0f」と出ました。
早速ATOKさんに文字コードをおうかがいしてみると、「VARIATION SELECTOR-16」と。
そんだけじゃぜんぜんわかんないですが、調べると「異体字セレクタ」とゆうもので、ベーシックな絵文字「❤」に、VARIATION SELECTOR-16(U+FE0F)を付けることで絵文字スタイルを指定したり、VARIATION SELECTOR-15(U+FE0E)を付けることでテキストスタイルを指定したりできるようになってるってことなんですね。
1~16(U+FE00〜U+FE0F)まで(SVS)のほかに、VS17~VS256(U+E0100〜U+E01EF)(IVS)もあって、漢字の異体字とかをアレしたりするヤツのようだけどよく分からん。
とりあえず、異体字セレクタも正規表現で判定して、文字数を-1したり、前の親文字と分離しないようにすれば良いのだろうか。
●異体字セレクタ判定(Sが|Sの|Sを)
Sを「^[\uFE00-\uFE0F]|\uDB40[\uDD00-\uDDEF]」で正規表現マッチ。
もし、それがNULLならば、いいえで戻る。
違えば、はいで戻る。
ここまで。
●異体字セレクタ個数(Sの)
Sを「[\uFE00-\uFE0F]|\uDB40[\uDD00-\uDDEF]」で正規表現マッチ。
もし、それ=NULLならば、0で戻る。
違えば、それの要素数で戻る。
ここまで。
こんな感じ?
使う当ては無いけど、一応念のためIVSのほうまで全部判定できるようにしています。
「すき❤️すき❤すき❤︎」の異字体セレクタ個数を表示。
2
と出ました☆
わかりにくいけど、絵文字スタイルのハート、異体字セレクタの無いベーシックなハート、テキストスタイルのハートと並べています。無事VS-15とVS-16が検出されました。
サロゲートペアと違って、親文字の方では判定難しいので、常にいっこ後の文字を確認しなきゃなのかもしれない。
ハート=「すき❤️すき❤すき❤︎」
数=3。
ハートの1から数を文字抜き出し。
それを表示。
ハートの数+1から1を文字抜き出し。
もし、(それの異体字セレクタ判定)=はいならば、数=数+1。
ハートの1から数を文字抜き出し。
それを表示。
できました☆
合字
「🐈⬛」黒猫さんとか。
いやコレそもそもなでしこエディタに貼っ付けたら二文字になっちゃいますけどね。
しかも、いま見たらQiitaのエディタでも二文字で表示されてるので黒猫さん使えるアプリ自体少ないのかもですが。
Qiitaでならちゃんと一文字で表示される合字の絵文字あると思いますが、ウチのにゃんこは黒猫では無く白黒ですがおおむね黒いのでコレ使いたかったのねん(どうでもいい情報;)
(※Qiitaでは、スマホで見たら一文字で表示されていました! でもなでしこエディタでは、スマホからでもやっぱり二文字です。環境によるとしか言えないみたい? 2022/10/10追記)
それはともかく黒猫さんは、「🐈」猫の絵文字に「⬛」(黒四角特大というラシイ)を組み合わせて作られた絵文字だということなんですね。
なら二文字なのかと思いきや、
黒猫=「🐈⬛」
黒猫の文字数を表示。
3
だって!
「🐈」と「⬛」の間に、またもやナゾ文字が入ってるんです!
文字コードを調べると(U+200D)。「ZERO WIDTH JOINER」。
これは、ゼロ幅ジョイナー(ZWJ)というもので、2つ以上の文字を結合して新しい文字または絵文字を形成できるUnicode文字だとゆうことなんですね。
うーん、ゼロ幅ジョイナーを発見したら、それはカウントせず、前後二文字がひっつくわけだから、文字数-2ってコトですかね。
しかも、「2つ以上の文字を結合」とゆうわけで、「👨👩👦」みたく三文字結合してるヤツもあるわけで、どこまでは結合された絵文字なのか、ゼロ幅ジョイナーを追っていかなきゃならなそう。めんどー。
ちなみに、Qiita上では「👨👩👦」はわたしの環境では一文字に表示されましたが、なでしこエディタではやっぱり「👨」「👩」「👦」とばらけました。
なでしこエディタはどうやら合字の絵文字には対応してないみたい?
環境によって見た目一文字になる場合と二文字、三文字になっちゃう場合もあると考えると文字数1としてしまって良いのか悩ましい・・・
その後さらに、肌の色変えるヤツは、ゼロ幅ジョイナーなしで結合できる(髪型はダメらしい)とか、国旗はゼロ幅ジョイナーなしで2つの地域別インジケーターシンボルとやらを並べて表現しているとか知って、絵文字むじゅかしすぎる!
とりあえず、なでしこエディタ上で正しく表示できない合字については考えないでおくことにしようとおもう💧
まとめ
-
サロゲートペア
- 絵文字は、なでしこさん(UTF-16)ではサロゲートペアとして表されるため、見た目上は1文字だけど2文字分のバイト数になっている。
- 「文字数」と「文字列分解」はサロゲートペア文字を一文字として扱う。
- それ以外の文字列処理「何文字目」「文字挿入」「文字検索」「文字抜出」「文字右部分」などは、サロゲートペア文字が二文字として扱われてしまう。
-
異体字セレクタ
- 絵文字には絵文字スタイルとテキストスタイルが設定されているものがあり、それを指定する見えない異体字セレクタがくっついてる場合がある。
- 異体字セレクタは、「文字数」と「文字列分解」でも別途一文字として扱われる。
-
合字
- 複数の絵文字をゼロ幅ジョイナーで結合して作られている絵文字がある。
- 合字は、「文字数」と「文字列分解」でも、使われている絵文字とゼロ幅ジョイナーそれぞれを一文字として扱われる。
- 肌の色はゼロ幅ジョイナーなしでもありでも結合できる。
- 国旗の絵文字は2つの地域別インジケーターシンボルを並べて表現している。「🇯」+「🇵」→「🇯🇵」。ゼロ幅ジョイナーなしでもありでも結合できる。
- なでしこさんエディタは合字の絵文字を合字として表示できない。
ようするに・・・絵文字むじゅい!
むー・・・