世界中の数字をアラビア数字に変換した話

# この記事の成果物

https://www.npmjs.com/package/@satachito/replace-numeral

# はじめに

https://slip.828.tokyo/

[こんなサイト](https://slip.828.tokyo/)を作っていたら、世界中の数字をアラビア数字に変換するハメになった。世界中って言ってもUnicodeの世界だけれど。そして案の定文字とコードの泥沼にはまった。

Unicodeの中のものをカテゴライズした表がある。

http://www.unicode.org/reports/tr44/tr44-16.html#GC_Values_Table

世界中に数字はたくさんあるけれど、これの中のNd(Decimal_Number)に該当するものがアラビア数字に変換可能なもののようだ。

では、UnicodeのNdカテゴリにあるものはどこのコードポイントにアサインされているのだろう、と思って探したらそれらしきものがあった。一応 unicode.org だからこれが最新だと思われる。

https://util.unicode.org/UnicodeJsps/list-unicodeset.jsp?a=[:Numeric_Type=Decimal:]

```
000030: 0123456789
000660: ٠١٢٣٤٥٦٧٨٩
0006f0: ۰۱۲۳۴۵۶۷۸۹
0007c0: ߀߁߂߃߄߅߆߇߈߉
000966: ०१२३४५६७८९
0009e6: ০১২৩৪৫৬৭৮৯
000a66: ੦੧੨੩੪੫੬੭੮੯
000ae6: ૦૧૨૩૪૫૬૭૮૯
000b66: ୦୧୨୩୪୫୬୭୮୯
000be6: ௦௧௨௩௪௫௬௭௮௯
000c66: ౦౧౨౩౪౫౬౭౮౯
000ce6: ೦೧೨೩೪೫೬೭೮೯
000d66: ൦൧൨൩൪൫൬൭൮൯
000de6: ෦෧෨෩෪෫෬෭෮෯
000e50: ๐๑๒๓๔๕๖๗๘๙
000ed0: ໐໑໒໓໔໕໖໗໘໙
000f20: ༠༡༢༣༤༥༦༧༨༩
001040: ၀၁၂၃၄၅၆၇၈၉
001090: ႐႑႒႓႔႕႖႗႘႙
0017e0: ០១២៣៤៥៦៧៨៩
001810: ᠐᠑᠒᠓᠔᠕᠖᠗᠘᠙
001946: ᥆᥇᥈᥉᥊᥋᥌᥍᥎᥏
0019d0: ᧐᧑᧒᧓᧔᧕᧖᧗᧘᧙
001a80: ᪀᪁᪂᪃᪄᪅᪆᪇᪈᪉
001a90: ᪐᪑᪒᪓᪔᪕᪖᪗᪘᪙
001b50: ᭐᭑᭒᭓᭔᭕᭖᭗᭘᭙
001bb0: ᮰᮱᮲᮳᮴᮵᮶᮷᮸᮹
001c40: ᱀᱁᱂᱃᱄᱅᱆᱇᱈᱉
001c50: ᱐᱑᱒᱓᱔᱕᱖᱗᱘᱙
00a620: ꘠꘡꘢꘣꘤꘥꘦꘧꘨꘩
00a8d0: ꣐꣑꣒꣓꣔꣕꣖꣗꣘꣙
00a900: ꤀꤁꤂꤃꤄꤅꤆꤇꤈꤉
00a9d0: ꧐꧑꧒꧓꧔꧕꧖꧗꧘꧙
00a9f0: ꧰꧱꧲꧳꧴꧵꧶꧷꧸꧹
00aa50: ꩐꩑꩒꩓꩔꩕꩖꩗꩘꩙
00abf0: ꯰꯱꯲꯳꯴꯵꯶꯷꯸꯹
00ff10: ０１２３４５６７８９
0104a0: 𐒠𐒡𐒢𐒣𐒤𐒥𐒦𐒧𐒨𐒩
010d30: 𐴰𐴱𐴲𐴳𐴴𐴵𐴶𐴷𐴸𐴹
011066: 𑁦𑁧𑁨𑁩𑁪𑁫𑁬𑁭𑁮𑁯
0110f0: 𑃰𑃱𑃲𑃳𑃴𑃵𑃶𑃷𑃸𑃹
011136: 𑄶𑄷𑄸𑄹𑄺𑄻𑄼𑄽𑄾𑄿
0111d0: 𑇐𑇑𑇒𑇓𑇔𑇕𑇖𑇗𑇘𑇙
0112f0: 𑋰𑋱𑋲𑋳𑋴𑋵𑋶𑋷𑋸𑋹
011450: 𑑐𑑑𑑒𑑓𑑔𑑕𑑖𑑗𑑘𑑙
0114d0: 𑓐𑓑𑓒𑓓𑓔𑓕𑓖𑓗𑓘𑓙
011650: 𑙐𑙑𑙒𑙓𑙔𑙕𑙖𑙗𑙘𑙙
0116c0: 𑛀𑛁𑛂𑛃𑛄𑛅𑛆𑛇𑛈𑛉
011730: 𑜰𑜱𑜲𑜳𑜴𑜵𑜶𑜷𑜸𑜹
0118e0: 𑣠𑣡𑣢𑣣𑣤𑣥𑣦𑣧𑣨𑣩
011950: 𑥐𑥑𑥒𑥓𑥔𑥕𑥖𑥗𑥘𑥙
011c50: 𑱐𑱑𑱒𑱓𑱔𑱕𑱖𑱗𑱘𑱙
011d50: 𑵐𑵑𑵒𑵓𑵔𑵕𑵖𑵗𑵘𑵙
011da0: 𑶠𑶡𑶢𑶣𑶤𑶥𑶦𑶧𑶨𑶩
016a60: 𖩠𖩡𖩢𖩣𖩤𖩥𖩦𖩧𖩨𖩩
016ac0: 𖫀𖫁𖫂𖫃𖫄𖫅𖫆𖫇𖫈𖫉
016b50: 𖭐𖭑𖭒𖭓𖭔𖭕𖭖𖭗𖭘𖭙
01d7ce: 𝟎𝟏𝟐𝟑𝟒𝟓𝟔𝟕𝟖𝟗
01d7d8: 𝟘𝟙𝟚𝟛𝟜𝟝𝟞𝟟𝟠𝟡
01d7e2: 𝟢𝟣𝟤𝟥𝟦𝟧𝟨𝟩𝟪𝟫
01d7ec: 𝟬𝟭𝟮𝟯𝟰𝟱𝟲𝟳𝟴𝟵
01d7f6: 𝟶𝟷𝟸𝟹𝟺𝟻𝟼𝟽𝟾𝟿
01e140: 𞅀𞅁𞅂𞅃𞅄𞅅𞅆𞅇𞅈𞅉
01e2f0: 𞋰𞋱𞋲𞋳𞋴𞋵𞋶𞋷𞋸𞋹
01e950: 𞥐𞥑𞥒𞥓𞥔𞥕𞥖𞥗𞥘𞥙
01fbf0: 🯰🯱🯲🯳🯴🯵🯶🯷🯸🯹
```

# 実装その１

0 にあたる文字を[]でグループ化した正規表現にマッチした文字列を0に置き換える、1-9も同様、という考え方でまずやってみた。

```
console.log(
	'０１２３４５６７８９'
	.replaceAll( RegExp( '[' + String.fromCodePoint( 0xFF10, 0x104A0 ) + ']', 'g' ), '0' )
)
0１２３４５６７８９
```
うまくいっているように見えるのだが、

```
console.log(
	'𐒠𐒡𐒢𐒣𐒤𐒥𐒦𐒧𐒨𐒩'
	.replaceAll( RegExp( '[' + String.fromCodePoint( 0xFF10, 0x104A0 ) + ']', 'g' ), '0' )
)
000�0�0�0�0�0�0�0�0�000�0�0�0�0�0�0�0�0�
```

これはうまくいかない。

```
console.log(
	'𐒠𐒡𐒢𐒣𐒤𐒥𐒦𐒧𐒨𐒩'
	.replaceAll( String.fromCodePoint( 0x104A0 ), '0' )
)
0𐒡𐒢𐒣𐒤𐒥𐒦𐒧𐒨𐒩
```
しかしこれはうまくいくので、0x10000以上の文字コードを[]でグループ化した正規表現がうまくいかないようだ。というわけで失敗。

この問題に対し全文字コード（660個）でreplaceAllをかける方法もあるのだが、660回入力文字列をスキャンするのは性能的に問題が出ること必至なので、コード化することにした。

# 実装その２

javascript の文字列は２バイトの文字コードの集まりである。ところが上記の表から分かるように、２バイト（0x10000未満)で表せない部分にいくつか文字コードがある。この（0x10000以上）場合、サロゲートペアという２つの２バイトでこの部分を表現する方法がとられる。

