nhb-toolbox
Version:
A versatile collection of smart, efficient, and reusable utility functions and classes for everyday development needs.
225 lines (224 loc) • 7.86 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.convertToRomanNumerals = void 0;
exports.numberToWords = numberToWords;
exports.numberToWordsOrdinal = numberToWordsOrdinal;
exports.wordsToNumber = wordsToNumber;
const primitives_1 = require("../guards/primitives");
const specials_1 = require("../guards/specials");
const constants_1 = require("./constants");
const helpers_1 = require("./helpers");
/**
* * Converts a numeric value into its corresponding English word representation.
* @warning ***Supports numeric values up to `10e19` or `10^20` (one hundred quintillion).***
* @warning ***Decimal values are ignored; only the integer part is converted.***
* @param number - The number to convert into words.
* @returns The number converted in words.
*/
function numberToWords(num) {
let number = Math.trunc(Number(num));
if (!Number.isFinite(number) || isNaN(number)) {
return 'Invalid Number!';
}
const isNegative = number < 0;
if (number === 0)
return 'zero';
number = Math.abs(number);
let i = 0;
let result = '';
while (number > 0) {
if (i >= constants_1.THOUSANDS.length) {
return `Number exceeds supported range (max is 10e19 aka 10^20)`;
}
if (number % 1000 !== 0) {
const isLastGroup = i === 0 && number % 100 < 100;
const prefix = (0, helpers_1._convertLessThanThousand)(number % 1000, isLastGroup);
result = `${prefix} ${constants_1.THOUSANDS[i]} ${result}`;
}
number = Math.floor(number / 1000);
i++;
}
const finalResult = result.trim().replace(/\s+/g, ' ');
return isNegative ? `minus ${finalResult}` : finalResult;
}
/**
* * Converts a number to a Roman numeral.
* @param num - The number to convert. Number must be `between 1 and 3999`.
* @returns The Roman numeral representation.
*
* @example convertToRomanNumerals(29) → "XXIX"
*/
const convertToRomanNumerals = (num) => {
let number = Number(num);
if (number <= 0 || number >= 4000)
throw new RangeError('Number must be between 1 and 3999');
const romanMap = [
[1000, 'M'],
[900, 'CM'],
[500, 'D'],
[400, 'CD'],
[100, 'C'],
[90, 'XC'],
[50, 'L'],
[40, 'XL'],
[10, 'X'],
[9, 'IX'],
[5, 'V'],
[4, 'IV'],
[1, 'I'],
];
let result = '';
for (const [value, numeral] of romanMap) {
while (number >= value) {
result += numeral;
number -= value;
}
}
return result;
};
exports.convertToRomanNumerals = convertToRomanNumerals;
/**
* * Converts a number, numeric string, or cardinal word string into its ordinal word representation.
*
* @param number - A number (e.g. `42`), numeric string (e.g. `"42"`), or cardinal word (e.g. `"forty-two"`).
* @returns The ordinal word form (always in lowercase) of the input.
*
* @example
* numberToWordsOrdinal(1); // "first"
* numberToWordsOrdinal("23"); // "twenty-third"
* numberToWordsOrdinal("twenty-three"); // "twenty-third"
*/
function numberToWordsOrdinal(number) {
const TEEN_OR_HUNDRED = /(teen|hundred|thousand|(m|b|tr|quadr)illion)$/;
const UNDER_TEEN = /(zero|one|two|three|four|five|six|seven|eight|nine|ten|eleven|twelve)$/;
const _fixUnderTeen = (cardinal) => {
return constants_1.ORDINAL_UNDER_TEEN[cardinal];
};
const wordNumber = (0, specials_1.isNumericString)(number) || (0, primitives_1.isNumber)(number) ?
numberToWords(number)
: number?.trim()?.toLowerCase();
if (TEEN_OR_HUNDRED.test(wordNumber)) {
return wordNumber + 'th';
}
else if (/y$/.test(wordNumber)) {
return wordNumber.replace(/y$/, 'ieth');
}
else if (UNDER_TEEN.test(wordNumber)) {
return wordNumber.replace(UNDER_TEEN, _fixUnderTeen);
}
else {
return wordNumber;
}
}
/**
* * Convert an English cardinal/ordinal word string into a number.
*
* - Accepts hyphenated words, "and", ordinals (first, second, etc.), negatives, and large scales (thousand, million etc.).
*
* @example
* wordsToNumber('forty-two') // 42
* wordsToNumber('one hundred and seven') // 107
* wordsToNumber('two thousand three hundred') // 2300
* wordsToNumber('twenty-first') // 21
* wordsToNumber('negative five') // -5
*
* @param word - A human readable number (cardinal or ordinal) in words
* @returns Numeric value of the word or NaN if cannot parse
*
* @remarks
* **NOTE** - *For very large numbers (e.g. more than quintillion) results may not always be correct.*
*/
function wordsToNumber(word) {
if (!word || typeof word !== 'string')
return NaN;
const trimmed = word.trim();
// Handle direct numeric strings with commas
if (/^[+-]?\d{1,3}(,\d{3})*(\.\d+)?$/.test(trimmed)) {
return Number(trimmed.replace(/,/g, ''));
}
// Handle simple numeric strings
if (/^[+-]?\d+(\.\d+)?$/.test(trimmed))
return Number(trimmed);
let input = trimmed.toLowerCase();
// Handle negative prefix words
let negative = false;
input = input.replace(/^\s*(minus|negative)\s+/, () => {
negative = true;
return '';
});
// Normalize: replace hyphens; remove "and"
input = input
.replace(/-/g, ' ')
.replace(/\s+and\s+/g, ' ')
.replace(/\s+/g, ' ')
.trim();
if (!input)
return NaN;
// build lookup maps from arrays
const onesMap = new Map(constants_1.ONES.map((w, i) => [w === '' ? 'zero' : w, i]));
const teensMap = new Map(constants_1.TEENS.map((w, i) => [w, 10 + i]));
const tensMap = new Map(constants_1.TENS.map((w, i) => [w, i * 10]));
const scalesMap = new Map(constants_1.THOUSANDS.map((w, i) => [w, i === 0 ? 1 : 1000 ** i]));
const PROTECTED_TOKENS = new Set([...constants_1.ONES, ...constants_1.TEENS, ...constants_1.TENS, ...constants_1.THOUSANDS, 'hundred', 'zero'].filter(Boolean));
// Handle ordinal forms
const tokens = input
.split(' ')
.map((token) => {
if (constants_1.ORDINAL_TO_CARDINAL[token]) {
return constants_1.ORDINAL_TO_CARDINAL[token];
}
if (PROTECTED_TOKENS.has(token)) {
return token;
}
// Only strip suffixes if not protected
return token.replace(/(teenth|tieth|ieth|th|st|nd|rd)$/i, '');
})
.filter(Boolean);
if (tokens.length === 0)
return NaN;
let total = 0;
let currentNumber = 0;
let hasInvalidToken = false;
for (const token of tokens) {
if (hasInvalidToken)
break;
// Handle direct maps
if (onesMap.has(token)) {
currentNumber += onesMap.get(token);
continue;
}
if (teensMap.has(token)) {
currentNumber += teensMap.get(token);
continue;
}
if (tensMap.has(token)) {
currentNumber += tensMap.get(token);
continue;
}
// Handle 'hundred' multiplier
if (token === 'hundred') {
currentNumber = (currentNumber || 1) * 100;
continue;
}
// Handle scale words (thousand, million, etc.)
if (scalesMap.has(token)) {
const scale = scalesMap.get(token);
if (scale > 1) {
total += (currentNumber || 1) * scale;
currentNumber = 0;
continue;
}
}
// Handle numeric tokens
if (/^\d+$/.test(token)) {
currentNumber += Number(token);
continue;
}
hasInvalidToken = true;
}
if (hasInvalidToken)
return NaN;
// Add any remaining value
total += currentNumber;
return negative ? -total : total;
}