文字列から特定文字を除外して全体の容量を削減、あるいは縮めやすくする魔法を紹介します。本記事ではこの工程を濾過と呼びます。
その対象文字となるのは全角英数字、Greek、Cyrillic、平仮名、片仮名、半角片仮名となります。同種の文字が連続しているほど効果が高くなります(2文字目からは1 byte)。
原理
以下の規則で変換します。当然可逆変換です。元の文字列に[#-~]
が含まれていても問題ありません。
- 全角英数字 → 半角英数字. 但し
\(\uFF3C)
,~(\uFF5E)
は除外 - Greek(
[\u0391-\u03CE]
) →[A-~]
- Cyrillic(
[\u0410-\u044F]
) →[?-~]
- 平仮名(
[\u3041-\3093]
) →[#-u]
- 片仮名(
[\u03A1-\u30FC]
→[#-~]
- 半角片仮名(
[\uFF61-\uFF9F]
→[@-~]
実装
/* 対象文字自動設定
@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("")
}
使用例
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に組み込まれているとかいないとか…