JavaScript: 文字Code自作講座

Shift-JISとかUnicodeとかLatin1とかいう類の文字Codeの話です。できるだけ多くの文字を表現でき、bit数も少ない独自形式を作っていきます。

## Escaped UTF16(EU16)
escape文字でUnicode区間を切り替えていく。復号が簡単で快速。符号の種類が少なく、Unicode番号が近い文字が密集していれば符号文は短くなる。

## Escaped UTF16a(EU16a)
Shift JISで扱う文字は殆ど1～2 Bytesで表現可能。JIS漢字も殆ど2Bytesで扱える。上記EU16も復号可能。

## Shift JIS + UTF16(SJU)
ASCII文字は1 Byte、いわゆるJIS漢字は殆ど2Bytesで表現し、Shift JISっぽい符号になる。それ以外は3 Bytesで符号化。

## 実装編
念の為UTF8のようなものも実装。
```js
if(!this.CharCoder)this.CharCoder={
eEU16(S){
	for(var a=0,b=0,c,d,k=128,n,z=S.length,A=[];a<z;A[b++]=d){
		c=S.charCodeAt(a++);
		if(d=c&127,n=c>>7,c<256){
			if(k!=(n|=128))k=A[b++]=n
		}else if(c<0x3100&&c>0x2FFF){
			if(k!=(n+=34))k=A[b++]=n
		}else if(c>0xFEFF){
			if(k!=(n-=378))k=A[b++]=n
		}else if(c<0x2680&&c>0x20FF){
			if(k!=(n+=70))k=A[b++]=n
		}else if(c>0x047F||c<0x0380){
			if(k!=(n=(c-(c%=19968))/19968+147))k=A[b++]=n;
			A[b++]=(c-(d=c%156))/156
		}else if(k!=(n+=127))k=A[b++]=n
	}return A
},
dEU16(A){
	var a=0,b=0,c,d=0,e=1,z=A.length,B=[];
	for(;a<z;c<128?B[b++]=d+(e?c:c*156+A[a++]):d=(c&=e=31)<2?c<<7:c<4?c+94<<7:c>18?(e=0,c-19)*19968:c<6?c+506<<7:c<8?c+1<<7:c+58<<7)c=A[a++];
	B.length=b;return CharCoder.by(B)
},
eEU16A(S){
	for(var a=0,b=0,c,d,k=128,n,z=S.length,A=[];a<z;A[b++]=d){
		c=S.charCodeAt(a++);d=c&127;n=c>>7;
		if(c<256){
			if(k!=(n|=128))k=A[b++]=n
		}else if(c<0x3100&&c>0x2FFF){
			if(k!=(n+=34))k=A[b++]=n
		}else if(c>0x4DFF&&c<0xA010)
			c-=19968,A[b++]=((c-(c%=208))/208)+151,d=c+32;
		else if(c>0xFEFF){
			if(k!=(n-=378))k=A[b++]=n
		}else if(c<0x2680&&c>0x20FF){
			if(k!=(n+=70))k=A[b++]=n
		}else if(c>0x047F||c<0x0380){
			if(k!=(n=(c-(c%=19968))/19968+147))k=A[b++]=n;
			A[b++]=(c-(d=c%156))/156
		}else if(k!=(n+=127))k=A[b++]=n
	}return A
},
dEU16A(A){
	var a=0,b=0,c,d=0,e=1,z=A.length,B=[];
	for(;a<z;c<128?B[b++]=d+(e?c:c*156+A[a++]):c>150?B[b++]=(c-151)*208+A[a++]+19936:d=(c&=e=31)<2?c<<7:c<4?c+94<<7:c>18?(e=0,c-19)*19968:c<6?c+506<<7:c<8?c+1<<7:c+58<<7)c=A[a++];
	B.length=b;return CharCoder.by(B)
},
eSJU(S,n){
	if((n&=255)<32||n>64)n=64;
	var a=0,b=0,c,m=252-n,p=127-n,z=S.length,A=[];
	for(;a<z;A[b++]=c&255)
		if((c=S.charCodeAt(a++))>127)
			if(c<0x3100&&c>0x2FFF)A[b++]=128;
			else if(c>0x4DFF&&c<0xA040)A[b++]=(((c-=19968)-(c%=m))/m)+129,c+=c<p?n:n+1;
			else if(c>0xFEFF)A[b++]=241;
			else if(c>0x1FFF&&c<0x2700)A[b++]=(c>>8)+210;
			else if(c<0x500&&c>0x2FF)A[b++]=(c>>8)+249;
			else A[b++]=251,A[b++]=c>>8;
	return A
},
dSJU(A,n){
	if((n&=255)<32||n>64)n=64;
	var a=0,b=0,c,m=252-n,z=A.length,B=[];
	for(;a<z;)c=A[a++],
		B[b++]=c<128?c
		:c<129?12288|A[a++]
		:c<241?(c-129)*m+(c=A[a++])-(c<127?n:n+1)+19968
		:c<242?65280|A[a++]
		:c<249?c-210<<8|A[a++]
		:c<251?c-249<<8|A[a++]
		:A[a++]<<8|A[a++];
	B.length=b;
	return CharCoder.by(B)
},
eU8(S){// utf8 encoding
	if(self.TextEncoder)return new TextEncoder().encode(S);
	var a=0,b,A=new Uint8Array(S.length*4);
	for(b of S){
		b=b.codePointAt();
		if(b>127){
			if(b<2048)A[a++]=b>>6|192;
			else{
				if(b<65536)A[a++]=b>>12|224;
				else A[a++]=b>>18|240,A[a++]=b>>12&63|128;
				A[a++]=b>>6&63|128
			}b=b&63|128
		}A[a++]=b
	}return new Uint8Array(A.buffer,0,a)
},
dU8(A){// utf8 decoding
	if(self.TextDecoder)return new TextDecoder().decode(A);
	var a=0,b=0,c,z=A.length,B=[],S=String.fromCodePoint;
	if(S)for(;a<z;B[b++]=c<128?c:c<224?(c&31)<<6|A[a++]&63:c<240?(c&15)<<12|(A[a++]&63)<<6|A[a++]&63:(c&7)<<18|(A[a++]&63)<<12|(A[a++]&63)<<6|A[a++]&63)
			c=A[a++];
	else for(S=String.fromCharCode;a<z;)
		if((c=A[a++])<240)
			B[b++]=c<128?c:c<224?(c&31)<<6|A[a++]&63:(c&15)<<12|(A[a++]&63)<<6|A[a++]&63;
		else c=((c&7)<<18|(A[a++]&63)<<12|(A[a++]&63)<<6|A[a++]&63)-65536,
			B[b++]=0xd800|c>>10,B[b++]=0xdc00|c&0x3ff;
	if(z<65565)return S.apply(0,B);
	for(a=0,b="",z=65536;A=B.slice(a,a+=z),z=A.length;)b+=S.apply(0,A);
	return b
},
by(A){
	var a=A.length,S=String.fromCharCode;
	if(a<65536)S.apply(0,A);
	let b=0,B,C="";
	for(a=65536;B=A.slice(b,b+=a),a=B.length;)C+=S.apply(0,B);
	return C
}}
```
```js:使用例
var s="あいアイαβgammma delta一丁";
var e=CharCoder.eEU16(s), d=CharCoder.dEU16(e);
console.log(e,d)
```
試し斬り
<p class=codepen data-height="300" data-default-tab="result" data-slug-hash="XWLKKLY" data-pen-title="Unicode converter" data-user="xezz" style="height:300px;box-sizing:border-box;display:flex;align-items:center;justify-content:center;border:2px solid;margin:1em 0;padding:1em"> <span>See the Pen <a href="https://codepen.io/xezz/pen/XWLKKLY">Unicode converter</a> by xezz (<a href="https://codepen.io/xezz">@xezz</a>) on <a href="https://codepen.io">CodePen</a>.</span></p><script async src="https://cpwebassets.codepen.io/assets/embed/ei.js"></script>

## benchmark
LZMAによる[源氏物語 1巻 桐壷](http://www.genji.co.jp/yosano/yy01.html)の圧縮結果
<table>
<tr><th>符号化方式<th>符号文<th>圧縮後
<tr><td>Shift-JIS<td>32963<td>14924
<tr><td>SJU<td>32963<td>15245
<tr><td>EU16<td>30393<td>15316
<tr><td>EU16a<td>25630<td>15220
<tr><td>UTF8<td>48739<td>16136
</table>

Shift-JIS様の圧縮しやすさには脱帽。しかしJavaScriptでShift-JISを模倣するには面倒臭い配列を用意しなければならず、実用的ではないのだ。符号文はEU16aがぶっちぎりで小さい