javascriptで文字コード変換

# javascriptで文字コード変換

## 概要
 javascriptの文字列はUTF-16で String#charCodeAt(i) で取得出来る数値は2byte(0x0000から0xffff)になる。 escape("あ") は UTF-16がそのままエスケープされ "%u3042" になるが encodeURI("あ"),encodeURIComponent("あ") などではUTF-8に変換されて "%E3%81%82" になる。

```js
var str = "文字列をUTF-8に";
var utf8str = unescape(encodeURIComponent(str));
var utf16str = decodeURIComponent(escape(utf8str));
if (str == utf16str) {alert(true);} //=> true
```

とすることで UTF-16<=>UTF-8 の変換は出来るが、その他の文字コードは変換テーブルを用意する必要がある。 Shift_JIS(MS932), EUC-JP, UTF-8, JIS(iso-2022-jp)に対応したライブラリを2つ見つけたので検証する。 

* Encoding.js - Converts character encoding. http://polygon-planet-log.blogspot.jp/2012/04/javascript.html
* Escape Codec Library: ecl.js (Ver.041208) http://www.junoe.jp/downloads/itoh/enc_js.shtml または http://www.vector.co.jp/soft/other/java/se342855.html

また、検証を進める中でecl.jsのJIS漢字テーブルを使った文字コード変換ライブラリを自作してしまったのでこれも合わせて検証する。文字列を一度数値配列にしてから変換するのでecl_array.jsとした。

* ecl_array.js https://github.com/wealandwoe/ecl_array.js

(2014-09-19追記)さらに

* inexorabletash/text-encoding - Polyfill for the Encoding Living Standard's API. https://github.com/inexorabletash/text-encoding

というライブラリについて検証を追加した

## 検証

### 各ライブラリの特徴

 ecl.jsは各文字コードへURLエスケープする為のライブラリであって、直接文字コード変換する機能はない。 よって UNICODE(Javascript文字列) から SJIS へ変換する場合は

```js
var str = "UNICODE文字列をシフトJISへ";
var sjis = UnescapeUnicode(EscapeSJIS(str));
var str2 = UnescapeSJIS(EscapeUnicode(sjis));
if (str==str2){alert("true");} //=> true
```

のようにする必要がある。

```
Javascript文字列→SJISに変換してURLエスケープ→URLアンエスケープ(SJIS文字列)
```

このエスケープ・アンエスケープ処理を挟んでいる分ecl.jsは不利である。

 encoding.jsの文字コード変換は配列を渡し配列で返るので文字列から配列に変換する関数は別途用意する必要がある。

```js
// 文字列を配列に変換
var str2array = function(str) {
	var array = [],i,il=str.length;
	for(i=0;i<il;i++) array.push(str.charCodeAt(i));
	return array;
};
var str   = "あいうえお",
		array = str2array( str ),
		sjis_array = Encoding.convert(array, "SJIS", "UNICODE"),
		sjis       = Encoding.codeToString( sjis_array ); // 配列から文字列変換する関数は用意されている
alert(sjis);

```

