nhb-toolbox
Version:
A versatile collection of smart, efficient, and reusable utility functions, classes and types for everyday development needs.
234 lines (233 loc) • 7.52 kB
JavaScript
import { isInteger, isNonEmptyString, isNumber } from '../guards/primitives.js';
import { isNumericString } from '../guards/specials.js';
import { BN_DIGITS, ONES, ORDINAL_TO_CARDINAL, ORDINAL_UNDER_TEEN, TEENS, TENS, THOUSANDS, } from './constants.js';
import { _convertLessThanThousand } from './helpers.js';
import { normalizeNumber } from './utilities.js';
export function numberToWords(num) {
let number = Math.trunc(Number(num));
if (!Number.isFinite(number) || 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 >= 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 = _convertLessThanThousand(number % 1000, isLastGroup);
result = `${prefix} ${THOUSANDS[i]} ${result}`;
}
number = Math.floor(number / 1000);
i++;
}
const finalResult = result.trim().replace(/\s+/g, ' ');
return isNegative ? `minus ${finalResult}` : finalResult;
}
export const convertToRomanNumerals = (value) => {
let num = normalizeNumber(value);
if (!isInteger(num) || num <= 0 || num >= 4000) {
throw new RangeError('Value must be an integer and 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 (num >= value) {
result += numeral;
num -= value;
}
}
return result;
};
export const romanToInteger = (roman) => {
const romanMap = {
I: 1,
V: 5,
X: 10,
L: 50,
C: 100,
D: 500,
M: 1000,
};
if (!isNonEmptyString(roman) || !roman?.trim()) {
throw new TypeError('Input must be a non-empty string');
}
const upperRoman = roman?.toUpperCase()?.trim();
let total = 0;
let prevValue = 0;
for (let i = upperRoman.length - 1; i >= 0; i--) {
const char = upperRoman[i];
const value = romanMap[char];
if (!value) {
throw new Error(`Invalid Roman numeral character: '${char}'`);
}
if (value < prevValue) {
total -= value;
}
else {
total += value;
prevValue = value;
}
}
if (total <= 0 || total >= 4000) {
throw new RangeError('Resulting number must be between 1 and 3999');
}
if (convertToRomanNumerals(total) !== upperRoman) {
throw new Error('Invalid or malformed Roman numeral!');
}
return total;
};
export 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 ORDINAL_UNDER_TEEN[cardinal];
};
const wordNumber = isNumericString(number) || 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;
}
}
export function wordsToNumber(word) {
if (!isNonEmptyString(word))
return NaN;
const trimmed = word.trim();
if (/^[+-]?\d{1,3}(,\d{3})*(\.\d+)?$/.test(trimmed)) {
return Number(trimmed.replace(/,/g, ''));
}
if (/^[+-]?\d+(\.\d+)?$/.test(trimmed))
return Number(trimmed);
let input = trimmed.toLowerCase();
let negative = false;
input = input.replace(/^\s*(minus|negative)\s+/, () => {
negative = true;
return '';
});
input = input
.replace(/-/g, ' ')
.replace(/\s+and\s+/g, ' ')
.replace(/\s+/g, ' ')
.trim();
if (!input)
return NaN;
const onesMap = new Map(ONES.map((w, i) => [w === '' ? 'zero' : w, i]));
const teensMap = new Map(TEENS.map((w, i) => [w, 10 + i]));
const tensMap = new Map(TENS.map((w, i) => [w, i * 10]));
const scalesMap = new Map(THOUSANDS.map((w, i) => [w, i === 0 ? 1 : 1000 ** i]));
const PROTECTED_TOKENS = new Set([...ONES, ...TEENS, ...TENS, ...THOUSANDS, 'hundred', 'zero'].filter(Boolean));
const tokens = input
.split(' ')
.map((token) => {
if (ORDINAL_TO_CARDINAL[token]) {
return ORDINAL_TO_CARDINAL[token];
}
if (PROTECTED_TOKENS.has(token)) {
return token;
}
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;
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;
}
if (token === 'hundred') {
currentNumber = (currentNumber || 1) * 100;
continue;
}
if (scalesMap.has(token)) {
const scale = scalesMap.get(token);
if (scale > 1) {
total += (currentNumber || 1) * scale;
currentNumber = 0;
continue;
}
}
if (/^\d+$/.test(token)) {
currentNumber += Number(token);
continue;
}
hasInvalidToken = true;
}
if (hasInvalidToken)
return NaN;
total += currentNumber;
return negative ? -total : total;
}
export function banglaToDigit(bnDigit, forceNumber = true) {
if (!isNonEmptyString(bnDigit))
return (forceNumber ? NaN : '');
const digitStr = bnDigit.replace(/[০১২৩৪৫৬৭৮৯]/g, (d) => String(BN_DIGITS[d]));
if (forceNumber) {
return Number(digitStr
.split('')
.filter((dig) => !Number.isNaN(Number(dig)))
.join(''));
}
return digitStr;
}
export function digitToBangla(digit, preserveNonDigit = true) {
const banglaDigits = Object.keys(BN_DIGITS);
const _matchAndConvert = (value) => {
return value.replace(/\d/g, (dig) => banglaDigits[Number(dig)]);
};
if (isNumber(digit))
return _matchAndConvert(String(digit));
if (isNonEmptyString(digit)) {
const bnDigStr = _matchAndConvert(digit);
if (preserveNonDigit || isNumericString(digit)) {
return bnDigStr;
}
return bnDigStr
.split('')
.filter((dig) => banglaDigits.includes(dig))
.join('');
}
return '';
}