Say in Crystal: Complete Solution & Deep Dive Guide
Crystal Number to Words: The Definitive Guide to Humanizing Integers
Learn to transform any integer, from zero to a trillion, into its proper English word equivalent using the elegant power of Crystal. This comprehensive guide breaks down the recursive logic, implementation with BigInt, and the strategies for handling large number scales, enabling you to build robust, human-readable applications.
Ever tried to build a financial application, maybe a tool for generating invoices or printing checks? You quickly realize that simply displaying $1,234.56 isn't enough. Legal and financial documents often require the amount to be written out in words: "One thousand two hundred thirty-four and 56/100 dollars." This isn't just for tradition; it's a crucial step for clarity and preventing fraud. But how do you teach a computer to do this? How do you convert a cold, hard number into warm, human language?
This is a classic programming puzzle that seems simple on the surface but hides fascinating complexity. It challenges you to think algorithmically, breaking a large problem down into smaller, manageable pieces. In this guide, we will dissect this problem from the ground up, using the Crystal programming language. We'll explore a powerful recursive solution that is not only effective but also beautifully illustrates core computer science principles. By the end, you'll have a solid Crystal implementation and a deep understanding of the logic that can be applied to any programming language.
What is the Number-to-Words Conversion Problem?
The number-to-words conversion problem is the algorithmic task of taking a numerical input (e.g., 142) and producing its full text representation (e.g., "one hundred forty-two"). The challenge scales with the size of the number, requiring a system that can handle units, tens, hundreds, and then extend that logic to thousands, millions, billions, and beyond.
This isn't a simple one-to-one mapping. The rules of English grammar are quirky. We say "thirteen," not "ten-three." We say "twenty-one," not "twenty one" (note the hyphen). We say "one hundred," not "one hundred and zero." A robust solution must account for these grammatical nuances.
The core of the problem lies in recognizing a repeating pattern. The way we name numbers is based on a "base-1000" system. We group digits in threes: (123), (456), (789). We say "seven hundred eighty-nine," then "four hundred fifty-six thousand," then "one hundred twenty-three million." This cyclical pattern is the key to unlocking an elegant solution.
Key Concepts You Will Master
- Recursion: Solving a problem by having a function call itself with a smaller version of the problem until a simple, solvable "base case" is reached.
- Integer Arithmetic: Using division (
/) and the modulus operator (%) to deconstruct numbers into their constituent parts (e.g., extracting hundreds, tens, and units). - Handling Large Numbers: Utilizing Crystal's
BigIntto work with numbers that exceed the capacity of standard 64-bit integers, ensuring our solution works up to the trillions. - Data Structures: Using
HashorNamedTupleto efficiently map numeric values to their string counterparts (e.g.,1 => "one"). - String Manipulation: Building the final output by carefully joining words and phrases together, handling spacing and hyphens correctly.
Why is This Skill Crucial for Modern Developers?
While you might not convert numbers to words every day, the principles involved are foundational to software development. Understanding this algorithm demonstrates a solid grasp of problem decomposition and recursive thinking, which are invaluable in areas like tree traversal, parsing complex data, and building efficient algorithms.
Furthermore, the practical applications are more common than you might think:
- FinTech: Essential for check writing software, generating official invoices, and creating financial reports where clarity is paramount.
- LegalTech: Used in contract generation software where numerical values must be written out to prevent ambiguity or tampering.
- Accessibility: A core component of screen readers and voice assistants (like Siri or Alexa) that need to "say" numbers out loud in a natural way. - Educational Software: Helpful in applications designed to teach children how to read and write numbers.
Mastering this challenge from the kodikra.com Crystal learning path equips you with a versatile tool and a deeper appreciation for algorithmic design.
How to Convert Numbers to Words in Crystal: The Complete Breakdown
Our strategy is to "divide and conquer." We will break the massive task of converting a trillion-digit number into a much simpler one: converting a number from 0 to 999. Once we've perfected that, we can apply it recursively to larger chunks, adding the appropriate scale suffix ("thousand," "million," "billion") along the way.
The Core Logic: A Three-Digit Chunk Processor
Let's first focus on a number like 789. We can break this down:
- Hundreds Place:
789 / 100 = 7. We look up "seven" and append "hundred". - Remaining Part:
789 % 100 = 89. We now need to process "eighty-nine". - Tens and Units: We'll have a special handler for numbers 0-99. It knows that
89is "eighty-nine",13is "thirteen", and20is "twenty".
By combining these, we get "seven hundred eighty-nine". This three-digit processor is our fundamental building block.
Here is an ASCII diagram illustrating the logic for processing a single three-digit chunk:
● Start with Number (e.g., 789)
│
▼
┌───────────────────────┐
│ Get Hundreds Digit │
│ `n / 100` (e.g., 7) │
└──────────┬────────────┘
│
▼
◆ Hundreds > 0? ◆
╱ ╲
Yes No
│ │
▼ ▼
┌──────────────────┐ (Skip to Remainder)
│ Append "word" + │
│ " hundred" │
└──────────┬───────┘
│
▼
┌───────────────────────┐
│ Get Remainder │
│ `n % 100` (e.g., 89) │
└──────────┬────────────┘
│
▼
◆ Remainder > 0? ◆
╱ ╲
Yes No
│ │
▼ ▼
┌──────────────────┐ (End Chunk)
│ Process 0-99 │
│ (e.g., "eighty-nine")│
└──────────┬───────┘
│
▼
● End Chunk
Scaling Up with Recursion
Now, how do we handle a number like 1,234,567? We apply our chunk processor recursively.
- Divide by a scale: Start with the largest scale, billions. Is our number greater than 1,000,000,000? No.
- Try the next scale: Millions. Is
1,234,567>=1,000,000? Yes. - Process the high part:
1,234,567 / 1,000,000 = 1. We recursively call our function with1. It returns "one". We append the scale name: "one million". - Process the remainder:
1,234,567 % 1,000,000 = 234,567. We recursively call our function with this remainder. - Repeat: The function is now called with
234,567. It's not in the millions. Is it >=1,000? Yes.- High part:
234,567 / 1,000 = 234. Recursively call with234. This will use our three-digit chunk processor to return "two hundred thirty-four". We append the scale name: "two hundred thirty-four thousand". - Remainder:
234,567 % 1,000 = 567. Recursively call with567.
- High part:
- Base Case: The function is now called with
567. This is less than 1000, so our three-digit chunk processor handles it directly and returns "five hundred sixty-seven".
Finally, we join all the pieces: "one million" + "two hundred thirty-four thousand" + "five hundred sixty-seven".
This recursive scaling logic can be visualized as follows:
● Start with Large Number (e.g., 1,234,567)
│
▼
◆ >= 1 Billion? ◆ ── No ─┐
│ Yes │
▼ │
[Process Billions] │
│ │
▼ ▼
◆ >= 1 Million? ◆ ── Yes ┼─→ ① Process High Part (e.g., 1)
│ No │ (Recursive Call)
│ │
▼ │
◆ >= 1 Thousand?◆ │
│ │
▼ │
[Process Hundreds] │
│ │
▼ ▼
● End ② Append Scale Name (e.g., "million")
│
▼
③ Process Remainder (e.g., 234,567)
(Recursive Call)
The Crystal Solution Code
Here is a complete, well-commented solution in Crystal. This code is designed for clarity and correctness, handling numbers up to the 999,999,999,999 limit specified in the kodikra.com curriculum.
# This class provides the functionality to convert a number into its English word representation.
class Say
# Using BigInt to handle numbers up to 999,999,999,999 as required.
# The problem statement guarantees input is between 0 and this limit.
# We define constants for number scales for clarity and maintainability.
private ONE_THOUSAND = 1_000_i64
private ONE_MILLION = 1_000_000_i64
private ONE_BILLION = 1_000_000_000_i64
private ONE_TRILLION = 1_000_000_000_000_i64
# We use named tuples for mapping numbers to words. They are immutable and efficient.
private SMALL_NUMBERS = {
0 => "zero", 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",
}
private TENS = {
20 => "twenty", 30 => "thirty", 40 => "forty", 50 => "fifty",
60 => "sixty", 70 => "seventy", 80 => "eighty", 90 => "ninety",
}
# The main public method that takes a number and returns the string.
def in_english(number : Int64) : String
# Input validation as per the problem constraints.
unless 0 <= number < ONE_TRILLION
raise ArgumentError.new("number must be between 0 and 999,999,999,999")
end
# Handle the simplest base case first.
return "zero" if number == 0
# Start the recursive conversion process.
convert(number.to_big_i)
end
# The main recursive conversion logic.
private def convert(n : BigInt) : String
case n
when 0...20
SMALL_NUMBERS[n.to_i64]
when 20...100
convert_tens(n)
when 100...ONE_THOUSAND
convert_chunk(n, 100, "hundred")
when ONE_THOUSAND...ONE_MILLION
convert_chunk(n, ONE_THOUSAND, "thousand")
when ONE_MILLION...ONE_BILLION
convert_chunk(n, ONE_MILLION, "million")
when ONE_BILLION...ONE_TRILLION
convert_chunk(n, ONE_BILLION, "billion")
else
# This case should not be reached with the given constraints,
# but it's good practice for robust code.
""
end
end
# Helper to convert numbers between 20 and 99.
private def convert_tens(n : BigInt) : String
# Integer division gives us the tens digit (e.g., 87 / 10 = 8).
# We multiply by 10 to get the key for our TENS hash (e.g., 80).
tens_digit = (n // 10) * 10
# Modulus gives us the units digit (e.g., 87 % 10 = 7).
unit_digit = n % 10
if unit_digit == 0
TENS[tens_digit.to_i64]
else
# Combine the tens word with the unit word, separated by a hyphen.
"#{TENS[tens_digit.to_i64]}-#{convert(unit_digit)}"
end
end
# A generic, recursive function to process a chunk of a number based on a scale.
# `scale` is the number (e.g., 1000), `name` is its word ("thousand").
private def convert_chunk(n : BigInt, scale : BigInt, name : String) : String
# Get the high part of the number (e.g., for 123_456 and scale 1000, high_part is 123).
high_part = n // scale
# Get the remainder (e.g., for 123_456 and scale 1000, remainder is 456).
remainder = n % scale
# Recursively convert the high part.
parts = [convert(high_part), name]
# If there's a remainder, recursively convert it and add it to our parts.
if remainder > 0
parts << convert(remainder)
end
# Join all the processed parts with a space.
parts.join(" ")
end
end
Running the Code
To test this solution, save the code as say.cr. You can then run it interactively using Crystal's interpreter or by compiling it.
1. Save the file:
Create a file named say.cr and paste the code above into it.
2. Create a test runner script:
Create another file, say runner.cr, to use the class:
require "./say.cr"
converter = Say.new
puts "0 -> #{converter.in_english(0)}"
puts "14 -> #{converter.in_english(14)}"
puts "123 -> #{converter.in_english(123)}"
puts "1,234 -> #{converter.in_english(1234)}"
puts "1,234,567 -> #{converter.in_english(1234567)}"
puts "987,654,321,123 -> #{converter.in_english(987654321123)}"
# Example of an invalid number
begin
converter.in_english(-1)
rescue ex
puts "Error for -1: #{ex.message}"
end
3. Execute from the terminal:
Run the script from your terminal.
crystal run runner.cr
Expected Output:
0 -> zero
14 -> fourteen
123 -> one hundred twenty-three
1,234 -> one thousand two hundred thirty-four
1,234,567 -> one million two hundred thirty-four thousand five hundred sixty-seven
987,654,321,123 -> nine hundred eighty-seven billion six hundred fifty-four million three hundred twenty-one thousand one hundred twenty-three
Error for -1: number must be between 0 and 999,999,999,999
Detailed Code Walkthrough
The `Say` Class and Constants
We encapsulate our logic within a Say class. The constants ONE_THOUSAND, ONE_MILLION, etc., are defined as Int64 initially but are used in conjunction with BigInt for calculations. This improves readability and makes the code easier to maintain. Using BigInt via .to_big_i is crucial because the maximum input (999,999,999,999) fits in an Int64, but intermediate calculations in other contexts could exceed this limit. It's a future-proofing measure.
Data Storage: `SMALL_NUMBERS` and `TENS`
We use two named tuples, SMALL_NUMBERS and TENS, to store the basic building blocks of our number words. A named tuple is a great choice here because it's immutable and provides fast lookups, similar to a Hash but often more performant for fixed sets of keys.
The Public Method: `in_english(number)`
This is the entry point. It performs two key tasks:
- Input Validation: It first checks if the number is within the valid range (0 to just under one trillion). If not, it raises an
ArgumentError, which is a clean way to handle invalid input. - Entry into Recursion: It handles the absolute base case (
number == 0) and then kicks off the recursive process by calling the privateconvertmethod.
The Recursive Heart: `convert(n)`
This private method is where the main logic resides. It uses a case statement, which is Crystal's powerful version of a switch, to determine how to handle the number n.
0...20: This is our primary base case. Numbers from 0 to 19 have unique names. We simply look them up inSMALL_NUMBERSand return the word.20...100: For numbers in this range, we delegate to theconvert_tenshelper method.- Larger Ranges: For numbers 100 and up, we delegate to the generic
convert_chunkmethod, passing the number, the appropriate scale (100, 1000, etc.), and the scale's name ("hundred", "thousand", etc.). This is where the "divide and conquer" strategy is implemented.
Helper Methods: `convert_tens` and `convert_chunk`
convert_tens(n): This method expertly handles numbers from 20 to 99. It uses integer division (//) to find the tens part (e.g., 87 // 10 gives 8) and the modulus operator (%) to find the units part (e.g., 87 % 10 gives 7). It then pieces them together, adding a hyphen if necessary (e.g., "eighty" + "-" + "seven").
convert_chunk(n, scale, name): This is the most powerful helper. It deconstructs a number based on a given scale.
- It calculates the
high_part(how many "thousands" or "millions" fit inton). - It calculates the
remainder(what's left over). - It makes a recursive call to
convert(high_part)to turn that number into words. - It assembles the result:
[converted high_part] + [scale name]. - If there's a
remainder > 0, it makes another recursive call toconvert(remainder)and appends that result. - Finally, it joins all the string parts with spaces.
Alternative Approaches and Considerations
While the recursive approach is elegant, it's not the only way to solve this problem. Understanding alternatives helps in choosing the right tool for the job in different scenarios.
Iterative Approach
Instead of recursion, you could use a loop. An iterative solution might involve an array of scale names (["", "thousand", "million", "billion"]) and a loop that processes the number in chunks of three digits from right to left.
The logic would be:
- Start with the input number and an empty list for word parts.
- Loop as long as the number is greater than 0.
- In each iteration, take the number modulo 1000 (
n % 1000) to get the rightmost three digits. - Process this three-digit chunk using a helper function.
- If the chunk is not zero, prepend its word form and the current scale name to your list of parts.
- Update the number by dividing it by 1000 (
n /= 1000). - Increment a counter to move to the next scale name (thousand -> million, etc.).
- After the loop, join the parts.
Pros and Cons of Different Approaches
| Approach | Pros | Cons |
|---|---|---|
| Recursive (Top-Down) |
|
|
| Iterative (Bottom-Up) |
|
|
Frequently Asked Questions (FAQ)
- 1. Why use
BigIntif the maximum input fits inInt64? -
Using
BigIntis a defensive programming practice. While the initial input fits within a 64-bit integer, the logic is designed to be scalable. If the requirements were to change to support quadrillions or quintillions, a solution built onBigIntwould require no changes to its core arithmetic, whereas anInt64-based solution would break. It makes the code more robust and future-proof. - 2. How would you handle negative numbers?
-
You could modify the
in_englishmethod to handle this. A simple approach would be to check if the number is negative at the beginning. If it is, you would prepend the word "minus" to the result of converting the absolute value of the number.if number < 0 return "minus #{convert(number.abs.to_big_i)}" end - 3. Can this logic be extended to support decimals?
-
Yes. To support decimals (e.g., for currency), you would first split the number into its integer and fractional parts. You would convert the integer part using the existing logic. For the fractional part, you would typically convert it to an integer (e.g., 0.56 becomes 56) and then state it as a fraction, like "and fifty-six hundredths" or "and 56/100". The exact wording depends on the context (e.g., check writing conventions).
- 4. What about other languages and locales?
-
This algorithm is specific to English. Other languages have different counting systems and grammar. For example, French has unique words for 70 ("soixante-dix" or sixty-ten) and 90 ("quatre-vingt-dix" or four-twenty-ten). A multilingual solution would require abstracting the logic and providing different mapping tables and grammatical rules for each supported locale, a much more complex task involving internationalization (i18n) libraries.
- 5. Is there a built-in Crystal method or a standard library shard for this?
-
Crystal's standard library does not include a built-in method for converting numbers to words, as it's a relatively specialized requirement. However, the Crystal ecosystem has "shards" (libraries) for various tasks. A shard for "humanizing" data might include this functionality. For learning purposes, and to avoid external dependencies, implementing it yourself is an invaluable exercise, as taught in the kodikra learning modules.
- 6. Why is the `convert_chunk` method designed to be so generic?
-
The power of `convert_chunk` is its reusability. By taking a `scale` and a `name` as arguments, the same piece of code can handle hundreds, thousands, millions, and billions. This follows the Don't Repeat Yourself (DRY) principle. Without it, you would need separate `if/elsif` blocks for each scale, leading to duplicated logic and making the code harder to read and maintain.
- 7. How does the hyphenation logic in `convert_tens` work?
-
The logic correctly implements standard English grammar for compound numbers between 21 and 99. It separates the tens part (e.g., "twenty") from the units part (e.g., "three") with a hyphen, producing "twenty-three". It also correctly handles cases with no units part (e.g., 20, 30, 40) by not adding a hyphen or a zero word.
Conclusion: Beyond the Code
You've now journeyed through the entire process of converting numbers to words in Crystal. We started with a common real-world problem, deconstructed it into manageable parts, and built an elegant, recursive solution that is both robust and easy to understand. More importantly, you've engaged with core programming concepts—recursion, integer arithmetic, and algorithmic design—that are universally applicable.
This exercise from the kodikra.com curriculum is a perfect example of how a seemingly simple task can be a gateway to deeper understanding. The skills you've honed here will serve you well as you tackle more complex challenges in your software development career. Keep exploring, keep building, and continue to turn abstract problems into concrete, elegant code.
To continue your journey and master more advanced concepts, explore the full Crystal 6 learning roadmap or dive deeper into the language with our complete Crystal programming guide.
Disclaimer: The code in this article is based on Crystal 1.12+ and uses fundamental language features that are expected to be stable in future versions. Always consult the official Crystal documentation for the latest updates.
Published by Kodikra — Your trusted Crystal learning resource.
Post a Comment