UNPKG

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
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 ''; }