Say in Coffeescript: Complete Solution & Deep Dive Guide

a close up of a computer screen with code on it

The Complete Guide to Converting Numbers to Words in CoffeeScript

Converting numbers to their English word equivalents in CoffeeScript is a classic programming challenge that involves string manipulation, array processing, and logical chunking. This guide breaks down the algorithm, from handling trillions to managing unique cases like "thirteen," providing a complete solution for developers.


You’ve seen it on bank checks, in financial reports, and heard it in automated voice systems. The task of turning 1,234,567 into "one million, two hundred thirty-four thousand, five hundred sixty-seven" is a fundamental challenge that bridges the gap between raw data and human-readable information. It seems simple at first glance, but the edge cases and logical branching can quickly become a tangled mess.

Many developers, when first encountering this problem from the kodikra.com exclusive curriculum, try to tackle it with a single, monolithic function, only to get lost in a forest of `if-else` statements. The secret isn't brute force; it's an elegant, scalable strategy. This article will not only give you a working solution in CoffeeScript but will deconstruct the "divide and conquer" algorithm that makes it possible, turning a seemingly monumental task into a series of simple, repeatable steps.

What is the Number-to-Word Conversion Problem?

At its core, the number-to-word conversion problem is an exercise in algorithmic thinking and base-10 arithmetic. The goal is to write a program that accepts a non-negative integer and returns its full English word representation as a string. For this guide, we will adhere to the requirements of the kodikra learning path, which specifies handling all integers from 0 up to 999,999,999,999 (nine hundred ninety-nine billion, nine hundred ninety-nine million, nine hundred ninety-nine thousand, nine hundred ninety-nine).

This task requires us to codify the grammatical rules of English numeration. We must account for unique names for numbers 0-19, the pattern for tens (twenty, thirty, forty), and the scaling words that denote magnitude: "thousand," "million," and "billion." A robust solution must be able to assemble these components correctly for any number within the specified range.

Why is This a Foundational Programming Challenge?

Solving this problem demonstrates mastery over several key programming concepts, making it a staple in technical interviews and coding bootcamps. It's not just about getting the right output; it's about building a clean, maintainable, and scalable algorithm.

  • Algorithmic Decomposition: It forces you to break a large, complex problem into smaller, manageable sub-problems (e.g., converting a 3-digit number, then applying a scale).
  • String and Array Manipulation: The solution heavily relies on slicing strings, mapping over arrays, filtering empty results, and joining them back together—common tasks in any language.
  • Handling Edge Cases: A good solution must gracefully handle inputs like 0, numbers with internal zeros (like 1,000,001), and the upper/lower bounds of the input range.
  • Code Readability: A well-structured solution with helper functions is far more readable and maintainable than a single, massive function. CoffeeScript's syntactic sugar can make this particularly elegant.

Applications for this logic are widespread, including financial software for writing checks, accessibility tools that read numbers aloud for visually impaired users (text-to-speech), and automated reporting systems that require formal, human-readable output.


How to Design the Conversion Algorithm

The most effective strategy is to "divide and conquer." Instead of viewing 1234567890 as one giant number, we can treat it as a sequence of 3-digit chunks. Each chunk can be processed using the same logic, and then a "scale" word can be appended based on its position.

The core idea is to process the number from right to left, in groups of three digits.

  1. Validate Input: First, check if the number is within the acceptable range (0 to 999,999,999,999) and handle the simplest case: if the number is 0, return "zero".
  2. Chunk the Number: Convert the number to a string. Starting from the right, slice this string into chunks of up to three digits. For example, 1234567 becomes ["1", "234", "567"].
  3. Process Each Chunk: For each 3-digit chunk, convert it into its English word form. For example, "567" becomes "five hundred sixty-seven". A chunk like "001" becomes "one", and "000" becomes an empty string.
  4. Apply Scale: Based on the chunk's position from the right, append the correct scale word. The first chunk (rightmost) has no scale. The second has "thousand". The third has "million", and the fourth has "billion".
  5. Combine and Clean: Join all the processed, scaled chunks together. You'll need to filter out any empty results (from chunks of "000") and reverse the array of words before joining, since we processed from right to left.

High-Level Algorithm Flow

