Say in C: Complete Solution & Deep Dive Guide

a close up of a computer screen with code on it

The Ultimate Guide to Converting Numbers to Words in C

Converting numbers to their English word equivalents in C is a foundational challenge that elegantly tests your grasp of logic, string manipulation, and memory management. This guide breaks down the problem from first principles to a complete, robust C solution, perfect for handling numbers up to the trillions.

Imagine you're building a system for a bustling deli, just like your friend Yaʻqūb. He needs to call out ticket numbers, but to ensure clarity over the noise, he reads them in full English: "Now serving, number one thousand two hundred thirty-four!" This isn't just a quaint practice; it's a real-world requirement in financial software for writing checks, in accessibility tools for screen readers, and in reporting systems. You've probably seen this problem and thought, "How hard can it be?" The truth is, the path from 1234 to "one thousand two hundred thirty-four" is paved with interesting logical hurdles, especially in a low-level language like C. This article is your map through that terrain, turning a complex task into a manageable and rewarding coding exercise from the exclusive kodikra.com curriculum.


What is the Number-to-Word Conversion Problem?

At its core, the number-to-word conversion problem, often called "Say," is an algorithmic task. The goal is to write a function that accepts a numerical input—in our case, a 64-bit signed integer (long long)—and returns a dynamically allocated string containing the number's full English name. The range we must support is from 0 up to 999,999,999,999.

For example:

  • say(14) should return "fourteen"
  • say(100) should return "one hundred"
  • say(12345) should return "twelve thousand three hundred forty-five"
  • say(987654321123) should return "nine hundred eighty-seven billion six hundred fifty-four million three hundred twenty-one thousand one hundred twenty-three"

Why This is a Deceptively Complex Challenge in C

While modern languages might offer libraries or simpler string handling, C forces you to confront the mechanics head-on. The difficulty doesn't lie in a single complex algorithm but in the meticulous orchestration of several smaller pieces:

  • No Built-in String Class: In C, strings are null-terminated character arrays. This means all concatenation, resizing, and manipulation must be done manually using functions from <string.h> and <stdio.h>.
  • Manual Memory Management: The final string cannot have a fixed size, as the length of "one" is vastly different from the length of "nine hundred ninety-nine billion...". You must use malloc, calloc, or realloc to allocate memory on the heap, and the caller of your function is responsible for calling free to prevent memory leaks.
  • Handling Irregularities: The English language is full of special cases. The numbers 1-19 are unique. After that, the pattern becomes more regular (twenty, thirty, forty), but you still need to combine them (e.g., "twenty-one").
  • Logical Chunking: You can't process a number like 1,234,567 digit by digit. The logic must recognize that this is "one million," "two hundred thirty-four thousand," and "five hundred sixty-seven." This requires breaking the number down into groups of three.

Mastering this problem in C is a significant milestone. It demonstrates proficiency in fundamental concepts that are critical for systems programming, embedded systems, and performance-critical applications. This kodikra module is specifically designed to build that foundational strength.


The Blueprint: Deconstructing the Logic with the 5W1H Framework

How Do We Approach This Logically?

The most effective strategy is to "divide and conquer." A massive number like 987,654,321,123 is intimidating. But if you look closer, it's just a repeating pattern of three-digit numbers attached to a magnitude.

  • 987 billion
  • 654 million
  • 321 thousand
  • 123

This insight is our key. Our main function will iterate through the number in chunks of 1,000. For each chunk, it will call a helper function to convert the three-digit number (0-999) into words and then append the correct magnitude ("thousand", "million", "billion").

The High-Level Algorithm Flow

Here is a conceptual overview of how our main say function will operate. It processes the number from the largest magnitude down to the smallest.

● Start (Input: long long number)
│
▼
┌───────────────────────────┐
│ Validate Input (0 to 999T) │
└────────────┬──────────────┘
             │
             ▼
      ◆ number == 0? ◆
     ╱                ╲
   Yes                No
    │                  │
    ▼                  ▼
