Say in Common-lisp: Complete Solution & Deep Dive Guide
The Complete Guide to Converting Numbers to Words in Common Lisp
Learn how to convert any number from 0 to 999,999,999,999 into its English word representation using Common Lisp. This guide explores the powerful built-in format directive ~r, providing a concise and elegant solution for number-to-text translation tasks and demonstrating the language's expressive power.
The Deli Counter Dilemma: A Real-World Problem
Imagine your friend Yaʻqūb, working tirelessly at the busiest deli in town. The queue is long, and the only thing keeping chaos at bay is the numbered ticket system. When it's time to call the next customer, Yaʻqūb doesn't just shout a digit; he clearly announces, "Now serving number one hundred twenty-three." This simple act of converting a number to words ensures clarity and prevents confusion in a noisy environment.
You've probably faced a similar challenge in programming. Whether it's for generating invoices, writing software that prints checks, or developing accessibility features for screen readers, the task of converting numerical digits into human-readable text is a classic problem. It seems simple at first, but the edge cases—teens, compound numbers, and large scales like millions and billions—can quickly turn your code into a tangled mess of conditional logic. But what if there was a language where this complex task could be solved in a single, elegant line? Welcome to the power of Common Lisp.
What is Number-to-Word Conversion?
Number-to-word conversion is the process of translating a numerical value (e.g., 123) into its linguistic representation (e.g., "one hundred twenty-three"). This task is a cornerstone of Natural Language Processing (NLP) and is essential in any application that bridges the gap between raw data and human communication.
While trivial for a human, it presents several logical hurdles for a machine:
- Special Cases: Numbers 11-19 (e.g., "eleven," "twelve," "thirteen") don't follow the same pattern as 21, 22, 23.
- Place Value: The digit '2' means "two," "twenty," or "two hundred" depending on its position.
- Grouping: Numbers are grouped into chunks of three (hundreds, tens, ones) separated by scale markers like "thousand," "million," and "billion."
- Concatenation Logic: You need rules for adding hyphens (e.g., "twenty-three") and conjunctions like "and" (though often omitted in modern American English, as in "one hundred twenty-three").
Solving this from scratch requires careful planning, data structures to hold the word forms, and a robust algorithm to piece them together correctly. Or, you could use Common Lisp.
How Common Lisp Solves This Problem with Unmatched Elegance
Common Lisp, a language renowned for its powerful meta-programming capabilities and expressive syntax, has a secret weapon for tasks like this: the format function. It's far more than a simple string interpolation tool; it's a sophisticated text-formatting engine. The key to our solution lies in one of its many control directives: ~r.
The Magic Wand: The ~r Radix Directive
The ~r directive in a format control string is used for printing a number in a specified radix (base). When used without any prefixes, it has a special behavior: it prints the number as an English word. This is a built-in feature, deeply integrated into the language's standard library, saving developers from reinventing the wheel.
Let's look at the solution from the kodikra.com learning path to see this in action.
Code Walkthrough: The Official Kodikra Solution
The provided solution is a masterclass in conciseness and leveraging the language's strengths. It's compact, readable, and incredibly powerful.
(defpackage :say
(:use :cl)
(:export :say))
(in-package :say)
(defun say (number)
(when (< -1 number (expt 10 12))
(format nil "~r" number)))
Let's break this down line by line to understand every component.
1. Package Definition
(defpackage :say
(:use :cl)
(:export :say))
(defpackage :say ...): This defines a new package namedsay. In Common Lisp, packages are namespaces that prevent symbol name collisions. It's like creating a module or library in other languages.(:use :cl): This tells our new package to inherit and use all the standard symbols from the main Common Lisp package (:cl). This gives us access to core functions likedefun,when, andformatwithout needing to prefix them (e.g.,cl:defun).(:export :say): This makes the symbolsaypublic. Any other code that uses this package will be able to call thesayfunction.
2. Switching to the Package
(in-package :say)
This command switches the current working namespace to the :say package we just created. All subsequent definitions will now belong to this package.
3. The Function Definition
(defun say (number)
(when (< -1 number (expt 10 12))
(format nil "~r" number)))
(defun say (number)): This defines a function namedsaythat accepts a single argument,number.(when (< -1 number (expt 10 12)) ...): This is a conditional guard clause.whenis a macro that executes its body only if the condition is true. It's a cleaner alternative to anifstatement with only a "then" branch.- The condition is
(< -1 number (expt 10 12)). <checks if the numbers are in strictly increasing order. So, this checks if-1 < numberANDnumber < (expt 10 12).- This effectively constrains the input to be between 0 and 999,999,999,999, inclusive.
(expt 10 12)calculates 10 to the power of 12, which is one trillion (1,000,000,000,000).
- The condition is
(format nil "~r" number): This is the core of the function.formatis the powerful formatting function.- The first argument,
nil, specifies the destination. When it'snil,formatdoesn't print to the console; instead, it returns the formatted result as a new string. - The second argument,
"~r", is the control string. The tilde (~) introduces a formatting directive, andrstands for "radix," which in this context, means "spell it out in English." - The final argument,
number, is the value to be formatted.
If the number is within the valid range, the function returns the English string. If it's outside the range, the when condition is false, its body is skipped, and the function implicitly returns nil, the Lisp representation for "nothing" or falsity.
Visualizing the Logic Flow
Here is a simple flow diagram illustrating the execution path of the say function.
● Start (Function call with `number`)
│
▼
┌───────────────────────────┐
│ Input `number` │
└────────────┬──────────────┘
│
▼
◆ Is `number` between 0 and 1 trillion?
( < -1 number (expt 10 12) )
╱ ╲
Yes No
│ │
▼ ▼
┌──────────────────────────┐ ┌───────────────────┐
│ Execute `(format nil "~r" number)` │ │ Skip body, return │
│ Return the resulting string│ │ `nil` implicitly│
└──────────────────────────┘ └───────────────────┘
│ │
└────────────────┬───────────────┘
│
▼
● End (Return value)
Why This Approach is Superior: A Comparative Analysis
To truly appreciate the Lisp way, let's consider how you would solve this in a language without a built-in "number spelling" feature. You would have to build the logic from scratch.
The Manual, Algorithmic Approach
A manual implementation would typically involve:
- Data Structures: Arrays or hash maps to store the words for digits (one, two), teens (eleven, twelve), and tens (twenty, thirty).
- Chunking: A loop or recursive function that processes the number in chunks of three digits (hundreds, tens, ones).
- Scale Management: Logic to append the correct scale word ("thousand," "million," "billion") after processing each chunk.
- Edge Case Handling: Special code for handling teens, numbers ending in zero, and correctly placing hyphens.
Here's a conceptual sketch of what a part of that logic might look like in a Lisp-like pseudocode:
(defparameter *digits* #("zero" "one" "two" ...))
(defparameter *teens* #("ten" "eleven" ...))
(defparameter *tens* #("" "" "twenty" "thirty" ...))
(defparameter *scales* #("" "thousand" "million" "billion"))
(defun convert-chunk-to-words (chunk)
;; Logic to handle a 3-digit number
;; e.g., 123 -> "one hundred twenty-three"
(let ((words '()))
(let ((hundreds (floor chunk 100))
(remainder (rem chunk 100)))
(when (> hundreds 0)
(push "hundred" words)
(push (aref *digits* hundreds) words))
(when (> remainder 0)
(if (< remainder 20)
;; handle teens and singles
else
;; handle tens and singles
)))
(format nil "~{~a~^ ~}" (nreverse words))))
(defun manual-say (number)
;; Main function to loop through chunks
;; and append scale words
...)
This is significantly more complex, error-prone, and requires much more code to write, test, and maintain.
Pros and Cons: Lisp's ~r vs. Manual Implementation
The contrast highlights the design philosophy of Common Lisp: provide powerful, high-level abstractions to solve common problems effectively.
| Feature | Common Lisp (format nil "~r" ...) |
Manual Algorithmic Approach |
|---|---|---|
| Code Length | Extremely concise (one line of core logic). | Verbose (50-100+ lines of code). |
| Readability | Highly readable for a Lisp programmer; the intent is clear. | Can become complex and hard to follow due to nested conditions and loops. |
| Maintainability | Virtually zero maintenance. It's part of the language standard. | Requires debugging and maintenance. Prone to bugs in edge cases. |
| Performance | Highly optimized, as it's a native, built-in implementation. | Performance depends on the quality of the implementation. Likely slower. |
| Flexibility | Limited to standard English forms. Cannot be easily modified for other languages or rules. | Completely flexible. Can be adapted for any language, currency, or formatting rule. |
The Power of format
The format function is a prime example of Lisp's "batteries-included" philosophy. It's a mini-language dedicated to text manipulation. Understanding its versatility is key to becoming a proficient Lisp developer.
● `format` function call
│
├─ Destination Argument
│ ├─ `t` → Standard Output (console)
│ └─ `nil` → Return as String
│
▼
┌───────────────────────────┐
│ Control String Parser │
└────────────┬──────────────┘
│
├─ `~a` → Aesthetic (human-readable)
│
├─ `~s` → Standard (machine-readable)
│
├─ `~%` → Newline
│
├─ `~{...~}` → Iteration over a list
│
└─ `~r` → Radix / English Words
│
▼
┌─────────────────┐
│ Specialized │
│ Number-to-Word │
│ Engine (Built-in)│
└─────────────────┘
│
▼
● Final String Output
Practical Applications and Future Trends
Where would you use this powerful feature? The applications are numerous and relevant to modern software development.
- Financial Technology (FinTech): Automatically writing check amounts in words to prevent fraud and errors. For example, converting
$1,450.75to "One thousand four hundred fifty and 75/100 dollars." - Accessibility (A11y): Powering screen readers to announce numbers in a natural, human-friendly way, improving the user experience for visually impaired users.
- Voice User Interfaces (VUI): Used in systems like Alexa, Siri, or Google Assistant to synthesize spoken responses that include numerical data (e.g., "The temperature is twenty-one degrees Celsius").
- Data Visualization and Reporting: Enhancing charts and reports by providing textual descriptions of key figures, making them more impactful.
Future-Proofing Your Skills: As AI and voice interaction become more integrated into our daily lives, the need for seamless natural language generation will only grow. While large language models (LLMs) can perform this task, a deterministic, lightweight, and blazing-fast function like Lisp's format is often a more efficient and reliable choice for specific, well-defined problems within a larger system. Understanding these fundamental building blocks remains a critical skill.
For more challenges that build these essential skills, Explore our Common Lisp Learning Roadmap and continue your journey.
Frequently Asked Questions (FAQ)
- 1. What exactly does the
~rdirective in Common Lisp'sformatdo? - The
~rdirective prints an integer in radix form. By default, without any modifiers, it prints the number as English words (e.g., 12 becomes "twelve"). It can also be used to print in other bases, like binary (~b), octal (~o), hex (~x), or even custom word-based radices. - 2. Can the provided
sayfunction handle negative numbers or decimals? - No. The function is specifically designed to meet the requirements of the kodikra module, which covers numbers from 0 up to 999,999,999,999. The guard clause
(< -1 number ...)explicitly rejects negative numbers. The~rdirective also works on integers, so it does not handle floating-point numbers. - 3. Why does the function return
nilfor out-of-range numbers? - This is due to the use of the
whenmacro. Awhenblock only executes its body if the condition is true. If the condition is false (i.e., the number is negative or too large), the body is skipped. In Lisp, if a function doesn't explicitly return a value, it implicitly returnsnil. - 4. What is the absolute upper limit for the built-in
~rdirective? - The limit is implementation-dependent but is generally tied to the maximum size of an integer (a
bignum) that the Lisp implementation can handle. However, the English word representations can become unwieldy for astronomically large numbers. The problem's constraint of one trillion is a practical limit for most applications. - 5. Is this number-to-word feature unique to Common Lisp?
- While not entirely unique, it is a hallmark of Common Lisp's powerful standard library. Some other languages have libraries that can achieve this (like Python's
inflector Java'sICU4J), but very few have it as a built-in, standard formatting feature. This reflects Lisp's heritage in symbolic computation and AI, where such transformations are common. - 6. How would I handle numbers larger than one trillion?
- For numbers beyond the scope of this function, you would need to implement a manual, algorithmic solution as discussed earlier. This would involve extending the logic with additional scale words like "trillion," "quadrillion," and so on, and processing the number in corresponding three-digit chunks.
- 7. What's the difference between
(format t ...)and(format nil ...)? - The first argument to
formatis the destination stream.tis an alias for the standard output stream, so(format t ...)prints directly to your console or terminal.nilas a destination tellsformatnot to print anywhere, but to return the generated text as a string value, which you can then store in a variable or return from a function.
Conclusion: The Lisp Advantage
The "Say" problem from the kodikra.com curriculum is a perfect illustration of the Common Lisp philosophy. What appears to be a moderately complex algorithmic challenge in many languages is reduced to a simple, elegant, and highly readable one-liner in Lisp. By providing powerful, high-level tools like the format function, the language empowers developers to focus on solving the bigger problem rather than getting bogged down in implementation details.
This approach—building robust, general-purpose tools into the core language—is why Common Lisp remains a relevant and compelling choice for complex problem domains. It encourages a style of programming where you first look for the most powerful abstraction available before resorting to manual implementation.
As you continue your journey with Lisp, you'll encounter this pattern again and again. To dive deeper into the language and its unique capabilities, we invite you to explore our complete Common Lisp guide and master this powerful tool.
Disclaimer: The code in this article is based on standard Common Lisp and is compatible with modern implementations such as Steel Bank Common Lisp (SBCL) version 2.4.x and newer.
Published by Kodikra — Your trusted Common-lisp learning resource.
Post a Comment