Skip to content

Commit 7b931d7

Browse files
committed
Split files
1 parent 368885c commit 7b931d7

File tree

5 files changed

+223
-177
lines changed

5 files changed

+223
-177
lines changed

src/read/digits.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
/**
2+
* Mapping single digit to Vietnamese word
3+
*/
4+
const DIGIT_MAP: readonly string[] = [
5+
'không',
6+
'một',
7+
'hai',
8+
'ba',
9+
'bốn',
10+
'năm',
11+
'sáu',
12+
'bảy',
13+
'tám',
14+
'chín',
15+
]
16+
17+
/**
18+
* Get Vietnamese word for a digit
19+
*/
20+
export function getDigitWord(digit: string): string {
21+
const d = Number(digit)
22+
return DIGIT_MAP[d] || ''
23+
}

src/read/groups.ts

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
import { readThreeDigits } from './three-digits.ts'
2+
import { allFollowingGroupsAreZero } from './utils.ts'
3+
4+
/**
5+
* Calculate group types for each position
6+
* @returns Array where index is group position, value is type (0=units, 1=thousand, 2=million, 3=billion)
7+
*/
8+
export function calculateGroupTypes(groupCount: number): number[] {
9+
const groupTypes: number[] = []
10+
for (let i = groupCount - 1, type = 0; i >= 0; i--) {
11+
groupTypes[i] = type
12+
type++
13+
if (type === 4) type = 1 // cycle back after billion
14+
}
15+
return groupTypes
16+
}
17+
18+
/**
19+
* Get unit suffix for a group based on its type
20+
*/
21+
function getUnitSuffix(
22+
type: number,
23+
positionFromRight: number,
24+
hasTrailingZeros: boolean,
25+
): string {
26+
const needsBillionSuffix = positionFromRight >= 3 && hasTrailingZeros
27+
28+
if (type === 3) {
29+
// Billion: double "tỷ" for second billion cycle (position >= 6)
30+
if (positionFromRight >= 6 && hasTrailingZeros) {
31+
return ' tỷ tỷ'
32+
}
33+
return ' tỷ'
34+
}
35+
36+
if (type === 2) {
37+
return needsBillionSuffix ? ' triệu tỷ' : ' triệu'
38+
}
39+
40+
if (type === 1) {
41+
return needsBillionSuffix ? ' nghìn tỷ' : ' nghìn'
42+
}
43+
44+
// Type 0 (units)
45+
return needsBillionSuffix ? ' tỷ' : ''
46+
}
47+
48+
/**
49+
* Process a single group and return its reading with unit suffix
50+
*/
51+
export function processGroup(
52+
group: string,
53+
index: number,
54+
groups: string[],
55+
groupTypes: number[],
56+
): string {
57+
const isFirst = index === 0
58+
const type = groupTypes[index]
59+
const nextGroupType = index + 1 < groups.length ? groupTypes[index + 1] : -1
60+
const isBeforeBillion = type === 0 && nextGroupType === 3
61+
62+
const groupReading = readThreeDigits(group, isFirst, isBeforeBillion)
63+
if (!groupReading) return ''
64+
65+
const positionFromRight = groups.length - 1 - index
66+
const hasTrailingZeros = allFollowingGroupsAreZero(groups, index)
67+
const unitSuffix = getUnitSuffix(type, positionFromRight, hasTrailingZeros)
68+
69+
return `${groupReading}${unitSuffix}`
70+
}

src/read/index.ts