┌───────────┐    ┌─────────────────────────┐
│ Return "zero" │    │ Initialize empty string │
└───────────┘    └────────────┬────────────┘
    │                          │
    ● End                      ▼
                         ┌────────────────────┐
                         │ Loop through Magnitudes │
                         │ (Billion -> Million -> ...) │
                         └────────────┬────────────┘
                                      │
                                      ▼
                               ◆ Chunk > 0? ◆
                              ╱              ╲
                            Yes              No (Skip)
                             │
                             ▼
                ┌───────────────────────────┐
                │ Convert 3-digit chunk to words │
                │ (Using helper function)      │
                └────────────┬──────────────┘
                             │
                             ▼
                ┌───────────────────────────┐
                │ Append chunk words to result │
                └────────────┬──────────────┘
                             │
                             ▼
                ┌───────────────────────────┐
                │ Append magnitude to result │
                │ (e.g., " billion")        │
                └────────────┬──────────────┘
                             │
                             ▼
                         Loop continues
                             │
                             ▼
                      ┌────────────────┐
                      │ Return final string │
                      └────────────────┘
                             │
                             ▼
                          ● End

The Core Helper: Converting Numbers from 0-999

The heart of our solution is a helper function that can take any number from 0 to 999 and return its English representation. This function simplifies the main loop significantly. Its internal logic must handle hundreds, tens, and the special "teens."

● Start (Input: int n, 0-999)
│
▼
┌──────────────────┐
│ Handle Hundreds  │
│ n / 100          │
└─────────┬────────┘
          │
          ▼
   ◆ Hundreds > 0? ◆
  ╱                 ╲
Yes                 No
 │                   │
 ▼                   ▼
┌────────────────┐  (Proceed)
│ Append "X hundred" │
└─────────┬────────┘
          │
          ▼
┌──────────────────┐
│ Remainder: n % 100 │
└─────────┬────────┘
          │
          ▼
    ◆ Remainder >= 20? ◆
   ╱                    ╲
 Yes                    No
  │                      │
  ▼                      ▼
┌───────────────┐   ◆ Remainder >= 10? ◆
│ Append "twenty",  │  ╱                    ╲
│ "thirty", etc.  │ Yes                    No
└───────┬───────┘  │                      │
        │          ▼                      ▼
        ▼         ┌─────────────────┐    ◆ Remainder > 0? ◆
┌───────────────┐ │ Append "ten",     │   ╱                  ╲
│ Remainder: n % 10 │ │ "eleven", etc.  │  Yes                 No
└───────┬───────┘ └───────┬─────────┘   │                   │
        │                 │             ▼                   ▼
        └─────────┐       │          ┌───────────────┐   (Done)
                  │       │          │ Append "one",   │
                  ▼       │          │ "two", etc.     │
           ◆ Remainder > 0? ◆         └───────────────┘
          ╱                 ╲
         Yes                No
          │                  │
          ▼                  ▼
      (Append "one",..)   (Done)
          │
          ▼
       ● End

Deep Dive: A Line-by-Line Code Walkthrough in C

Now, let's translate our logical blueprint into functioning C code. We'll structure our solution into a header file (say.h) for the public interface and an implementation file (say.c) for the logic. This is standard practice for creating reusable C modules.

The Header File: say.h

The header file is simple. It declares the function signature that other parts of a larger program can use. The #ifndef/#define/#endif guard prevents issues if the header is included multiple times.


#ifndef SAY_H
#define SAY_H

#include <stdint.h>

int say(int64_t n, char **ans);

#endif
  • #include <stdint.h>: We include this to use int64_t, which guarantees a 64-bit integer, making our code portable and explicit about the range of numbers it handles.
  • int say(int64_t n, char **ans): This is our function prototype.
    • It takes an int64_t number n.
    • It also takes a char **ans. This is a pointer to a character pointer. We use this "output parameter" to pass back the address of the dynamically allocated string we create.
    • It returns an int. By convention, we'll return 0 on success and -1 on failure (e.g., invalid input or memory allocation error).

