@technobuddha/library
Version:
A large library of useful functions
260 lines (227 loc) • 10.5 kB
text/typescript
import isFinite from 'lodash/isFinite';
import isNaN from 'lodash/isNaN';
import { empty, space } from '../constants';
export type Options = OptionsCardinal & OptionsIllion;
export type OptionsCardinal = {
/** The number of groups to output, each group consists of three digits. */
groups?: number;
/** Use numbers instead of words for the group value, the group name is still output as text */
digits?: boolean;
};
export type OptionsIllion = {
/** Word to place after the hundreds. "one hundred and one" vs. "one hundred one" */
and?: string;
/** Places a character between the tens units and the ones units. "twenty-one" vs. "twenty one" */
hyphen?: string;
};
const ones = [ 'zero',
'one',
'two',
'three',
'four',
'five',
'six',
'seven',
'eight',
'nine',
'ten',
'eleven',
'twelve',
'thirteen',
'fourteen',
'fifteen',
'sixteen',
'seventeen',
'eighteen',
'nineteen' ];
const tens = [ 'twenty', 'thirty', 'forty', 'fifty', 'sixty', 'seventy', 'eighty', 'ninety' ];
const ZERO = 0;
const TEN = 10;
const TWENTY = 20;
const ONE_HUNDRED = 100;
/**
* Convert a number into text (the cardinal number)
*
* @remark There is no limit to the numbers that can be expressed, however Javascript/Typescript can only represent numbers
* up to uncentillions (1e308).
*
* @param input The number
* @param __namedParameters see {@link Options}
* @returns The number spelled out
*
* @default groups Infinity
* @default digits false
* @default and (empty)
* @default hyphen (space)
*/
export function cardinal(input: number, { groups = Infinity, digits = false, ...options }: Options = {}): string {
const words = [] as (string | string[])[];
if(isNaN(input)) { words.push('not a number'); } else {
if(input < ZERO) {
words.push('negative');
input = -input;
}
if(isFinite(input)) {
if(input === 0) {
words.push(ones[0]);
} else {
let { mantissa, exponent } = breakdown(input, groups);
while(Number.parseInt(mantissa, 10) > 0 && exponent >= 0 && groups-- > 0) {
let word: string | null;
let quantity: number;
({ quantity, mantissa, exponent, word } = illion(mantissa, exponent));
if(quantity) {
if(digits)
words.push(quantity.toString());
else
words.push(ordinal1000(quantity, options));
if(word) words.push(word);
}
}
}
} else { words.push('infinity'); }
}
return words.flat().join(space);
}
function breakdown(value: number, groups: number): { mantissa: string; exponent: number } {
let [ m, e ] = value.toExponential(15).split('e'); // - 1 because toExponential returns 1 digit before the decimal point
let mantissa = m.replace('.', empty);
let exponent = Number.parseInt(e, 10); // number of digits = (exponent + 1)
const groupCount = Math.floor(exponent / 3) + 1;
if(groupCount > groups) {
const digits = (groupCount * 3) - (2 - exponent % 3);
if(digits < 16) {
([ m, e ] = value.toExponential(digits - 2).split('e'));
mantissa = m.replace('.', empty);
exponent = Number.parseInt(e, 10);
}
}
return { mantissa, exponent };
}
function ordinal1000(input: number, { and, hyphen = space }: OptionsIllion): string[] {
const words = [] as string[];
if(input >= ONE_HUNDRED) {
words.push(ones[Math.floor(input / ONE_HUNDRED)], 'hundred');
input = input % ONE_HUNDRED;
if(and && input > ZERO) words.push(and);
}
if(input > ZERO) {
if(input < TWENTY) words.push(ones[input]); else if(input % TEN === ZERO)
words.push(tens[Math.floor(input / TEN) - 2]);
else
words.push(tens[Math.floor(input / TEN) - 2] + hyphen + ones[input % TEN]);
}
return words;
}
type IllionReturn = {
quantity: number;
mantissa: string;
exponent: number;
word: string | null;
};
function illion(mantissa: string, exponent: number): IllionReturn {
let factor = Math.floor((exponent - 3) / 3);
let quantity = 0;
switch(exponent - ((factor * 3) + 3)) {
case 0: quantity = Number.parseInt(mantissa.slice(0, 1), 10); mantissa = mantissa.slice(1); exponent -= 1; break;
case 1: quantity = Number.parseInt(mantissa.slice(0, 2), 10); mantissa = mantissa.slice(2); exponent -= 2; break;
case 2: quantity = Number.parseInt(mantissa.slice(0, 3), 10); mantissa = mantissa.slice(3); exponent -= 3; break;
}
if(factor < 0) return { quantity, mantissa, exponent, word: null };
if(factor === 0) return { quantity, mantissa, exponent, word: 'thousand' };
let word = 'on';
while(factor > 0) {
let a = false; // ones; use the prefixed form; tens change end from 'i' to 'a'
let s = false; // ones: tre => tres; se => ses
let x = false; // ones: tre => tres; se => sex
let m = false; // ones: septe = septem; nove => novem
let n = false; // ones: septe = septen; nove => noven
const factor0 = Math.floor(factor / 1) % 10;
const factor1 = Math.floor(factor / 10) % 10;
const factor2 = Math.floor(factor / 100) % 10;
word = `lli${word}`;
// hundreds
switch(factor2) {
case 1: word = `centi${word}`; a = true; s = false; x = true; m = false; n = true; break;
case 2: word = `ducenti${word}`; a = true; s = false; x = false; m = false; n = true; break;
case 3: word = `trecenti${word}`; a = true; s = true; x = false; m = false; n = true; break;
case 4: word = `quadringenti${word}`; a = true; s = true; x = false; m = false; n = true; break;
case 5: word = `quingenti${word}`; a = true; s = true; x = false; m = false; n = true; break;
case 6: word = `sescenti${word}`; a = true; s = false; x = false; m = false; n = true; break;
case 7: word = `septingenti${word}`; a = true; s = false; x = false; m = false; n = true; break;
case 8: word = `octingenti${word}`; a = true; s = false; x = true; m = true; n = false; break;
case 9: word = `nongenti${word}`; a = true; s = false; x = false; m = false; n = false; break;
}
// tens
switch(factor1) {
case 1: word = `deci${word}`; a = true; s = false; x = false; m = false; n = true; break;
case 2: word = `viginti${word}`; a = true; s = true; x = false; m = true; n = false; break;
case 3: word = (a ? 'triginta' : 'triginti') + word; a = true; s = true; x = false; m = false; n = true; break;
case 4: word = (a ? 'quadraginta' : 'quadraginti') + word; a = true; s = true; x = false; m = false; n = true; break;
case 5: word = (a ? 'quinquaginta' : 'quinquaginti') + word; a = true; s = true; x = false; m = false; n = true; break;
case 6: word = (a ? 'sexaginta' : 'sexaginti') + word; a = true; s = false; x = false; m = false; n = false; break;
case 7: word = (a ? 'septuaginta' : 'septuaginti') + word; a = true; s = false; x = false; m = false; n = true; break;
case 8: word = (a ? 'octoginta' : 'octoginti') + word; a = true; s = false; x = true; m = true; n = false; break;
case 9: word = (a ? 'nonaginta' : 'nonginti') + word; a = true; s = false; x = false; m = false; n = false; break;
}
// ones
if(a) {
switch(factor0) {
case 1: word = `un${word}`; break;
case 2: word = `duo${word}`; break;
case 3: word = (s ? 'tres' : x ? 'tres' : 'tre') + word; break;
case 4: word = `quattuor${word}`; break;
case 5: word = `quinqua${word}`; break;
case 6: word = (s ? 'ses' : x ? 'sex' : 'se') + word; break;
case 7: word = (n ? 'septen' : m ? 'septem' : 'septe') + word; break;
case 8: word = `octo${word}`; break;
case 9: word = (n ? 'noven' : m ? 'novem' : 'nove') + word; break;
}
} else {
switch(factor0) {
case 0: word = `ni${word}`; break;
case 1: word = `mi${word}`; break;
case 2: word = `bi${word}`; break;
case 3: word = `tri${word}`; break;
case 4: word = `quadri${word}`; break;
case 5: word = `quniti${word}`; break;
case 6: word = `sexti${word}`; break;
case 7: word = `septi${word}`; break;
case 8: word = `octi${word}`; break;
case 9: word = `noni${word}`; break;
}
}
factor = Math.floor(factor / 1000);
}
return { quantity, mantissa, exponent, word };
}
/**
* Get the spelled out word for an exponent
*
* @remarks This is only using the exponent, There is no limit to the numbers this function can represents, however Javascript/Typescript can only represent
* numbers up to 1e308, which limits the numbers that this method can represent to 10^10^308 which is really really big.
*
* @example 6 is "million"
* @example 303 is "centillion"
* @param exponent The exponent to convert
* @returns Order of Magnitude as text
*/
export function orderOfMagnitude(exponent: number): string | null {
return illion('000', exponent).word;
}
/**
* Get a short description of a number
*
* @remarks this is a shortcut to calling cardinal with options {groups: 1, digits: true}
*
* @example 1000000 "1 million"
* @example 101323847382459 "101 trillion"
*
* @param input number to convert
* @param options see {@link OptionsIllion}
* @return number as text
*/
export function summarize(input: number, options: OptionsIllion = {}): string {
return cardinal(input, { groups: 1, digits: true, ...options });
}
export default cardinal;