JavaScript: 文字列ろ過

文字列から特定文字を除外して全体の容量を削減、あるいは縮めやすくする魔法を紹介します。本記事ではこの工程を濾過と呼びます。
その対象文字となるのは**全角英数字、Greek、Cyrillic、平仮名、片仮名、半角片仮名**となります。同種の文字が連続しているほど効果が高くなります(2文字目からは1 byte)。

## 原理
以下の規則で変換します。当然可逆変換です。元の文字列に`[#-~]`が含まれていても問題ありません。

* 全角英数字 → 半角英数字. 但し `＼(\uFF3C)`, `～(\uFF5E)`は除外
* Greek(`[\u0391-\u03CE]`) → `[A-~]`
* Cyrillic(`[\u0410-\u044F]`) → `[?-~]`
* 平仮名(`[\u3041-\3093]`) → `[#-u]`
* 片仮名(`[\u03A1-\u30FC]` → `[#-~]`
* 半角片仮名(`[\uFF61-\uFF9F]` → `[@-~]`

## 実装
```js
/* 対象文字自動設定
@txt: 文字列
@encode: 真なら濾過、偽なら逆濾過
*/
function selectFilter(txt,encode){return encode?
	/[！＃-＆’（-［］-｝￥]/.test(txt)|
	/[\u0391-\u03CE]/.test(txt)<<1|
	/[\u0410-\u044F]/.test(txt)<<2|
	/[\u3041-\u3093]/.test(txt)<<3|
	/[\u03A1-\u30FC]/.test(txt)<<4|
	/[\uFF61-\uFF9F]/.test(txt)<<5:
	/\1[!#-\}]/.test(txt)|
	/\2[A-~]/.test(txt)<<1|
	/\3[?-~]/.test(txt)<<2|
	/\4[#-u]/.test(txt)<<3|
	/\5[#-~]/.test(txt)<<4|
	/\6[@-~]/.test(txt)<<5
}
/* encoder
@txt: 文字列
@flag: 濾過対象文字. 以下の数値の合計で決まる.
	例えばflag=8なら平仮名, flag=6ならGreek + Cyrillic
	1: 全角英数字
	2: Greek
	4: Cyrillic
	8: 平仮名
	16: 片仮名
	32: 半角片仮名
*/
function strFiltering(txt,flag){if(!flag)return txt;
	var a=0,b,c=96,d=7,A=[],F=[],G=[],C=[],H=[],K=[],L=[],S=String.fromCharCode;
	for(;d;)F[--d]=[];
	while(c)F[0][S(--c+32)]=1;
	// build filter
	if(flag&1){for(c=93,A[S(8217)]="'",A[S(65509)]="\\";c;)if(--c^59&&c^6&&c^1)F[1][A[S(c+65281)]=S(c+33)]=1;txt=txt.replace(/\1/g,"\1\1")}
	if(flag&2){for(c=62;c;)F[2][G[S(--c+913)]=S(c+65)]=1;txt=txt.replace(/\2/g,"\2\2")}
	if(flag&4){for(c=64;c;)F[3][C[S(--c+1040)]=S(c+63)]=1;txt=txt.replace(/\3/g,"\3\3")}
	if(flag&8){for(c=83;c;)F[4][H[S(--c+12353)]=S(c+35)]=1;txt=txt.replace(/\4/g,"\4\4")}
	if(flag&16){for(c=92;c;)F[5][K[S(--c+12449)]=S(c+35)]=1;txt=txt.replace(/\5/g,"\5\5")}
	if(flag&32){for(c=63;c;)F[6][L[S(--c+65377)]=S(c+64)]=1;txt=txt.replace(/\6/g,"\6\6")}
	// encoding loop
	for(txt=txt.split(flag="");c=txt[a];txt[a++]=b+c){b="";
		if(A[c]){if(d^1)b=flag="\1",d=1;c=A[c]}
		else if(G[c]){if(d^2)b=flag="\2",d=2;c=G[c]}
		else if(C[c]){if(d^3)b=flag="\3",d=3;c=C[c]}
		else if(H[c]){if(d^4)b=flag="\4",d=4;c=H[c]}
		else if(K[c]){if(d^5)b=flag="\5",d=5;c=K[c]}
		else if(L[c]){if(d^6)b=flag="\6",d=6;c=L[c]}
		else if(d&&F[d][c])b=flag,d=0
	}return txt.join("")
}
/* decoder
@txt: 文字列
@flag: 濾過対象文字. strFilteringと同じ仕様
*/
function strFiltered(txt,flag){if(!flag)return txt;
	for(var a=0,b=0,c,d,e=6,C=[],D,S=String.fromCharCode;e;)if(flag>>--e&1)C[S(e+1)]=e?[]:{"'":S(8217),"\\":S(65509)};
	// build filter
	if(flag&1)for(c=93;c;)if(--c^59&&c^6&&c^1)C[S(1)][S(c+33)]=S(c+65281);
	if(flag&2)for(c=62;c;)C[S(2)][S(--c+65)]=S(c+0x391);
	if(flag&4)for(c=64;c;)C[S(3)][S(--c+63)]=S(c+1040);
	if(flag&8)for(c=83;c;)C[S(4)][S(--c+35)]=S(c+0x3041);
	if(flag&16)for(c=92;c;)C[S(5)][S(--c+35)]=S(c+0x30A1);
	if(flag&32)for(c=63;c;)C[S(6)][S(--c+64)]=S(c+0xFF61);
	// decoding loop
	for(txt=txt.split("");c=txt[a++];)
		C[c]?c!=txt[a]?D=C[c==d?e^=1:e=1,d=c]:txt[a++,b++]=c:txt[b++]=e&&D[c]||c;
	txt.length=b;return txt.join("")
}
```
```js:使用例
var s="アホハあおより蒼紫様ハチマキまきまち操気弾";
var e=strFiltering(s,selectFilter(s,1));
var d=strFiltered(e,selectFilter(e));
console.log(e,d);
// 対象文字手動選択
s="Соседская хурма часто ест клиентов.";
e=strFiltering(s,4);
d=strFiltered(e,4);
console.log(e,d);
```
ちなみに`selectFilter`は濾過(`strFiltering`)に使うには問題無いですが、逆濾過(`strFiltered`)に使う場合は要注意です。濾過前の文字列に`[\x01-\x06]`と`[#-~]`の組が含まれていると、正しく逆濾過できない事があります。
それが心配なら逆濾過で`selectFilter`を使わず、引数`flag`に数値を直接入力。上記例で言えば`d=strFiltered(e,24)`と記述。何でもかんでもまとめて処理したいなら、両方の関数の第2引数には63を指定すれば良い。

## 余談
ひょっとしたら見覚えのある人もいるかも。確か15年くらい前に公開した化石programに組み込まれているとかいないとか…