The Implementation File: say.c

This is where all the logic resides. We'll build it up piece by piece.

1. Includes and Data Structures

We start by including necessary headers and defining our lookup tables. Using static const makes these arrays private to this file and ensures they are stored in read-only memory, which is efficient.


#include "say.h"
#include <stdlib.h>
#include <string.h>
#include <stdio.h>

// Arrays for basic number words
static const char *below_20[] = {
    "zero", "one", "two", "three", "four", "five", "six", "seven", "eight", "nine",
    "ten", "eleven", "twelve", "thirteen", "fourteen", "fifteen", "sixteen",
    "seventeen", "eighteen", "nineteen"
};

static const char *tens[] = {
    "", "", "twenty", "thirty", "forty", "fifty", "sixty", "seventy", "eighty", "ninety"
};

// Arrays for magnitudes
static const char *magnitudes[] = {
    "", "thousand", "million", "billion"
};

// A helper function prototype (forward declaration)
static char* convert_chunk(int n);
  • #include <stdlib.h>: For memory allocation (malloc, free).
  • #include <string.h>: For string manipulation (strcpy, strcat).
  • #include <stdio.h>: For sprintf, a powerful tool for building strings.
  • Lookup Arrays: We define arrays for numbers 0-19, for the tens (20, 30, ...), and for the magnitudes. This is far cleaner and more efficient than a giant switch statement.
  • Forward Declaration: We declare our helper function convert_chunk so the main say function knows about it before it's fully defined later in the file.

2. The Helper Function: convert_chunk(int n)

This function implements the logic from our second ASCII diagram. It converts any number from 0 to 999 into words.


#define BUFFER_SIZE 100 // A safe buffer size for a 3-digit number word

static char* convert_chunk(int n) {
    if (n == 0) {
        return strdup(""); // Return empty string for a zero chunk
    }

    char *buf = malloc(BUFFER_SIZE);
    buf[0] = '\0'; // Start with an empty string

    if (n >= 100) {
        sprintf(buf, "%s hundred", below_20[n / 100]);
        n %= 100;
        if (n > 0) {
            strcat(buf, " ");
        }
    }

    if (n > 0) {
        char temp_buf[BUFFER_SIZE];
        if (n < 20) {
            sprintf(temp_buf, "%s", below_20[n]);
        } else {
            sprintf(temp_buf, "%s", tens[n / 10]);
            if (n % 10 > 0) {
                sprintf(temp_buf + strlen(temp_buf), "-%s", below_20[n % 10]);
            }
        }
        strcat(buf, temp_buf);
    }
    
    return buf;
}
  • malloc(BUFFER_SIZE): We allocate a buffer to build our string. 100 bytes is more than enough for "nine hundred ninety-nine".
  • Hundreds Place: If n is 100 or more, we use integer division (n / 100) to get the digit. We use sprintf to format "X hundred" into our buffer. We then use the modulo operator (n %= 100) to get the remaining part of the number.
  • Tens and Units:
    • If the remainder is less than 20, we can look it up directly in our below_20 array.
    • Otherwise, we handle the tens place (tens[n / 10]) and the units place (below_20[n % 10]) separately, adding a hyphen as needed.
  • strdup(""): If the chunk is 0 (e.g., in 1,000,500 the "thousand" chunk is 0), we return an empty string. `strdup` is a convenient non-standard function that allocates memory and copies a string. If it's not available, you can implement it with `malloc` and `strcpy`.

3. The Main Function: say(int64_t n, char **ans)

This function orchestrates the entire process, implementing the logic from our first ASCII diagram.


#define MAX_RESULT_SIZE 256 // Initial size for the final answer string