This ASCII diagram illustrates the overall "chunk and scale" process.

    ● Start (Input: 1234567)
    │
    ▼
  ┌──────────────────────────┐
  │ Convert to String "1234567" │
  └────────────┬─────────────┘
               │
               ▼
  ┌──────────────────────────┐
  │ Chunk from Right (3 digits) │
  │ Chunks: ["1", "234", "567"] │
  └────────────┬─────────────┘
               │
               ▼
  ┌──────────────────────────┐
  │ Process Each Chunk w/ Scale │
  ├──────────────────────────┤
  │ "567" → "five hundred sixty-seven"  (Scale: "")
  │ "234" → "two hundred thirty-four"   (Scale: "thousand")
  │ "1"   → "one"                       (Scale: "million")
  └────────────┬─────────────┘
               │
               ▼
  ┌──────────────────────────┐
  │   Filter & Reverse Order    │
  └────────────┬─────────────┘
               │
               ▼
  ┌──────────────────────────┐
  │ Join with Spaces         │
  │ "one million two hundred..." │
  └────────────┬─────────────┘
               │
               ▼
    ● End (Final String)

The Complete CoffeeScript Solution: A Deep Dive

Now, let's translate our algorithm into clean, idiomatic CoffeeScript. We'll encapsulate the logic within a Say class. This approach promotes reusability and keeps our conversion logic neatly organized.

Here is the complete, annotated code from the kodikra learning module.


class Say
  ONES = %w(zero one two three four five six seven eight nine ten eleven twelve thirteen fourteen fifteen sixteen seventeen eighteen nineteen)
  TENS = %w(_ _ twenty thirty forty fifty sixty seventy eighty ninety)
  SCALES = %w(_ thousand million billion)

  # Main public method
  say: (number) ->
    throw new Error 'Number must be between 0 and 999,999,999,999.' if number < 0 or number > 999999999999

    return 'zero' if number is 0

    # 1. Chunk the number string from the right
    chunks = @chunksFromRight number.toString()

    # 2. Process each chunk
    words = chunks
      .map (chunk, i) => @processChunk(parseInt(chunk), i)
      .filter (word) -> word isnt ''

    # 3. Combine and clean
    words.reverse().join(' ').trim()

  # Helper to slice a string into 3-digit chunks from the right
  chunksFromRight: (str) ->
    chunks = []
    working = str
    while working.length > 0
      chunks.push working.slice -3
      working = working.slice 0, -3
    chunks

  # Helper to process one chunk and apply its scale
  processChunk: (num, scaleIndex) ->
    return '' if num is 0

    word = @toWords(num)
    scale = SCALES[scaleIndex]

    if scale isnt '_'
      "#{word} #{scale}"
    else
      word

  # The core logic: converts a number from 0-999 to words
  toWords: (num) ->
    parts = []

    # Handle hundreds place
    hundreds = Math.floor(num / 100)
    if hundreds > 0
      parts.push "#{ONES[hundreds]} hundred"
      num %= 100

    # Handle tens and ones place
    if num > 0
      if num < 20
        # Numbers 1-19 have unique names
        parts.push ONES[num]
      else
        # Numbers 20-99 follow a pattern
        tens = Math.floor(num / 10)
        parts.push TENS[tens]
        ones = num % 10
        parts.push ONES[ones] if ones > 0

    parts.join(' ')

# Example Usage:
# sayer = new Say()
# console.log sayer.say(12345) # "twelve thousand three hundred forty-five"

Code Walkthrough: Line by Line

Let's break down how this class works, method by method.

Class Properties: ONES, TENS, SCALES


ONES = %w(zero one two three four five six seven eight nine ten eleven twelve thirteen fourteen fifteen sixteen seventeen eighteen nineteen)
TENS = %w(_ _ twenty thirty forty fifty sixty seventy eighty ninety)
SCALES = %w(_ thousand million billion)
  • CoffeeScript's %w() syntax is a convenient shortcut for creating an array of strings.
  • ONES: This array stores the unique names for numbers 0 through 19. The index directly corresponds to the number (e.g., ONES[5] is "five").
  • TENS: This array stores the names for the tens places (twenty, thirty, etc.). We use underscores as placeholders for indices 0 and 1, as these are covered by the ONES array. This aligns the indices nicely (e.g., TENS[2] is "twenty").
  • SCALES: This array holds the scale words. The index corresponds to the chunk's position from the right (0-indexed). Index 0 has no scale, index 1 is "thousand", and so on.

The Main Method: say(number)


