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/src/groups.ts ADDED
@@ -0,0 +1,31 @@
1
+ import { UNIT_EXP4 } from './constants.js'
2
+
3
+ const GROUP_BASE = 10_000n
4
+ const GROUP_UNITS = ['', ...UNIT_EXP4] as const
5
+ const GROUP_COUNT = GROUP_UNITS.length
6
+ const MAX_EXCLUSIVE = GROUP_BASE ** BigInt(GROUP_COUNT)
7
+ const MAX_DIGITS = GROUP_COUNT * 4
8
+
9
+ // 有効な最上位から4桁ごとのグループを返す。途中のゼログループも含む。
10
+ export function groups4(num: bigint): Array<[number, string]> {
11
+ if (num < 0n || num >= MAX_EXCLUSIVE) {
12
+ throw new RangeError(`Kansuji formatters require a bigint between 0 and 10^${MAX_DIGITS} - 1`)
13
+ }
14
+
15
+ const ret: Array<[number, string]> = []
16
+ let value = num
17
+ let unitIndex = 0
18
+ do {
19
+ const unit = GROUP_UNITS[unitIndex]
20
+ if (unit === undefined) {
21
+ throw new RangeError(
22
+ `Kansuji formatters require a bigint between 0 and 10^${MAX_DIGITS} - 1`,
23
+ )
24
+ }
25
+ ret.push([Number(value % GROUP_BASE), unit])
26
+ value /= GROUP_BASE
27
+ unitIndex++
28
+ } while (value > 0n)
29
+
30
+ return ret.reverse()
31
+ }
package/src/index.ts ADDED
@@ -0,0 +1,13 @@
1
+ export { toBigInt, toNumber } from './parse.js'
2
+ export { UNIT_EXP3, UNIT_EXP4 } from './constants.js'
3
+ export {
4
+ getFormatter,
5
+ registerFormatter,
6
+ toKan,
7
+ type KansujiFormatter,
8
+ type KansujiFormatterOptions,
9
+ } from './formatters.js'
10
+ export { simple } from './formatter/simple.js'
11
+ export { gov } from './formatter/gov.js'
12
+ export { lawyer } from './formatter/lawyer.js'
13
+ export { judicH, judicV } from './formatter/judic.js'
package/src/parse.ts ADDED
@@ -0,0 +1,136 @@
1
+ import { NUM_ALT_CHARS, NUM_NORMALIZED_CHARS, UNIT_EXP3, UNIT_EXP4 } from './constants.js'
2
+
3
+ const altChars = Array.from(NUM_ALT_CHARS)
4
+ const normChars = Array.from(NUM_NORMALIZED_CHARS)
5
+ const NORMALIZE_MAP = new Map(altChars.map((c, i) => [c, normChars[i] as string]))
6
+ const SEPARATORS = new Set([
7
+ ',', ',', '、',
8
+ '\u0009', '\u000A', '\u000B', '\u000C', '\u000D', '\u0020', '\u00A0', '\u1680',
9
+ '\u2000', '\u2001', '\u2002', '\u2003', '\u2004', '\u2005', '\u2006', '\u2007',
10
+ '\u2008', '\u2009', '\u200A', '\u2028', '\u2029', '\u202F', '\u205F', '\u3000',
11
+ '\uFEFF',
12
+ ])
13
+ const DIGIT_VALUES = [0n, 1n, 2n, 3n, 4n, 5n, 6n, 7n, 8n, 9n] as const
14
+ const SPECIAL_NUMBER_VALUES = new Map<string, bigint>([
15
+ ['卄', 20n],
16
+ ['廿', 20n],
17
+ ['卅', 30n],
18
+ ['丗', 30n],
19
+ ['卌', 40n],
20
+ ['皕', 200n],
21
+ ])
22
+
23
+ const SINGLE_UNITS = [...UNIT_EXP3, ...UNIT_EXP4.filter((u) => Array.from(u).length === 1)]
24
+ const MULTI_UNITS = UNIT_EXP4.filter((u) => Array.from(u).length > 1)
25
+ const CHAR_CLASS = [
26
+ ...new Set([...altChars, ...normChars, ...SINGLE_UNITS, ...SPECIAL_NUMBER_VALUES.keys()]),
27
+ ].join('')
28
+
29
+ const PART_SOURCE = `${MULTI_UNITS.join('|')}|[${CHAR_CLASS}]`
30
+ const KANSUJI_REGEXP = new RegExp(`(?:マイナス)?(?:${PART_SOURCE})+`, 'u')
31
+ const PART_REGEXP = new RegExp(PART_SOURCE, 'gu')
32
+
33
+ function createUnitValues(units: readonly string[], base: bigint): Map<string, bigint> {
34
+ let value = base
35
+ return new Map(units.map((unit) => {
36
+ const entry: [string, bigint] = [unit, value]
37
+ value *= base
38
+ return entry
39
+ }))
40
+ }
41
+
42
+ const UNIT_EXP3_VALUES = createUnitValues(UNIT_EXP3, 10n)
43
+ const UNIT_EXP4_VALUES = createUnitValues(UNIT_EXP4, 10_000n)
44
+ const MAX_SAFE_BIGINT = BigInt(Number.MAX_SAFE_INTEGER)
45
+
46
+ function clean(str: string): string {
47
+ let result = ''
48
+ for (const c of str) {
49
+ const normalized = NORMALIZE_MAP.get(c) ?? c
50
+ if (!SEPARATORS.has(normalized)) result += normalized
51
+ }
52
+ return result
53
+ }
54
+
55
+ /**
56
+ * 漢数字・大字・全角数字が混じったテキストを bigint に変換する。
57
+ *
58
+ * 照合前にカンマ・読点・各種空白などの区切り文字を除去し、先頭から連続してマッチする
59
+ * 数値表現だけを読み取る(例: `toBigInt('五x六')` は `5n`)。マッチしなければ `0n` を返す。
60
+ * 先頭の「マイナス」は負号として扱うが、ASCII の `-` や U+2212(−)は負号として扱わない。
61
+ * 値の上限はなく、無量大数(10^68)を超える大きさもそのまま扱える。
62
+ *
63
+ * @param str 変換対象の文字列
64
+ * @returns テキストが表す整数値
65
+ * @example
66
+ * toBigInt('一〇二四') // => 1024n
67
+ * toBigInt('マイナス千二百') // => -1200n
68
+ * toBigInt('一無量大数') // => 10n ** 68n
69
+ */
70
+ export function toBigInt(str: string): bigint {
71
+ const cleaned = clean(String(str))
72
+ const matched = KANSUJI_REGEXP.exec(cleaned)
73
+ if (!matched) return 0n
74
+ const matchedText = matched[0]
75
+ if (matchedText === undefined) return 0n
76
+
77
+ let ret3 = 0n
78
+ let ret4 = 0n
79
+ let curnum: bigint | null = null
80
+ for (const c of matchedText.match(PART_REGEXP) ?? []) {
81
+ if (c >= '1' && c <= '9') {
82
+ const digit = DIGIT_VALUES[c.charCodeAt(0) - 48]!
83
+ curnum = (curnum ?? 0n) * 10n + digit
84
+ } else if (c === '0') {
85
+ if (curnum !== null) curnum *= 10n
86
+ } else {
87
+ const specialNumber = SPECIAL_NUMBER_VALUES.get(c)
88
+ if (specialNumber !== undefined) {
89
+ ret3 += specialNumber
90
+ curnum = null
91
+ } else {
92
+ const unit4 = UNIT_EXP4_VALUES.get(c)
93
+ if (unit4 !== undefined) {
94
+ if (curnum !== null) {
95
+ ret3 += curnum
96
+ curnum = null
97
+ }
98
+ if (ret3 === 0n) ret3 = 1n
99
+ ret4 += ret3 * unit4
100
+ ret3 = 0n
101
+ } else {
102
+ const unit3 = UNIT_EXP3_VALUES.get(c)
103
+ if (unit3 !== undefined) {
104
+ curnum ??= 1n
105
+ ret3 += curnum * unit3
106
+ curnum = null
107
+ }
108
+ }
109
+ }
110
+ }
111
+ }
112
+ if (curnum !== null) ret3 += curnum
113
+ const ret = ret4 + ret3
114
+ return matchedText.startsWith('マイナス') ? -ret : ret
115
+ }
116
+
117
+ /**
118
+ * {@link toBigInt} と同じ規則でテキストを解釈し、`number` として返す。
119
+ *
120
+ * 結果の絶対値が `Number.MAX_SAFE_INTEGER`(2^53 − 1)を超える場合は `RangeError` を
121
+ * 投げる。安全整数を超えうる値を扱うときは {@link toBigInt} を使うこと。
122
+ *
123
+ * @param str 変換対象の文字列
124
+ * @returns テキストが表す数値
125
+ * @throws {RangeError} 値が安全整数の範囲を超える場合
126
+ * @example
127
+ * toNumber('一〇二四') // => 1024
128
+ * toNumber('一無量大数') // throws RangeError
129
+ */
130
+ export function toNumber(str: string): number {
131
+ const value = toBigInt(str)
132
+ if (value > MAX_SAFE_BIGINT || value < -MAX_SAFE_BIGINT) {
133
+ throw new RangeError(`kansuji value exceeds Number.MAX_SAFE_INTEGER: ${str}`)
134
+ }
135
+ return Number(value)
136
+ }