cjk-number 0.1.0 → 0.2.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/README.md CHANGED
@@ -1,6 +1,15 @@
1
1
  # cjk-number
2
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.
3
+ Convert numbers between Arabic values and CJK numeral systems.
4
+
5
+ Supported domains include:
6
+
7
+ - Traditional and simplified Chinese numerals
8
+ - Formal and informal variants
9
+ - Heavenly stems and earthly branches
10
+ - Korean and Japanese numeric kanji/hanja styles
11
+ - Kana sequence systems (gojuon and iroha)
12
+ - Negative numbers, decimals, and very large BigInt values
4
13
 
5
14
  ## Install
6
15
 
@@ -8,39 +17,56 @@ Convert numbers to and from CJK numeral systems, including traditional/simplifie
8
17
  npm install cjk-number
9
18
  ```
10
19
 
20
+ ## Runtime
21
+
22
+ - Node.js 18+
23
+ - ESM package
24
+
11
25
  ## Quick Start
12
26
 
13
27
  ```js
14
28
  import {
29
+ integer,
15
30
  cjkIdeographic,
16
- cjkHeavenlyStem,
17
- cjkEarthlyBranch,
18
- tradChineseFormal,
19
31
  tradChineseInformal,
32
+ tradChineseFormal,
20
33
  simpChineseInformal,
21
34
  simpChineseFormal,
22
- tradFormalPositionalUnits,
23
- integer
35
+ cjkHeavenlyStem,
36
+ cjkEarthlyBranch,
37
+ koreanHangulFormal,
38
+ japaneseFormal,
39
+ hiragana
24
40
  } from "cjk-number";
25
41
 
42
+ // parse string -> number/bigint
26
43
  integer.parseInt("一千零二十三"); // 1023
27
44
  integer.parseInt("壹仟零貳拾參"); // 1023
28
- integer.parseInt(""); // 10
29
- integer.parseInt(""); // 12
45
+ integer.parseInt("負一百零二"); // -102
46
+ integer.parseInt("一點二三"); // 1.23
47
+ integer.parseInt("一無量大數", { preferBigInt: true }); // 10n ** 68n
30
48
 
49
+ // format number/bigint -> string
50
+ cjkIdeographic.parse(1023); // "一千零二十三"
31
51
  tradChineseFormal.parse(1023); // "壹仟零貳拾參"
32
52
  simpChineseFormal.parse(1023); // "壹仟零贰拾叁"
53
+ koreanHangulFormal.parse(10n ** 68n); // "일무량대수"
54
+ japaneseFormal.parse(10n ** 68n); // "壱無量大数"
55
+
56
+ // stem/branch
33
57
  cjkHeavenlyStem.parse(10); // "癸"
34
58
  cjkEarthlyBranch.parse(12); // "亥"
35
59
 
36
- tradFormalPositionalUnits[68]; // "無量大數"
60
+ // sequence systems
61
+ hiragana.parse(1); // "あ"
62
+ hiragana.parse(46); // "ん"
37
63
  ```
38
64
 
39
65
  ## API
40
66
 
41
67
  ### integer.parseInt(input, options?)
42
68
 
43
- Parses CJK string to number or bigint.
69
+ Parses CJK text into number or bigint.
44
70
 
45
71
  Options:
46
72
 
@@ -49,16 +75,26 @@ Options:
49
75
  - heavenlyStemMode?: "fixed" | "cyclic"
50
76
  - earthlyBranchMode?: "fixed" | "cyclic"
51
77
 
78
+ Behavior summary:
79
+
80
+ - If the value fits safely, returns number by default.
81
+ - If out of Number safe range, returns bigint automatically.
82
+ - If preferBigInt is true, always returns bigint for integer parse paths.
83
+ - Decimal parse returns number.
84
+
52
85
  Examples:
53
86
 
54
87
  ```js
55
- integer.parseInt("負一百零二"); // -102
56
- integer.parseInt("一點二三"); // 1.23
57
88
  integer.parseInt("九千零七兆一", { preferBigInt: true }); // 9007000000000001n
58
- integer.parseInt("一無量大數", { preferBigInt: true }); // 10n ** 68n
89
+ integer.parseInt(""); // 10
90
+ integer.parseInt("亥"); // 12
91
+ integer.parseInt("壱京", { preferBigInt: true }); // 10n ** 16n
92
+ integer.parseInt("ぬ"); // 10 (iroha sequence symbol)
59
93
  ```
60
94
 
61
- ### Formatter.parse(value)
95
+ ### Formatters
96
+
97
+ All formatters expose parse(value).
62
98
 
63
99
  Available formatters:
64
100
 
@@ -69,6 +105,15 @@ Available formatters:
69
105
  - simpChineseFormal
70
106
  - cjkHeavenlyStem
71
107
  - cjkEarthlyBranch
108
+ - koreanHangulFormal
109
+ - koreanHanjaFormal
110
+ - koreanHanjaInformal
111
+ - japaneseFormal
112
+ - japaneseInformal
113
+ - hiragana
114
+ - hiraganaIroha
115
+ - katakana
116
+ - katakanaIroha
72
117
 
73
118
  Examples:
74
119
 
@@ -82,6 +127,38 @@ cjkHeavenlyStem.parse(11, { mode: "cyclic" }); // "甲"
82
127
  cjkEarthlyBranch.parse(13, { mode: "cyclic" }); // "子"
83
128
  ```
