Edited at

文字列を1文字ずつ配列化(サロゲートペアを考慮)

More than 1 year has passed since last update.

JavaScriptで𩸽(ホッケの漢字)や🌕(満月の絵文字)を扱おうとすると、やや面倒な事になります。

例えば、文字列から「1文字ずつに分解した配列」を生成したい時。

普通はsplitメソッドを使用した以下のような処理でうまく生成できます。


コード

"満月の夜にホッケ食べたい".split("")



生成されるデータ

["", "", "", "", "", "", "", "", "", "", "", ""]


しかし、冒頭で挙げた文字を含んでしまうとうまくいきません。


コード

"🌕の夜に𩸽食べたい".split("")



生成されるデータ

["", "", "", "", "", "", "", "", "", "", ""]


絵文字や漢字の箇所が、1文字のはずなのに2つに別れて格納されてしまい、そのうえ見事に文字化けしてしまいます。

この記事では、これを解決する方法について紹介します。


原因

このセクションには、誤解を招くような説明が含まれている可能性があります。可能であれば、自分で調べてください。

これは、𩸽(ホッケの漢字)や🌕(満月の絵文字)が「サロゲートペア」に該当する文字だから起きてしまう現象です。

JavaScriptの文字列は、UTF-16という形式でUnicodeの文字を扱うようになっています。

そして、UTF-16は、基本的には1文字を2バイトで扱います。

文字






Unicode
U+6E80
U+6708
U+306B
U+306F
U+30DB
U+30C3
U+30B1

UTF-16
U+6E80
U+6708
U+306B
U+306F
U+30DB
U+30C3
U+30B1

データ
0x6E 0x80
0x67 0x08
0x30 0x6B
0x30 0x6F
0x30 0xDB
0x30 0xC3
0x30 0xB1

ところでUnicodeでは、文字はU+0000からU+10FFFFまで定義されています。しかし、UTF-16では1文字が2バイトなので、0からFFFFまでしか扱えません。1048576個足りなくなってしまいます。

そこで、U+10000からU+10FFFFの文字は4バイトで扱うように定められました。具体的には、「U+D800からU+DBFFの文字」を前半の2バイト、「U+DC00からU+DFFFの文字」を後半の2バイトとして組み合わせ、4バイトで扱うようにしています。

UTF-16では2バイトで1文字ですが、U+10000からU+10FFFFの文字は例外で1文字 = 4バイトで表現するようになっています。

これがサロゲートペアです。

文字
🌕


𩸽

Unicode
U+1F315
U+306B
U+306F
U+29E3D

UTF-16
U+D83C
U+DF15
U+306B
U+306F
U+D867
U+DE3D

データ
0xD8 0x3C 0xDF 0x15
0x30 0x6B
0x30 0x6F
0xD8 0x67 0xDE 0x3D

しかし、JavaScriptの文字列関係の関数やメソッドは、文字列を1文字 = 2バイトで扱うように作られています。

すなわち、1文字 = 4バイトのサロゲートペアに対応していません1

サロゲートペアを扱おうとすると、無理矢理2バイトずつに分割して処理してしまいます。

分割された2バイトのデータは、一応は「U+D800からU+DBFFの文字」または「U+DC00からU+DFFFの文字」です。しかしこの文字は、サロゲートペアのための特殊な文字として定められていて、普通の「表示するための文字」としては扱えません。

このため、文字化けしたような表示になってしまいます。


コード

"🌕には𩸽".split("")



生成されるデータ

["", "", "", "", "", ""]


前述した「1文字のはずなのに2つに別れて」しまうのも、「4バイトで1文字」のサロゲートペアを2バイトの1文字として分割してしまうせいです。


対応方法


スプレッド構文(スプレッド演算子)を使用した方法

ECMAScript 2015(ECMAScript 6)から追加されたスプレッド構文(スプレッド演算子)を使用すると、以下のように記述することで簡単に分解することができます。


コード

[..."🌕には𩸽"]



生成されるデータ

["🌕", "", "", "𩸽"]


ただし、この方法は古いWebブラウザでは使用できません。

最近のWebブラウザはほとんど対応しているものの、Internet Explorerでは使用できません。

実際に使用する時は、Babelなどで古いブラウザ向けに変換しましょう。


Array.fromメソッドを使用した方法

ECMAScript 2015(ECMAScript 6)から追加されたArray.fromメソッドを使用すると、以下のようなコードで分解することができます。


コード

Array.from("🌕には𩸽")



生成されるデータ

["🌕", "", "", "𩸽"]


この方法も、古いWebブラウザでは使用できません。

実際に使用する時は、Babelなどで古いブラウザ向けに変換しましょう。

なお、古いブラウザ向けにArray.fromメソッドだけを追加定義してくれるPolyfill(ポリフィル)という分類のライブラリもありますが、だいたいのものは文字列のサロゲートペアを考慮していないため、この用途では使えません。

MDNのページに載っている「ポリフィル」も対応していません。

このため、Polyfill(ポリフィル)の使用はオススメしません

どうしてもPolyfill(ポリフィル)を使用したい場合は「mathiasbynens/Array.from」を使いましょう。コレはサロゲートペアに対応しています。


matchメソッドを使用した方法

JavaScriptでのサロゲートペア文字列のメモ - Qiita #IV-II. サロゲートペアに対応した配列化

この記事にあるように、正規表現でサロゲートペアを考慮した文字を取得できるようにすれば解決します。

例えば以下のようにすると、文字列を正しく分割してくれます。


コード

"🌕には𩸽".match(/[\uD800-\uDBFF][\uDC00-\uDFFF]|[\s\S]/g)



生成されるデータ

["🌕", "", "", "𩸽"]



空文字列の場合に生じる問題の解決方法

ただし、この方法は、空文字列が指定された場合にnullが返されてしまう問題があります。


コード

"".match(/[\uD800-\uDBFF][\uDC00-\uDFFF]|[\s\S]/g)



生成されるデータ

null


そこで、||演算子をエルビス演算子のように使用してこう書きます。


コード

"".match(/[\uD800-\uDBFF][\uDC00-\uDFFF]|[\s\S]/g) || []



生成されるデータ

[]


または、空文字列の場合にもマッチするよう正規表現を修正し、filterメソッドで空文字列を除外します。


コード

"".match(/[\uD800-\uDBFF][\uDC00-\uDFFF]|[\s\S]|^$/g).filter(Boolean)



生成されるデータ

[]


これで、常に配列が返ってくるようになります。





  1. ただし、ECMAScript 2015(ECMAScript 6)から追加された関数やメソッドはサロゲートペアに対応しています。例えばString.fromCodePoint関数やString.prototype.codePointAtメソッドがこれに該当します。