DEV Community

Dirisu Jesse
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.

GitHub logo dirisujesse / VanillaNumerals

JS Numerals Test Solution Implementation

Code Files

  • index.html
<html>
    <head>
        <meta charset="utf-8">
        <meta name="viewport" content="width=device-width, initial-scale=1">
        <link rel="stylesheet" href="index.css">
    </head>

    <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"
            submitBtn.addEventListener("click", handleSubmit)
            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 {
                        alert(errMessage);
                    }
                } else {
                    alert(errMessage);
                    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;
    padding: 3rem;
    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%);
    padding: 2rem;
    background-color: #EFF2F5;
    border-radius: 20px;
    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;
    padding: 1rem;
    width: 100%;
    box-shadow: none;
    border-radius: 5px;
    border: 2px solid #007CBB;
}

#submit-btn {
    color: #FFF;
    background-color: #007CBB;
    padding: 1rem;
    width: 100%;
    box-shadow: none;
    border-radius: 5px;
    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) {
        alert(errors.unsafeNum)
    }
    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

Top comments (0)