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/dist/index.js ADDED
@@ -0,0 +1,389 @@
1
+ //#region src/constants.ts
2
+ /** 10^1〜10^3 の位を表す漢数字の単位(十・百・千)。 */
3
+ const UNIT_EXP3 = [
4
+ "十",
5
+ "百",
6
+ "千"
7
+ ];
8
+ /** 10^4 ごとに繰り上がる漢数字の単位(万・億・兆…無量大数)。 */
9
+ const UNIT_EXP4 = [
10
+ "万",
11
+ "億",
12
+ "兆",
13
+ "京",
14
+ "垓",
15
+ "𥝱",
16
+ "穣",
17
+ "溝",
18
+ "澗",
19
+ "正",
20
+ "載",
21
+ "極",
22
+ "恒河沙",
23
+ "阿僧祇",
24
+ "那由他",
25
+ "不可思議",
26
+ "無量大数"
27
+ ];
28
+ const KAN_DIGITS = "〇一二三四五六七八九";
29
+ const NUM_ALT_CHARS = `${KAN_DIGITS}0123456789零壱壹弌弐貳貮参參弎肆伍陸漆質柒捌玖拾什陌佰阡仟萬秭`;
30
+ const NUM_NORMALIZED_CHARS = "01234567890123456789011122233345677789十十百百千千万𥝱";
31
+ //#endregion
32
+ //#region src/parse.ts
33
+ const altChars = Array.from(NUM_ALT_CHARS);
34
+ const normChars = Array.from(NUM_NORMALIZED_CHARS);
35
+ const NORMALIZE_MAP = new Map(altChars.map((c, i) => [c, normChars[i]]));
36
+ const SEPARATORS = /* @__PURE__ */ new Set([
37
+ ",",
38
+ ",",
39
+ "、",
40
+ " ",
41
+ "\n",
42
+ "\v",
43
+ "\f",
44
+ "\r",
45
+ " ",
46
+ "\xA0",
47
+ " ",
48
+ " ",
49
+ " ",
50
+ " ",
51
+ " ",
52
+ " ",
53
+ " ",
54
+ " ",
55
+ " ",
56
+ " ",
57
+ " ",
58
+ " ",
59
+ "\u2028",
60
+ "\u2029",
61
+ " ",
62
+ " ",
63
+ " ",
64
+ ""
65
+ ]);
66
+ const DIGIT_VALUES = [
67
+ 0n,
68
+ 1n,
69
+ 2n,
70
+ 3n,
71
+ 4n,
72
+ 5n,
73
+ 6n,
74
+ 7n,
75
+ 8n,
76
+ 9n
77
+ ];
78
+ const SPECIAL_NUMBER_VALUES = /* @__PURE__ */ new Map([
79
+ ["卄", 20n],
80
+ ["廿", 20n],
81
+ ["卅", 30n],
82
+ ["丗", 30n],
83
+ ["卌", 40n],
84
+ ["皕", 200n]
85
+ ]);
86
+ const SINGLE_UNITS = [...UNIT_EXP3, ...UNIT_EXP4.filter((u) => Array.from(u).length === 1)];
87
+ const MULTI_UNITS = UNIT_EXP4.filter((u) => Array.from(u).length > 1);
88
+ const CHAR_CLASS = [.../* @__PURE__ */ new Set([
89
+ ...altChars,
90
+ ...normChars,
91
+ ...SINGLE_UNITS,
92
+ ...SPECIAL_NUMBER_VALUES.keys()
93
+ ])].join("");
94
+ const PART_SOURCE = `${MULTI_UNITS.join("|")}|[${CHAR_CLASS}]`;
95
+ const KANSUJI_REGEXP = new RegExp(`(?:マイナス)?(?:${PART_SOURCE})+`, "u");
96
+ const PART_REGEXP = new RegExp(PART_SOURCE, "gu");
97
+ function createUnitValues(units, base) {
98
+ let value = base;
99
+ return new Map(units.map((unit) => {
100
+ const entry = [unit, value];
101
+ value *= base;
102
+ return entry;
103
+ }));
104
+ }
105
+ const UNIT_EXP3_VALUES = createUnitValues(UNIT_EXP3, 10n);
106
+ const UNIT_EXP4_VALUES = createUnitValues(UNIT_EXP4, 10000n);
107
+ const MAX_SAFE_BIGINT = BigInt(Number.MAX_SAFE_INTEGER);
108
+ function clean(str) {
109
+ let result = "";
110
+ for (const c of str) {
111
+ const normalized = NORMALIZE_MAP.get(c) ?? c;
112
+ if (!SEPARATORS.has(normalized)) result += normalized;
113
+ }
114
+ return result;
115
+ }
116
+ /**
117
+ * 漢数字・大字・全角数字が混じったテキストを bigint に変換する。
118
+ *
119
+ * 照合前にカンマ・読点・各種空白などの区切り文字を除去し、先頭から連続してマッチする
120
+ * 数値表現だけを読み取る(例: `toBigInt('五x六')` は `5n`)。マッチしなければ `0n` を返す。
121
+ * 先頭の「マイナス」は負号として扱うが、ASCII の `-` や U+2212(−)は負号として扱わない。
122
+ * 値の上限はなく、無量大数(10^68)を超える大きさもそのまま扱える。
123
+ *
124
+ * @param str 変換対象の文字列
125
+ * @returns テキストが表す整数値
126
+ * @example
127
+ * toBigInt('一〇二四') // => 1024n
128
+ * toBigInt('マイナス千二百') // => -1200n
129
+ * toBigInt('一無量大数') // => 10n ** 68n
130
+ */
131
+ function toBigInt(str) {
132
+ const cleaned = clean(String(str));
133
+ const matched = KANSUJI_REGEXP.exec(cleaned);
134
+ if (!matched) return 0n;
135
+ const matchedText = matched[0];
136
+ if (matchedText === void 0) return 0n;
137
+ let ret3 = 0n;
138
+ let ret4 = 0n;
139
+ let curnum = null;
140
+ for (const c of matchedText.match(PART_REGEXP) ?? []) if (c >= "1" && c <= "9") {
141
+ const digit = DIGIT_VALUES[c.charCodeAt(0) - 48];
142
+ curnum = (curnum ?? 0n) * 10n + digit;
143
+ } else if (c === "0") {
144
+ if (curnum !== null) curnum *= 10n;
145
+ } else {
146
+ const specialNumber = SPECIAL_NUMBER_VALUES.get(c);
147
+ if (specialNumber !== void 0) {
148
+ ret3 += specialNumber;
149
+ curnum = null;
150
+ } else {
151
+ const unit4 = UNIT_EXP4_VALUES.get(c);
152
+ if (unit4 !== void 0) {
153
+ if (curnum !== null) {
154
+ ret3 += curnum;
155
+ curnum = null;
156
+ }
157
+ if (ret3 === 0n) ret3 = 1n;
158
+ ret4 += ret3 * unit4;
159
+ ret3 = 0n;
160
+ } else {
161
+ const unit3 = UNIT_EXP3_VALUES.get(c);
162
+ if (unit3 !== void 0) {
163
+ curnum ??= 1n;
164
+ ret3 += curnum * unit3;
165
+ curnum = null;
166
+ }
167
+ }
168
+ }
169
+ }
170
+ if (curnum !== null) ret3 += curnum;
171
+ const ret = ret4 + ret3;
172
+ return matchedText.startsWith("マイナス") ? -ret : ret;
173
+ }
174
+ /**
175
+ * {@link toBigInt} と同じ規則でテキストを解釈し、`number` として返す。
176
+ *
177
+ * 結果の絶対値が `Number.MAX_SAFE_INTEGER`(2^53 − 1)を超える場合は `RangeError` を
178
+ * 投げる。安全整数を超えうる値を扱うときは {@link toBigInt} を使うこと。
179
+ *
180
+ * @param str 変換対象の文字列
181
+ * @returns テキストが表す数値
182
+ * @throws {RangeError} 値が安全整数の範囲を超える場合
183
+ * @example
184
+ * toNumber('一〇二四') // => 1024
185
+ * toNumber('一無量大数') // throws RangeError
186
+ */
187
+ function toNumber(str) {
188
+ const value = toBigInt(str);
189
+ if (value > MAX_SAFE_BIGINT || value < -MAX_SAFE_BIGINT) throw new RangeError(`kansuji value exceeds Number.MAX_SAFE_INTEGER: ${str}`);
190
+ return Number(value);
191
+ }
192
+ //#endregion
193
+ //#region src/groups.ts
194
+ const GROUP_BASE = 10000n;
195
+ const GROUP_UNITS = ["", ...UNIT_EXP4];
196
+ const GROUP_COUNT = GROUP_UNITS.length;
197
+ const MAX_EXCLUSIVE = GROUP_BASE ** BigInt(GROUP_COUNT);
198
+ const MAX_DIGITS = GROUP_COUNT * 4;
199
+ function groups4(num) {
200
+ if (num < 0n || num >= MAX_EXCLUSIVE) throw new RangeError(`Kansuji formatters require a bigint between 0 and 10^${MAX_DIGITS} - 1`);
201
+ const ret = [];
202
+ let value = num;
203
+ let unitIndex = 0;
204
+ do {
205
+ const unit = GROUP_UNITS[unitIndex];
206
+ if (unit === void 0) throw new RangeError(`Kansuji formatters require a bigint between 0 and 10^${MAX_DIGITS} - 1`);
207
+ ret.push([Number(value % GROUP_BASE), unit]);
208
+ value /= GROUP_BASE;
209
+ unitIndex++;
210
+ } while (value > 0n);
211
+ return ret.reverse();
212
+ }
213
+ //#endregion
214
+ //#region src/formatter/gov.ts
215
+ /**
216
+ * 「公用文作成の要領」方式で整形する(4桁ごとにアラビア数字+漢字単位、読点区切り)。
217
+ *
218
+ * @param num 0 以上 `10^72 − 1` 以下の bigint
219
+ * @returns 整形後の文字列。`0n` は `'0'`
220
+ * @throws {RangeError} 範囲外の値の場合。負数の整形には {@link toKan} を使う
221
+ * @example
222
+ * gov(12340005n) // => '1234万, 5'
223
+ * gov(10003n) // => '1万, 3'
224
+ */
225
+ function gov(num) {
226
+ if (num === 0n) return "0";
227
+ const parts = [];
228
+ for (const [i4, unit4] of groups4(num)) {
229
+ if (i4 === 0) continue;
230
+ parts.push(`${i4}${unit4}`);
231
+ }
232
+ return parts.join(", ");
233
+ }
234
+ //#endregion
235
+ //#region src/formatter/judic.ts
236
+ function judic(num, zero, mapDigits) {
237
+ if (num === 0n) return zero;
238
+ let ret = "";
239
+ let head = true;
240
+ for (const [i4, unit4] of groups4(num)) {
241
+ if (i4 === 0) continue;
242
+ ret += (head ? String(i4) : String(i4).padStart(4, "0")) + unit4;
243
+ head = false;
244
+ }
245
+ return mapDigits(ret);
246
+ }
247
+ /**
248
+ * 裁判判例の縦書き方式で整形する(4桁ごとに漢数字、下位グループはゼロ埋め、単位は漢字)。
249
+ *
250
+ * @param num 0 以上 `10^72 − 1` 以下の bigint
251
+ * @returns 整形後の文字列。`0n` は `'〇'`
252
+ * @throws {RangeError} 範囲外の値の場合。負数の整形には {@link toKan} を使う
253
+ * @example
254
+ * judicV(12340005n) // => '一二三四万〇〇〇五'
255
+ */
256
+ function judicV(num) {
257
+ return judic(num, "〇", (s) => s.replace(/[0-9]/g, (d) => KAN_DIGITS.charAt(Number(d))));
258
+ }
259
+ /**
260
+ * 最高裁判例の横書き方式で整形する(4桁ごとに全角数字、下位グループはゼロ埋め)。
261
+ *
262
+ * @param num 0 以上 `10^72 − 1` 以下の bigint
263
+ * @returns 整形後の文字列。`0n` は `'0'`
264
+ * @throws {RangeError} 範囲外の値の場合。負数の整形には {@link toKan} を使う
265
+ * @example
266
+ * judicH(12340005n) // => '1234万0005'
267
+ */
268
+ function judicH(num) {
269
+ return judic(num, "0", (s) => s.replace(/[0-9]/g, (d) => String.fromCharCode(d.charCodeAt(0) + 65248)));
270
+ }
271
+ //#endregion
272
+ //#region src/formatter/lawyer.ts
273
+ /**
274
+ * 行政・司法で用いられる方式で整形する(4桁ごとにアラビア数字+漢字単位、千の位にカンマ)。
275
+ *
276
+ * @param num 0 以上 `10^72 − 1` 以下の bigint
277
+ * @returns 整形後の文字列。`0n` は `'0'`
278
+ * @throws {RangeError} 範囲外の値の場合。負数の整形には {@link toKan} を使う
279
+ * @example
280
+ * lawyer(12340005n) // => '1,234万5'
281
+ * lawyer(10003n) // => '1万3'
282
+ */
283
+ function lawyer(num) {
284
+ if (num === 0n) return "0";
285
+ let ret = "";
286
+ for (const [i4, unit4] of groups4(num)) {
287
+ if (i4 === 0) continue;
288
+ const s = String(i4);
289
+ ret += (i4 >= 1e3 ? `${s[0]},${s.slice(1)}` : s) + unit4;
290
+ }
291
+ return ret;
292
+ }
293
+ //#endregion
294
+ //#region src/formatter/simple.ts
295
+ const POWERS_10 = [
296
+ 1,
297
+ 10,
298
+ 100,
299
+ 1e3
300
+ ];
301
+ /**
302
+ * 標準的な漢数字へ整形する(位ごとに漢数字の単位を使う)。
303
+ *
304
+ * @param num 0 以上 `10^72 − 1` 以下の bigint
305
+ * @returns 漢数字文字列。`0n` は `'零'`
306
+ * @throws {RangeError} 範囲外の値の場合。負数の整形には {@link toKan} を使う
307
+ * @example
308
+ * simple(12345n) // => '一万二千三百四十五'
309
+ * simple(10003n) // => '一万三'
310
+ */
311
+ function simple(num) {
312
+ if (num === 0n) return "零";
313
+ let ret = "";
314
+ for (const [i4, unit4] of groups4(num)) {
315
+ if (i4 === 0) continue;
316
+ for (let j = UNIT_EXP3.length; j >= 0; j--) {
317
+ const unit3 = UNIT_EXP3[j - 1] ?? "";
318
+ const i3 = Math.floor(i4 / (POWERS_10[j] ?? 1)) % 10;
319
+ if (i3 === 0) continue;
320
+ ret += i3 === 1 && unit3 !== "" ? unit3 : KAN_DIGITS.charAt(i3) + unit3;
321
+ }
322
+ ret += unit4;
323
+ }
324
+ return ret;
325
+ }
326
+ //#endregion
327
+ //#region src/formatters.ts
328
+ const formatters = /* @__PURE__ */ new Map([
329
+ ["simple", simple],
330
+ ["gov", gov],
331
+ ["lawyer", lawyer],
332
+ ["judic_v", judicV],
333
+ ["judic_h", judicH]
334
+ ]);
335
+ /**
336
+ * フォーマッタを名前付きで登録する。既存の同名フォーマッタ(ビルトイン含む)は警告なく
337
+ * 上書きされる。
338
+ *
339
+ * レジストリは ESM 版と CJS 版で共有されない。登録側と利用側は同じモジュール形式
340
+ * (`import` または `require`) で `ya-kansuji` を読み込むこと。
341
+ *
342
+ * @param name フォーマッタ名。{@link toKan} の第2引数に渡して選択する
343
+ * @param formatter `(num: bigint, options?) => string`。`num` は常に 0 以上
344
+ * @throws {TypeError} `formatter` が関数でない場合
345
+ */
346
+ function registerFormatter(name, formatter) {
347
+ if (typeof formatter !== "function") throw new TypeError("Registering invalid formatter.");
348
+ formatters.set(name, formatter);
349
+ }
350
+ /**
351
+ * 登録済みフォーマッタを名前で取得する。未登録なら `undefined`。
352
+ *
353
+ * @param name フォーマッタ名
354
+ * @returns フォーマッタ関数、なければ `undefined`
355
+ */
356
+ function getFormatter(name) {
357
+ return formatters.get(name);
358
+ }
359
+ /**
360
+ * 数値を漢数字・漢字混じりの文字列へ整形する。
361
+ *
362
+ * 負数は先頭に「マイナス」を付け、絶対値をフォーマッタに渡す(フォーマッタが受け取るのは
363
+ * 常に非負値)。`number` は安全整数でなければならず、より大きな値は `bigint` で渡すこと。
364
+ * ビルトインフォーマッタが表現できるのは絶対値 `10^72 − 1` まで。
365
+ *
366
+ * @param num 変換対象。`number`(安全整数)または `bigint`
367
+ * @param formatter フォーマッタ名、またはフォーマッタ関数。既定は `'simple'`
368
+ * @param options フォーマッタへ渡すオプション(ビルトインは未使用)
369
+ * @returns 整形後の文字列
370
+ * @throws {RangeError} `num` が非整数、安全整数を超える `number`、またはビルトイン
371
+ * フォーマッタの表現範囲を超える場合
372
+ * @throws {TypeError} 指定した名前のフォーマッタが見つからない場合
373
+ * @example
374
+ * toKan(12345) // => '一万二千三百四十五'
375
+ * toKan(12340005, 'gov') // => '1234万, 5'
376
+ * toKan(-1234) // => 'マイナス千二百三十四'
377
+ */
378
+ function toKan(num, formatter = "simple", options = {}) {
379
+ let value;
380
+ if (typeof num === "bigint") value = num;
381
+ else if (Number.isSafeInteger(num)) value = BigInt(num);
382
+ else throw new RangeError(`toKan requires a safe integer or bigint: ${num}`);
383
+ if (value < 0n) return `マイナス${toKan(-value, formatter, options)}`;
384
+ const fn = typeof formatter === "function" ? formatter : formatters.get(formatter);
385
+ if (!fn) throw new TypeError(`Unable to find formatter ${String(formatter)}`);
386
+ return fn(value, options);
387
+ }
388
+ //#endregion
389
+ export { UNIT_EXP3, UNIT_EXP4, getFormatter, gov, judicH, judicV, lawyer, registerFormatter, simple, toBigInt, toKan, toNumber };
package/package.json ADDED
@@ -0,0 +1,69 @@
1
+ {
2
+ "name": "ya-kansuji",
3
+ "version": "0.1.0",
4
+ "description": "Yet another Japanese kansuji library — converts Japanese numerals (漢数字・大字・全角) to/from numbers",
5
+ "keywords": [
6
+ "kansuji",
7
+ "japanese",
8
+ "numeral",
9
+ "漢数字",
10
+ "大字"
11
+ ],
12
+ "license": "MIT",
13
+ "author": "Tatsuki Sugiura <sugi@nemui.org>",
14
+ "type": "module",
15
+ "main": "./dist/index.cjs",
16
+ "module": "./dist/index.js",
17
+ "types": "./dist/index.d.ts",
18
+ "unpkg": "./dist/index.iife.min.js",
19
+ "jsdelivr": "./dist/index.iife.min.js",
20
+ "exports": {
21
+ ".": {
22
+ "import": {
23
+ "types": "./dist/index.d.ts",
24
+ "default": "./dist/index.js"
25
+ },
26
+ "require": {
27
+ "types": "./dist/index.d.cts",
28
+ "default": "./dist/index.cjs"
29
+ }
30
+ }
31
+ },
32
+ "files": [
33
+ "dist",
34
+ "src",
35
+ "README.md",
36
+ "CHANGELOG.md",
37
+ "LICENSE"
38
+ ],
39
+ "sideEffects": false,
40
+ "engines": {
41
+ "node": ">=16"
42
+ },
43
+ "scripts": {
44
+ "build": "tsdown",
45
+ "pretest": "tsdown",
46
+ "test": "vitest run",
47
+ "typecheck": "tsc --noEmit",
48
+ "prepublishOnly": "npm test && npx publint && npx @arethetypeswrong/cli --pack .",
49
+ "test:coverage": "vitest run --coverage"
50
+ },
51
+ "devDependencies": {
52
+ "@arethetypeswrong/cli": "^0.18.5",
53
+ "@types/node": "^26.1.1",
54
+ "@vitest/coverage-v8": "^4.1.10",
55
+ "publint": "^0.3.21",
56
+ "tsdown": "^0.22.7",
57
+ "typescript": "^7.0.2",
58
+ "unrun": "^0.3.1",
59
+ "vitest": "^4.1.10"
60
+ },
61
+ "repository": {
62
+ "type": "git",
63
+ "url": "git+https://github.com/sugi/ya-kansuji-js.git"
64
+ },
65
+ "bugs": {
66
+ "url": "https://github.com/sugi/ya-kansuji-js/issues"
67
+ },
68
+ "homepage": "https://github.com/sugi/ya-kansuji-js#readme"
69
+ }
@@ -0,0 +1,16 @@
1
+ /** 10^1〜10^3 の位を表す漢数字の単位(十・百・千)。 */
2
+ export const UNIT_EXP3 = ['十', '百', '千'] as const
3
+
4
+ /** 10^4 ごとに繰り上がる漢数字の単位(万・億・兆…無量大数)。 */
5
+ export const UNIT_EXP4 = [
6
+ '万', '億', '兆', '京', '垓', '𥝱', '穣', '溝', '澗',
7
+ '正', '載', '極', '恒河沙', '阿僧祇', '那由他', '不可思議', '無量大数',
8
+ ] as const
9
+
10
+ export const KAN_DIGITS = '〇一二三四五六七八九'
11
+
12
+ // Ruby 版の tr 用ペア文字列と同一。コードポイント単位で位置対応している。
13
+ export const NUM_ALT_CHARS =
14
+ `${KAN_DIGITS}0123456789零壱壹弌弐貳貮参參弎肆伍陸漆質柒捌玖拾什陌佰阡仟萬秭`
15
+ export const NUM_NORMALIZED_CHARS =
16
+ '01234567890123456789011122233345677789十十百百千千万𥝱'
@@ -0,0 +1,21 @@
1
+ import { groups4 } from '../groups.js'
2
+
3
+ /**
4
+ * 「公用文作成の要領」方式で整形する(4桁ごとにアラビア数字+漢字単位、読点区切り)。
5
+ *
6
+ * @param num 0 以上 `10^72 − 1` 以下の bigint
7
+ * @returns 整形後の文字列。`0n` は `'0'`
8
+ * @throws {RangeError} 範囲外の値の場合。負数の整形には {@link toKan} を使う
9
+ * @example
10
+ * gov(12340005n) // => '1234万, 5'
11
+ * gov(10003n) // => '1万, 3'
12
+ */
13
+ export function gov(num: bigint): string {
14
+ if (num === 0n) return '0'
15
+ const parts: string[] = []
16
+ for (const [i4, unit4] of groups4(num)) {
17
+ if (i4 === 0) continue
18
+ parts.push(`${i4}${unit4}`)
19
+ }
20
+ return parts.join(', ')
21
+ }
@@ -0,0 +1,42 @@
1
+ import { KAN_DIGITS } from '../constants.js'
2
+ import { groups4 } from '../groups.js'
3
+
4
+ function judic(num: bigint, zero: string, mapDigits: (s: string) => string): string {
5
+ if (num === 0n) return zero
6
+ let ret = ''
7
+ let head = true
8
+ for (const [i4, unit4] of groups4(num)) {
9
+ if (i4 === 0) continue
10
+ ret += (head ? String(i4) : String(i4).padStart(4, '0')) + unit4
11
+ head = false
12
+ }
13
+ return mapDigits(ret)
14
+ }
15
+
16
+ /**
17
+ * 裁判判例の縦書き方式で整形する(4桁ごとに漢数字、下位グループはゼロ埋め、単位は漢字)。
18
+ *
19
+ * @param num 0 以上 `10^72 − 1` 以下の bigint
20
+ * @returns 整形後の文字列。`0n` は `'〇'`
21
+ * @throws {RangeError} 範囲外の値の場合。負数の整形には {@link toKan} を使う
22
+ * @example
23
+ * judicV(12340005n) // => '一二三四万〇〇〇五'
24
+ */
25
+ export function judicV(num: bigint): string {
26
+ return judic(num, '〇', (s) => s.replace(/[0-9]/g, (d) => KAN_DIGITS.charAt(Number(d))))
27
+ }
28
+
29
+ /**
30
+ * 最高裁判例の横書き方式で整形する(4桁ごとに全角数字、下位グループはゼロ埋め)。
31
+ *
32
+ * @param num 0 以上 `10^72 − 1` 以下の bigint
33
+ * @returns 整形後の文字列。`0n` は `'0'`
34
+ * @throws {RangeError} 範囲外の値の場合。負数の整形には {@link toKan} を使う
35
+ * @example
36
+ * judicH(12340005n) // => '1234万0005'
37
+ */
38
+ export function judicH(num: bigint): string {
39
+ return judic(num, '0', (s) =>
40
+ s.replace(/[0-9]/g, (d) => String.fromCharCode(d.charCodeAt(0) + 0xfee0)),
41
+ )
42
+ }
@@ -0,0 +1,22 @@
1
+ import { groups4 } from '../groups.js'
2
+
3
+ /**
4
+ * 行政・司法で用いられる方式で整形する(4桁ごとにアラビア数字+漢字単位、千の位にカンマ)。
5
+ *
6
+ * @param num 0 以上 `10^72 − 1` 以下の bigint
7
+ * @returns 整形後の文字列。`0n` は `'0'`
8
+ * @throws {RangeError} 範囲外の値の場合。負数の整形には {@link toKan} を使う
9
+ * @example
10
+ * lawyer(12340005n) // => '1,234万5'
11
+ * lawyer(10003n) // => '1万3'
12
+ */
13
+ export function lawyer(num: bigint): string {
14
+ if (num === 0n) return '0'
15
+ let ret = ''
16
+ for (const [i4, unit4] of groups4(num)) {
17
+ if (i4 === 0) continue
18
+ const s = String(i4)
19
+ ret += (i4 >= 1000 ? `${s[0]},${s.slice(1)}` : s) + unit4
20
+ }
21
+ return ret
22
+ }
@@ -0,0 +1,30 @@
1
+ import { KAN_DIGITS, UNIT_EXP3 } from '../constants.js'
2
+ import { groups4 } from '../groups.js'
3
+
4
+ const POWERS_10 = [1, 10, 100, 1000] as const
5
+
6
+ /**
7
+ * 標準的な漢数字へ整形する(位ごとに漢数字の単位を使う)。
8
+ *
9
+ * @param num 0 以上 `10^72 − 1` 以下の bigint
10
+ * @returns 漢数字文字列。`0n` は `'零'`
11
+ * @throws {RangeError} 範囲外の値の場合。負数の整形には {@link toKan} を使う
12
+ * @example
13
+ * simple(12345n) // => '一万二千三百四十五'
14
+ * simple(10003n) // => '一万三'
15
+ */
16
+ export function simple(num: bigint): string {
17
+ if (num === 0n) return '零'
18
+ let ret = ''
19
+ for (const [i4, unit4] of groups4(num)) {
20
+ if (i4 === 0) continue
21
+ for (let j: number = UNIT_EXP3.length; j >= 0; j--) {
22
+ const unit3 = UNIT_EXP3[j - 1] ?? ''
23
+ const i3 = Math.floor(i4 / (POWERS_10[j] ?? 1)) % 10
24
+ if (i3 === 0) continue
25
+ ret += i3 === 1 && unit3 !== '' ? unit3 : KAN_DIGITS.charAt(i3) + unit3
26
+ }
27
+ ret += unit4
28
+ }
29
+ return ret
30
+ }
@@ -0,0 +1,88 @@
1
+ import { gov } from './formatter/gov.js'
2
+ import { judicH, judicV } from './formatter/judic.js'
3
+ import { lawyer } from './formatter/lawyer.js'
4
+ import { simple } from './formatter/simple.js'
5
+
6
+ /** フォーマッタに渡す任意のオプション。ビルトインフォーマッタは参照しない。 */
7
+ export type KansujiFormatterOptions = Record<string, unknown>
8
+
9
+ /**
10
+ * 非負の bigint を文字列へ整形する関数。{@link toKan} は負数の符号処理を済ませたうえで
11
+ * 常に 0 以上の値を渡す。
12
+ */
13
+ export type KansujiFormatter = (num: bigint, options?: KansujiFormatterOptions) => string
14
+
15
+ const formatters = new Map<string, KansujiFormatter>([
16
+ ['simple', simple],
17
+ ['gov', gov],
18
+ ['lawyer', lawyer],
19
+ ['judic_v', judicV],
20
+ ['judic_h', judicH],
21
+ ])
22
+
23
+ /**
24
+ * フォーマッタを名前付きで登録する。既存の同名フォーマッタ(ビルトイン含む)は警告なく
25
+ * 上書きされる。
26
+ *
27
+ * レジストリは ESM 版と CJS 版で共有されない。登録側と利用側は同じモジュール形式
28
+ * (`import` または `require`) で `ya-kansuji` を読み込むこと。
29
+ *
30
+ * @param name フォーマッタ名。{@link toKan} の第2引数に渡して選択する
31
+ * @param formatter `(num: bigint, options?) => string`。`num` は常に 0 以上
32
+ * @throws {TypeError} `formatter` が関数でない場合
33
+ */
34
+ export function registerFormatter(name: string, formatter: KansujiFormatter): void {
35
+ if (typeof formatter !== 'function') {
36
+ throw new TypeError('Registering invalid formatter.')
37
+ }
38
+ formatters.set(name, formatter)
39
+ }
40
+
41
+ /**
42
+ * 登録済みフォーマッタを名前で取得する。未登録なら `undefined`。
43
+ *
44
+ * @param name フォーマッタ名
45
+ * @returns フォーマッタ関数、なければ `undefined`
46
+ */
47
+ export function getFormatter(name: string): KansujiFormatter | undefined {
48
+ return formatters.get(name)
49
+ }
50
+
51
+ /**
52
+ * 数値を漢数字・漢字混じりの文字列へ整形する。
53
+ *
54
+ * 負数は先頭に「マイナス」を付け、絶対値をフォーマッタに渡す(フォーマッタが受け取るのは
55
+ * 常に非負値)。`number` は安全整数でなければならず、より大きな値は `bigint` で渡すこと。
56
+ * ビルトインフォーマッタが表現できるのは絶対値 `10^72 − 1` まで。
57
+ *
58
+ * @param num 変換対象。`number`(安全整数)または `bigint`
59
+ * @param formatter フォーマッタ名、またはフォーマッタ関数。既定は `'simple'`
60
+ * @param options フォーマッタへ渡すオプション(ビルトインは未使用)
61
+ * @returns 整形後の文字列
62
+ * @throws {RangeError} `num` が非整数、安全整数を超える `number`、またはビルトイン
63
+ * フォーマッタの表現範囲を超える場合
64
+ * @throws {TypeError} 指定した名前のフォーマッタが見つからない場合
65
+ * @example
66
+ * toKan(12345) // => '一万二千三百四十五'
67
+ * toKan(12340005, 'gov') // => '1234万, 5'
68
+ * toKan(-1234) // => 'マイナス千二百三十四'
69
+ */
70
+ export function toKan(
71
+ num: number | bigint,
72
+ formatter: string | KansujiFormatter = 'simple',
73
+ options: KansujiFormatterOptions = {},
74
+ ): string {
75
+ let value: bigint
76
+ if (typeof num === 'bigint') {
77
+ value = num
78
+ } else if (Number.isSafeInteger(num)) {
79
+ value = BigInt(num)
80
+ } else {
81
+ throw new RangeError(`toKan requires a safe integer or bigint: ${num}`)
82
+ }
83
+ if (value < 0n) return `マイナス${toKan(-value, formatter, options)}`
84
+
85
+ const fn = typeof formatter === 'function' ? formatter : formatters.get(formatter)
86
+ if (!fn) throw new TypeError(`Unable to find formatter ${String(formatter)}`)
87
+ return fn(value, options)
88
+ }