スポンサーリンク

JavaScriptの動かないコード (中級編) 文字数のカウントに失敗する


以下のJavaScriptコードが意図した動作をしないのは,なぜですか。(制限時間1分)


やりたい事:

  • 入力された文字列の長さ(文字数)を取得する。
ここに入力して下さい:<input type="text" id="hoge" value=""><br>

<input type="button" onclick="c()" value="文字数を数える">


<script>

function c()
{
	// 入力内容の文字列
	var str = hoge.value;
	
	// 普通に文字数をカウント
	var len = str.length;
	
	// 表示
	alert( len );
}

</script>

入力に利用する文字列のサンプル:

  • 叱られる
  • 𠮟られる

発生する問題

「叱られる」は4文字だが,
「𠮟られる」は,なぜか5文字になる。


JavaScriptが,文字長のカウントに失敗している。

lengthプロパティの挙動がおかしい。


問題の原因

カウントに失敗した理由は,UTF-16の「サロゲートペア」の文字数をカウントしようとしたためだ。


Unicodeのエンコーディング方式では,ほとんどの文字を「1文字=2バイト」として扱う。

ところが,Windows Vista以降は「1文字=4バイト」の文字が300文字ほど追加され,文字入力時に利用可能になった。

これをサロゲートペアと呼ぶ。(※surrogateとは「代理人」の意。)


サロゲートペアとは何かの解説,一覧表付き:

サロゲートペア入門 / Vistaから増えたUnicode環境で使用できる日本語漢字の対応方法
http://codezine.jp/article/detail/1592

  • Windows VistaのJIS2004対応により、WindowsのUnicode環境で使用できる日本語漢字の数が増えた
  • 追加された907字の中には「サロゲートペア」という特殊な文字が304字ある
  • 従来のUnicodeでは未使用だった0xD800〜0xDBFF(1024通り)を「上位サロゲート」、0xDC00〜0xDFFF(1024通り)を「下位サロゲート」と規定


Unicodeについて
http://maxy.sourceforge.jp/help/HTML/...

  • 1つ1つの文字に割り当てられた数値のことをコードポイントと呼ぶ。パソコンで一般に使われる日本語の文字コードはShift-JIS、ワークステーションはEUC-JPのように別々に発展してきたから、同じ文字でもShift-JISとEUC-JPで別々のコードポイントが割り当てられた
  • 混乱を解消するため,ゼロックスが「世界中の文字をすべて含んだ新しい文字コード」(Unicode,単一の文字コード)を提唱。しかし,欧米人は諸外国(特にアジア)がたくさんの文字を使っているとは知らず,Unicode1.0(16ビット・2バイト)では不足と判明。
  • 素のUnicodeでは足りないので,サロゲートペアのアイデアを導入して文字を増やしたのがUTF-16。Windowsでも文字列の内部表現として採用。「ワイド文字」と呼ぶ。なお,Unicodeはビット単位で考えるのに対し,UTFはそれらのビット列をバイト単位にエンコードする。
  • UTF-16が面倒でややこしいので,もっと扱いやすい符号化方式が欲しい,という事で生まれたのがUTF8

そして,たいていのプログラミング言語では,サロゲートペアの「4バイト分」を「2文字」として解釈してしまう。

文字(Char)オブジェクトが2個存在する,とみなされてしまうのである。


原因は,プログラミング言語の内側で,内部的な文字列表現がUTF-16で実装されているためだ。

(UTF-16は,素のUnicodeに対してサロゲートペアを導入した方式である,と考えて差し支えない。)


JavaScriptでも,内部的には,文字列オブジェクトはUTF-16として実装されている。

よって,JavaScriptでサロゲートペアに対して.split() メソッドを呼ぶと,「文字化けした文字」(1文字だけでは成立せず解釈できない文字)が2文字分入った配列が返ってくる。


これは厄介な問題だ。


C#,Java, JavaScriptの各言語で,サロゲートペアで問題が起こるケースのサンプルコード:

C#プログラムでサロゲート・ペアの動作を検証する
http://itpro.nikkeibp.co.jp/article/C...

  • Stringクラスでは,サロゲート・ペアはcharオブジェクト二つを使って表現。文字列操作において,二つのcharオブジェクトを,本来の一つの文字(人間が1文字として認識する文字)として扱うのは,プログラマの責任
  • 「魚へん」に「圭」の漢字は「サケ」で,「魚へん」に「花」という字は「ホッケ」と読む。「ホッケ」はサロゲート・ペアを使用しており,4バイトで1文字
  • プロパティをどういう意味として使用しているのか(可読の文字数か,char単位の数か)を見極める必要がある


Javaでの文字数カウント(サロゲートペア)に関する実験
http://d.hatena.ne.jp/t_gaisho/201011...

  • 「齟齬がある」の異字体は7文字とカウントされる
  • JDK1.5から追加されたString#codePointCount()メソッドを利用してカウント。文字の符号位置の数
  • JDK6から導入されたjava.text.Normalizerというクラスを利用して正規合成を行い、結合文字を1つの文字に変換(「か」+「゛」を 「が」に変換)


サロゲート・ペアに対応した文字列操作関数を書いてみた
http://liosk.blog103.fc2.com/blog-ent...

  • JavaScriptの文字列型はUTF-16を採用しているから、サロゲートペアを使用した文字が混ざるといろいろと厄介
  • 1文字なのにlengthが2になる


したがって,JavaScriptに限った話ではなく,アプリケーション開発の際には

こういった特殊なUnicode文字がインプットされる事も想定しなければならない。

  • サロゲートペアが入力された時の挙動を作りこんでおく。
  • 元からシステム仕様として,「環境依存文字はお断り」という仕様にしておく。
    • (※ただしその場合でも,もしもサロゲートペアが入力されてしまったらどういう挙動になるのか把握しておかねばならない。)

解決策

lengthプロパティを普通に使っている限り,サロゲートペアの分だけ個数が余分にカウントされてしまう。

ならば,サロゲートペアの個数を引いておけば,とりあえず正しい文字数が得られる。

ここに入力して下さい:<input type="text" id="hoge" value=""><br>

<input type="button" onclick="c()" value="文字数を数える">


<script>

function c()
{
	// 入力内容の文字列
	var str = hoge.value;
	
	// 普通に文字数をカウント
	var len = str.length;
	
	// サロゲートペアの個数をカウント
	var surrogate_num = str.split( /[\uD800-\uDBFF][\uDC00-\uDFFF]/g ).length - 1;
	
	// 表示
	alert( len - surrogate_num );
}

</script>


参考として,
JavaScriptで「surrogate-aware」(サロゲート安全)なメソッドをコーディングする例:

is it at all possible to handle post-BMP characters in JavaScript?
http://stackoverflow.com/questions/37...

  • 「JavaScript: the Good Parts」の本にも「JavaScript was built at a time when Unicode was a 16-bit character set, so all characters in JavaScript are 16 bits wide.」と指摘


全てのシステムで,こういった対策を施す必要があるのか?

必ずしも完全対策の必要はないかもしれない。


それでも,こういった問題が発生する仕組みと,いざという時の対処法を知っておく必要はある。

例えば下記のようなコードを見たときに,「このアルゴリズムは,split()を使っている部分がサロゲート安全でない。」と判断できる。

そうすれば,「一部の環境依存文字には対応していません。」という正確な但し書きを付与した上で,関数をライブラリとして提供できる。

JavaScriptで,文字列を反復する / 逆順に並び替える方法
http://language-and-engineering.hatenablog.jp/entry/20080924/1222174957