2
\$\begingroup\$

I read this question Figure/number to text converter javascript implementation. Like the answer said, that version has two main issues. I tried to solve them.

The main issues with the original:

  • The assumption you can switch language by just providing the words for the main numbers is not correct, since numbers are built differently in different languages.

    My solution: I have one function per language and some global helper functions.

  • It uses too many design patterns, especially fluent APIs.

    My solution: It consists of function calls with an inner helper function for the hundreds and some outer helper functions.

Please give ome feedback on the programming itself. You don't have necessarily have to correct my Dutch ;-)

Note: I only support positive integers. It's a linguistic question, but if someone knows: 2.24 can be pronounced as twee komma vierentwintig (two point twenty four), but how do you pronounce decimal fractions starting with zero like 2.024. Is it twee komma nul vierentwintig (two point zero twenty four) or twee komma nul twee vier (two point zero two four)?

function transcribe_nl(number) {
    let NUMBER_0 = 'nul'
    let NUMBER_10 = 'tien'
    let NUMBERS_FROM_0_TO_19 = [NUMBER_0, 'een', 'twee', 'drie', 'vier', 'vijf', 'zes', 'zeven', 'acht', 'negen', NUMBER_10, 'elf', 'twaalf', 'dertien', 'veertien', 'vijftien', 'zestien', 'zeventien', 'achttien', 'negentien']
    let MULTITUDES_OF_10 = [undefined, NUMBER_10, 'twintig', 'dertig', 'veertig', 'vijftig', 'zestig', 'zeventig', 'tachtig', 'negentig']
    let NUMBER_100 = 'honderd'
    let NUMBER_1000 = 'duizend'
    let POWERS_OF_1000 = [undefined, NUMBER_1000, 'miljoen', 'miljard', 'biljoen', 'biljard', 'triljoen', 'triljard', 'quadriljoen', 'quadriljard', 'quintiljoen', 'quintiljard']

    if (number === 0) {
        return NUMBER_0
    }

    let text_full = []
    let _3_digit_group_numbers = [...split_in_digit_groups(number, 3)]
    for (let power_of_1000 = _3_digit_group_numbers.length - 1; power_of_1000 >= 0; --power_of_1000) {
        let number_3_digit_group = _3_digit_group_numbers[power_of_1000]
        
        // translate 3 digit group
        let text_3_digit_group = transcribe_xxx(number_3_digit_group)

        if (text_3_digit_group === undefined) continue

        let text_power_of_1000 = POWERS_OF_1000[power_of_1000]
        let text_xxx_power_of_1000
        if (power_of_1000 === 0) {
            text_xxx_power_of_1000 = text_3_digit_group
        } else if (power_of_1000 === 1) {
            if (number_3_digit_group === 1) {
                text_xxx_power_of_1000 = NUMBER_1000
            } else {
                text_xxx_power_of_1000 = text_3_digit_group + text_power_of_1000
            }
        } else {
            text_xxx_power_of_1000 = text_3_digit_group + ' ' + text_power_of_1000
        }

        text_full.push(text_xxx_power_of_1000)
    }

    return text_full.join(' ')

    function transcribe_xxx(number_xxx) {
        let [digit_x, digit_x0, digit_x00] = split_in_digits(number_xxx)
        
        // assemble 00-99 
        let text_xx
        let number_xx = number_xxx % 100
        if (number_xx === 0) {
            text_xx = undefined
        } else if (digit_x === 0) { // pure multiple of 10
            text_xx = MULTITUDES_OF_10[digit_x0]
        } else if (number_xx < 20) { // between 11 and 19
            text_xx = NUMBERS_FROM_0_TO_19[number_xx]
        } else { // non-pure multiple of 10 between 21 and 99
            let text_x = NUMBERS_FROM_0_TO_19[digit_x]
            let text_x0 = MULTITUDES_OF_10[digit_x0]
            let last_letter_x_1 = text_x[text_x.length - 1]
            let text_and = (/[aeiou]/gi).test(last_letter_x_1) ? 'ën' : 'en'

            text_xx = text_x + text_and + text_x0 // e.g. eenentwintig, tweeëntwintig
        }

        // assemble hundreds
        let text_x00
        if (number_xxx < 100) {
            text_x00 = undefined
        } else if (digit_x00 === 1) {
            text_x00 = NUMBER_100
        } else {
            text_x00 = NUMBERS_FROM_0_TO_19[digit_x00] + NUMBER_100
        }

        // combine 00-99 and hundreds
        let text_xxx
        if (text_x00 === undefined) {
            text_xxx = text_xx
        } else if (text_xx === undefined) {
            text_xxx = text_xxx
        } else if (number_xx <= 12) {
            text_xxx =  text_x00 + 'en' + text_xx
        } else {
            text_xxx =  text_x00 + text_xx
        }

        return text_xxx
    }
}