int say(int64_t n, char **ans) {
    // 1. Input Validation
    if (n < 0 || n >= 1000000000000LL) {
        return -1; // Invalid input
    }
    
    // 2. Handle Zero Case
    if (n == 0) {
        *ans = strdup(below_20[0]);
        return 0;
    }

    // 3. Allocate memory for the final result
    *ans = malloc(MAX_RESULT_SIZE);
    if (!*ans) return -1; // Allocation failed
    (*ans)[0] = '\0'; // Start with an empty string

    // 4. Main loop to process chunks
    int magnitude_index = 0;
    char final_string[MAX_RESULT_SIZE] = "";

    while (n > 0) {
        if (n % 1000 != 0) {
            int chunk = n % 1000;
            char *chunk_words = convert_chunk(chunk);
            
            char current_part[MAX_RESULT_SIZE];
            if (magnitude_index > 0) {
                sprintf(current_part, "%s %s", chunk_words, magnitudes[magnitude_index]);
            } else {
                strcpy(current_part, chunk_words);
            }
            
            free(chunk_words); // Free memory from helper

            char temp_final[MAX_RESULT_SIZE];
            if (strlen(final_string) > 0) {
                sprintf(temp_final, "%s %s", current_part, final_string);
            } else {
                strcpy(temp_final, current_part);
            }
            strcpy(final_string, temp_final);
        }
        
        n /= 1000;
        magnitude_index++;
    }

    strcpy(*ans, final_string);
    return 0;
}
  • Step 1 & 2: We first validate the input to ensure it's within our supported range (0 to 999,999,999,999). We then handle the simple edge case of n = 0 immediately.
  • Step 3: We allocate a generous buffer for the final result. A more robust solution might use realloc to grow the buffer as needed, but for this problem, a fixed large buffer is simpler and often sufficient. We assign the allocated memory's address to *ans.
  • Step 4: The while (n > 0) loop is the engine.
    • n % 1000 extracts the rightmost three digits (our current chunk).
    • We call our helper convert_chunk to get the words for this chunk.
    • We combine the chunk's words with the correct magnitude (e.g., "one hundred twenty-three" + "thousand").
    • Crucially, we free(chunk_words) to prevent a memory leak from the memory allocated inside our helper function.
    • We prepend this new part to our final string. We process from right to left (units -> thousands -> millions) but build the string so the order is correct (millions -> thousands -> units).
    • n /= 1000 effectively shifts the number three places to the right, preparing the next chunk for the next iteration.
    • magnitude_index++ keeps track of whether we are in the thousands, millions, or billions.

Weighing the Approach: Pros and Cons

Every implementation has trade-offs. This C solution is a classic example of prioritizing control and efficiency at the cost of complexity.

Pros Cons
  • High Performance: Direct memory manipulation and lookup tables are extremely fast. There is very little overhead compared to solutions in higher-level languages.
  • No Dependencies: The solution uses only the C standard library. It can be compiled and run on almost any platform, from a web server to a tiny embedded device.
  • Excellent Learning Tool: This problem forces you to engage with core C concepts: pointers, memory allocation (malloc/free), string arrays, and modular design.
  • Memory Efficient: While we use fixed buffers for simplicity, the approach can be adapted with realloc to use precisely the amount of memory needed.
  • Prone to Memory Errors: Manual memory management is powerful but dangerous. Forgetting to free memory leads to leaks. Writing past a buffer's boundary (a buffer overflow) can cause crashes or security vulnerabilities.
  • Complex String Logic: Building strings with sprintf and strcat in a loop can be hard to read and debug. One misplaced space or null terminator can break the entire output.
  • Not Easily Extensible: Adapting this code for another language (e.g., French, which has different rules for numbers like 90 - "quatre-vingt-dix") would require a significant rewrite of the logic and data.
  • Fixed-Size Buffers: Our use of fixed-size buffers, while simple, is not robust. A more professional implementation would calculate the required size or dynamically resize the buffer with realloc.

Alternative Approaches & Future-Proofing