say: (number) ->
  throw new Error 'Number must be between 0 and 999,999,999,999.' if number < 0 or number > 999999999999
  return 'zero' if number is 0
  • This is the public-facing method. It starts with crucial input validation, throwing an error for out-of-range numbers. This is a robust practice known as a "guard clause."
  • It then handles the simplest base case: if the input is 0, it immediately returns "zero" and exits.

  chunks = @chunksFromRight number.toString()
  • The number is converted to a string to enable slicing.
  • It calls the @chunksFromRight helper method (@ is CoffeeScript's shorthand for this.) to perform the chunking logic. For 12345, this would return ['345', '12'].

  words = chunks
    .map (chunk, i) => @processChunk(parseInt(chunk), i)
    .filter (word) -> word isnt ''
  • This is a beautiful example of CoffeeScript's functional programming style.
  • .map (chunk, i) => @processChunk(...): We iterate over the chunks array. For each chunk string, we convert it back to an integer with parseInt and pass it, along with its index i (which represents the scale), to the @processChunk helper. The fat arrow => ensures that @ inside the callback still refers to the Say class instance.
  • .filter (word) -> word isnt '': The processChunk method might return an empty string (if a chunk was "000"). This filter operation cleanly removes those empty results from our array of words.

  words.reverse().join(' ').trim()
  • .reverse(): Since we processed from right to left (smallest scale to largest), we must reverse the array to get the correct order (e.g., from ["three hundred forty-five", "twelve thousand"] to ["twelve thousand", "three hundred forty-five"]).
  • .join(' '): We join the array elements into a single string, separated by spaces.
  • .trim(): A final cleanup step to remove any potential leading or trailing whitespace.

The Chunking Helper: chunksFromRight(str)


chunksFromRight: (str) ->
  chunks = []
  working = str
  while working.length > 0
    chunks.push working.slice -3
    working = working.slice 0, -3
  chunks
  • This method implements a simple but effective loop.
  • while working.length > 0: The loop continues as long as there are digits left in the string.
  • chunks.push working.slice -3: slice -3 extracts the last three characters from the working string and pushes them into our chunks array.
  • working = working.slice 0, -3: We then remove those last three characters from working, preparing for the next iteration.

The Core Converter: toWords(num)

This is the heart of the number-to-word logic, responsible for handling any number between 0 and 999. Its internal flow is just as logical as the main algorithm.

    ● Start (Input: num, e.g., 245)
    │
    ▼
  ┌──────────────────┐
  │ Get Hundreds Digit │
  │ hundreds = 2     │
  └────────┬─────────┘
           │
           ▼
    ◆ hundreds > 0?
   ╱           ╲
 Yes (2 > 0)     No
  │              │
  ▼              ▼
┌──────────────────┐
│ Add "two hundred"│  [Skip]
└────────┬─────────┘
         │
         ▼
  ┌──────────────────┐
  │ Remainder = 45   │
  └────────┬─────────┘
           │
           ▼
    ◆ Remainder < 20?
   ╱           ╲
 No (45 >= 20)   Yes
  │              │
  ▼              ▼
┌──────────────────┐
│ Get Tens Digit (4) │ [Get word from ONES]
│ Add "forty"      │
└────────┬─────────┘
         │
         ▼
┌──────────────────┐
│ Get Ones Digit (5) │
│ Add "five"       │
└────────┬─────────┘
         │
         ▼
  ┌──────────────────┐
  │ Join all parts   │
  │ "two hundred forty five" │
  └────────┬─────────┘
           │
           ▼
    ● End (Return String)

The code implements this logic perfectly:


toWords: (num) ->
  parts = []

  # Handle hundreds place
  hundreds = Math.floor(num / 100)
  if hundreds > 0
    parts.push "#{ONES[hundreds]} hundred"
    num %= 100 # Get the remainder

  # Handle tens and ones place
  if num > 0
    if num < 20
      parts.push ONES[num]
    else
      tens = Math.floor(num / 10)
      parts.push TENS[tens]
      ones = num % 10
      parts.push ONES[ones] if ones > 0

  parts.join(' ')
  • It first calculates the hundreds digit. If it's greater than zero, it adds "X hundred" to an array called parts and uses the modulo operator (%) to get the remaining two-digit number.
  • Next, it checks the remainder. If it's less than 20, it's a unique word that can be looked up directly in the ONES array.
  • If the remainder is 20 or greater, it follows the standard pattern: look up the tens place in the TENS array, and if there's a non-zero ones digit, look that up in the ONES array.
  • Finally, it joins the collected parts with a space. This handles cases like 101 ("one hundred one") and 123 ("one hundred twenty-three") gracefully.

Pros & Cons of This Approach

The iterative, chunk-based approach presented here is highly effective, but it's useful to understand its trade-offs compared to other potential designs, such as a purely recursive solution.

Aspect Iterative Chunking (This Solution) Recursive Approach
Readability High. The separation into helper methods (`chunksFromRight`, `toWords`) makes the logic easy to follow. Can be very elegant but harder to trace for beginners. The base cases and recursive calls can obscure the overall flow.
Performance Excellent. No risk of stack overflow errors with large numbers, as it doesn't use deep recursion. Generally good, but theoretically could hit stack depth limits on extremely large numbers (not an issue in our range).
State Management Straightforward. State is passed explicitly through arrays (`chunks`, `words`) and loop variables. Can be more complex. State is managed implicitly through the call stack, which can sometimes lead to subtle bugs.
Extensibility Very high. To add "trillion," you simply add the word to the `SCALES` array. The logic doesn't need to change. Also extensible, but might require modifying the recursive function's signature or logic to handle new scales.

Frequently Asked Questions (FAQ)

Why do we process the number from right to left?

Processing from right to left (least significant digits to most significant) provides a consistent way to assign scale words. The first 3-digit chunk is always the base unit, the second is always "thousand," the third is always "million," and so on. This mapping of chunk index to scale is simple and reliable. Processing left to right would require complex calculations to determine the starting scale.

What does => @processChunk(...) do in CoffeeScript?

The "fat arrow" (=>) in CoffeeScript creates a function and binds the value of this (or @) from the outer scope. When using .map, the callback function normally has its own this context. The fat arrow ensures that when we call @processChunk inside the map, @ still refers to our instance of the Say class, giving us access to its helper methods and properties.

How would I extend this to handle larger numbers like "quadrillion"?

This algorithm is highly extensible. To support quadrillions, you would simply add the next scale word to the SCALES array: SCALES = %w(_ thousand million billion quadrillion). You would also need to update the input validation to allow for a larger maximum number. The core chunking and processing logic would remain unchanged.

What is the purpose of .filter (word) -> word isnt ''?

This is for handling numbers that contain a chunk of all zeros, like 1,000,500. When chunked, this number becomes ["1", "000", "500"]. The processChunk method will correctly return an empty string for the "000" chunk. The filter removes this empty string from the results, preventing extra spaces or incorrect phrasing like "one million thousand five hundred."

Is CoffeeScript still relevant for new projects?

While CoffeeScript's popularity has waned with the rise of modern JavaScript (ES6+), its core ideas have profoundly influenced the JavaScript ecosystem. Features like arrow functions, classes, and destructuring in modern JS were popularized by CoffeeScript. It's still a capable language that compiles to clean JavaScript, and understanding it provides valuable historical context. For new projects, developers often choose TypeScript or modern vanilla JavaScript, but many legacy codebases still use CoffeeScript effectively. You can learn more in our complete guide to CoffeeScript.

How could this code be adapted to handle decimals?

The current code is designed for integers only. To handle decimals, you would first split the number into its integer and fractional parts at the decimal point. You would run the integer part through the existing say method. For the fractional part, you would typically read each digit individually. For example, 123.45 would become "one hundred twenty-three and forty-five cents" (in a currency context) or "one hundred twenty-three point four five." This requires adding new logic to handle the "and" or "point" separator and the digit-by-digit reading of the decimal part.


Conclusion and Final Thoughts

The number-to-word conversion challenge is a perfect illustration of algorithmic decomposition. By breaking a massive number into simple, 3-digit chunks, we transformed an intimidating problem into a straightforward process of translation, scaling, and assembly. The CoffeeScript solution, with its expressive syntax and functional array methods like map and filter, provides an elegant and highly readable implementation.

The key takeaways are the power of the "divide and conquer" strategy, the importance of helper functions for clarity, and the necessity of handling edge cases and cleaning up data. This robust, well-structured approach is not just a solution to one problem; it's a blueprint for tackling complex data transformation tasks in any programming language.

Disclaimer: The code in this article is based on modern CoffeeScript conventions and is designed to be run with the latest stable compilers. The logic is timeless, but syntax and tooling may evolve.

Ready to tackle the next challenge? Explore the full CoffeeScript learning path on kodikra.com to continue building your skills.


Published by Kodikra — Your trusted Coffeescript learning resource.