function get_max_power_of_1000(number) {
    return Math.floor(Math.log10(number) / 3)
}

function slice_number(number, index, end_index) {
    let size = end_index - index
    if (number < 10 ** index) return undefined
    return Math.floor(number / 10 ** index % (10 ** size))
}

function * split_in_digits(number) {
    yield * split_in_digit_groups(number, 1)
}

function * split_in_digit_groups(number, group_size) {
    while (true) {
        let unit = number % (10 ** group_size)
        yield unit
        number = (number - unit) / (10 ** group_size)
        
        if (number == 0) break
    }
}

function transcribe_en(number) {
    
}

let languages = {
    nl: transcribe_nl,
    en: transcribe_en,
}

/**
 * @param {number} number 
 * @param {string} language 
 */
function transcribe(number, language) {
    if (languages[language]) {
        languages[language](number);
    } else {
        throw new Error("Language not supported" + language);
    }
}

console.log(transcribe_nl(9876543210))
\$\endgroup\$
5
  • \$\begingroup\$ It uses too many design patterns => At face value this is meaningless. That's like asking: "when do we stop making coherent Objects?" Also it is too vague, too much of a blanket statement, "too much to unpack" to be useful. \$\endgroup\$
    – radarbob
    Commented Nov 16 at 16:06
  • \$\begingroup\$ I don't get it. Your comment on the answer I refered to: "stay away from design patterns until you really need them ... => A nice rule of thumb well said." - radarbob Aren't you seriously contradicting yourself? :-) \$\endgroup\$ Commented Nov 18 at 11:46
  • \$\begingroup\$ I see what you mean. What I mean is statements like "too many patterns" and "too many objects" imply some cosmological constants of software engineering. Just put in that correct, necessarily fixed number of, but not "too many", patterns | objects | LOC-per-method etc. and Deus ex machina! Everything will just work perfectly. What such statements actually say is the utterer does not understand such things. \$\endgroup\$
    – radarbob
    Commented Nov 19 at 17:09
  • \$\begingroup\$ "What such statements actually say is the utterer does not understand such things." - radarbob I feel you're being arrogant. \$\endgroup\$ Commented Nov 20 at 11:58
  • \$\begingroup\$ One time, at code camp (work), we got this short shrift. That literal assessment of course did not inform the design with any insight. Those "too many" classes proved to be an excellent design; it was a joy to build because we were working with functional parts rather than individual minuscule lego pieces. A high level object exposing public methods could be a construct of multiple classes - we did not code against six classes, for example, but a coherent API \$\endgroup\$
    – radarbob
    Commented Nov 21 at 1:49

1 Answer 1

3
\$\begingroup\$

meaningful identifier

    let MULTITUDES_OF_10 = [ ... ]

Typically we would name this MULTIPLES_OF_10. OTOH, POWERS_OF_1000 is a lovely name.

JSDoc

/**
 * @param {number} number 
 * @param {string} language 
 */

Ummm, wouldn't you like to start with a one-sentence description? Something like "Transcribes a number into words of the given language."

We stubbed transcribe_en(), fine. But you may as well return something non-nil, e.g. "12" instead of the more challenging "twelve".

nit: Rather than a diagnostic of "... not supportedfr", it would be better to insert an extra SPACE.

domain restrictions

I found the negative input behavior of split_in_digit_groups(-12345, 3) to be unintuitive. Each returned value copies the negative sign. It may be worth asserting non-negative, or documenting what input values are anticipated.

good use of helpers

You nicely defined several sensible helpers that are very clear, thank you.

It still seems like transcribe_xxx() could be promoted to top level and exposed to jest unit tests, rather than being hidden.

Jest tests of helpers would be an aid to the reader, and to the maintainer.

I strongly doubt that you actually tested up to the quintiljard range.

floating point

You make some adventurous assumptions about integers and about real numbers, such as multiply then divide by same constant, or vice versa, will bring the number back alive. JS instead uses FP, which introduces some well known rounding issues stemming from the fact that there's a finite number of doubles, just \$2^{64}\$ of them, and our base (ten) has an odd factor. In math we have the distributive property of multiplication over addition. In a finite field like IEEE-754, not so much.

Consider being less adventurous, and/or relying more heavily on string manipulation.

