ya-kansuji 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md ADDED
@@ -0,0 +1,19 @@
1
+ # Changelog
2
+
3
+ このプロジェクトの主な変更点を記録する。書式は [Keep a Changelog](https://keepachangelog.com/ja/1.1.0/) に準拠し、[Semantic Versioning](https://semver.org/lang/ja/) に従う。
4
+
5
+ ## [Unreleased]
6
+
7
+ ## [0.1.0] - 2026-07-14
8
+
9
+ 初回リリース。Ruby gem [ya_kansuji](https://github.com/sugi/ya_kansuji) 1.1.0 の TypeScript/JavaScript 移植版。
10
+
11
+ ### Added
12
+
13
+ - 読み取り: `toNumber` / `toBigInt` — 漢数字・大字・全角数字・カンマ表現・位取り・単位なし表記が混在したテキストを数値へ変換。
14
+ - フォーマット: `toKan` と 5 つのビルトインフォーマッタ(`simple` / `gov` / `lawyer` / `judicV` / `judicH`)。
15
+ - `registerFormatter` / `getFormatter` によるフォーマッタプラグイン機構。
16
+ - ESM・CommonJS・IIFE(CDN 用)ビルドと型定義を同梱。
17
+
18
+ [Unreleased]: https://github.com/sugi/ya-kansuji-js/compare/v0.1.0...HEAD
19
+ [0.1.0]: https://github.com/sugi/ya-kansuji-js/releases/tag/v0.1.0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2026 Tatsuki Sugiura
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,169 @@
1
+ # ya-kansuji
2
+
3
+ [![CI](https://github.com/sugi/ya-kansuji-js/actions/workflows/ci.yml/badge.svg)](https://github.com/sugi/ya-kansuji-js/actions/workflows/ci.yml)
4
+
5
+ Yet another (ultimate) Japanese Kansuji library for JavaScript/TypeScript.
6
+
7
+ `ya-kansuji` は Ruby gem [ya_kansuji](https://github.com/sugi/ya_kansuji) の TypeScript/JavaScript 移植版です。漢数字・大字・全角数字混じりのテキストと数値を相互変換します。対応する Ruby 版は ya_kansuji 1.1.0 です。
8
+
9
+ 現状のサポートは日本語で万進な10進数だけです。歴史的に使われたことのあった万万進や他の漢字圏の漢数字はサポートしていません。
10
+
11
+ ## 機能
12
+
13
+ * 読み取り: 以下の混在したテキストを数値に変換できます
14
+ * 漢数字 (二万三千五百六十七)
15
+ * 全角数字 (1234)
16
+ * 単位なし / ゼロ (一六九〇)
17
+ * 位取り (2億0010万 / 千〇〇二)
18
+ * 大字 (千皕卅肆)
19
+ * カンマ表現 (12,345 / 5億3,456万 / 二万、五十)
20
+ * 数値から漢数字、漢字混じり数値文字列へのフォーマット
21
+ * 複数のビルトインフォーマッタ
22
+ * フォーマッタプラグイン機構
23
+
24
+ ## インストール
25
+
26
+ ```bash
27
+ npm install ya-kansuji
28
+ ```
29
+
30
+ ## 使い方
31
+
32
+ ### 読み取り (漢数字 → 数値)
33
+
34
+ `toNumber` に文字列を渡すと数値 (`number`) に変換できます。読み取りに関してオプションはありません。
35
+
36
+ ```ts
37
+ import { toNumber } from 'ya-kansuji'
38
+
39
+ toNumber('一〇二四') // => 1024
40
+ ```
41
+
42
+ 無量大数 (10^68) など `Number.MAX_SAFE_INTEGER` を超える値を扱う場合は、`bigint` を返す `toBigInt` を使ってください。`toNumber` は範囲を超える値に対して `RangeError` を投げます。
43
+
44
+ ```ts
45
+ import { toBigInt, toNumber } from 'ya-kansuji'
46
+
47
+ toBigInt('一無量大数') // => 100000000000000000000000000000000000000000000000000000000000000000000n
48
+ toNumber('一無量大数') // throws RangeError
49
+ ```
50
+
51
+ 先頭の「マイナス」は負の符号として解釈します。ASCII の `-` や Unicode の `−` (U+2212) は符号として扱いません。
52
+
53
+ ```ts
54
+ toNumber('マイナス千二百三十四') // => -1234
55
+ toNumber('マイナス 五十') // => -50 (区切り文字は除去してから照合します)
56
+ toNumber('2019-04') // => 2019
57
+ ```
58
+
59
+ ### フォーマット (数値 → 漢数字)
60
+
61
+ `toKan` に数値 (`number` または `bigint`) を渡すと、漢数字や漢字混じりの文字列に変換します。
62
+
63
+ ```ts
64
+ import { toKan } from 'ya-kansuji'
65
+
66
+ toKan(12345) // => '一万二千三百四十五'
67
+ ```
68
+
69
+ 第2引数にフォーマッタ名を渡すことで、出力形式を切り替えられます。
70
+
71
+ ```ts
72
+ toKan(12340005, 'simple') // => '千二百三十四万五'
73
+ toKan(12340005, 'gov') // => '1234万, 5'
74
+ toKan(12340005, 'lawyer') // => '1,234万5'
75
+ toKan(12340005, 'judic_v') // => '一二三四万〇〇〇五'
76
+ toKan(12340005, 'judic_h') // => '1234万0005'
77
+ ```
78
+
79
+ 現在あるビルトインフォーマッタは以下のとおりです (`toKan(10003, ...)` の実出力):
80
+
81
+ | フォーマッタ | 説明 | 出力例 |
82
+ | --- | --- | --- |
83
+ | `simple` | 標準 | `一万三` |
84
+ | `gov` | 「公用文作成の要領」方式 | `1万, 3` |
85
+ | `lawyer` | 行政司法協会方式 | `1万3` |
86
+ | `judic_v` | 裁判判例縦書き方式 | `一万〇〇〇三` |
87
+ | `judic_h` | 最高裁判例横書き方式 | `1万0003` |
88
+
89
+ 負数を渡すと先頭に「マイナス」を付け、絶対値をフォーマッタに渡します。フォーマッタが受け取るのは常に非負の値です。
90
+
91
+ ```ts
92
+ toKan(-1234) // => 'マイナス千二百三十四'
93
+ toKan(-10003, 'gov') // => 'マイナス1万, 3'
94
+ ```
95
+
96
+ 非整数の `number`、および `Number.MAX_SAFE_INTEGER` (2^53 - 1) を超える `number` は受け付けず、`RangeError` を投げます。
97
+ より大きな値は `bigint` で渡せますが、組み込みフォーマッタが表現できる範囲は絶対値が `10^72 - 1` 以下です。
98
+ 範囲を超える値には `RangeError` を投げます。
99
+
100
+ ```ts
101
+ toKan(1.5) // throws RangeError
102
+ toKan(10n ** 72n) // throws RangeError
103
+ ```
104
+
105
+ `simple`、`gov`、`lawyer`、`judicV`、`judicH` を直接呼び出す場合、入力は `0n` 以上 `10n ** 72n - 1n` 以下でなければなりません。
106
+ 負数や範囲を超える値には `RangeError` を投げます。
107
+ 負数をフォーマットする場合は、符号処理を行う `toKan` を使用してください。
108
+
109
+ #### 独自フォーマッタ
110
+
111
+ `registerFormatter` を使うと独自のフォーマッタを登録できます。第1引数に名前、第2引数に `(num: bigint, options?) => string` の関数を渡します。
112
+
113
+ ```ts
114
+ import { registerFormatter, toKan } from 'ya-kansuji'
115
+
116
+ registerFormatter('hoge', (num) => {
117
+ if (num === 0n) return ''
118
+ if (num === 1n) return 'いち'
119
+ if (num === 2n) return 'に'
120
+ return 'たくさん'
121
+ })
122
+
123
+ toKan(2, 'hoge') // => 'に'
124
+ toKan(123, 'hoge') // => 'たくさん'
125
+ ```
126
+
127
+ 登録済みフォーマッタは `getFormatter(name)` で取得できます。`toKan` の第2引数にはフォーマッタ名の代わりに関数を直接渡すこともできます。
128
+
129
+ ESM 版 (`dist/index.js`) と CJS 版 (`dist/index.cjs`) はそれぞれ別モジュールとしてロードされ、フォーマッタのレジストリ (`Map`) を共有しません。`registerFormatter` で登録したフォーマッタを使う側は、登録した側と同じモジュール形式 (`import` または `require`) で `ya-kansuji` を読み込んでください。
130
+
131
+ ### CDN での使用
132
+
133
+ npm 経由のインストールなしに、CDN から直接読み込むこともできます。
134
+
135
+ ```html
136
+ <!-- script タグ (グローバル YaKansuji) -->
137
+ <script src="https://cdn.jsdelivr.net/npm/ya-kansuji@0.1.0/dist/index.iife.min.js"></script>
138
+ <script>
139
+ console.log(YaKansuji.toKan(1234)); // => 千二百三十四
140
+ </script>
141
+
142
+ <!-- ES Modules -->
143
+ <script type="module">
144
+ import { toNumber } from 'https://cdn.jsdelivr.net/npm/ya-kansuji@0.1.0/+esm';
145
+ console.log(toNumber('九億6千万卌一')); // => 960000041
146
+ </script>
147
+ ```
148
+
149
+ ## Ruby 版との差分
150
+
151
+ Ruby 版 [ya_kansuji](https://github.com/sugi/ya_kansuji) からの移植にあたり、以下の点が異なります。
152
+
153
+ 1. **標準クラスの拡張は移植していません。** Ruby 版にある `String#to_i` の置き換えや `Integer#to_kan` の追加 (`core_ext` / `CoreRefine`) に相当する機能はありません。常に `toNumber` / `toBigInt` / `toKan` を明示的に呼び出してください。
154
+ 2. **`toKan` は非整数・安全範囲外の `number` を受け付けません。** 負数は Ruby 版 1.1.0 と同様に先頭へ「マイナス」を付け、絶対値をフォーマッタに渡して処理します (`toKan(-1234)` → `'マイナス千二百三十四'`、Ruby の `to_kan(-1234)` と一致)。一方、Ruby 版は `to_kan` の内部で `to_i` を呼ぶため小数は `Float#to_i` で暗黙に切り捨てて処理が通ります (`to_kan(1.5)` → `"一"`、`to_kan(-0.5)` → `"零"`) が、JS 版は安全整数でない `number` を `RangeError` にします。加えて `Number.MAX_SAFE_INTEGER` (2^53 - 1) を超える `number` を渡した場合も JS 版は `RangeError` です。`bigint` では絶対値 `10^72 - 1` まで扱えます。
155
+ 3. **`to_i` に相当する変換が `toBigInt` / `toNumber` の2関数に分かれています。** 無量大数 (10^68) は JavaScript の `Number.MAX_SAFE_INTEGER` (2^53 - 1) を大きく超えるため、常に安全な `bigint` を返す `toBigInt` と、安全な範囲を超えると `RangeError` を投げる `toNumber` を用途に応じて使い分けます。
156
+
157
+ ### 既知の非互換
158
+
159
+ かつて Ruby 版 `YaKansuji::REGEXP_PART` は 1.0.x まで、正規表現の文字クラスに配列を `#{}` 展開しており、`Array#to_s` (= `inspect`) の結果 (`["十", "百", ...]` のような、ダブルクォートやカンマを含む文字列表現) をそのまま埋め込む事故を起こしていました。この副作用で 1.0.x の `YaKansuji.to_i` は数値列中に `"` (U+0022) が混ざっていても無視して受理していました (`to_i('1"000')` → `1000`)。JS 版はこの事故を再現していないため、以前から `toBigInt('1"000')` は `"` の手前で読み取りが止まり `1` を返しており、**ya_kansuji 1.1.0 でこのバグが修正された結果、両者の挙動は一致しました** (Ruby 1.1.0 の `to_i('1"000')` も `1`)。
160
+
161
+ 空白除去の実装は Ruby 版の `[[:space:]]` と JS 版の `\s` とで対象が完全には一致せず、U+0085 (NEL) と U+FEFF (BOM) の2コードポイントだけ挙動が異なります (Ruby は NEL を空白として除去し BOM は除去しない、JS はその逆)。
162
+
163
+ ## License
164
+
165
+ The package is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
166
+
167
+ ## Author
168
+
169
+ Tatsuki Sugiura <sugi@nemui.org>
package/dist/index.cjs ADDED
@@ -0,0 +1,401 @@
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ //#region src/constants.ts
3
+ /** 10^1〜10^3 の位を表す漢数字の単位(十・百・千)。 */
4
+ const UNIT_EXP3 = [
5
+ "十",
6
+ "百",
7
+ "千"
8
+ ];
9
+ /** 10^4 ごとに繰り上がる漢数字の単位(万・億・兆…無量大数)。 */
10
+ const UNIT_EXP4 = [
11
+ "万",
12
+ "億",
13
+ "兆",
14
+ "京",
15
+ "垓",
16
+ "𥝱",
17
+ "穣",
18
+ "溝",
19
+ "澗",
20
+ "正",
21
+ "載",
22
+ "極",
23
+ "恒河沙",
24
+ "阿僧祇",
25
+ "那由他",
26
+ "不可思議",
27
+ "無量大数"
28
+ ];
29
+ const KAN_DIGITS = "〇一二三四五六七八九";
30
+ const NUM_ALT_CHARS = `${KAN_DIGITS}0123456789零壱壹弌弐貳貮参參弎肆伍陸漆質柒捌玖拾什陌佰阡仟萬秭`;
31
+ const NUM_NORMALIZED_CHARS = "01234567890123456789011122233345677789十十百百千千万𥝱";
32
+ //#endregion
33
+ //#region src/parse.ts
34
+ const altChars = Array.from(NUM_ALT_CHARS);
35
+ const normChars = Array.from(NUM_NORMALIZED_CHARS);
36
+ const NORMALIZE_MAP = new Map(altChars.map((c, i) => [c, normChars[i]]));
37
+ const SEPARATORS = /* @__PURE__ */ new Set([
38
+ ",",
39
+ ",",
40
+ "、",
41
+ " ",
42
+ "\n",
43
+ "\v",
44
+ "\f",
45
+ "\r",
46
+ " ",
47
+ "\xA0",
48
+ " ",
49
+ " ",
50
+ " ",
51
+ " ",
52
+ " ",
53
+ " ",
54
+ " ",
55
+ " ",
56
+ " ",
57
+ " ",
58
+ " ",
59
+ " ",
60
+ "\u2028",
61
+ "\u2029",
62
+ " ",
63
+ " ",
64
+ " ",
65
+ ""
66
+ ]);
67
+ const DIGIT_VALUES = [
68
+ 0n,
69
+ 1n,
70
+ 2n,
71
+ 3n,
72
+ 4n,
73
+ 5n,
74
+ 6n,
75
+ 7n,
76
+ 8n,
77
+ 9n
78
+ ];
79
+ const SPECIAL_NUMBER_VALUES = /* @__PURE__ */ new Map([
80
+ ["卄", 20n],
81
+ ["廿", 20n],
82
+ ["卅", 30n],
83
+ ["丗", 30n],
84
+ ["卌", 40n],
85
+ ["皕", 200n]
86
+ ]);
87
+ const SINGLE_UNITS = [...UNIT_EXP3, ...UNIT_EXP4.filter((u) => Array.from(u).length === 1)];
88
+ const MULTI_UNITS = UNIT_EXP4.filter((u) => Array.from(u).length > 1);
89
+ const CHAR_CLASS = [.../* @__PURE__ */ new Set([
90
+ ...altChars,
91
+ ...normChars,
92
+ ...SINGLE_UNITS,
93
+ ...SPECIAL_NUMBER_VALUES.keys()
94
+ ])].join("");
95
+ const PART_SOURCE = `${MULTI_UNITS.join("|")}|[${CHAR_CLASS}]`;
96
+ const KANSUJI_REGEXP = new RegExp(`(?:マイナス)?(?:${PART_SOURCE})+`, "u");
97
+ const PART_REGEXP = new RegExp(PART_SOURCE, "gu");
98
+ function createUnitValues(units, base) {
99
+ let value = base;
100
+ return new Map(units.map((unit) => {
101
+ const entry = [unit, value];
102
+ value *= base;
103
+ return entry;
104
+ }));
105
+ }
106
+ const UNIT_EXP3_VALUES = createUnitValues(UNIT_EXP3, 10n);
107
+ const UNIT_EXP4_VALUES = createUnitValues(UNIT_EXP4, 10000n);
108
+ const MAX_SAFE_BIGINT = BigInt(Number.MAX_SAFE_INTEGER);
109
+ function clean(str) {
110
+ let result = "";
111
+ for (const c of str) {
112
+ const normalized = NORMALIZE_MAP.get(c) ?? c;
113
+ if (!SEPARATORS.has(normalized)) result += normalized;
114
+ }
115
+ return result;
116
+ }
117
+ /**
118
+ * 漢数字・大字・全角数字が混じったテキストを bigint に変換する。
119
+ *
120
+ * 照合前にカンマ・読点・各種空白などの区切り文字を除去し、先頭から連続してマッチする
121
+ * 数値表現だけを読み取る(例: `toBigInt('五x六')` は `5n`)。マッチしなければ `0n` を返す。
122
+ * 先頭の「マイナス」は負号として扱うが、ASCII の `-` や U+2212(−)は負号として扱わない。
123
+ * 値の上限はなく、無量大数(10^68)を超える大きさもそのまま扱える。
124
+ *
125
+ * @param str 変換対象の文字列
126
+ * @returns テキストが表す整数値
127
+ * @example
128
+ * toBigInt('一〇二四') // => 1024n
129
+ * toBigInt('マイナス千二百') // => -1200n
130
+ * toBigInt('一無量大数') // => 10n ** 68n
131
+ */
132
+ function toBigInt(str) {
133
+ const cleaned = clean(String(str));
134
+ const matched = KANSUJI_REGEXP.exec(cleaned);
135
+ if (!matched) return 0n;
136
+ const matchedText = matched[0];
137
+ if (matchedText === void 0) return 0n;
138
+ let ret3 = 0n;
139
+ let ret4 = 0n;
140
+ let curnum = null;
141
+ for (const c of matchedText.match(PART_REGEXP) ?? []) if (c >= "1" && c <= "9") {
142
+ const digit = DIGIT_VALUES[c.charCodeAt(0) - 48];
143
+ curnum = (curnum ?? 0n) * 10n + digit;
144
+ } else if (c === "0") {
145
+ if (curnum !== null) curnum *= 10n;
146
+ } else {
147
+ const specialNumber = SPECIAL_NUMBER_VALUES.get(c);
148
+ if (specialNumber !== void 0) {
149
+ ret3 += specialNumber;
150
+ curnum = null;
151
+ } else {
152
+ const unit4 = UNIT_EXP4_VALUES.get(c);
153
+ if (unit4 !== void 0) {
154
+ if (curnum !== null) {
155
+ ret3 += curnum;
156
+ curnum = null;
157
+ }
158
+ if (ret3 === 0n) ret3 = 1n;
159
+ ret4 += ret3 * unit4;
160
+ ret3 = 0n;
161
+ } else {
162
+ const unit3 = UNIT_EXP3_VALUES.get(c);
163
+ if (unit3 !== void 0) {
164
+ curnum ??= 1n;
165
+ ret3 += curnum * unit3;
166
+ curnum = null;
167
+ }
168
+ }
169
+ }
170
+ }
171
+ if (curnum !== null) ret3 += curnum;
172
+ const ret = ret4 + ret3;
173
+ return matchedText.startsWith("マイナス") ? -ret : ret;
174
+ }
175
+ /**
176
+ * {@link toBigInt} と同じ規則でテキストを解釈し、`number` として返す。
177
+ *
178
+ * 結果の絶対値が `Number.MAX_SAFE_INTEGER`(2^53 − 1)を超える場合は `RangeError` を
179
+ * 投げる。安全整数を超えうる値を扱うときは {@link toBigInt} を使うこと。
180
+ *
181
+ * @param str 変換対象の文字列
182
+ * @returns テキストが表す数値
183
+ * @throws {RangeError} 値が安全整数の範囲を超える場合
184
+ * @example
185
+ * toNumber('一〇二四') // => 1024
186
+ * toNumber('一無量大数') // throws RangeError
187
+ */
188
+ function toNumber(str) {
189
+ const value = toBigInt(str);
190
+ if (value > MAX_SAFE_BIGINT || value < -MAX_SAFE_BIGINT) throw new RangeError(`kansuji value exceeds Number.MAX_SAFE_INTEGER: ${str}`);
191
+ return Number(value);
192
+ }
193
+ //#endregion
194
+ //#region src/groups.ts
195
+ const GROUP_BASE = 10000n;
196
+ const GROUP_UNITS = ["", ...UNIT_EXP4];
197
+ const GROUP_COUNT = GROUP_UNITS.length;
198
+ const MAX_EXCLUSIVE = GROUP_BASE ** BigInt(GROUP_COUNT);
199
+ const MAX_DIGITS = GROUP_COUNT * 4;
200
+ function groups4(num) {
201
+ if (num < 0n || num >= MAX_EXCLUSIVE) throw new RangeError(`Kansuji formatters require a bigint between 0 and 10^${MAX_DIGITS} - 1`);
202
+ const ret = [];
203
+ let value = num;
204
+ let unitIndex = 0;
205
+ do {
206
+ const unit = GROUP_UNITS[unitIndex];
207
+ if (unit === void 0) throw new RangeError(`Kansuji formatters require a bigint between 0 and 10^${MAX_DIGITS} - 1`);
208
+ ret.push([Number(value % GROUP_BASE), unit]);
209
+ value /= GROUP_BASE;
210
+ unitIndex++;
211
+ } while (value > 0n);
212
+ return ret.reverse();
213
+ }
214
+ //#endregion
215
+ //#region src/formatter/gov.ts
216
+ /**
217
+ * 「公用文作成の要領」方式で整形する(4桁ごとにアラビア数字+漢字単位、読点区切り)。
218
+ *
219
+ * @param num 0 以上 `10^72 − 1` 以下の bigint
220
+ * @returns 整形後の文字列。`0n` は `'0'`
221
+ * @throws {RangeError} 範囲外の値の場合。負数の整形には {@link toKan} を使う
222
+ * @example
223
+ * gov(12340005n) // => '1234万, 5'
224
+ * gov(10003n) // => '1万, 3'
225
+ */
226
+ function gov(num) {
227
+ if (num === 0n) return "0";
228
+ const parts = [];
229
+ for (const [i4, unit4] of groups4(num)) {
230
+ if (i4 === 0) continue;
231
+ parts.push(`${i4}${unit4}`);
232
+ }
233
+ return parts.join(", ");
234
+ }
235
+ //#endregion
236
+ //#region src/formatter/judic.ts
237
+ function judic(num, zero, mapDigits) {
238
+ if (num === 0n) return zero;
239
+ let ret = "";
240
+ let head = true;
241
+ for (const [i4, unit4] of groups4(num)) {
242
+ if (i4 === 0) continue;
243
+ ret += (head ? String(i4) : String(i4).padStart(4, "0")) + unit4;
244
+ head = false;
245
+ }
246
+ return mapDigits(ret);
247
+ }
248
+ /**
249
+ * 裁判判例の縦書き方式で整形する(4桁ごとに漢数字、下位グループはゼロ埋め、単位は漢字)。
250
+ *
251
+ * @param num 0 以上 `10^72 − 1` 以下の bigint
252
+ * @returns 整形後の文字列。`0n` は `'〇'`
253
+ * @throws {RangeError} 範囲外の値の場合。負数の整形には {@link toKan} を使う
254
+ * @example
255
+ * judicV(12340005n) // => '一二三四万〇〇〇五'
256
+ */
257
+ function judicV(num) {
258
+ return judic(num, "〇", (s) => s.replace(/[0-9]/g, (d) => KAN_DIGITS.charAt(Number(d))));
259
+ }
260
+ /**
261
+ * 最高裁判例の横書き方式で整形する(4桁ごとに全角数字、下位グループはゼロ埋め)。
262
+ *
263
+ * @param num 0 以上 `10^72 − 1` 以下の bigint
264
+ * @returns 整形後の文字列。`0n` は `'0'`
265
+ * @throws {RangeError} 範囲外の値の場合。負数の整形には {@link toKan} を使う
266
+ * @example
267
+ * judicH(12340005n) // => '1234万0005'
268
+ */
269
+ function judicH(num) {
270
+ return judic(num, "0", (s) => s.replace(/[0-9]/g, (d) => String.fromCharCode(d.charCodeAt(0) + 65248)));
271
+ }
272
+ //#endregion
273
+ //#region src/formatter/lawyer.ts
274
+ /**
275
+ * 行政・司法で用いられる方式で整形する(4桁ごとにアラビア数字+漢字単位、千の位にカンマ)。
276
+ *
277
+ * @param num 0 以上 `10^72 − 1` 以下の bigint
278
+ * @returns 整形後の文字列。`0n` は `'0'`
279
+ * @throws {RangeError} 範囲外の値の場合。負数の整形には {@link toKan} を使う
280
+ * @example
281
+ * lawyer(12340005n) // => '1,234万5'
282
+ * lawyer(10003n) // => '1万3'
283
+ */
284
+ function lawyer(num) {
285
+ if (num === 0n) return "0";
286
+ let ret = "";
287
+ for (const [i4, unit4] of groups4(num)) {
288
+ if (i4 === 0) continue;
289
+ const s = String(i4);
290
+ ret += (i4 >= 1e3 ? `${s[0]},${s.slice(1)}` : s) + unit4;
291
+ }
292
+ return ret;
293
+ }
294
+ //#endregion
295
+ //#region src/formatter/simple.ts
296
+ const POWERS_10 = [
297
+ 1,
298
+ 10,
299
+ 100,
300
+ 1e3
301
+ ];
302
+ /**
303
+ * 標準的な漢数字へ整形する(位ごとに漢数字の単位を使う)。
304
+ *
305
+ * @param num 0 以上 `10^72 − 1` 以下の bigint
306
+ * @returns 漢数字文字列。`0n` は `'零'`
307
+ * @throws {RangeError} 範囲外の値の場合。負数の整形には {@link toKan} を使う
308
+ * @example
309
+ * simple(12345n) // => '一万二千三百四十五'
310
+ * simple(10003n) // => '一万三'
311
+ */
312
+ function simple(num) {
313
+ if (num === 0n) return "零";
314
+ let ret = "";
315
+ for (const [i4, unit4] of groups4(num)) {
316
+ if (i4 === 0) continue;
317
+ for (let j = UNIT_EXP3.length; j >= 0; j--) {
318
+ const unit3 = UNIT_EXP3[j - 1] ?? "";
319
+ const i3 = Math.floor(i4 / (POWERS_10[j] ?? 1)) % 10;
320
+ if (i3 === 0) continue;
321
+ ret += i3 === 1 && unit3 !== "" ? unit3 : KAN_DIGITS.charAt(i3) + unit3;
322
+ }
323
+ ret += unit4;
324
+ }
325
+ return ret;
326
+ }
327
+ //#endregion
328
+ //#region src/formatters.ts
329
+ const formatters = /* @__PURE__ */ new Map([
330
+ ["simple", simple],
331
+ ["gov", gov],
332
+ ["lawyer", lawyer],
333
+ ["judic_v", judicV],
334
+ ["judic_h", judicH]
335
+ ]);
336
+ /**
337
+ * フォーマッタを名前付きで登録する。既存の同名フォーマッタ(ビルトイン含む)は警告なく
338
+ * 上書きされる。
339
+ *
340
+ * レジストリは ESM 版と CJS 版で共有されない。登録側と利用側は同じモジュール形式
341
+ * (`import` または `require`) で `ya-kansuji` を読み込むこと。
342
+ *
343
+ * @param name フォーマッタ名。{@link toKan} の第2引数に渡して選択する
344
+ * @param formatter `(num: bigint, options?) => string`。`num` は常に 0 以上
345
+ * @throws {TypeError} `formatter` が関数でない場合
346
+ */
347
+ function registerFormatter(name, formatter) {
348
+ if (typeof formatter !== "function") throw new TypeError("Registering invalid formatter.");
349
+ formatters.set(name, formatter);
350
+ }
351
+ /**
352
+ * 登録済みフォーマッタを名前で取得する。未登録なら `undefined`。
353
+ *
354
+ * @param name フォーマッタ名
355
+ * @returns フォーマッタ関数、なければ `undefined`
356
+ */
357
+ function getFormatter(name) {
358
+ return formatters.get(name);
359
+ }
360
+ /**
361
+ * 数値を漢数字・漢字混じりの文字列へ整形する。
362
+ *
363
+ * 負数は先頭に「マイナス」を付け、絶対値をフォーマッタに渡す(フォーマッタが受け取るのは
364
+ * 常に非負値)。`number` は安全整数でなければならず、より大きな値は `bigint` で渡すこと。
365
+ * ビルトインフォーマッタが表現できるのは絶対値 `10^72 − 1` まで。
366
+ *
367
+ * @param num 変換対象。`number`(安全整数)または `bigint`
368
+ * @param formatter フォーマッタ名、またはフォーマッタ関数。既定は `'simple'`
369
+ * @param options フォーマッタへ渡すオプション(ビルトインは未使用)
370
+ * @returns 整形後の文字列
371
+ * @throws {RangeError} `num` が非整数、安全整数を超える `number`、またはビルトイン
372
+ * フォーマッタの表現範囲を超える場合
373
+ * @throws {TypeError} 指定した名前のフォーマッタが見つからない場合
374
+ * @example
375
+ * toKan(12345) // => '一万二千三百四十五'
376
+ * toKan(12340005, 'gov') // => '1234万, 5'
377
+ * toKan(-1234) // => 'マイナス千二百三十四'
378
+ */
379
+ function toKan(num, formatter = "simple", options = {}) {
380
+ let value;
381
+ if (typeof num === "bigint") value = num;
382
+ else if (Number.isSafeInteger(num)) value = BigInt(num);
383
+ else throw new RangeError(`toKan requires a safe integer or bigint: ${num}`);
384
+ if (value < 0n) return `マイナス${toKan(-value, formatter, options)}`;
385
+ const fn = typeof formatter === "function" ? formatter : formatters.get(formatter);
386
+ if (!fn) throw new TypeError(`Unable to find formatter ${String(formatter)}`);
387
+ return fn(value, options);
388
+ }
389
+ //#endregion
390
+ exports.UNIT_EXP3 = UNIT_EXP3;
391
+ exports.UNIT_EXP4 = UNIT_EXP4;
392
+ exports.getFormatter = getFormatter;
393
+ exports.gov = gov;
394
+ exports.judicH = judicH;
395
+ exports.judicV = judicV;
396
+ exports.lawyer = lawyer;
397
+ exports.registerFormatter = registerFormatter;
398
+ exports.simple = simple;
399
+ exports.toBigInt = toBigInt;
400
+ exports.toKan = toKan;
401
+ exports.toNumber = toNumber;