cjk-number 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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Tse-Wei Chen
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 all
13
+ 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 THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,124 @@
1
+ # cjk-number
2
+
3
+ Convert numbers to and from CJK numeral systems, including traditional/simplified formats, heavenly stems, earthly branches, decimals, negatives, and very large BigInt values.
4
+
5
+ ## Install
6
+
7
+ ```sh
8
+ npm install cjk-number
9
+ ```
10
+
11
+ ## Quick Start
12
+
13
+ ```js
14
+ import {
15
+ cjkIdeographic,
16
+ cjkHeavenlyStem,
17
+ cjkEarthlyBranch,
18
+ tradChineseFormal,
19
+ tradChineseInformal,
20
+ simpChineseInformal,
21
+ simpChineseFormal,
22
+ tradFormalPositionalUnits,
23
+ integer
24
+ } from "cjk-number";
25
+
26
+ integer.parseInt("一千零二十三"); // 1023
27
+ integer.parseInt("壹仟零貳拾參"); // 1023
28
+ integer.parseInt("癸"); // 10
29
+ integer.parseInt("亥"); // 12
30
+
31
+ tradChineseFormal.parse(1023); // "壹仟零貳拾參"
32
+ simpChineseFormal.parse(1023); // "壹仟零贰拾叁"
33
+ cjkHeavenlyStem.parse(10); // "癸"
34
+ cjkEarthlyBranch.parse(12); // "亥"
35
+
36
+ tradFormalPositionalUnits[68]; // "無量大數"
37
+ ```
38
+
39
+ ## API
40
+
41
+ ### integer.parseInt(input, options?)
42
+
43
+ Parses CJK string to number or bigint.
44
+
45
+ Options:
46
+
47
+ - strict?: boolean
48
+ - preferBigInt?: boolean
49
+ - heavenlyStemMode?: "fixed" | "cyclic"
50
+ - earthlyBranchMode?: "fixed" | "cyclic"
51
+
52
+ Examples:
53
+
54
+ ```js
55
+ integer.parseInt("負一百零二"); // -102
56
+ integer.parseInt("一點二三"); // 1.23
57
+ integer.parseInt("九千零七兆一", { preferBigInt: true }); // 9007000000000001n
58
+ integer.parseInt("一無量大數", { preferBigInt: true }); // 10n ** 68n
59
+ ```
60
+
61
+ ### Formatter.parse(value)
62
+
63
+ Available formatters:
64
+
65
+ - cjkIdeographic
66
+ - tradChineseInformal
67
+ - tradChineseFormal
68
+ - simpChineseInformal
69
+ - simpChineseFormal
70
+ - cjkHeavenlyStem
71
+ - cjkEarthlyBranch
72
+
73
+ Examples:
74
+
75
+ ```js
76
+ tradChineseInformal.parse(-320); // "負三百二十"
77
+ simpChineseInformal.parse(12.34); // "十二點三四"
78
+ tradChineseFormal.parse(10n ** 68n); // "壹無量大數"
79
+ simpChineseFormal.parse(10n ** 64n); // "壹不可思议"
80
+
81
+ cjkHeavenlyStem.parse(11, { mode: "cyclic" }); // "甲"
82
+ cjkEarthlyBranch.parse(13, { mode: "cyclic" }); // "子"
83
+ ```
84
+
85
+ ## Supported Large Units
86
+
87
+ Traditional:
88
+
89
+ 萬, 億, 兆, 京, 垓, 秭, 穰, 溝, 澗, 正, 載, 極, 恆河沙, 阿僧祇, 那由他, 不可思議, 無量大數
90
+
91
+ Simplified:
92
+
93
+ 万, 亿, 兆, 京, 垓, 秭, 穰, 沟, 涧, 正, 载, 极, 恒河沙, 阿僧祇, 那由他, 不可思议, 无量大数
94
+
95
+ ## Development
96
+
97
+ ```sh
98
+ npm install
99
+ npm run typecheck
100
+ npm test
101
+ npm run build
102
+ ```
103
+
104
+ Property-style test environment variables:
105
+
106
+ - CJK_TEST_SEED (default: 20260327)
107
+ - CJK_TEST_SAMPLES (default: 100)
108
+ - CJK_TEST_MAX_DIGITS (default: 69)
109
+
110
+ Example:
111
+
112
+ ```sh
113
+ CJK_TEST_SEED=42 CJK_TEST_SAMPLES=1000 CJK_TEST_MAX_DIGITS=69 npm test
114
+ ```
115
+
116
+ ## Notes
117
+
118
+ - Decimal parsing currently returns number.
119
+ - Very large integers can return bigint when preferBigInt is enabled.
120
+ - Heavenly stem and earthly branch parsing is auto-detected by symbol.
121
+
122
+ ## License
123
+
124
+ MIT
@@ -0,0 +1,42 @@
1
+ export type CyclicMode = "fixed" | "cyclic";
2
+ export interface SystemParseOptions {
3
+ mode?: CyclicMode;
4
+ }
5
+ export interface IntegerParseOptions {
6
+ strict?: boolean;
7
+ preferBigInt?: boolean;
8
+ heavenlyStemMode?: CyclicMode;
9
+ earthlyBranchMode?: CyclicMode;
10
+ }
11
+ type IntegerLike = number | bigint;
12
+ export declare const tradFormalPositionalUnits: string[];
13
+ export declare const integer: {
14
+ parseInt(input: string, options?: IntegerParseOptions): number | bigint;
15
+ };
16
+ export declare const cjkIdeographic: {
17
+ parse(value: IntegerLike): string;
18
+ };
19
+ export declare const tradChineseInformal: {
20
+ parse(value: IntegerLike): string;
21
+ };
22
+ export declare const tradChineseFormal: {
23
+ parse(value: IntegerLike): string;
24
+ };
25
+ export declare const simpChineseInformal: {
26
+ parse(value: IntegerLike): string;
27
+ };
28
+ export declare const simpChineseFormal: {
29
+ parse(value: IntegerLike): string;
30
+ };
31
+ export declare const cjkHeavenlyStem: {
32
+ parse(value: IntegerLike, options?: SystemParseOptions): string;
33
+ };
34
+ export declare const cjkEarthlyBranch: {
35
+ parse(value: IntegerLike, options?: SystemParseOptions): string;
36
+ };
37
+ export declare const systems: {
38
+ heavenlyStem: readonly ["甲", "乙", "丙", "丁", "戊", "己", "庚", "辛", "壬", "癸"];
39
+ earthlyBranch: readonly ["子", "丑", "寅", "卯", "辰", "巳", "午", "未", "申", "酉", "戌", "亥"];
40
+ };
41
+ export {};
42
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,UAAU,GAAG,OAAO,GAAG,QAAQ,CAAC;AAE5C,MAAM,WAAW,kBAAkB;IACjC,IAAI,CAAC,EAAE,UAAU,CAAC;CACnB;AAED,MAAM,WAAW,mBAAmB;IAClC,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,gBAAgB,CAAC,EAAE,UAAU,CAAC;IAC9B,iBAAiB,CAAC,EAAE,UAAU,CAAC;CAChC;AAED,KAAK,WAAW,GAAG,MAAM,GAAG,MAAM,CAAC;AAwGnC,eAAO,MAAM,yBAAyB,UAA6C,CAAC;AAqXpF,eAAO,MAAM,OAAO;oBACF,MAAM,YAAY,mBAAmB,GAAG,MAAM,GAAG,MAAM;CAGxE,CAAC;AAEF,eAAO,MAAM,cAAc;iBACZ,WAAW,GAAG,MAAM;CAMlC,CAAC;AAEF,eAAO,MAAM,mBAAmB;iBACjB,WAAW,GAAG,MAAM;CAMlC,CAAC;AAEF,eAAO,MAAM,iBAAiB;iBACf,WAAW,GAAG,MAAM;CAMlC,CAAC;AAEF,eAAO,MAAM,mBAAmB;iBACjB,WAAW,GAAG,MAAM;CAMlC,CAAC;AAEF,eAAO,MAAM,iBAAiB;iBACf,WAAW,GAAG,MAAM;CAMlC,CAAC;AAEF,eAAO,MAAM,eAAe;iBACb,WAAW,YAAW,kBAAkB,GAAQ,MAAM;CAGpE,CAAC;AAEF,eAAO,MAAM,gBAAgB;iBACd,WAAW,YAAW,kBAAkB,GAAQ,MAAM;CAGpE,CAAC;AAEF,eAAO,MAAM,OAAO;;;CAGnB,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,444 @@
1
+ const STEMS = ["甲", "乙", "丙", "丁", "戊", "己", "庚", "辛", "壬", "癸"];
2
+ const BRANCHES = ["子", "丑", "寅", "卯", "辰", "巳", "午", "未", "申", "酉", "戌", "亥"];
3
+ const CANONICAL_DIGITS = {
4
+ "零": 0,
5
+ "〇": 0,
6
+ "○": 0,
7
+ "一": 1,
8
+ "壹": 1,
9
+ "二": 2,
10
+ "貳": 2,
11
+ "贰": 2,
12
+ "兩": 2,
13
+ "两": 2,
14
+ "三": 3,
15
+ "參": 3,
16
+ "叁": 3,
17
+ "四": 4,
18
+ "肆": 4,
19
+ "五": 5,
20
+ "伍": 5,
21
+ "六": 6,
22
+ "陸": 6,
23
+ "陆": 6,
24
+ "七": 7,
25
+ "柒": 7,
26
+ "八": 8,
27
+ "捌": 8,
28
+ "九": 9,
29
+ "玖": 9
30
+ };
31
+ const SMALL_UNITS = {
32
+ 十: 10n,
33
+ 拾: 10n,
34
+ 百: 100n,
35
+ 佰: 100n,
36
+ 千: 1000n,
37
+ 仟: 1000n
38
+ };
39
+ const TRAD_BIG_UNITS = [
40
+ "萬",
41
+ "億",
42
+ "兆",
43
+ "京",
44
+ "垓",
45
+ "秭",
46
+ "穰",
47
+ "溝",
48
+ "澗",
49
+ "正",
50
+ "載",
51
+ "極",
52
+ "恆河沙",
53
+ "阿僧祇",
54
+ "那由他",
55
+ "不可思議",
56
+ "無量大數"
57
+ ];
58
+ const SIMP_BIG_UNITS = [
59
+ "万",
60
+ "亿",
61
+ "兆",
62
+ "京",
63
+ "垓",
64
+ "秭",
65
+ "穰",
66
+ "沟",
67
+ "涧",
68
+ "正",
69
+ "载",
70
+ "极",
71
+ "恒河沙",
72
+ "阿僧祇",
73
+ "那由他",
74
+ "不可思议",
75
+ "无量大数"
76
+ ];
77
+ function buildFormalPositionalUnits(bigUnits) {
78
+ const result = [""];
79
+ for (const unit of bigUnits) {
80
+ result.push("拾", "佰", "仟", unit);
81
+ }
82
+ return result;
83
+ }
84
+ function createBigUnitOrder(units) {
85
+ return units
86
+ .map((unit, index) => [unit, 10n ** BigInt((index + 1) * 4)])
87
+ .reverse();
88
+ }
89
+ export const tradFormalPositionalUnits = buildFormalPositionalUnits(TRAD_BIG_UNITS);
90
+ const TRAD_INFORMAL_SET = {
91
+ zero: "零",
92
+ digits: ["一", "二", "三", "四", "五", "六", "七", "八", "九"],
93
+ smallUnits: ["十", "百", "千"],
94
+ bigUnits: [...TRAD_BIG_UNITS]
95
+ };
96
+ const TRAD_FORMAL_SET = {
97
+ zero: "零",
98
+ digits: ["壹", "貳", "參", "肆", "伍", "陸", "柒", "捌", "玖"],
99
+ smallUnits: ["拾", "佰", "仟"],
100
+ bigUnits: [...TRAD_BIG_UNITS]
101
+ };
102
+ const SIMP_INFORMAL_SET = {
103
+ zero: "零",
104
+ digits: ["一", "二", "三", "四", "五", "六", "七", "八", "九"],
105
+ smallUnits: ["十", "百", "千"],
106
+ bigUnits: [...SIMP_BIG_UNITS]
107
+ };
108
+ const SIMP_FORMAL_SET = {
109
+ zero: "零",
110
+ digits: ["壹", "贰", "叁", "肆", "伍", "陆", "柒", "捌", "玖"],
111
+ smallUnits: ["拾", "佰", "仟"],
112
+ bigUnits: [...SIMP_BIG_UNITS]
113
+ };
114
+ const BIG_UNIT_ORDER = createBigUnitOrder(SIMP_BIG_UNITS);
115
+ function normalizeInput(raw) {
116
+ const trimmed = raw.trim();
117
+ if (!trimmed) {
118
+ throw new SyntaxError("Empty string is not a valid number");
119
+ }
120
+ return trimmed
121
+ .replace(/負|负/g, "-")
122
+ .replace(/點|点/g, ".")
123
+ .replace(/無量大數/g, "无量大数")
124
+ .replace(/不可思議/g, "不可思议")
125
+ .replace(/恆河沙/g, "恒河沙")
126
+ .replace(/載/g, "载")
127
+ .replace(/極/g, "极")
128
+ .replace(/溝/g, "沟")
129
+ .replace(/澗/g, "涧")
130
+ .replace(/億/g, "亿")
131
+ .replace(/萬/g, "万")
132
+ .replace(/拾/g, "十")
133
+ .replace(/佰/g, "百")
134
+ .replace(/仟/g, "千");
135
+ }
136
+ function toBigInt(value) {
137
+ if (typeof value === "bigint") {
138
+ return value;
139
+ }
140
+ if (!Number.isFinite(value) || !Number.isInteger(value)) {
141
+ throw new RangeError("Expected an integer value");
142
+ }
143
+ return BigInt(value);
144
+ }
145
+ function parseDigitsOnly(input) {
146
+ let result = "";
147
+ for (const ch of input) {
148
+ const digit = CANONICAL_DIGITS[ch];
149
+ if (digit === undefined) {
150
+ throw new SyntaxError(`Unexpected character ${ch} in digit sequence`);
151
+ }
152
+ result += String(digit);
153
+ }
154
+ return BigInt(result || "0");
155
+ }
156
+ function parseSection(section) {
157
+ if (!section) {
158
+ return 0n;
159
+ }
160
+ if (/^[0-9]+$/.test(section)) {
161
+ return BigInt(section);
162
+ }
163
+ const hasSmallUnit = /十|百|千/.test(section);
164
+ if (!hasSmallUnit) {
165
+ return parseDigitsOnly(section);
166
+ }
167
+ let total = 0n;
168
+ let currentDigit = null;
169
+ for (const ch of section) {
170
+ if (ch in CANONICAL_DIGITS) {
171
+ currentDigit = BigInt(CANONICAL_DIGITS[ch]);
172
+ continue;
173
+ }
174
+ const unit = SMALL_UNITS[ch];
175
+ if (!unit) {
176
+ throw new SyntaxError(`Unexpected character ${ch} in section`);
177
+ }
178
+ const effectiveDigit = currentDigit ?? 1n;
179
+ total += effectiveDigit * unit;
180
+ currentDigit = null;
181
+ }
182
+ if (currentDigit !== null) {
183
+ total += currentDigit;
184
+ }
185
+ return total;
186
+ }
187
+ function parseChineseInteger(raw) {
188
+ if (/^[0-9]+$/.test(raw)) {
189
+ return BigInt(raw);
190
+ }
191
+ let rest = raw;
192
+ let total = 0n;
193
+ for (const [unitChar, unitValue] of BIG_UNIT_ORDER) {
194
+ const index = rest.indexOf(unitChar);
195
+ if (index < 0) {
196
+ continue;
197
+ }
198
+ const left = rest.slice(0, index);
199
+ const right = rest.slice(index + unitChar.length);
200
+ const sectionValue = left ? parseSection(left) : 1n;
201
+ total += sectionValue * unitValue;
202
+ rest = right;
203
+ }
204
+ total += parseSection(rest);
205
+ return total;
206
+ }
207
+ function parseFractionDigits(raw) {
208
+ if (!raw) {
209
+ throw new SyntaxError("Fraction part cannot be empty");
210
+ }
211
+ let decimal = "";
212
+ for (const ch of raw) {
213
+ if (/[0-9]/.test(ch)) {
214
+ decimal += ch;
215
+ continue;
216
+ }
217
+ const digit = CANONICAL_DIGITS[ch];
218
+ if (digit === undefined) {
219
+ throw new SyntaxError(`Unexpected character ${ch} in fraction part`);
220
+ }
221
+ decimal += String(digit);
222
+ }
223
+ return Number(`0.${decimal}`);
224
+ }
225
+ function toBestNumeric(value, preferBigInt) {
226
+ if (preferBigInt) {
227
+ return value;
228
+ }
229
+ if (value > BigInt(Number.MAX_SAFE_INTEGER) || value < BigInt(Number.MIN_SAFE_INTEGER)) {
230
+ return value;
231
+ }
232
+ return Number(value);
233
+ }
234
+ function validateStrictCharacters(input) {
235
+ const allowed = /^[0-9零〇○一二三四五六七八九十百千萬万億亿兆京垓秭穰溝沟澗涧正載载極极恆恒河沙阿僧祇那由他不思議议可無无量大數数點点壹貳贰參叁肆伍陸陆柒捌玖兩两拾佰仟負负.-]+$/;
236
+ if (!allowed.test(input)) {
237
+ throw new SyntaxError("Input contains unsupported characters in strict mode");
238
+ }
239
+ }
240
+ function fromCycle(value, chars, mode) {
241
+ const asBigInt = toBigInt(value);
242
+ const length = BigInt(chars.length);
243
+ if (mode === "fixed") {
244
+ if (asBigInt < 1n || asBigInt > length) {
245
+ throw new RangeError(`Value must be in 1..${chars.length} when mode is fixed`);
246
+ }
247
+ return chars[Number(asBigInt - 1n)];
248
+ }
249
+ const normalized = ((asBigInt - 1n) % length + length) % length;
250
+ return chars[Number(normalized)];
251
+ }
252
+ function parseCycle(input, chars, mode) {
253
+ const index = chars.indexOf(input);
254
+ if (index < 0) {
255
+ throw new SyntaxError(`Unknown symbol ${input}`);
256
+ }
257
+ const value = index + 1;
258
+ if (mode === "fixed") {
259
+ return value;
260
+ }
261
+ return value;
262
+ }
263
+ function formatSection(section, set) {
264
+ if (section === 0) {
265
+ return "";
266
+ }
267
+ const values = [1000, 100, 10, 1];
268
+ const units = [set.smallUnits[2], set.smallUnits[1], set.smallUnits[0], ""];
269
+ let output = "";
270
+ let pendingZero = false;
271
+ for (let i = 0; i < values.length; i += 1) {
272
+ const value = values[i];
273
+ const digit = Math.floor(section / value) % 10;
274
+ if (digit === 0) {
275
+ if (output) {
276
+ pendingZero = true;
277
+ }
278
+ continue;
279
+ }
280
+ if (pendingZero) {
281
+ output += set.zero;
282
+ pendingZero = false;
283
+ }
284
+ const isTenPosition = value === 10;
285
+ const canDropOne = isTenPosition && digit === 1 && output.length === 0;
286
+ if (!canDropOne) {
287
+ output += set.digits[digit - 1];
288
+ }
289
+ output += units[i];
290
+ }
291
+ return output;
292
+ }
293
+ function formatChineseInteger(value, set) {
294
+ const raw = toBigInt(value);
295
+ if (raw === 0n) {
296
+ return set.zero;
297
+ }
298
+ const negative = raw < 0n;
299
+ let n = negative ? -raw : raw;
300
+ const sections = [];
301
+ while (n > 0n) {
302
+ sections.push(Number(n % 10000n));
303
+ n /= 10000n;
304
+ }
305
+ let output = "";
306
+ let pendingZero = false;
307
+ for (let i = sections.length - 1; i >= 0; i -= 1) {
308
+ const section = sections[i];
309
+ if (section === 0) {
310
+ if (output) {
311
+ pendingZero = true;
312
+ }
313
+ continue;
314
+ }
315
+ if (output && (pendingZero || section < 1000)) {
316
+ output += set.zero;
317
+ }
318
+ output += formatSection(section, set);
319
+ if (i > 0) {
320
+ const unit = set.bigUnits[i - 1];
321
+ if (!unit) {
322
+ throw new RangeError("Value exceeds supported big unit range");
323
+ }
324
+ output += unit;
325
+ }
326
+ pendingZero = false;
327
+ }
328
+ return negative ? `負${output}` : output;
329
+ }
330
+ function formatDecimal(value, set) {
331
+ if (!Number.isFinite(value)) {
332
+ throw new RangeError("Expected a finite number");
333
+ }
334
+ if (Number.isInteger(value)) {
335
+ return formatChineseInteger(value, set);
336
+ }
337
+ const negative = value < 0;
338
+ const source = String(Math.abs(value));
339
+ const [intPart, fracPart] = source.split(".");
340
+ if (!fracPart) {
341
+ return formatChineseInteger(value, set);
342
+ }
343
+ const intText = formatChineseInteger(BigInt(intPart), set);
344
+ let fracText = "";
345
+ for (const ch of fracPart) {
346
+ const digit = Number(ch);
347
+ fracText += digit === 0 ? set.zero : set.digits[digit - 1];
348
+ }
349
+ const withSign = `${intText}點${fracText}`;
350
+ return negative ? `負${withSign}` : withSign;
351
+ }
352
+ function parseValue(input, options = {}) {
353
+ const modeStem = options.heavenlyStemMode ?? "fixed";
354
+ const modeBranch = options.earthlyBranchMode ?? "fixed";
355
+ const stemIndex = STEMS.indexOf(input);
356
+ if (stemIndex >= 0) {
357
+ return parseCycle(input, STEMS, modeStem);
358
+ }
359
+ const branchIndex = BRANCHES.indexOf(input);
360
+ if (branchIndex >= 0) {
361
+ return parseCycle(input, BRANCHES, modeBranch);
362
+ }
363
+ const normalized = normalizeInput(input);
364
+ if (options.strict) {
365
+ validateStrictCharacters(normalized);
366
+ }
367
+ const negative = normalized.startsWith("-");
368
+ const body = negative ? normalized.slice(1) : normalized;
369
+ if (!body) {
370
+ throw new SyntaxError("Missing numeric content");
371
+ }
372
+ if (body.includes(".")) {
373
+ const [intRaw, fracRaw] = body.split(".");
374
+ const intValue = intRaw ? parseChineseInteger(intRaw) : 0n;
375
+ const fracValue = parseFractionDigits(fracRaw ?? "");
376
+ if (intValue > BigInt(Number.MAX_SAFE_INTEGER)) {
377
+ throw new RangeError("Decimal parse does not support integer part above MAX_SAFE_INTEGER");
378
+ }
379
+ const composed = Number(intValue) + fracValue;
380
+ return negative ? -composed : composed;
381
+ }
382
+ const parsed = parseChineseInteger(body);
383
+ const signed = negative ? -parsed : parsed;
384
+ return toBestNumeric(signed, options.preferBigInt ?? false);
385
+ }
386
+ export const integer = {
387
+ parseInt(input, options) {
388
+ return parseValue(input, options);
389
+ }
390
+ };
391
+ export const cjkIdeographic = {
392
+ parse(value) {
393
+ if (typeof value === "number" && !Number.isInteger(value)) {
394
+ return formatDecimal(value, TRAD_INFORMAL_SET);
395
+ }
396
+ return formatChineseInteger(value, TRAD_INFORMAL_SET);
397
+ }
398
+ };
399
+ export const tradChineseInformal = {
400
+ parse(value) {
401
+ if (typeof value === "number" && !Number.isInteger(value)) {
402
+ return formatDecimal(value, TRAD_INFORMAL_SET);
403
+ }
404
+ return formatChineseInteger(value, TRAD_INFORMAL_SET);
405
+ }
406
+ };
407
+ export const tradChineseFormal = {
408
+ parse(value) {
409
+ if (typeof value === "number" && !Number.isInteger(value)) {
410
+ return formatDecimal(value, TRAD_FORMAL_SET);
411
+ }
412
+ return formatChineseInteger(value, TRAD_FORMAL_SET);
413
+ }
414
+ };
415
+ export const simpChineseInformal = {
416
+ parse(value) {
417
+ if (typeof value === "number" && !Number.isInteger(value)) {
418
+ return formatDecimal(value, SIMP_INFORMAL_SET);
419
+ }
420
+ return formatChineseInteger(value, SIMP_INFORMAL_SET);
421
+ }
422
+ };
423
+ export const simpChineseFormal = {
424
+ parse(value) {
425
+ if (typeof value === "number" && !Number.isInteger(value)) {
426
+ return formatDecimal(value, SIMP_FORMAL_SET);
427
+ }
428
+ return formatChineseInteger(value, SIMP_FORMAL_SET);
429
+ }
430
+ };
431
+ export const cjkHeavenlyStem = {
432
+ parse(value, options = {}) {
433
+ return fromCycle(value, STEMS, options.mode ?? "fixed");
434
+ }
435
+ };
436
+ export const cjkEarthlyBranch = {
437
+ parse(value, options = {}) {
438
+ return fromCycle(value, BRANCHES, options.mode ?? "fixed");
439
+ }
440
+ };
441
+ export const systems = {
442
+ heavenlyStem: STEMS,
443
+ earthlyBranch: BRANCHES
444
+ };
package/package.json ADDED
@@ -0,0 +1,49 @@
1
+ {
2
+ "name": "cjk-number",
3
+ "version": "0.1.0",
4
+ "description": "Convert between numbers and CJK number systems",
5
+ "author": "gary2",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/gary2/cjk-number.git"
9
+ },
10
+ "bugs": {
11
+ "url": "https://github.com/gary2/cjk-number/issues"
12
+ },
13
+ "homepage": "https://github.com/gary2/cjk-number#readme",
14
+ "type": "module",
15
+ "main": "./dist/index.js",
16
+ "types": "./dist/index.d.ts",
17
+ "exports": {
18
+ ".": {
19
+ "import": "./dist/index.js",
20
+ "types": "./dist/index.d.ts"
21
+ }
22
+ },
23
+ "files": [
24
+ "dist"
25
+ ],
26
+ "scripts": {
27
+ "build": "tsc -p tsconfig.json",
28
+ "typecheck": "tsc -p tsconfig.json --noEmit",
29
+ "test": "vitest run",
30
+ "dev:test": "vitest",
31
+ "pack:check": "npm pack --dry-run",
32
+ "release:check": "npm run typecheck && npm run test && npm run build && npm run pack:check",
33
+ "prepublishOnly": "npm run typecheck && npm run test && npm run build"
34
+ },
35
+ "engines": {
36
+ "node": ">=18"
37
+ },
38
+ "keywords": [
39
+ "cjk",
40
+ "chinese",
41
+ "number"
42
+ ],
43
+ "license": "MIT",
44
+ "devDependencies": {
45
+ "@types/node": "^25.5.0",
46
+ "typescript": "^5.8.3",
47
+ "vitest": "^4.1.2"
48
+ }
49
+ }