Lines changed: 7 additions & 177 deletions
Original file line numberDiff line numberDiff line change
@@ -1,114 +1,5 @@
1-
/**
2-
* Mapping single digit to Vietnamese word
3-
*/
4-
const DIGIT_MAP: readonly string[] = [
5-
'không',
6-
'một',
7-
'hai',
8-
'ba',
9-
'bốn',
10-
'năm',
11-
'sáu',
12-
'bảy',
13-
'tám',
14-
'chín',
15-
]
16-
17-
/**
18-
* Get Vietnamese word for a digit
19-
*/
20-
function getDigitWord(digit: string): string {
21-
const d = Number(digit)
22-
return DIGIT_MAP[d] || ''
23-
}
24-
25-
/**
26-
* Read a 3-digit group
27-
* @param group - 3-digit string (can be 1-3 chars)
28-
* @param isFirst - is this the first group
29-
* @param isBeforeBillion - is this group before a billion group
30-
* @returns Vietnamese reading of the group
31-
*/
32-
function readThreeDigits(
33-
group: string,
34-
isFirst: boolean,
35-
isBeforeBillion: boolean,
36-
): string {
37-
const len = group.length
38-
const first = len > 2 ? group[len - 3] : '0'
39-
const second = len > 1 ? group[len - 2] : '0'
40-
const last = group[len - 1] || '0'
41-
42-
// All zeros
43-
if (first === '0' && second === '0' && last === '0') {
44-
return isFirst ? 'không' : ''
45-
}
46-
47-
let result = ''
48-
49-
// First digit (hundreds)
50-
// Only processes if the group has at least 3 digits (has a hundreds position)
51-
if (len > 2) {
52-
result = `${getDigitWord(first)} trăm`
53-
}
54-
55-
// If the last two digits are zero, skip
56-
if (second === '0' && last === '0') {
57-
if (isFirst && isBeforeBillion) {
58-
result += ' nghìn'
59-
}
60-
return result.trim()
61-
}
62-
63-
// Second digit (tens)
64-
// Only process if group has at least 2 digits
65-
if (len > 1) {
66-
if (second === '0') {
67-
result += ' lẻ'
68-
} else if (second === '1') {
69-
result += ' mười'
70-
} else {
71-
result += ` ${getDigitWord(second)} mươi`
72-
}
73-
}
74-
75-
// Last digit (ones)
76-
if (len > 1) {
77-
// Has tens digit - use special rules
78-
if (second !== '0' && second !== '1' && last === '1') {
79-
result += ' mốt'
80-
} else if (last === '5' && second !== '0') {
81-
result += ' lăm'
82-
} else if (last !== '0') {
83-
result += ` ${getDigitWord(last)}`
84-
}
85-
} else {
86-
// No ten digit - just add the last digit
87-
result += ` ${getDigitWord(last)}`
88-
}
89-
90-
if (isFirst && isBeforeBillion) {
91-
result += ' nghìn'
92-
}
93-
94-
return result.trim()
95-
}
96-
97-
/**
98-
* Split number string into 3-digit groups from right to left
99-
*/
100-
function splitIntoGroups(numStr: string): string[] {
101-
const groups: string[] = []
102-
let i = numStr.length
103-
104-
while (i > 0) {
105-
const start = Math.max(0, i - 3)
106-
groups.unshift(numStr.slice(start, i))
107-
i = start
108-
}
109-
110-
return groups
111-
}
1+
import { calculateGroupTypes, processGroup } from './groups.ts'
2+
import { splitIntoGroups } from './utils.ts'
1123