> [...split_in_digit_groups(12345e18, 3)]
[
  856, 0,   0,  0,
    0, 0, 345, 12
]
> [...split_in_digit_groups(12345e17, 3)]
[
  928, 744, 999, 999,
  999, 499, 234,   1
]

repeating fraction

Floats are not real numbers, though there is a resemblance.

Reals are dense in the unit interval -- there's an infinite amount of them, even between zero and one. (There's a lot of rationals in the unit interval, also. But in a sense there's "less" of them, as there is merely a countable infinity of them.)

So by the pigeonhole principle, trying to represent "a lot" of reals with a mere \$2^{64}\$ distinct binary values won't work out, we can't do it exactly. We will need to compromise. Let's look at a compromise. But first, get out your pocket calculator.

Turn it on, and type \$1\$ \$\div\$ \$3\$ \$=\$, and see \$0.33333 ...\$. We call that a "repeating decimal", and it goes on forever. Where did it come from? Well \$1 \,\div\, 2\$ would have given a nice clean \$0.5\$, because \$2\$ is a factor of ten and \$\frac{5}{10}\$ works out exactly. Not so with \$3\$, which is relatively prime w.r.t. \$2\$ and \$5\$. So we're faced with increasingly accurate approximations like \$\frac{3}{10}\$, \$\frac{33}{100}\$, and so on, getting ever closer but never quite arriving.

Now let's add some floats:

> .1 + .2
0.30000000000000004

What happened there?!? Same thing, pretty much, though we call this a "repeating binary" problem. Neither \$\frac{1}{10}\$ nor \$\frac{2}{10}\$ is exactly representable as a fraction having a power of \$2\$ in the denominator. And rational numbers of the form \$\frac{x}{2^n}\$ are the only numbers that IEEE-754 can represent. So something's got to give, and you see it there in the trailing digits. Just as \$\frac{1}{3}\$ lies somewhere between \$\frac{33}{100}\$ and \$\frac{34}{100}\$, for some \$x\$ we see that \$\frac{1}{10}\$ lies between \$\frac{x}{2^{53}}\$ and \$\frac{x+1}{2^{53}}\$.

Did we do something wrong? No. Everything is working according to plan. We buy into "noise" digits (bits) at the end of such numbers, it's just a part of doing business when you're dealing with floats.

application design

Given such constraints, an app author should be careful when manipulating floats. Mathematical truths about the reals won't necessarily translate into a computer context which uses floats. If you divide a number by \$3\$ and then multiply by \$3\$, you may not get the exact number back, there may be rounding error. If you add and then subtract, it might not work out exactly as you anticipated:

> .1 + .2 - .1
0.20000000000000004
>
> .1 + .2 - .2
0.10000000000000003
>
> .1 + .2 - .3
5.551115123125783e-17
>
> .1 + .2 - .1 - .2
2.7755575615628914e-17

In contrast, integers are easy. Especially if you stick to "small" integers, which go up to about \$2^{53}\$ for JS applications.

And you can always resort to expressions like BigInt(1234) or BigInt('9876543210123456') in an app that wishes to safely manipulate very large integers. Or you might choose to work with true rationals that are not constrained to have a power of two in the denominator.

\$\endgroup\$
5
  • \$\begingroup\$ Thanks for your remarks. I have a question about the last one: I intended to only support positive integers. Is this floating point issue relevant for integers? \$\endgroup\$ Commented Nov 6 at 10:00
  • \$\begingroup\$ FP1: I'm not that into the maths of floating points, but I'm willing to learn. our base (ten) has an odd factor. How I understand: The factors of the number 10 are 5 and 2, one of them is odd. Whenever you divide by such a number, the result might be inaccurate? \$\endgroup\$ Commented Nov 6 at 10:33
  • \$\begingroup\$ FP2: The answer to the original question mentions that as well, but because the program in the original question is limited to 10 digit numbers, I forgot about this. That's probably why you mention the quantiljard. It won't work. \$\endgroup\$ Commented Nov 6 at 10:33
  • \$\begingroup\$ FP3: If I would like to make it work, I'd better let the number be passed as a string. When the parameter would require a number type, it might not be possible to pass all numbers and it might already have lost precision (e.g. when it is entered by a user and converted) \$\endgroup\$ Commented Nov 6 at 10:38
  • \$\begingroup\$ Hi, many thanks for your extra information about floating points. I only see it now, because I posted my revisit of this question: codereview.stackexchange.com/questions/294440/… \$\endgroup\$ Commented Nov 16 at 12:20

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.