84
129
 
130
+ ## System Coverage
131
+
132
+ ### Full numeric systems (large-unit aware)
133
+
134
+ These support complete number formatting/parsing and large units up to 無量大數 / 无量大数:
135
+
136
+ - tradChineseInformal
137
+ - tradChineseFormal
138
+ - simpChineseInformal
139
+ - simpChineseFormal
140
+ - koreanHangulFormal
141
+ - koreanHanjaFormal
142
+ - koreanHanjaInformal
143
+ - japaneseFormal
144
+ - japaneseInformal
145
+
146
+ ### Sequence systems
147
+
148
+ These are order-based symbol sequences, not positional decimal numerals:
149
+
150
+ - cjkHeavenlyStem: 10 symbols
151
+ - cjkEarthlyBranch: 12 symbols
152
+ - hiragana: modern gojuon sequence (46 symbols)
153
+ - katakana: modern gojuon sequence (46 symbols)
154
+ - hiraganaIroha: traditional iroha sequence (47 symbols, includes ゐ and ゑ)
155
+ - katakanaIroha: traditional iroha sequence (47 symbols, includes ヰ and ヱ)
156
+
157
+ For sequence formatters:
158
+
159
+ - fixed mode: only accepts 1..length
160
+ - cyclic mode: wraps by sequence length
161
+
85
162
  ## Supported Large Units
86
163
 
87
164
  Traditional:
@@ -92,8 +169,43 @@ Simplified:
92
169
 
93
170
  万, 亿, 兆, 京, 垓, 秭, 穰, 沟, 涧, 正, 载, 极, 恒河沙, 阿僧祇, 那由他, 不可思议, 无量大数
94
171
 
172
+ Korean naming set:
173
+
174
+ 만, 억, 조, 경, 해, 자, 양, 구, 간, 정, 재, 극, 항하사, 아승기, 나유타, 불가사의, 무량대수
175
+
176
+ Japanese naming set:
177
+
178
+ 万, 億, 兆, 京, 垓, 秭, 穣, 溝, 澗, 正, 載, 極, 恒河沙, 阿僧祇, 那由他, 不可思議, 無量大数
179
+
180
+ Note:
181
+
182
+ - Historical texts may assign different powers for some high-order names.
183
+ - This package uses a fixed modern 10^4-step progression across supported large-unit systems.
184
+
185
+ ## Strict Mode
186
+
187
+ strict: true validates input characters against an allowed set.
188
+
189
+ Use it when you want to reject unexpected symbols early.
190
+
191
+ ```js
192
+ integer.parseInt("一億", { strict: true }); // ok
193
+ integer.parseInt("abc", { strict: true }); // throws SyntaxError
194
+ ```
195
+
196
+ ## Error Cases
197
+
198
+ Common thrown errors:
199
+
200
+ - SyntaxError: unsupported/invalid text shape
201
+ - RangeError: invalid formatter range in fixed sequence mode
202
+ - RangeError: non-integer passed to integer-only paths
203
+ - RangeError: decimal parse integer part exceeds Number.MAX_SAFE_INTEGER
204
+
95
205
  ## Development
96
206
 
207
+ Install and verify:
208
+
97
209
  ```sh
98
210
  npm install
99
211
  npm run typecheck
@@ -101,6 +213,18 @@ npm test
101
213
  npm run build
102
214
  ```
103
215
 
216
+ Release preflight:
217
+
218
+ ```sh
219
+ npm run release:check
220
+ ```
221
+
222
+ Dry-run package content:
223
+
224
+ ```sh
225
+ npm run pack:check
226
+ ```
227
+
104
228
  Property-style test environment variables:
105
229
 
106
230
  - CJK_TEST_SEED (default: 20260327)
@@ -113,12 +237,6 @@ Example:
113
237
  CJK_TEST_SEED=42 CJK_TEST_SAMPLES=1000 CJK_TEST_MAX_DIGITS=69 npm test