https://ja.wikipedia.org/wiki/Unicode#サロゲートペア

プログラムで書くとサロゲートペアは以下のようになる

```
const
surrogatePair = _ => [ ( 0xd800 | _ >> 10 ) - 0x40, 0xdc00 | ( _ & 0x3ff ) ]
```

例えば 0104a0 なら以下のようになる
```
[ ( 0xd800 | 0x0104a0 >> 10 ) - 0x40, 0xdc00 | ( 0x0104a0 & 0x3ff ) ]
[ ( 0xd800 | 0x41 ) - 0x40, 0xdc00 | 0xa0 ]
[ 0xd801, 0xdca0 ]
```

実際のデータ

```
> const
> $ = '𐒠𐒡𐒢𐒣𐒤𐒥𐒦𐒧𐒨𐒩'    // 0104a0-0104a9
> console.log( $.length )
20
> for ( let _ = 0; _ < $.length; _++ ) console.log( $.charCodeAt( _ ).toString( 16 ) )
d801
dca0
d801
dca1
d801
dca2
d801
dca3
d801
dca4
d801
dca5
d801
dca6
d801
dca7
d801
dca8
d801
dca9
```

ここまで来れば、0x10000未満と0x10000以上で２回文字列スキャンして置き換えればいい。
コードは長くなっちゃうので、githubで。

https://github.com/Satachito/ReplaceNumeral/blob/main/_.js

# 成果物

もしかしたら他に必要になる人がいるかもしれないので、NPMのpackageにしておきました。

https://www.npmjs.com/package/@satachito/replace-numeral