1134
/**
1145
* This is a helper that convert a number to a string like the way a real Vietnamese.
@@ -126,74 +17,13 @@ function splitIntoGroups(numStr: string): string[] {
12617
export function readVnNumber(number: string | number | bigint): string {
12718
const numStr = number.toString()
12819
const groups = splitIntoGroups(numStr)
129-
const groupCount = groups.length
130-
131-
// Map each group index to its type (0=units, 1=thousand, 2=million, 3=billion)
132-
// After billion, it cycles: 4->1, 5->2, 6->3, 7->1, etc.
133-
const groupTypes: number[] = []
134-
for (let i = groupCount - 1, type = 0; i >= 0; i--) {
135-
groupTypes[i] = type
136-
type++
137-
if (type === 4) type = 1 // cycle back after billion
138-
}
20+
const groupTypes = calculateGroupTypes(groups.length)
13921

14022
const parts: string[] = []
141-
142-
// Check if all groups after index i are zeros
143-
const allFollowingGroupsAreZero = (index: number): boolean => {
144-
for (let j = index + 1; j < groupCount; j++) {
145-
const g = groups[j]
146-
if (g !== '000' && g !== '00' && g !== '0') {
147-
return false
148-
}
149-
}
150-
return true
151-
}
152-
153-
for (let i = 0; i < groupCount; i++) {
154-
const group = groups[i]
155-
const isFirst = i === 0
156-
const type = groupTypes[i]
157-
const positionFromRight = groupCount - 1 - i
158-
const nextGroupType = i + 1 < groupCount ? groupTypes[i + 1] : -1
159-
// beforeBillion only applies to plain Numbers (type 0), not to Thousand/Million/Billion
160-
const isBeforeBillion = type === 0 && nextGroupType === 3
161-
162-
const groupReading = readThreeDigits(group, isFirst, isBeforeBillion)
163-
164-
if (groupReading) {
165-
// Check if this is a group in the billion+ range with all following groups being zero
166-
const needsBillionSuffix =
167-
positionFromRight >= 3 && allFollowingGroupsAreZero(i)
168-
169-
// Add unit suffix based on type (but not if already added by readThreeDigits)
170-
if (type === 3) {
171-
// Billion
172-
// Double "tỷ" only for second billion cycle and beyond (position >= 6)
173-
if (positionFromRight >= 6 && allFollowingGroupsAreZero(i)) {
174-
parts.push(`${groupReading} tỷ tỷ`)
175-
} else {
176-
parts.push(`${groupReading} tỷ`)
177-
}
178-
} else if (type === 2) {
179-
// Million
180-
if (needsBillionSuffix) {
181-
parts.push(`${groupReading} triệu tỷ`)
182-
} else {
183-
parts.push(`${groupReading} triệu`)
184-
}
185-
} else if (type === 1) {
186-
// Thousand
187-
if (needsBillionSuffix) {
188-
parts.push(`${groupReading} nghìn tỷ`)
189-
} else {
190-
parts.push(`${groupReading} nghìn`)
191-
}
192-
} else if (needsBillionSuffix) { // Units
193-
parts.push(`${groupReading} tỷ`)
194-
} else {
195-
parts.push(groupReading)
196-
}
23+
for (let i = 0; i < groups.length; i++) {
24+
const result = processGroup(groups[i], i, groups, groupTypes)
25+
if (result) {
26+
parts.push(result)
19727
}
19828
}
19929

src/read/three-digits.ts

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
import { getDigitWord } from './digits.ts'
2+
3+
/**
4+
* Read the hundreds digit
5+
*/
6+
function readHundreds(first: string, hasHundredsPosition: boolean): string {
7+
if (!hasHundredsPosition) return ''
8+
return `${getDigitWord(first)} trăm`
9+
}
10+
11+
/**
12+
* Read the tens digit
13+
*/
14+
function readTens(second: string, hasTensPosition: boolean): string {
15+
if (!hasTensPosition) return ''
16+
17+
if (second === '0') return ' lẻ'
18+
if (second === '1') return ' mười'
19+
return ` ${getDigitWord(second)} mươi`
20+
}
21+
22+
/**
23+
* Read the ones digit with special rules
24+
*/
25+
function readOnes(
26+
last: string,
27+
second: string,
28+
hasTensPosition: boolean,
29+
): string {
30+
if (!hasTensPosition) {
31+
return ` ${getDigitWord(last)}`
32+
}
33+
34+
// Apply special rules when there's a tens position
35+
if (second !== '0' && second !== '1' && last === '1') {
36+
return ' mốt'
37+
}
38+
if (last === '5' && second !== '0') {
39+
return ' lăm'
40+
}
41+
if (last !== '0') {
42+
return ` ${getDigitWord(last)}`
43+
}
44+
return ''
45+
}
46+
47+
/**
48+
* Add suffix for first group before billion
49+
*/
50+
function addFirstBeforeBillionSuffix(
51+
isFirst: boolean,
52+
isBeforeBillion: boolean,
53+
): string {
54+
return isFirst && isBeforeBillion ? ' nghìn' : ''
55+
}
56+
57+
/**
58+
* Read a 3-digit group
59+
* @param group - 3-digit string (can be 1-3 chars)
60+
* @param isFirst - is this the first group
61+
* @param isBeforeBillion - is this group before a billion group
62+
* @returns Vietnamese reading of the group
63+
*/
64+
export function readThreeDigits(
65+
group: string,
66+
isFirst: boolean,
67+
isBeforeBillion: boolean,
68+
): string {
69+
const len = group.length
70+
const first = len > 2 ? group[len - 3] : '0'
71+
const second = len > 1 ? group[len - 2] : '0'
72+
const last = group[len - 1] || '0'
73+
74+
// Handle all zeros
75+
if (first === '0' && second === '0' && last === '0') {
76+
return isFirst ? 'không' : ''
77+
}
78+
79+
let result = readHundreds(first, len > 2)
80+
81+
// If the last two digits are zero, return early
82+
if (second === '0' && last === '0') {
83+
result += addFirstBeforeBillionSuffix(isFirst, isBeforeBillion)
84+
return result.trim()
85+
}
86+
87+
result += readTens(second, len > 1)
88+
result += readOnes(last, second, len > 1)
89+
result += addFirstBeforeBillionSuffix(isFirst, isBeforeBillion)
90+
91+
return result.trim()
92+
}

0 commit comments

Comments
 (0)