Twelve Days in Clojure: Complete Solution & Deep Dive Guide

Tabs labeled

The Ultimate Guide to Solving Twelve Days in Clojure: From Zero to Functional Hero

Solving the "Twelve Days" problem in Clojure provides a perfect showcase of functional programming elegance. This guide demonstrates a data-driven approach using vectors for gifts and ordinals, combined with core functions like map, range, and string manipulation to dynamically and immutably construct each classic verse.

You've probably encountered programming challenges that involve generating repetitive text with slight variations. An imperative approach might lead you down a rabbit hole of nested loops, mutable string builders, and complex conditional logic. It works, but it often feels clunky and hard to maintain. What if there was a more declarative, elegant way to describe the solution?

This is where Clojure shines. The "Twelve Days of Christmas" song, with its cumulative verse structure, is a fantastic problem to explore the power of functional thinking. We won't be building loops; instead, we'll define our data, create small, pure functions that transform that data, and compose them together to build the final output. Prepare to see how thinking in terms of data transformations can lead to code that is not only concise but also deeply intuitive.


What is the "Twelve Days" Challenge in Clojure?

The core task is to programmatically generate the complete lyrics for the classic Christmas carol, "The Twelve Days of Christmas." The song has a unique structure where each verse builds upon the previous one.

On the first day, one gift is given. On the second day, a new gift is added, and the gift from the first day is repeated. This cumulative pattern continues for all twelve days, making the final verse the longest, listing all twelve gifts in descending order.

