spell-vn-number
Version:
Vietnamese number speller
322 lines (321 loc) • 11.7 kB
JavaScript
import { InvalidFormatError, InvalidNumberError, SpellerConfig } from './types';
import { cleanInputNumber, handleRedundantZeros } from './utils';
/**
* Process a section of the number by splitting it into groups and spelling each group
* @param config SpellerConfig instance
* @param numberStr The number string to process
* @returns Array of spelled parts
*/
function processPart(config, numberStr) {
if (numberStr === '') {
return [];
}
var mod = numberStr.length % config.UNIT_EACH_GROUP.length;
// Add zeros to the beginning if length is not divisible by UNIT_EACH_GROUP.length
var paddedNumber = numberStr;
if (mod !== 0) {
var offset = config.UNIT_EACH_GROUP.length - mod;
paddedNumber = '0'.repeat(offset) + numberStr;
}
var arrNum = paddedNumber.split('');
var numbSpelled = [];
var totalDigitGroup = arrNum.length / config.UNIT_EACH_GROUP.length; // Not odd because already padded
var periodGroupSize = config.UNIT_GROUP.length;
var periodGroupMod = totalDigitGroup % periodGroupSize;
var totalPeriodGroup = periodGroupMod === 0
? totalDigitGroup / periodGroupSize
: Math.floor(totalDigitGroup / periodGroupSize) + 1;
var groupEachPeriodIndex = periodGroupMod === 0 ? periodGroupMod : periodGroupSize - periodGroupMod;
var isFirst = true;
var i = 0;
var remainingGroups = totalPeriodGroup;
while (remainingGroups > 0) {
// THOUSAND/MILLION/BILLION (Unit of UNIT_GROUP)
if (!isFirst) {
// Add unit for the group
var unit = config.UNIT_OF_GROUP[config.UNIT_GROUP[groupEachPeriodIndex]];
numbSpelled.push(unit);
}
for (; groupEachPeriodIndex < config.UNIT_GROUP.length; groupEachPeriodIndex++) {
// Process three digits at a time
var arrNumb = [arrNum[i], arrNum[i + 1], arrNum[i + 2]];
i += 3;
if (isFirst) {
isFirst = false;
var firstSpelled = firstSpellThreeDigit(config, arrNumb, groupEachPeriodIndex);
for (var j = 0; j < firstSpelled.length; j++) {
numbSpelled.push(firstSpelled[j]);
}
}
else {
var spelled = spellThreeDigit(config, arrNumb, groupEachPeriodIndex);
for (var j = 0; j < spelled.length; j++) {
numbSpelled.push(spelled[j]);
}
}
}
remainingGroups--;
groupEachPeriodIndex = 0;
}
return numbSpelled;
}
/**
* Spell the first three digits of a number
* @param config SpellerConfig instance
* @param arrNumb Array of three digit characters
* @param groupEachPeriodIndex Index of the unit group
* @returns Array of spelled parts
*/
function firstSpellThreeDigit(config, arrNumb, groupEachPeriodIndex) {
var spelled = [];
var isFirst = true;
for (var atIndex = 0; atIndex < arrNumb.length; atIndex++) {
if (isFirst && arrNumb[atIndex] !== '0') {
isFirst = false;
}
if (!isFirst) {
var specialSpelled = spellSpecialDigit(config, arrNumb, atIndex, groupEachPeriodIndex);
for (var j = 0; j < specialSpelled.length; j++) {
spelled.push(specialSpelled[j]);
}
}
}
// If all digits are zero, return "không"
if (spelled.length === 0) {
spelled.push(config.digits['0']);
}
return spelled;
}
/**
* Spell three digits of a number
* @param config SpellerConfig instance
* @param arrNumb Array of three digit characters
* @param groupEachPeriodIndex Index of the unit group
* @returns Array of spelled parts
*/
function spellThreeDigit(config, arrNumb, groupEachPeriodIndex) {
var spelled = [];
for (var atIndex = 0; atIndex < arrNumb.length; atIndex++) {
var specialSpelled = spellSpecialDigit(config, arrNumb, atIndex, groupEachPeriodIndex);
for (var j = 0; j < specialSpelled.length; j++) {
spelled.push(specialSpelled[j]);
}
}
return spelled;
}
/**
* Spell a special digit based on its position and context
* @param config SpellerConfig instance
* @param arrNumb Array of three digit characters
* @param atIndex Index within the three-digit group
* @param groupEachPeriodIndex Index of the unit group
* @returns Array of spelled parts
*/
function spellSpecialDigit(config, arrNumb, atIndex, groupEachPeriodIndex) {
var currentNumb = arrNumb[atIndex];
var parts = [];
// 1.SPELL currentNumb
switch (currentNumb) {
case '0':
if (atIndex === config.AT_UNIT) {
// UNIT position
// Empty for both cases
}
else if (atIndex === config.AT_TEN) {
// TENS position
if (arrNumb[atIndex + 1] !== '0') {
parts.push(config.oddText);
}
}
else {
// HUNDREDS position
if (arrNumb[atIndex + 1] === '0' && arrNumb[atIndex + 2] === '0') {
// Empty
}
else {
parts.push(config.digits[currentNumb]);
}
}
break;
case '1':
if (atIndex === config.AT_UNIT) {
// UNIT position
var previousNumb = arrNumb[atIndex - 1];
if (previousNumb !== '0' && previousNumb !== '1') {
parts.push(config.oneToneText);
}
else {
parts.push(config.digits[currentNumb]);
}
}
else if (atIndex === config.AT_TEN) {
// TENS position
parts.push(config.tenText);
}
else {
// HUNDREDS position
parts.push(config.digits[currentNumb]);
}
break;
case '4':
if (atIndex === config.AT_UNIT) {
// UNIT position
if (arrNumb[atIndex - 1] !== '0' && arrNumb[atIndex - 1] !== '1') {
parts.push(config.fourToneText);
}
else {
parts.push(config.digits[currentNumb]);
}
}
else {
// TENS or HUNDREDS position
parts.push(config.digits[currentNumb]);
}
break;
case '5':
if (atIndex === config.AT_UNIT) {
// UNIT position
if (arrNumb[atIndex - 1] !== '0') {
parts.push(config.fiveToneText);
}
else {
parts.push(config.digits[currentNumb]);
}
}
else {
// TENS or HUNDREDS position
parts.push(config.digits[currentNumb]);
}
break;
default:
// For all other digits
parts.push(config.digits[currentNumb]);
break;
}
// 2.ADD UNIT FOR DIGIT
var groupName = config.UNIT_GROUP[groupEachPeriodIndex];
if (parts.length > 0) {
// parts is not empty
if (!(currentNumb === '1' && atIndex === config.AT_TEN) &&
!(parts[0] === config.oddText && atIndex === config.AT_TEN)) {
// Number 1 is not read as "mười mươi" && number 0 is not read as "lẻ mươi"
var unit = config.UNIT_GROUP_MAPPER[groupName][atIndex];
if (unit !== '') {
parts.push(unit);
}
}
}
else if (atIndex === config.AT_UNIT &&
currentNumb === '0' &&
(arrNumb[atIndex - 1] !== '0' || arrNumb[atIndex - 2] !== '0')) {
// Zero in unit position -> this reads the unit for the whole group (3 digits -> UNIT_EACH_GROUP.length).
// (hundred & tens are not both zero)
var unit = config.UNIT_GROUP_MAPPER[groupName][atIndex];
if (unit !== '') {
parts.push(unit);
}
}
return parts;
}
/**
* Parse a number string into structured number data
* @param config SpellerConfig instance
* @param input InputNumber
* @returns Structured number data
*/
function parseNumberData(config, input) {
// Clean and validate input
var numberStr = cleanInputNumber(input, config);
// Handle negative sign
var isNegative = numberStr.startsWith(config.negativeSign);
numberStr = isNegative ? numberStr.substring(config.negativeSign.length) : numberStr;
// Trim redundant zeros
numberStr = handleRedundantZeros(config, numberStr);
// Split into integral and fractional parts
var pointPos = numberStr.indexOf(config.decimalPoint);
var integralPart = pointPos === -1 ? numberStr : numberStr.substring(0, pointPos);
var fractionalPart = pointPos === -1 ? '' : numberStr.substring(pointPos + 1);
return {
isNegative: isNegative,
integralPart: integralPart,
fractionalPart: fractionalPart,
};
}
/**
* Main function to spell a Vietnamese number
* @param config SpellerConfig instance
* @param input Number to spell
* @returns Vietnamese spelling of the number
*/
export function spellVnNumber(config, input) {
// Parse the number
var numberData = parseNumberData(config, input);
// Spell out each part
var numbSpelled = [];
// Add negative sign if needed
if (numberData.isNegative) {
numbSpelled.push(config.negativeText);
}
// Process integral part
var integralSpelling = processPart(config, numberData.integralPart);
for (var i = 0; i < integralSpelling.length; i++) {
numbSpelled.push(integralSpelling[i]);
}
// Process fractional part if exists
if (numberData.fractionalPart.length > 0) {
numbSpelled.push(config.pointText);
var fractionalSpelling = processPart(config, numberData.fractionalPart);
for (var i = 0; i < fractionalSpelling.length; i++) {
numbSpelled.push(fractionalSpelling[i]);
}
}
// Join all parts with the separator
var result = numbSpelled.join(config.separator);
// Capitalize the first letter if capitalizeInitial is true
if (config.capitalizeInitial) {
result = result.charAt(0).toUpperCase() + result.slice(1);
}
// After joining all parts, append the currency unit if provided
if (config.currencyUnit) {
result += " ".concat(config.currencyUnit);
}
return result;
}
/**
* Convenience function to spell a Vietnamese number with default config
* @param input Number to spell
* @returns Vietnamese spelling of the number
*/
export function spell(input) {
var config = new SpellerConfig();
return spellVnNumber(config, input);
}
/**
* Convenience function to spell a Vietnamese number with default value on error
* @param input Number to spell
* @param subConfig Partial<SpellerConfig>
* @param defaultOnError
* @returns Vietnamese spelling of the number
*/
export function spellOrDefault(input, subConfig, defaultOnError) {
if (subConfig === void 0) { subConfig = {}; }
try {
var config = new SpellerConfig(subConfig);
return spellVnNumber(config, input);
}
catch (err) {
if (defaultOnError === undefined) {
throw err;
}
if (err instanceof InvalidFormatError) {
console.warn('Định dạng input không hợp lệ');
}
else if (err instanceof InvalidNumberError) {
console.warn('Số không hợp lệ');
}
else {
console.error(err);
}
return defaultOnError;
}
}