Home > Reflections | โฎ๏ธ โญ๏ธ
2024-06-08 | ๐ฆต๐ผ Shin ๐ Splints | ๐งฎ Number To Words ๐บโจ๏ธ
๐ง Education
- ๐๐ฆต๐ค SHIN SPLINTS for Runners: Challenges, Causes, and Rehab
- ๐ฆด๐ค๐โโ๏ธ๐งช How to Reduce Bone Injury Risks | Running Science
๐๏ธ Revisiting numberToWords
โจ While planning my solution to numberToWords
yesterday, ๐ญ I mentioned that there was a ๐ recursive feel to the problem.
๐ However, I implemented an iterative solution.
๐ก๏ธ In practice, iterative solutions are safer and ๐ tend to be more performant.
๐จ But recursive solutions can be more elegant, intelligible, and โ๏ธ easier to write.
๐ This is partly because recursion allows us to leverage the implicit function call stack provided by the language runtime.
๐ง If weโre skilled at identifying recursive problems and implementing recursive solutions, we can save ourselves the time, effort, and complexity of implementing the stack that the corresponding iterative solution will require.
๐ Hereโs my recursive solution.
const UNDER20_WORDS = [
'Zero', 'One', 'Two', 'Three', 'Four', 'Five', 'Six', 'Seven', 'Eight', 'Nine', 'Ten',
'Eleven', 'Twelve', 'Thirteen', 'Fourteen', 'Fifteen', 'Sixteen', 'Seventeen', 'Eighteen', 'Nineteen'
]
const DECADE__WORDS = [,, 'Twenty', 'Thirty', 'Forty', 'Fifty', 'Sixty', 'Seventy', 'Eighty', 'Ninety']
const nonZeroNumberToWords = (n: number): string[] => n ? [numberToWords(n)] : []
const numberToWords = (n: number): string =>
n < 2e1
? UNDER_20_WORDS[n]
: n < 1e2
? [DECADE__WORDS[Math.floor(n / 1e1)], ...nonZeroNumberToWords(n % 1e1)].join(' ')
: n < 1e3
? [numberToWords(Math.floor(n / 1e2)), 'Hundred', ...nonZeroNumberToWords(n % 1e2)].join(' ')
: n < 1e6
? [numberToWords(Math.floor(n / 1e3)), 'Thousand', ...nonZeroNumberToWords(n % 1e3)].join(' ')
: n < 1e9
? [numberToWords(Math.floor(n / 1e6)), 'Million', ...nonZeroNumberToWords(n % 1e6)].join(' ')
: [numberToWords(Math.floor(n / 1e9)), 'Billion', ...nonZeroNumberToWords(n % 1e9)].join(' ')
๐ง If we squint at the ๐ recursive implementation, we may notice some ๐ฏโโ๏ธ repetition.
๐ก We can leverage the common pattern to derive a more โ๏ธ data-driven implementation.
โจ This version highlights the ๐ค sparsity of unique logic required.
๐งฎ A couple of ๐ lookup tables, some ๐ฆ threshold comparisons, โ division, ๐งต string concatenation, and ๐ recursion are all we need to convert ๐ข numbers to ๐ฃ๏ธ English words. ๐
I probably wouldnโt go this far in an interview, though. ๐ฌ
const UNDER20_WORDS = [
'Zero', 'One', 'Two', 'Three', 'Four', 'Five', 'Six', 'Seven', 'Eight', 'Nine', 'Ten',
'Eleven', 'Twelve', 'Thirteen', 'Fourteen', 'Fifteen', 'Sixteen', 'Seventeen', 'Eighteen', 'Nineteen'
]
const DECADE__WORDS = [,, 'Twenty', 'Thirty', 'Forty', 'Fifty', 'Sixty', 'Seventy', 'Eighty', 'Ninety']
const PATTERN = {
2e1: { divisor: 1e0, lookup: n => UNDER20_WORDS[n], suffix: [] },
1e2: { divisor: 1e1, lookup: n => DECADE__WORDS[n], suffix: [] },
1e3: { divisor: 1e2, lookup: n => numberToWords(n), suffix: ['Hundred'] },
1e6: { divisor: 1e3, lookup: n => numberToWords(n), suffix: ['Thousand'] },
1e9: { divisor: 1e6, lookup: n => numberToWords(n), suffix: ['Million'] },
5e9: { divisor: 1e9, lookup: n => numberToWords(n), suffix: ['Billion'] },
}
const THRESHOLDS = Object.keys(PATTERN)
const nonZero = (num: number): string[] => num ? [numberToWords(num)] : []
function numberToWords(n: number): string {
const { suffix, lookup, divisor } = PATTERN[THRESHOLDS.find(threshold => n < +threshold)]
return [lookup(Math.floor(n / divisor)), ...suffix, ...nonZero(n % divisor)].join(' ')
}