text-encodingは日本語以外の様々なエンコーディングにも対応している。Encoding Living Standard( https://encoding.spec.whatwg.org/ )に沿った実装がされているようだ。以下のように使う。

```js
var encoder = new TextEncoder("utf-8"),    // utf-8のエンコーダ(文字列→Uint8Array変換)を作成
    u8array = encoder.encode("あいうえお"), // => Uint8Array[240, 128, 128, 128, 240, 128, 128, 128, 240, 128, 128, 128, 240, 128, 128, 128, 240, 128, 128, 128]
    decoder = new TextDecoder("utf-8"),    // utf-8のデコーダ(Uint8Array→文字列)を作成
    decodedText = decoder.decode(u8array);  // => "あいうえお"
alert(decodedText);

//utf-8,utf-16以外のencodeにはオプションが必要
new TextEncoder("sjis", {NONSTANDARD_allowLegacyEncoding:true}).encode("ほげ"); // => Uint8Array[130, 216, 130, 176]
//decodeはOK
new TextDecoder("euc-jp").decode(new Uint8Array([164,226,164,178])); // => "もげ"
```

ちなみにTextEncoderおよびTextDecoderクラスはFirefoxでは実装済みなのでtext-encodingは不要。
ただしFirefoxのTextEncoderはエンコーディングにutf-8,utf-16le,utf-16be以外を指定するとエラーになる。
詳しくはMDNを参照 https://developer.mozilla.org/ja/docs/Web/API/TextEncoder

ファイルサイズはecl.jsが22KBとコンパクトなのに対し, encoding.jsは262KB js minifyしても212KBと少し大きい。
text-encodingは対応する文字コードが多いのでサイズも大きく600KB以上。
ecl.jsのJIS漢字テーブル(JCT11280)は独特な圧縮がされている上、全体的にサイズを小さくしようと凝縮された書き方がされている。はっきり言うと読みにくい、がjavascriptの文法の勉強になった。
encoding.jsはUTF8-JISの変換・逆変換テーブルがありこれが数値の配列で書かれているのでサイズが大きいがとてもわかりやすい。

自作のecl.jsベースの文字コード変換ライブラリecl_array.jsは、ecl.jsが1つのエスケープ関数で行っている処理(javascript文字列=Unicodeから各文字コードへ変換しエスケープする)をインターセプトして数値の配列で取り出すように設計した。文字コードはecl.jsで扱えるSJIS,EUCJP,JIS7,JIS8,UTF8,UTF7,UTF16LEに加えUTF16BE,MUTF7(modified utf-7),Base64を追加した。

```js
var str = "あいうえお";
// 文字列を16bit数値の列に変換
var array = ECL.charset.Unicode.parse(str);   //=> [12354, 12356, 12358, 12360, 12362]
ECL.convert_array(array, "SJIS");    //=> [130, 160, 130, 162, 130, 164, 130, 166, 130, 168]
ECL.convert_array(array, "EUCJP");   //=> [164, 162, 164, 164, 164, 166, 164, 168, 164, 170]
// 配列からJS文字列(Unicode)に戻す場合は ECL.charset.Unicode.stringify( array )
ECL.charset.Unicode.stringify( array ); //=> "あいうえお"

// UTF-8文字列からSJIS文字列へ
var utf8str = unescape(encodeURIComponent("あいうえお"));
var sjisstr = ECL.convert(utf8str, 'SJIS', 'UTF8');
sjisstr == "\x82\xA0\x82\xA2\x82\xA4\x82\xA6\x82\xA8"; //=> true


/*
 例: ecl_array.js と CryptoJS を使いSJISテキストの暗号文・Base64・SHA1ハッシュを作る
    CryptoJS : https://code.google.com/p/crypto-js/
*/
var msg = "あいうえお";
var sjis = ECL.convert(msg, 'SJIS');
var sjis_wordarray = CryptoJS.enc.Latin1.parse( sjis ); // CryptoJSではWordArrayオブジェクトを作りデータをやりとりするのが楽
var encrypted = CryptoJS.AES.encrypt(sjis_wordarray, "Secret Passphrase"); //=> [object]
var base64 = sjis_wordarray.toString(CryptoJS.enc.Base64); //=> "gqCCooKkgqaCqA=="
var sha1 = CryptoJS.SHA1(sjis_wordarray).toString(CryptoJS.enc.Hex); //=> "2c332e42c55ba9293827672e85f16f1cda65518e"
```

## 実験
4KBのテキストを1000回文字コード変換した時の処理秒数(ms) U : UNICODE, S : SJIS, 8 : UTF8, E : EUC-JP, J : JIS

|ライブラリ  | U->U | U->S | U->8 | U->E | U->J  |
|------------|-----:|-----:|-----:|-----:|------:|
|encoding.js |      | **495** | 336 | **507** | **503** |
|ecl.js      |      | 1551 | 1483 | 1549 | 1756  |
|ecl_array.js|      | 616  | **289** | 599 | 624   |
|native      |      |      | 344  |      |       |

|ライブラリ  | S->U | S->S | S->8 | S->E | S->J  |
|------------|-----:|-----:|-----:|-----:|------:|
|encoding.js | **278** |      | **203** | **214** | **229** |
|ecl.js      | 1516 |      | 2860 | 2829 | 3213  |
|ecl_array.js| 294  |      | 310  | 624  | 661   |

|ライブラリ  | 8->U | 8->S | 8->8 | 8->E | 8->J  |
|------------|-----:|-----:|-----:|-----:|------:|
|encoding.js | **195** | **338** |      | **347**  | **345** |
|ecl.js      | 1810 | 3329 |      | 3269 | 3298  |
|ecl_array.js| 314  | 616  |      | 607  | 638   |
|native      | 323  |      |      |      |       |

|ライブラリ  | E->U | E->S | E->8 | E->E | E->J  |
|------------|-----:|-----:|-----:|-----:|------:|
|encoding.js | **255** | **223** | **213** |      | **230** |
|ecl.js      | 2444 | 3826 | 3741 |      | 4008  |
|ecl_array.js| 288  | 609  | 304  |      | 634   |

|ライブラリ  | J->U | J->S | J->8 | J->E | J->J  |
|------------|-----:|-----:|-----:|-----:|------:|
|encoding.js | **277** | **217** | **247** | **209** |       |
|ecl.js      | 1786 | 3165 | 2981 | 3111 |       |
|ecl_array.js| 547  | 902  | 616  | 853  |       |

* nativeは unescape(encodeURIComponent(str))とdecodeURIComponent(escape(utf8str))のこと
* 同文字コードへの変換(無変換)はスルーしているので変換速度には影響しない。
* ほぼ全てにおいて encoding.js ＞ ecl_array.js ＞ ecl.js の順に速いという結果になった。
* ecl.js はURLエスケープ、アンエスケープの影響で3～10倍程度遅い
* ecl_array.js は UNICODEが絡む変換はそこそこ速いが、他はencoding.jsより2倍程度遅い。
* encoding.jsは各文字コード間を直接変換しているが、ecl_array.jsは一度UNICODE配列にしているのでUNICODEが絡まない変換が遅いと思われる。
* (2014-03-12追記) その後ecl_array.jsに手を入れ変換速度はencoding.jsと同程度になった
* (2014-09-19追記) text-encodingは未検証。少し試した限りではかなり(50倍ほど)遅かった。

4KBのテキストを1000回文字変換+URLエスケープ/アンエスケープした時の処理秒数(ms)

|ライブラリ|esc U|esc S|esc 8|esc E|esc J|unesc U|unesc S|unesc 8|unesc E|unesc J|
|----------|----:|----:|----:|----:|----:|------:|------:|------:|------:|------:|
|encoding.js|180|1524|1512|1194|1227|287|406|419|388|716|
|ecl.js|689|1133|1389|1123|1366|461|649|948|1656|964|
|ecl_array.js|**476**|**934**|775|**949**|**940**|**248**|**281**|313|**286**|**575**|
|native|||**118**|||||**306**|||

* JS文字列→UTF-8エスケープならばnativeが最速
* ecl.js のエスケープは遅い。ecl_array.js はエスケープ・アンエスケープ処理を変更したら速くなった(正規表現による置換をやめた)
* encoding.jsもアンエスケープは速い。ただUNICODEエスケープは内部でencodeURIComponentを使用しているためUTF-8へ変換されている

## 考察

text-encodingはサイズが大きく変換も遅いが対応する文字コードが多く便利。
日本語の文字コード変換をしたい場合は encoding.js か ecl_array.js を使うのがよいだろう。
Firefox19以降限定なら外部ライブラリなしでTextEncoder/TextDecoderが使える。

ecl.jsは昔からある有名なライブラリだがこのコンパクトさが素晴らしい。JIS漢字11280字34KB程をASCII印字可能文字とデコーダ含めて13KBに納めている。エンコーダが無くJIS X 0213等への対応が出来ないのが残念。