Dirisu Jesse

Posted on

# Going from Numbers to Words

Here I present a brief discussion of my solution to the simple numbers to word conversion problem. The solution in question primarily leverages recursion and as such this serves as a presentation on recursion in practice.

## dirisujesse / VanillaNumerals

### JS Numerals Test Solution Implementation

#### Code Files

• index.html
``````<html>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">

<body>
<div class="converter-container">
<h2>Number Converter</h3>
<div class="input-container">
<label for="num-input">Enter Number &darr;</label>
<input id="num-input" type="number">
<button id="submit-btn">convert</button>
</div>
<div class="result-container">
<p id="conversion-holder">English literal will show here</p>
</div>
</div>
<script src="index.js"></script>
<script>
let numInput = document.getElementById("num-input");
let submitBtn = document.getElementById("submit-btn");
let conversionHolder = document.getElementById("conversion-holder");
let errMessage = "The provided value appears invalid, provide a valid number to convert"
function handleSubmit(e) {
e.preventDefault();
e.stopPropagation();
let numString = numInput.value || null;
if (numString && !isNaN(+numString)) {
let englishPhrase = convertNum(numString);
if (englishPhrase !== errMessage) {
conversionHolder.innerText = englishPhrase;
} else {
}
} else {
return errMessage;
}
}
</script>
</body>
</html>
``````

The index.html file presents to the user a number input field as well as a submit button visually. The file imports the index.js file for the conversion logic, which is called within the embedded script in the click listener registered on the submit button.

• index.css
``````html {
font-size: 62.5%;
}

body {
max-height: 100vh;
max-width: 100vw;
background-color: #FFF;
color: #000;
font-family: Arial, Helvetica, sans-serif;
}

.converter-container {
position: absolute;
width: fit-content;
min-height: fit-content;
max-width: 80%;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background-color: #EFF2F5;
border: 2px dashed black;
}

h2 {
text-align: center;
font-size: 2rem;
text-transform: uppercase;
color: #007CBB;
}

.input-container {
font-size: 1.5rem;
}

.input-container * {
display: block;
margin-bottom: 0.5rem;
}

#num-input {
color: #000;
width: 100%;
border: 2px solid #007CBB;
}

#submit-btn {
color: #FFF;
background-color: #007CBB;
width: 100%;
text-transform: uppercase;
border: 0px solid transparent;
}

#conversion-holder {
font-size: 1.5rem;
text-align: center;
text-transform: capitalize;
}
``````

The index.css file provides basic styling for the html code.

• index.js
``````const units = [
'', '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 numDict = {
"0": "Zero", "1": "One",
"2": "Two", "3": "Three",
"4": "Four", "5": "Five",
"6": "Six", "7": "Seven",
"8": "Eight", "9": "Nine",
}

const errors = {
unsafeNum: "This is a pretty large number, don't expect much accuracy as our engine is not used to handling such massive figures",
isNanOrInvalid: "The provided value appears invalid, provide a valid number to convert",
unhandled: "Oops can't handle this, is this a valid number?, number may be too large",
};

// Scales correspond to lengths of numbers from thousands to centillions
// With sub scales corresponding to units, tens and hundreds subscales within the number scales
const scales = [
[ 4,  5,  6], [ 7,  8,  9], [10, 11, 12],
[13, 14, 15], [16, 17, 18], [19, 20, 21],
[22, 23, 24], [25, 26, 27], [28, 29, 30],
[31, 32, 33], [34, 35, 36], [37, 38, 39], [40, 41, 42],
[43, 44, 45], [46, 47, 48], [49, 50, 51],
[52, 53, 54], [55, 56, 57], [58, 59, 60],
[61, 62, 63], [64, 65, 66], [67, 68, 69]
];

// We handle numbers up to the centillion
const scaleNames = [
'thousand', 'million', 'billion', 'trillion', 'quadrillion', 'quintillion',
'sextillion', 'septillion', 'octillion', 'nonillion', 'decillion', 'undecillion', 'duodecillion',
'tredecillion', 'quatttuor-decillion', 'quindecillion', 'sexdecillion', 'septen-decillion',
'octodecillion', 'novemdecillion', 'vigintillion', 'centillion'
];

function convertNum(n) {
n = typeof n !== 'string' ? n.toString() : n;
// We declare upperlimit variable to check for numbers greater han native JS number bound
var upperLimit = (2 ** 53) - 1;

if (Math.abs(+n) > upperLimit) {
}
if (n && +n === 0) {
return "Zero";
}
if (!n || isNaN(+n)) {
return errors.isNanOrInvalid;
}
let isSubZero = n.startsWith('-');
// pruning the minus sign when the number is less than zero
n = isSubZero ? n.slice(1) : n;
if (n.includes('.')) {
let preDecimal, postDecimal;
let words = n.split('.');
if (+words[1]) {
preDecimal = convertPreDecimalNumber(words[0]);
postDecimal = handleDecimal(words[1]);
return `\${isSubZero ? 'Minus ' : ''}\${preDecimal || 'Zero'} point \${postDecimal}`;
} else {
preDecimal = convertPreDecimalNumber(words[0]);
return `\${isSubZero ? 'Minus ' : ''}\${preDecimal || 'Zero'}`;
}
} else {
let englishPhrase = convertPreDecimalNumber(n);
return `\${isSubZero ? 'Minus ' : ''}\${englishPhrase || 'Zero'}`;
}
}

// returns the literal names for post decimal numbers
function handleDecimal(string) {
return string.split('').map(wrd => numDict[wrd]).join(' ');
}

function convertPreDecimalNumber(strng) {
if (!+strng) {
return '';
}
// I parse strng as BigInt to handle numbers beyond native JS Number range
// Accuracy may be lost over (2**53) - 1 as JS cannot handle very large numbers
const numString = BigInt(strng).toString();
const lenNum = numString.length;
if (lenNum === 1 || +numString <= 19) {
return units[+numString];
} else if (lenNum === 2) {
return `\${tens[+numString[0]]} \${units[+numString[1]]}`.trim()
} else if (lenNum === 3) {
let [H, T] = [numString[0], +numString.slice(1)];
let tailString = convertPreDecimalNumber(T.toString());
tailString = tailString ? `and \${tailString}` : '';
return `\${units[H]} Hundred \${tailString}`.trim()
}  else {
const numScale = scales.find(it => it.includes(lenNum));
if (numScale && numScale !== undefined) {
const scaleName = scaleNames[scales.indexOf(numScale)];
if (scaleName && scaleName !== undefined) {
return handleThanHundredNums(numString, lenNum, scaleName, numScale);
}
}
return errors.unhandled;
}
}

function handleThanHundredNums(numString, lenNum, scale, numRange) {
const [minuend, startrange, midrange, _] = [numRange[0] - 1, ...numRange]
const [H, T] = [
lenNum === startrange ?
units[numString[0]] :
lenNum === midrange ?
convertPreDecimalNumber(numString.slice(0, 2)) :
convertPreDecimalNumber(numString.slice(0, 3)),
BigInt(numString.slice(lenNum - minuend)).toString()
];
const tailString = +T ? `\${+T >= 100 ? '' : 'and'} \${convertPreDecimalNumber(T.toString())}`.trim() : '';
return `\${H} \${scale} \${tailString}`.trim()
}

``````

A running example of this is to be found as here