2
\$\begingroup\$

If the numbers 1 to 5 are written out in words: one, two, three, four, five, then there are 3 + 3 + 5 + 4 + 4 = 19 letters used in total.

If all the numbers from 1 to 1000 (one thousand) inclusive were written out in words, how many letters would be used?

NOTE: Do not count spaces or hyphens. For example, 342 (three hundred and forty-two) contains 23 letters and 115 (one hundred and fifteen) contains 20 letters. The use of "and" when writing out numbers is in compliance with British usage.

Here's my implementation in Python:

def number_to_word(n):
    """Assumes is an integer from 1 - 1000.
    Returns number in words ex: 122 --> one hundred and twenty-two."""
    # num_to_alpha contains the unique values for numbers that will be returned according to repetitive patterns
    num_to_alpha =\
        {1: 'one', 2: 'two', 3: 'three', 4: 'four', 5: 'five', 6: 'six', 7: 'seven', 8: 'eight', 9: 'nine', 10: 'ten',
         11: 'eleven', 12: 'twelve', 13: 'thirteen', 14: 'fourteen', 15: 'fifteen', 16: 'sixteen', 17: 'seventeen',
         18: 'eighteen', 19: 'nineteen', 20: 'twenty', 30: 'thirty', 40: 'forty', 50: 'fifty', 60: 'sixty',
         70: 'seventy', 80: 'eighty', 90: 'ninety', 100: 'one hundred', 1000: 'one thousand'}
    # Numbers below 21 , 100, 1000 are unique words (cannot be formed using a repetitive rule)
    if 0 < n < 21 or n == 100 or n == 1000:
        return num_to_alpha[n]
    mod = n % 10
    # Numbers in range (21 - 99) have a single rule except the multiples of 10 (formed by a single word)
    if 20 < n < 100:
        if n % 10 != 0:
            return f'{num_to_alpha[n // 10 * 10]}-{num_to_alpha[mod]}'
        return num_to_alpha[n]
    # Numbers above 100 have a single rule except the following:
    if 100 < n < 1000:
        # a) multiples of 100
        if n % 100 == 0:
            return f'{num_to_alpha[n // 100]} hundred'
        # b) numbers whose last 2 digits are above 20 and are also multiples of 10
        if not n % 100 == 0 and n % 100 > 20 and n % 10 == 0:
            return f'{num_to_alpha[n // 100]} hundred and {num_to_alpha[n % 100]}'
        # c) numbers whose last 2 digits are below 20 and not multiples of 10
        if n % 100 < 21:
            second_part = num_to_alpha[n % 100]
            return f'{num_to_alpha[n // 100]} hundred and {second_part}'
        # d) numbers whose last 2 digits are above 20 and not multiples of 10
        if n % 100 > 20:
            return f'{num_to_alpha[n // 100]} hundred and {num_to_alpha[((n % 100) // 10) * 10]}-' \
                f'{num_to_alpha[(n % 100) % 10]}'
    # To prevent counting False values
    if n <= 0 or n > 1000:
        return ''


def count():
    """Cleans numbers from spaces and hyphens and returns count."""
    all_numbers = [number_to_word(x) for x in range(1, 1001)]
    numbers_without_spaces = [number.replace(' ', '') for number in all_numbers]
    clean_numbers = [number.replace('-', '') for number in numbers_without_spaces]
    total = 0
    for clean_number in clean_numbers:
        total += len(clean_number)
    return total


if __name__ == '__main__':
    print(count())
\$\endgroup\$
1
\$\begingroup\$

num_to_alpha =\ is not Pythonic. If a statement is “incomplete”, there is no need to add a trailing \ to indicate the line is continued. An “incomplete” statement is one which contains unclosed {, [, or (, so this line could be written without the trailing \ simply by moving the { up to the previous line:

num_to_alpha = {
    1: 'one', ...

The if 0 < n < 21 or n == 100 or n == 1000: test could be written more simply as if n in num_to_alpha:. This would also handle many other simple cases where the number exists in the num_to_alpha dictionary.

You compute mod = n % 10, and then go on to test if n % 10 != 0:. You could simply test the already computed value if mod != 0:, or since non-zero values are Truthy, if mod:.

You are testing for values which cannot possibly be true due to early tests. For instance, once a number is above 100, you test:

if n % 100 == 0:
    return ...

This is followed by:

if not n % 100 == 0 and ...:

If n % 100 evaluated to True, the first return statement would have been executed. Testing not n % 100 == 0 doesn’t add any value.

At the end of the function, you explicitly check if n <= 0 or n > 1000: and return the empty string. And if THAT test fails ... what happens? None will be returned? If the number passed any of the above tests, and explicit return statement would have already returned the desired string, so if the end of the function is reached, does it matter what the return value is? Could you not unconditionally return ''. Or better: raise ValueError()


DRY: Don’t Repeat Yourself

You code is repeating the same tests. Is the number between 1 and 20? Are the last 2 digits of a 3 digit number between 1 and 20?

If you structured your function in the following manner, you’d repeat yourself less, and find it easy to extend into ten thousands, hundred thousands, millions and beyond:

  • Start with an empty string
  • If n >= 1000
    • add num_to_alpha[n // 1000] and “thousand” to the string,
    • n = n % 1000
  • If n >= 100
    • add num_to_alpha[n // 100] and “hundred” to the string,
    • n = n % 100
  • If n > 0 and the string is not empty,
    • add “and” to the string
  • If n > 20,
    • add num_to_alpha[n // 10 * 10] to the string
    • n = n % 10
  • If n > 0
    • add num_to_alpha[n] to the string
  • Return the resulting string

Since you aren’t really using the returned strings, but eventually just counting the number of letters in the resulting string, you could optimize the function to not use strings at all. Just store the number of letters of each number in the dictionary:

num_to_length = {
    1: 3, 2: 3, 3: 5, 4: 4, ...
}

and add 8, 7, and 3 instead of the strings “thousand”, “hundred” & “and”.

\$\endgroup\$
  • \$\begingroup\$ I thought initially about storing numbers instead of words however I'm too lazy to count letters for each case plus it's a lot harder to debug, in case of mistakes in calculations. Regarding the repeat tests you have a point and I thought about making separate functions for each case but did not do it at the end. \$\endgroup\$ – Emad Boctor Jul 16 at 4:26
  • \$\begingroup\$ Yup. Much harder to debug. As for being too lazy to count, that’s what computers are for: num_to_length = { n: len(word) for n, word in num_to_alpha.items() } ... but beware of “one hundred” & “one thousand” (But I wasn’t using those values in the DRY algorithm) \$\endgroup\$ – AJNeufeld Jul 16 at 4:36

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

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