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>
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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()
}

Enter fullscreen mode Exit fullscreen mode

A running example of this is to be found as here

Sentry image

See why 4M developers consider Sentry, “not bad.”

Fixing code doesn’t have to be the worst part of your day. Learn how Sentry can help.

Learn more

Top comments (0)