From a programming perspective, the challenge lies in managing this cumulative state elegantly. The requirements are:

  • Generate 12 distinct verses.
  • Each verse must start with the line "On the [ordinal] day of Christmas my true love gave to me: ".
  • The list of gifts for a given day `n` must include all gifts from day `n` down to day 1.
  • The formatting, including commas, periods, and the special case of "and" before the final gift (if there's more than one), must be precise.

This problem is a staple in the kodikra Clojure learning path because it beautifully illustrates fundamental concepts without requiring complex algorithms.


Why Clojure is Perfectly Suited for This Task

Clojure, a modern Lisp dialect running on the JVM, approaches problems from a data-first perspective. Instead of focusing on step-by-step instructions (imperative), we focus on describing the data and the transformations it needs to undergo. This paradigm is exceptionally effective for the "Twelve Days" problem.

Key Clojure Advantages:

  • Data-Oriented Design: The first step in an idiomatic Clojure solution is to represent the song's core components—ordinals and gifts—as data structures. This separates the logic from the data, making the code cleaner and easier to modify.
  • - Immutability: Clojure's data structures are immutable by default. We never modify the original list of gifts. Instead, we create new sequences derived from it for each verse. This eliminates a whole class of bugs related to state management.
  • Powerful Core Library: The language provides a rich set of functions for working with sequences (lists, vectors, etc.). Functions like map, range, take, reverse, and clojure.string/join are the building blocks we'll use to compose our solution declaratively.
  • Composability: We will build small, single-purpose helper functions (e.g., one to get gifts for a day, one to format a verse) and then combine them to create the final result. This functional composition leads to highly readable and testable code.

In essence, we will describe *what* a verse is, not *how* to build it step-by-step with a loop. This declarative style is a hallmark of functional programming and a core strength Clojure brings to the table.


How to Build the Solution: A Step-by-Step Functional Approach

Let's break down the problem into manageable, functional pieces. Our strategy is simple: define the data, create helper functions to transform it, and then compose those helpers to generate the final output.

Step 1: Define the Core Data Structures

Everything starts with the data. We need two primary pieces of information: the ordinal numbers (first, second, etc.) and the gifts for each day. A vector is the perfect data structure for this, as it provides fast, indexed access.

We'll define these as top-level constants using def.

(ns twelve-days)

(def ordinals
  ["first" "second" "third" "fourth" "fifth" "sixth"
   "seventh" "eighth" "ninth" "tenth" "eleventh" "twelfth"])

(def gifts
  ["a Partridge in a Pear Tree."
   "two Turtle Doves, "
   "three French Hens, "
   "four Calling Birds, "
   "five Gold Rings, "
   "six Geese-a-Laying, "
   "seven Swans-a-Swimming, "
   "eight Maids-a-Milking, "
   "nine Ladies Dancing, "
   "ten Lords-a-Leaping, "
   "eleven Pipers Piping, "
   "twelve Drummers Drumming, "])

Notice the careful formatting within the gifts vector. We've included the trailing comma and space where necessary, and the period on the first gift. This pre-formatting simplifies our string construction logic later on.

Step 2: Create a Helper to List Gifts for a Specific Day

Next, we need a function that, given a day number (e.g., 3), can produce the correct list of gifts ("three French Hens, ", "two Turtle Doves, ", "and a Partridge in a Pear Tree.").

Here's the logic:

  1. Take the first `n` gifts from our main `gifts` vector.
  2. Reverse the order, as the song lists them from highest day to lowest.
  3. Handle the special case: if there's more than one gift, prepend "and " to the last item in the list (which is now the first gift).
  4. Join them all into a single string.

Here is the function that accomplishes this:

(defn- gifts-for-day [day-number]
  (let [gifts-subset (->> gifts
                          (take day-number)
                          reverse)
        gifts-count  (count gifts-subset)]
    (if (> gifts-count 1)
      (clojure.string/join (conj (vec (rest gifts-subset)) (str "and " (first gifts-subset))))
      (first gifts-subset))))

This function is marked as private with defn- because it's an internal helper; our main API will be the functions that generate a single verse or the whole song.

ASCII Art: Logic Flow for `gifts-for-day`

This diagram illustrates the data transformation pipeline within our helper function.

    ● Start with day-number (e.g., 3)
    │
    ▼
  ┌──────────────────────────┐
  │ `take` 3 from `gifts`    │
  │ ["a Partridge...",       │
  │  "two Turtle Doves, ",   │
  │  "three French Hens, "]  │
  └────────────┬─────────────┘
               │
               ▼
  ┌──────────────────────────┐
  │ `reverse` the sequence   │
  │ ["three French Hens, ",  │
  │  "two Turtle Doves, ",   │
  │  "a Partridge..."]       │
  └────────────┬─────────────┘
               │
               ▼
  ◆ More than one gift? (Yes)
  ╱
Yes
│
▼
  ┌──────────────────────────┐
  │ Prepend "and " to last   │
  │ item ("a Partridge...")  │
  └────────────┬─────────────┘
               │
               ▼
  ┌──────────────────────────┐
  │ `join` into single string│
  └────────────┬─────────────┘
               │
               ▼
    ● "three French Hens, two Turtle Doves, and a Partridge..."

Step 3: Create a Function to Generate a Single Verse

Now that we can get the gift list for any day, creating a full verse is straightforward. This function will take a day number (from 1 to 12), fetch the corresponding ordinal, get the formatted gift list using our helper, and combine them into the final verse string using format.

Since our vectors are 0-indexed, we'll need to subtract 1 from the day number to get the correct index.

(defn verse [day-number]
  (let [ordinal (nth ordinals (dec day-number))
        gift-phrase (gifts-for-day day-number)]
    (format "On the %s day of Christmas my true love gave to me: %s"
            ordinal
            gift-phrase)))

Step 4: The Main Function to Generate the Entire Song

The final piece is a function that generates the whole song. This is where the power of Clojure's sequence functions becomes incredibly apparent. We don't need a loop. We simply need to apply our verse function to each day from 1 to 12.

The range function generates a sequence of numbers, and map applies a function to each item in that sequence. Finally, clojure.string/join concatenates all the generated verses with a newline character.

(defn sing []
  (->> (range 1 13)
       (map verse)
       (clojure.string/join "\n")))

The `->>` (thread-last) macro makes the data flow explicit and readable: take the range of numbers, then map `verse` over it, then join the results.

ASCII Art: Overall Song Generation Logic

This diagram shows the high-level composition of our functions.

    ● Call `sing()`
    │
    ▼
  ┌─────────────────┐
  │ `(range 1 13)`  │
  │ Creates seq:    │
  │ (1 2 3 ... 12)  │
  └────────┬────────┘
           │
           ▼
  ┌─────────────────┐
  │ `map` `verse`   │
  │ over the seq    │
  └────────┬────────┘
           │
           ├───────────→ For each number `n`:
           │           │
           │           ▼
           │       ┌───────────────┐
           │       │ call `verse(n)` │
           │       └───────┬───────┘
           │               │
           │               ├─ Gets ordinal
           │               │
           │               └─ Calls `gifts-for-day(n)`
           │
           ▼
  ┌─────────────────┐
  │ Result is a seq │
  │ of 12 strings:  │
  │ ("Verse 1" ...) │
  └────────┬────────┘
           │
           ▼
  ┌─────────────────┐
  │ `join` with "\n"│
  └────────┬────────┘
           │
           ▼
    ● Final Song String

The Complete Solution Code

Here is the final, clean, and commented code for our `twelve-days` module. This represents an idiomatic, data-driven solution in Clojure.

(ns twelve-days
  "This module generates the lyrics for 'The Twelve Days of Christmas'.
  It follows a functional, data-driven approach common in idiomatic Clojure.")

(def ordinals
  "A vector of ordinal strings for days 1 through 12."
  ["first" "second" "third" "fourth" "fifth" "sixth"
   "seventh" "eighth" "ninth" "tenth" "eleventh" "twelfth"])

(def gifts
  "A vector of gift phrases. Note the pre-formatted commas, spaces, and period."
  ["a Partridge in a Pear Tree."
   "two Turtle Doves, "
   "three French Hens, "
   "four Calling Birds, "
   "five Gold Rings, "
   "six Geese-a-Laying, "
   "seven Swans-a-Swimming, "
   "eight Maids-a-Milking, "
   "nine Ladies Dancing, "
   "ten Lords-a-Leaping, "
   "eleven Pipers Piping, "
   "twelve Drummers Drumming, "])

(defn- gifts-for-day
  "Constructs the string of gifts for a given day number (1-12).
  Handles the special 'and' for the last gift."
  [day-number]
  (let [gifts-subset (->> gifts
                          (take day-number) ; Get the first n gifts
                          reverse)         ; Reverse them for correct song order
        gifts-count  (count gifts-subset)]
    (if (> gifts-count 1)
      ;; If more than one gift, handle the 'and' case.
      ;; We take all but the first item, convert to a vector,
      ;; add a modified first item (with "and ") to the end,
      ;; and then join them.
      (clojure.string/join (conj (vec (rest gifts-subset)) (str "and " (first gifts-subset))))
      ;; If only one gift, just return it.
      (first gifts-subset))))

(defn verse
  "Generates a single verse for a given day number (1-12)."
  [day-number]
  (let [ordinal (nth ordinals (dec day-number)) ; 0-indexed access
        gift-phrase (gifts-for-day day-number)]
    (format "On the %s day of Christmas my true love gave to me: %s"
            ordinal
            gift-phrase)))

(defn sing
  "Generates the entire song by mapping the verse function over days 1-12."
  []
  (->> (range 1 13) ; Generate numbers from 1 up to (but not including) 13
       (map verse)
       (clojure.string/join "\n")))

Alternative Approaches and Considerations

While our `map`-based solution is highly idiomatic, Clojure's flexibility allows for other valid approaches. Exploring them can deepen your understanding of the language's capabilities.

1. Using `loop`/`recur`

A more traditional, recursive approach could use loop/recur. This is Clojure's mechanism for performing recursion without consuming stack space, making it equivalent to a loop in other languages.

(defn sing-recursive []
  (loop [day 1
         verses []]
    (if (> day 12)
      (clojure.string/join "\n" verses)
      (recur (inc day) (conj verses (verse day))))))

This version explicitly builds up the `verses` vector, which can be more intuitive for developers coming from an imperative background. However, the `map` version is generally considered more declarative and concise.

2. A `reduce`-based Solution

One could also frame the problem as a reduction. You can start with an empty string and iteratively append each new verse. This is less common for this specific problem but is a powerful pattern to know.

(defn sing-reduce []
  (let [verses (reduce (fn [acc day]
                         (conj acc (verse day)))
                       []
                       (range 1 13))]
    (clojure.string/join "\n" verses)))

This is functionally very similar to the `map` version but demonstrates another core functional tool.

Pros and Cons of Different Approaches

Approach Pros Cons
map and range (Main Solution) - Highly idiomatic and declarative.
- Extremely concise and readable.
- Leverages lazy sequences for potential efficiency.
- Might be slightly less intuitive for absolute beginners to functional programming.
loop/recur - Explicit state management (the `verses` accumulator).
- Easy to understand for those with an imperative background.
- Very efficient and stack-safe.
- More verbose than the `map` version.
- Less declarative; focuses more on the "how".
reduce - A powerful and flexible functional primitive.
- Good for more complex aggregations.
- Can be overkill for a simple transformation like this.
- Arguably less readable than `map` for this specific use case.

For this challenge from the kodikra.com curriculum, the `map` approach is the most elegant and showcases the intended functional style. You can learn more about these fundamental concepts in our main Clojure language guide.


Frequently Asked Questions (FAQ)

Why do you use (dec day-number) to access the vectors?

In programming, it's conventional for array-like data structures to be "0-indexed," meaning the first element is at index 0, the second at index 1, and so on. Our ordinals and gifts vectors follow this convention. Since the song's days are "1-indexed" (starting from the "first" day, or day 1), we must subtract 1 from the day number to get the correct 0-based index. (dec day-number) is just a concise way of writing (- day-number 1).

What's the purpose of the ->> (thread-last) macro in the sing function?

The thread-last macro ->> is syntactic sugar that helps improve readability by arranging a series of function calls in a top-to-bottom data pipeline. An expression like (->> (range 1 13) (map verse) (clojure.string/join "\n")) is automatically rewritten by Clojure into the more nested form: (clojure.string/join "\n" (map verse (range 1 13))). The threaded version is often much easier to read as it clearly shows the sequence of transformations applied to the initial data.

Could I have used a loop/recur instead of map?

Absolutely. As shown in the "Alternative Approaches" section, a loop/recur solution is perfectly valid and very efficient. It's a fundamental tool for recursion in Clojure. However, for transformations where you are applying the same function to every item in a sequence, map is generally preferred because it's more declarative—it describes the desired result rather than the step-by-step process of achieving it.

How does immutability benefit this specific solution?

Immutability simplifies our logic immensely. The original gifts vector is never changed. When we call (take day-number gifts), Clojure doesn't modify the original vector; it returns a *new* sequence containing the desired elements. This means we can call our functions repeatedly with different inputs and be 100% certain that they won't have unintended side effects on our core data. There's no need to worry about the state of our data at any point in the program's execution.

Why was the "and" handled with string manipulation instead of a different data structure?

The logic for handling "and " in the gifts-for-day function is a good example of a practical trade-off. We could have stored the gifts without the trailing commas and handled all punctuation in the logic, but that would complicate the function. By pre-formatting the data in the gifts vector, we simplify the common case. The special "and" logic is an edge case that only applies when there's more than one gift, so handling it with a simple string concatenation (str "and " ...) is a clean and localized solution.

Is this solution efficient for a much larger number of days?

Yes, for two main reasons. First, Clojure's sequence functions like map and take are lazy. They don't compute their results until they are actually needed. While this doesn't provide a huge benefit for just 12 verses, it's a powerful feature for larger datasets. Second, the operations themselves are computationally inexpensive. The time complexity grows quadratically with the number of days (since each verse gets longer), which is inherent to the problem itself, not a flaw in the implementation.


Conclusion: The Power of Data-Driven Design

Successfully completing the "Twelve Days" module demonstrates a fundamental shift in thinking that is central to mastering Clojure. We didn't write complex loops or manually manage string builders. Instead, we took a step back, modeled the song's components as simple data structures, and then composed small, pure functions from the core library to achieve our goal. This data-driven, declarative approach resulted in code that is not only robust and correct but also remarkably readable and easy to reason about.

The key takeaways from this exercise are the importance of separating data from logic, leveraging the power of immutable collections, and understanding how to build solutions by composing functions in a pipeline. These principles are applicable to problems of any scale, from generating song lyrics to building complex, enterprise-level applications.

Disclaimer: The solution and code examples provided are based on idiomatic practices for Clojure 1.11+ and are expected to be forward-compatible. As the language evolves, new functions or macros may offer alternative ways to solve this problem.

Ready for your next challenge? Continue your journey on the kodikra Clojure learning path or explore more advanced Clojure concepts on our main content page.


Published by Kodikra — Your trusted Clojure learning resource.