114
238
  ```
115
239
 
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
240
  ## License
123
241
 
124
242
  MIT
package/dist/index.d.ts CHANGED
@@ -9,7 +9,6 @@ export interface IntegerParseOptions {
9
9
  earthlyBranchMode?: CyclicMode;
10
10
  }
11
11
  type IntegerLike = number | bigint;
12
- export declare const tradFormalPositionalUnits: string[];
13
12
  export declare const integer: {
14
13
  parseInt(input: string, options?: IntegerParseOptions): number | bigint;
15
14
  };
@@ -34,9 +33,45 @@ export declare const cjkHeavenlyStem: {
34
33
  export declare const cjkEarthlyBranch: {
35
34
  parse(value: IntegerLike, options?: SystemParseOptions): string;
36
35
  };
36
+ export declare const koreanHangulFormal: {
37
+ parse(value: IntegerLike): string;
38
+ };
39
+ export declare const koreanHanjaFormal: {
40
+ parse(value: IntegerLike): string;
41
+ };
42
+ export declare const koreanHanjaInformal: {
43
+ parse(value: IntegerLike): string;
44
+ };
45
+ export declare const japaneseFormal: {
46
+ parse(value: IntegerLike): string;
47
+ };
48
+ export declare const japaneseInformal: {
49
+ parse(value: IntegerLike): string;
50
+ };
51
+ export declare const hiragana: {
52
+ parse(value: IntegerLike, options?: SystemParseOptions): string;
53
+ };
54
+ export declare const hiraganaIroha: {
55
+ parse(value: IntegerLike, options?: SystemParseOptions): string;
56
+ };
57
+ export declare const katakana: {
58
+ parse(value: IntegerLike, options?: SystemParseOptions): string;
59
+ };
60
+ export declare const katakanaIroha: {
61
+ parse(value: IntegerLike, options?: SystemParseOptions): string;
62
+ };
37
63
  export declare const systems: {
38
64
  heavenlyStem: readonly ["甲", "乙", "丙", "丁", "戊", "己", "庚", "辛", "壬", "癸"];
39
65
  earthlyBranch: readonly ["子", "丑", "寅", "卯", "辰", "巳", "午", "未", "申", "酉", "戌", "亥"];
66
+ koreanHangulFormal: readonly ["일", "이", "삼", "사", "오", "육", "칠", "팔", "구"];
67
+ koreanHanjaFormal: readonly ["壹", "貳", "參", "四", "五", "六", "七", "八", "九"];
68
+ koreanHanjaInformal: readonly ["一", "二", "三", "四", "五", "六", "七", "八", "九"];
69
+ japaneseFormal: readonly ["壱", "弐", "参", "四", "五", "六", "七", "八", "九"];
70
+ japaneseInformal: readonly ["一", "二", "三", "四", "五", "六", "七", "八", "九"];
71
+ hiragana: readonly ["あ", "い", "う", "え", "お", "か", "き", "く", "け", "こ", "さ", "し", "す", "せ", "そ", "た", "ち", "つ", "て", "と", "な", "に", "ぬ", "ね", "の", "は", "ひ", "ふ", "へ", "ほ", "ま", "み", "む", "め", "も", "や", "ゆ", "よ", "ら", "り", "る", "れ", "ろ", "わ", "を", "ん"];
72
+ hiraganaIroha: readonly ["い", "ろ", "は", "に", "ほ", "へ", "と", "ち", "り", "ぬ", "る", "を", "わ", "か", "よ", "た", "れ", "そ", "つ", "ね", "な", "ら", "む", "う", "ゐ", "の", "お", "く", "や", "ま", "け", "ふ", "こ", "え", "て", "あ", "さ", "き", "ゆ", "め", "み", "し", "ゑ", "ひ", "も", "せ", "す"];
73
+ katakana: readonly ["ア", "イ", "ウ", "エ", "オ", "カ", "キ", "ク", "ケ", "コ", "サ", "シ", "ス", "セ", "ソ", "タ", "チ", "ツ", "テ", "ト", "ナ", "ニ", "ヌ", "ネ", "ノ", "ハ", "ヒ", "フ", "ヘ", "ホ", "マ", "ミ", "ム", "メ", "モ", "ヤ", "ユ", "ヨ", "ラ", "リ", "ル", "レ", "ロ", "ワ", "ヲ", "ン"];
74
+ katakanaIroha: readonly ["イ", "ロ", "ハ", "ニ", "ホ", "ヘ", "ト", "チ", "リ", "ヌ", "ル", "ヲ", "ワ", "カ", "ヨ", "タ", "レ", "ソ", "ツ", "ネ", "ナ", "ラ", "ム", "ウ", "ヰ", "ノ", "オ", "ク", "ヤ", "マ", "ケ", "フ", "コ", "エ", "テ", "ア", "サ", "キ", "ユ", "メ", "ミ", "シ", "ヱ", "ヒ", "モ", "セ", "ス"];
40
75
  };
41
76
  export {};
42
77
  //# sourceMappingURL=index.d.ts.map
@@ -1 +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"}
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;AA8oBnC,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,kBAAkB;iBAChB,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,cAAc;iBACZ,WAAW,GAAG,MAAM;CAMlC,CAAC;AAEF,eAAO,MAAM,gBAAgB;iBACd,WAAW,GAAG,MAAM;CAMlC,CAAC;AAEF,eAAO,MAAM,QAAQ;iBACN,WAAW,YAAW,kBAAkB,GAAQ,MAAM;CAGpE,CAAC;AAEF,eAAO,MAAM,aAAa;iBACX,WAAW,YAAW,kBAAkB,GAAQ,MAAM;CAGpE,CAAC;AAEF,eAAO,MAAM,QAAQ;iBACN,WAAW,YAAW,kBAAkB,GAAQ,MAAM;CAGpE,CAAC;AAEF,eAAO,MAAM,aAAa;iBACX,WAAW,YAAW,kBAAkB,GAAQ,MAAM;CAGpE,CAAC;AAEF,eAAO,MAAM,OAAO;;;;;;;;;;;;CAYnB,CAAC"}
package/dist/index.js CHANGED
@@ -1,7 +1,109 @@
1
1
  const STEMS = ["甲", "乙", "丙", "丁", "戊", "己", "庚", "辛", "壬", "癸"];
2
2
  const BRANCHES = ["子", "丑", "寅", "卯", "辰", "巳", "午", "未", "申", "酉", "戌", "亥"];
3
+ const KOREAN_HANGUL_DIGITS = ["일", "이", "삼", "사", "오", "육", "칠", "팔", "구"];
4
+ const KOREAN_HANJA_FORMAL_DIGITS = ["壹", "貳", "參", "四", "五", "六", "七", "八", "九"];
5
+ const KOREAN_HANJA_INFORMAL_DIGITS = ["一", "二", "三", "四", "五", "六", "七", "八", "九"];
6
+ const JAPANESE_FORMAL_DIGITS = ["壱", "弐", "参", "四", "五", "六", "七", "八", "九"];
7
+ const JAPANESE_INFORMAL_DIGITS = ["一", "二", "三", "四", "五", "六", "七", "八", "九"];
8
+ const HIRAGANA = [
9
+ "あ", "い", "う", "え", "お",
10
+ "か", "き", "く", "け", "こ",
11
+ "さ", "し", "す", "せ", "そ",
12
+ "た", "ち", "つ", "て", "と",
13
+ "な", "に", "ぬ", "ね", "の",
14
+ "は", "ひ", "ふ", "へ", "ほ",
15
+ "ま", "み", "む", "め", "も",
16
+ "や", "ゆ", "よ",
17
+ "ら", "り", "る", "れ", "ろ",
18
+ "わ", "を", "ん"
19
+ ];
20
+ const HIRAGANA_IROHA = [
21
+ "い", "ろ", "は", "に", "ほ", "へ", "と",
22
+ "ち", "り", "ぬ", "る", "を", "わ", "か",
23
+ "よ", "た", "れ", "そ", "つ", "ね", "な",
24
+ "ら", "む", "う", "ゐ", "の", "お", "く",
25
+ "や", "ま", "け", "ふ", "こ", "え", "て",
26
+ "あ", "さ", "き", "ゆ", "め", "み", "し",
27
+ "ゑ", "ひ", "も", "せ", "す"
28
+ ];
29
+ const KATAKANA = [
30
+ "ア", "イ", "ウ", "エ", "オ",
31
+ "カ", "キ", "ク", "ケ", "コ",
32
+ "サ", "シ", "ス", "セ", "ソ",
33
+ "タ", "チ", "ツ", "テ", "ト",
34
+ "ナ", "ニ", "ヌ", "ネ", "ノ",
35
+ "ハ", "ヒ", "フ", "ヘ", "ホ",
36
+ "マ", "ミ", "ム", "メ", "モ",
37
+ "ヤ", "ユ", "ヨ",
38
+ "ラ", "リ", "ル", "レ", "ロ",
39
+ "ワ", "ヲ", "ン"
40
+ ];
41
+ const KATAKANA_IROHA = [
42
+ "イ", "ロ", "ハ", "ニ", "ホ", "ヘ", "ト",
43
+ "チ", "リ", "ヌ", "ル", "ヲ", "ワ", "カ",
44
+ "ヨ", "タ", "レ", "ソ", "ツ", "ネ", "ナ",
45
+ "ラ", "ム", "ウ", "ヰ", "ノ", "オ", "ク",
46
+ "ヤ", "マ", "ケ", "フ", "コ", "エ", "テ",
47
+ "ア", "サ", "キ", "ユ", "メ", "ミ", "シ",
48
+ "ヱ", "ヒ", "モ", "セ", "ス"
49
+ ];
50
+ const KOREAN_BIG_UNITS = [
51
+ "만",
52
+ "억",
53
+ "조",
54
+ "경",
55
+ "해",
56
+ "자",
57
+ "양",
58
+ "구",
59
+ "간",
60
+ "정",
61
+ "재",
62
+ "극",
63
+ "항하사",
64
+ "아승기",
65
+ "나유타",
66
+ "불가사의",
67
+ "무량대수"
68
+ ];
69
+ const JAPANESE_BIG_UNITS = [
70
+ "万",
71
+ "億",
72
+ "兆",
73
+ "京",
74
+ "垓",
75
+ "秭",
76
+ "穣",
77
+ "溝",
78
+ "澗",
79
+ "正",
80
+ "載",
81
+ "極",
82
+ "恒河沙",
83
+ "阿僧祇",
84
+ "那由他",
85
+ "不可思議",
86
+ "無量大数"
87
+ ];
88
+ const SEQUENCE_SYMBOL_TO_NUMBER = (() => {
89
+ const map = {};
90
+ const put = (chars) => {
91
+ chars.forEach((char, index) => {
92
+ map[char] = index + 1;
93
+ });
94
+ };
95
+ put(STEMS);
96
+ put(BRANCHES);
97
+ put(HIRAGANA);
98
+ put(HIRAGANA_IROHA);
99
+ put(KATAKANA);
100
+ put(KATAKANA_IROHA);
101
+ return map;
102
+ })();
3
103
  const CANONICAL_DIGITS = {
4
104
  "零": 0,
105
+ "영": 0,
106
+ "령": 0,
5
107
  "〇": 0,
6
108
  "○": 0,
7
109
  "一": 1,
@@ -74,43 +176,74 @@ const SIMP_BIG_UNITS = [
74
176
  "不可思议",
75
177
  "无量大数"
76
178
  ];
77
- function buildFormalPositionalUnits(bigUnits) {
78
- const result = [""];
79
- for (const unit of bigUnits) {
80
- result.push("拾", "佰", "仟", unit);
81
- }
82
- return result;
83
- }
84
179
  function createBigUnitOrder(units) {
85
180
  return units
86
181
  .map((unit, index) => [unit, 10n ** BigInt((index + 1) * 4)])
87
182
  .reverse();
88
183
  }
89
- export const tradFormalPositionalUnits = buildFormalPositionalUnits(TRAD_BIG_UNITS);
90
184
  const TRAD_INFORMAL_SET = {
91
185
  zero: "零",
186
+ point: "點",
92
187
  digits: ["一", "二", "三", "四", "五", "六", "七", "八", "九"],
93
188
  smallUnits: ["十", "百", "千"],
94
189
  bigUnits: [...TRAD_BIG_UNITS]
95
190
  };
96
191
  const TRAD_FORMAL_SET = {
97
192
  zero: "零",
193
+ point: "點",
98
194
  digits: ["壹", "貳", "參", "肆", "伍", "陸", "柒", "捌", "玖"],
99
195
  smallUnits: ["拾", "佰", "仟"],
100
196
  bigUnits: [...TRAD_BIG_UNITS]
101
197
  };
102
198
  const SIMP_INFORMAL_SET = {
103
199
  zero: "零",
200
+ point: "点",
104
201
  digits: ["一", "二", "三", "四", "五", "六", "七", "八", "九"],
105
202
  smallUnits: ["十", "百", "千"],
106
203
  bigUnits: [...SIMP_BIG_UNITS]
107
204
  };
108
205
  const SIMP_FORMAL_SET = {
109
206
  zero: "零",
207
+ point: "点",
110
208
  digits: ["壹", "贰", "叁", "肆", "伍", "陆", "柒", "捌", "玖"],
111
209
  smallUnits: ["拾", "佰", "仟"],
112
210
  bigUnits: [...SIMP_BIG_UNITS]
113
211
  };
212
+ const KOREAN_HANGUL_SET = {
213
+ zero: "영",
214
+ point: "점",
215
+ digits: [...KOREAN_HANGUL_DIGITS],
216
+ smallUnits: ["십", "백", "천"],
217
+ bigUnits: [...KOREAN_BIG_UNITS]
218
+ };
219
+ const KOREAN_HANJA_FORMAL_SET = {
220
+ zero: "零",
221
+ point: "점",
222
+ digits: [...KOREAN_HANJA_FORMAL_DIGITS],
223
+ smallUnits: ["拾", "佰", "仟"],
224
+ bigUnits: [...TRAD_BIG_UNITS]
225
+ };
226
+ const KOREAN_HANJA_INFORMAL_SET = {
227
+ zero: "零",
228
+ point: "점",
229
+ digits: [...KOREAN_HANJA_INFORMAL_DIGITS],
230
+ smallUnits: ["十", "百", "千"],
231
+ bigUnits: [...TRAD_BIG_UNITS]
232
+ };
233
+ const JAPANESE_FORMAL_SET = {
234
+ zero: "零",
235
+ point: "点",
236
+ digits: [...JAPANESE_FORMAL_DIGITS],
237
+ smallUnits: ["拾", "百", "千"],
238
+ bigUnits: [...JAPANESE_BIG_UNITS]
239
+ };
240
+ const JAPANESE_INFORMAL_SET = {
241
+ zero: "零",
242
+ point: "点",
243
+ digits: [...JAPANESE_INFORMAL_DIGITS],
244
+ smallUnits: ["十", "百", "千"],
245
+ bigUnits: [...JAPANESE_BIG_UNITS]
246
+ };
114
247
  const BIG_UNIT_ORDER = createBigUnitOrder(SIMP_BIG_UNITS);
115
248
  function normalizeInput(raw) {
116
249
  const trimmed = raw.trim();
@@ -119,9 +252,43 @@ function normalizeInput(raw) {
119
252
  }
120
253
  return trimmed
121
254
  .replace(/負|负/g, "-")
255
+ .replace(/점/g, ".")
122
256
  .replace(/點|点/g, ".")
257
+ .replace(/무량대수/g, "无量大数")
258
+ .replace(/불가사의/g, "不可思议")
259
+ .replace(/나유타/g, "那由他")
260
+ .replace(/아승기/g, "阿僧祇")
261
+ .replace(/항하사/g, "恒河沙")
262
+ .replace(/재/g, "载")
263
+ .replace(/정/g, "正")
264
+ .replace(/간/g, "涧")
265
+ .replace(/구/g, "沟")
266
+ .replace(/양/g, "穰")
267
+ .replace(/자/g, "秭")
268
+ .replace(/해/g, "垓")
269
+ .replace(/경/g, "京")
270
+ .replace(/조/g, "兆")
271
+ .replace(/억/g, "亿")
272
+ .replace(/만/g, "万")
273
+ .replace(/천/g, "千")
274
+ .replace(/백/g, "百")
275
+ .replace(/십/g, "十")
276
+ .replace(/일/g, "一")
277
+ .replace(/이/g, "二")
278
+ .replace(/삼/g, "三")
279
+ .replace(/사/g, "四")
280
+ .replace(/오/g, "五")
281
+ .replace(/육/g, "六")
282
+ .replace(/칠/g, "七")
283
+ .replace(/팔/g, "八")
284
+ .replace(/구/g, "九")
285
+ .replace(/壱/g, "一")
286
+ .replace(/弐/g, "二")
287
+ .replace(/参/g, "三")
123
288
  .replace(/無量大數/g, "无量大数")
289
+ .replace(/無量大数/g, "无量大数")
124
290
  .replace(/不可思議/g, "不可思议")
291
+ .replace(/穣/g, "穰")
125
292
  .replace(/恆河沙/g, "恒河沙")
126
293
  .replace(/載/g, "载")
127
294
  .replace(/極/g, "极")
@@ -151,7 +318,7 @@ function parseDigitsOnly(input) {
151
318
  }
152
319
  result += String(digit);
153
320
  }
154
- return BigInt(result || "0");
321
+ return BigInt(result);
155
322
  }
156
323
  function parseSection(section) {
157
324
  if (!section) {
@@ -232,7 +399,7 @@ function toBestNumeric(value, preferBigInt) {
232
399
  return Number(value);
233
400
  }
234
401
  function validateStrictCharacters(input) {
235
- const allowed = /^[0-9零〇○一二三四五六七八九十百千萬万億亿兆京垓秭穰溝沟澗涧正載载極极恆恒河沙阿僧祇那由他不思議议可無无量大數数點点壹貳贰參叁肆伍陸陆柒捌玖兩两拾佰仟負负.-]+$/;
402
+ const allowed = /^[0-9零〇○一二三四五六七八九十百千萬万億亿兆京垓秭穰溝沟澗涧正載载極极恆恒河沙阿僧祇那由他不思議议可無无量大數数點点점壹貳贰參叁肆伍陸陆柒捌玖兩两拾佰仟負负壱弐参ぁ-ゟ゠-ヿ가-힣.-]+$/;
236
403
  if (!allowed.test(input)) {
237
404
  throw new SyntaxError("Input contains unsupported characters in strict mode");
238
405
  }
@@ -261,9 +428,6 @@ function parseCycle(input, chars, mode) {
261
428
  return value;
262
429
  }
263
430
  function formatSection(section, set) {
264
- if (section === 0) {
265
- return "";
266
- }
267
431
  const values = [1000, 100, 10, 1];
268
432
  const units = [set.smallUnits[2], set.smallUnits[1], set.smallUnits[0], ""];
269
433
  let output = "";
@@ -307,9 +471,7 @@ function formatChineseInteger(value, set) {
307
471
  for (let i = sections.length - 1; i >= 0; i -= 1) {
308
472
  const section = sections[i];
309
473
  if (section === 0) {
310
- if (output) {
311
- pendingZero = true;
312
- }
474
+ pendingZero = Boolean(output);
313
475
  continue;
314
476
  }
315
477
  if (output && (pendingZero || section < 1000)) {
@@ -346,20 +508,28 @@ function formatDecimal(value, set) {
346
508
  const digit = Number(ch);
347
509
  fracText += digit === 0 ? set.zero : set.digits[digit - 1];
348
510
  }
349
- const withSign = `${intText}點${fracText}`;
511
+ const withSign = `${intText}${set.point}${fracText}`;
350
512
  return negative ? `負${withSign}` : withSign;
351
513
  }
352
514
  function parseValue(input, options = {}) {
353
515
  const modeStem = options.heavenlyStemMode ?? "fixed";
354
516
  const modeBranch = options.earthlyBranchMode ?? "fixed";
355
- const stemIndex = STEMS.indexOf(input);
356
- if (stemIndex >= 0) {
517
+ try {
357
518
  return parseCycle(input, STEMS, modeStem);
358
519
  }
359
- const branchIndex = BRANCHES.indexOf(input);
360
- if (branchIndex >= 0) {
520
+ catch {
521
+ // Not a heavenly stem, continue to next parser.
522
+ }
523
+ try {
361
524
  return parseCycle(input, BRANCHES, modeBranch);
362
525
  }
526
+ catch {
527
+ // Not an earthly branch, continue to sequence/numeric parsing.
528
+ }
529
+ const sequenceValue = SEQUENCE_SYMBOL_TO_NUMBER[input];
530
+ if (sequenceValue !== undefined) {
531
+ return sequenceValue;
532
+ }
363
533
  const normalized = normalizeInput(input);
364
534
  if (options.strict) {
365
535
  validateStrictCharacters(normalized);
@@ -370,9 +540,9 @@ function parseValue(input, options = {}) {
370
540
  throw new SyntaxError("Missing numeric content");
371
541
  }
372
542
  if (body.includes(".")) {
373
- const [intRaw, fracRaw] = body.split(".");
543
+ const [intRaw, fracRaw = ""] = body.split(".");
374
544
  const intValue = intRaw ? parseChineseInteger(intRaw) : 0n;
375
- const fracValue = parseFractionDigits(fracRaw ?? "");
545
+ const fracValue = parseFractionDigits(fracRaw);
376
546
  if (intValue > BigInt(Number.MAX_SAFE_INTEGER)) {
377
547
  throw new RangeError("Decimal parse does not support integer part above MAX_SAFE_INTEGER");
378
548
  }
@@ -390,7 +560,7 @@ export const integer = {
390
560
  };
391
561
  export const cjkIdeographic = {
392
562
  parse(value) {
393
- if (typeof value === "number" && !Number.isInteger(value)) {
563
+ if (typeof value === "number") {
394
564
  return formatDecimal(value, TRAD_INFORMAL_SET);
395
565
  }
396
566
  return formatChineseInteger(value, TRAD_INFORMAL_SET);
@@ -398,7 +568,7 @@ export const cjkIdeographic = {
398
568
  };
399
569
  export const tradChineseInformal = {
400
570
  parse(value) {
401
- if (typeof value === "number" && !Number.isInteger(value)) {
571
+ if (typeof value === "number") {
402
572
  return formatDecimal(value, TRAD_INFORMAL_SET);
403
573
  }
404
574
  return formatChineseInteger(value, TRAD_INFORMAL_SET);
@@ -406,7 +576,7 @@ export const tradChineseInformal = {
406
576
  };
407
577
  export const tradChineseFormal = {
408
578
  parse(value) {
409
- if (typeof value === "number" && !Number.isInteger(value)) {
579
+ if (typeof value === "number") {
410
580
  return formatDecimal(value, TRAD_FORMAL_SET);
411
581
  }
412
582
  return formatChineseInteger(value, TRAD_FORMAL_SET);
@@ -414,7 +584,7 @@ export const tradChineseFormal = {
414
584
  };
415
585
  export const simpChineseInformal = {
416
586
  parse(value) {
417
- if (typeof value === "number" && !Number.isInteger(value)) {
587
+ if (typeof value === "number") {
418
588
  return formatDecimal(value, SIMP_INFORMAL_SET);
419
589
  }
420
590
  return formatChineseInteger(value, SIMP_INFORMAL_SET);
@@ -422,7 +592,7 @@ export const simpChineseInformal = {
422
592
  };
423
593
  export const simpChineseFormal = {
424
594
  parse(value) {
425
- if (typeof value === "number" && !Number.isInteger(value)) {
595
+ if (typeof value === "number") {
426
596
  return formatDecimal(value, SIMP_FORMAL_SET);
427
597
  }
428
598
  return formatChineseInteger(value, SIMP_FORMAL_SET);
@@ -438,7 +608,76 @@ export const cjkEarthlyBranch = {
438
608
  return fromCycle(value, BRANCHES, options.mode ?? "fixed");
439
609
  }
440
610
  };
611
+ export const koreanHangulFormal = {
612
+ parse(value) {
613
+ if (typeof value === "number") {
614
+ return formatDecimal(value, KOREAN_HANGUL_SET);
615
+ }
616
+ return formatChineseInteger(value, KOREAN_HANGUL_SET);
617
+ }
618
+ };
619
+ export const koreanHanjaFormal = {
620
+ parse(value) {
621
+ if (typeof value === "number") {
622
+ return formatDecimal(value, KOREAN_HANJA_FORMAL_SET);
623
+ }
624
+ return formatChineseInteger(value, KOREAN_HANJA_FORMAL_SET);
625
+ }
626
+ };
627
+ export const koreanHanjaInformal = {
628
+ parse(value) {
629
+ if (typeof value === "number") {
630
+ return formatDecimal(value, KOREAN_HANJA_INFORMAL_SET);
631
+ }
632
+ return formatChineseInteger(value, KOREAN_HANJA_INFORMAL_SET);
633
+ }
634
+ };
635
+ export const japaneseFormal = {
636
+ parse(value) {
637
+ if (typeof value === "number") {
638
+ return formatDecimal(value, JAPANESE_FORMAL_SET);
639
+ }
640
+ return formatChineseInteger(value, JAPANESE_FORMAL_SET);
641
+ }
642
+ };
643
+ export const japaneseInformal = {
644
+ parse(value) {
645
+ if (typeof value === "number") {
646
+ return formatDecimal(value, JAPANESE_INFORMAL_SET);
647
+ }
648
+ return formatChineseInteger(value, JAPANESE_INFORMAL_SET);
649
+ }
650
+ };
651
+ export const hiragana = {
652
+ parse(value, options = {}) {
653
+ return fromCycle(value, HIRAGANA, options.mode ?? "fixed");
654
+ }
655
+ };
656
+ export const hiraganaIroha = {
657
+ parse(value, options = {}) {
658
+ return fromCycle(value, HIRAGANA_IROHA, options.mode ?? "fixed");
659
+ }
660
+ };
661
+ export const katakana = {
662
+ parse(value, options = {}) {
663
+ return fromCycle(value, KATAKANA, options.mode ?? "fixed");
664
+ }
665
+ };
666
+ export const katakanaIroha = {
667
+ parse(value, options = {}) {
668
+ return fromCycle(value, KATAKANA_IROHA, options.mode ?? "fixed");
669
+ }
670
+ };
441
671
  export const systems = {
442
672
  heavenlyStem: STEMS,
443
- earthlyBranch: BRANCHES
673
+ earthlyBranch: BRANCHES,
674
+ koreanHangulFormal: KOREAN_HANGUL_DIGITS,
675
+ koreanHanjaFormal: KOREAN_HANJA_FORMAL_DIGITS,
676
+ koreanHanjaInformal: KOREAN_HANJA_INFORMAL_DIGITS,
677
+ japaneseFormal: JAPANESE_FORMAL_DIGITS,
678
+ japaneseInformal: JAPANESE_INFORMAL_DIGITS,
679
+ hiragana: HIRAGANA,
680
+ hiraganaIroha: HIRAGANA_IROHA,
681
+ katakana: KATAKANA,
682
+ katakanaIroha: KATAKANA_IROHA
444
683
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cjk-number",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "Convert between numbers and CJK number systems",
5
5
  "author": "gary2",
6
6
  "repository": {
@@ -27,6 +27,7 @@
27
27
  "build": "tsc -p tsconfig.json",
28
28
  "typecheck": "tsc -p tsconfig.json --noEmit",
29
29
  "test": "vitest run",
30
+ "coverage": "vitest run --coverage",
30
31
  "dev:test": "vitest",
31
32
  "pack:check": "npm pack --dry-run",
32
33
  "release:check": "npm run typecheck && npm run test && npm run build && npm run pack:check",
@@ -43,6 +44,7 @@
43
44
  "license": "MIT",
44
45
  "devDependencies": {
45
46
  "@types/node": "^25.5.0",
47
+ "@vitest/coverage-v8": "^4.1.2",
46
48
  "typescript": "^5.8.3",
47
49
  "vitest": "^4.1.2"
48
50
  }