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 +19 -0
- package/LICENSE +21 -0
- package/README.md +169 -0
- package/dist/index.cjs +401 -0
- package/dist/index.d.cts +148 -0
- package/dist/index.d.ts +148 -0
- package/dist/index.iife.min.js +3 -0
- package/dist/index.js +389 -0
- package/package.json +69 -0
- package/src/constants.ts +16 -0
- package/src/formatter/gov.ts +21 -0
- package/src/formatter/judic.ts +42 -0
- package/src/formatter/lawyer.ts +22 -0
- package/src/formatter/simple.ts +30 -0
- package/src/formatters.ts +88 -0
- package/src/groups.ts +31 -0
- package/src/index.ts +13 -0
- package/src/parse.ts +136 -0
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
|
+
}
|
package/src/constants.ts
ADDED
|
@@ -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
|
+
}
|