While our iterative approach is solid, a recursive solution is also a very elegant way to solve this problem. A recursive function could call itself with n / 1000 and then process the remainder n % 1000, building the string on the way back up the call stack. This can sometimes lead to cleaner-looking code, but it's important to be mindful of the potential for stack overflow with extremely large numbers (not an issue in our case).

For production systems requiring internationalization, developers would not write this logic from scratch. They would use a dedicated library like ICU (International Components for Unicode). ICU has robust, pre-built formatters that can handle number-to-word conversion for dozens of languages, accounting for all their unique grammatical rules.

Looking ahead, this fundamental logic remains relevant. Understanding how to break down problems algorithmically is a timeless skill. In modern systems languages like Rust, the compiler would help prevent memory errors through its ownership and borrowing system, while in Go, garbage collection and simpler string handling would remove the need for manual malloc/free. However, both still require the same core chunking logic you've learned here. This kodikra learning path module provides the essential C foundation that makes learning these newer languages easier.


Frequently Asked Questions (FAQ)

Why use int64_t instead of a simple long long?

Using int64_t from <stdint.h> provides an explicit guarantee that the integer is exactly 64 bits wide. While long long is at least 64 bits on most modern systems, the C standard doesn't strictly enforce its exact size. Using int64_t makes the code more portable and its intent clearer.

What is the purpose of the char **ans parameter?

This is how C functions "return" dynamically allocated memory. A function can only have one direct return value (which we use for an error code). To pass back the string, we pass a pointer to where the caller is storing its character pointer (char *my_string;). Inside the function, we can then use *ans = malloc(...) to change the caller's pointer to point to the new memory we've allocated.

How is memory managed in this solution, and what are the risks?

Memory is allocated on the heap using malloc and strdup (which itself uses malloc). The main risk is a memory leak. The contract of our say function is that whoever calls it is responsible for calling free(*ans) on the returned string when they are done with it. Forgetting to do so will cause the allocated memory to become orphaned and unusable for the life of the program.

Could this problem be solved recursively?

Absolutely. A recursive function could handle the base cases (n < 20) and for larger numbers, it could call itself with n / 1000 to get the "million" part, then process the n % 1000 part. This can result in very clean and elegant code, though sometimes at the cost of being slightly less intuitive to debug for beginners.

How would you adapt this for other languages, like Spanish?

Adapting this requires more than just translating the word arrays. You'd need to change the logic. For example, in Spanish, "one hundred" is "cien," but "one hundred one" is "ciento uno." Gender also comes into play (e.g., "uno" vs. "un"). A robust solution would require a complete rethink of the string construction rules, highlighting why internationalization libraries are so valuable.

Is using sprintf and strcat safe here?

In our controlled example with large fixed buffers, it's relatively safe. However, in production code, sprintf and strcat are risky because they don't check for buffer boundaries. A safer alternative is to use snprintf, which takes the buffer size as an argument and prevents writing past the end of the buffer, thus avoiding dangerous buffer overflow vulnerabilities.


Conclusion: From Numbers to Narratives

You have successfully journeyed from a simple numerical input to a full English-word output. This kodikra module demonstrates that the "Say" problem is a perfect microcosm of C programming. It demands precision, a solid understanding of memory, and the ability to break a large problem into smaller, manageable parts. The solution we've built is efficient, self-contained, and a powerful piece of code to have in your portfolio.

The key takeaways are the power of the "chunking" strategy, the necessity of helper functions to maintain clean code, and the ever-present responsibility of manual memory management in C. By mastering these concepts, you are well on your way to tackling even more complex challenges in systems and application development.

Technology Disclaimer: The code and concepts discussed are based on the C99/C11 standard and are compatible with modern compilers like GCC and Clang. The logic is timeless, but always be mindful of your specific compiler and system architecture when dealing with low-level memory operations.

Ready to tackle more challenges? Explore the complete C Learning Path on kodikra.com.

Dive deeper into the C language with our comprehensive C language guide.


Published by Kodikra — Your trusted C learning resource.