Leap in Common-lisp: Complete Solution & Deep Dive Guide

a close up of a computer screen with code on it

Mastering Leap Year Logic in Common Lisp: A Zero to Hero Guide

Determining if a year is a leap year in Common Lisp is a classic logic puzzle that elegantly combines mathematical rules with boolean operators. The core logic hinges on three conditions: the year must be divisible by 4, unless it is also divisible by 100 but not by 400.


You’ve just started building a new application. Perhaps it's a calendar, a financial projection tool, or a system for historical data analysis. Everything is going smoothly until you hit a seemingly simple task: handling dates correctly. Suddenly, you're confronted with an ancient rule, a small but critical detail that could throw your entire system off by a day—the leap year. It feels like a trivial problem, but the edge cases can be surprisingly tricky.

Many developers stumble here, writing complex, nested `if` statements that are hard to read and even harder to maintain. You're worried about getting it wrong, about introducing a subtle bug that only appears once every few years. This is a common pain point, a rite of passage that tests not just your coding skills, but your ability to translate precise logical rules into clean, effective code.

This guide is your definitive solution. We will not only demystify the rules of the Gregorian calendar but also show you how to implement them in Common Lisp with idiomatic elegance. By the end, you'll have a robust, readable, and efficient function that solves the leap year problem, solidifying your understanding of Lisp's powerful logical operators and functional programming style.


What Exactly is a Leap Year? A Quick Dive into History and Logic

Before we write a single line of code, it's crucial to understand the "what" and "why" behind the leap year. This isn't just an arbitrary rule; it's a clever astronomical and mathematical correction that keeps our calendars in sync with the Earth's orbit around the Sun.

The Earth takes approximately 365.2425 days to complete one full orbit. Our standard Gregorian calendar, however, only has 365 days. If we ignored that extra ~0.2425 of a day, our seasons would gradually drift. After 100 years, the calendar would be off by about 24 days, and summer would eventually start happening in what we call December!

To correct this, the concept of a "leap day" (February 29th) was introduced. But simply adding a day every four years is a slight overcorrection. This is where the specific, multi-layered rules come into play.

The Three Definitive Rules of a Leap Year

The logic, as established by the Gregorian calendar, can be broken down into a clear set of conditions. A year is a leap year if it satisfies the following hierarchy:

  • Rule 1: The year must be evenly divisible by 4.
  • Rule 2: However, if the year is also evenly divisible by 100, it is NOT a leap year.
  • Rule 3: Unless... the year is also evenly divisible by 400. In this case, it IS a leap year.

Let's see this logic in action with some examples:

  • 1997: Is not divisible by 4. It fails Rule 1 immediately. Result: Not a leap year.
  • 2024: Is divisible by 4. It is not divisible by 100. It passes Rule 1 and doesn't trigger Rule 2. Result: Is a leap year.
  • 1900: Is divisible by 4 and by 100. This triggers the exception in Rule 2. We then check Rule 3: is it divisible by 400? No. Result: Not a leap year.
  • 2000: Is divisible by 4 and by 100. This triggers Rule 2. We check Rule 3: is it divisible by 400? Yes. The exception to the exception applies. Result: Is a leap year.

This nested logic is what makes the problem interesting. It's a perfect test case for expressing compound boolean conditions in a programming language.


Why is This a Foundational Challenge in Programming?

The leap year problem appears in countless programming courses and technical interviews for a reason. It's a "micro-problem" that effectively assesses several core developer skills, especially in a language like Common Lisp.

  • Boolean Logic Mastery: It forces you to think clearly about AND, OR, and NOT conditions and how they combine. Translating the English rules ("unless," "except if") into formal logic is the heart of the challenge.
  • Modularity and Function Purity: The problem naturally lends itself to creating small, pure functions. A function that only checks for divisibility can be reused, promoting clean, DRY (Don't Repeat Yourself) code.
  • Readability and Idiomatic Code: You can solve this with a messy series of nested `if`s, or you can solve it with a clean, one-line logical expression. How you write the code says a lot about your understanding of the language's style and strengths. In Lisp, this often means leveraging its prefix notation and functional composition.
  • Real-World Relevance: As mentioned, any software dealing with time, dates, scheduling, or financial calculations will inevitably need this logic. From calculating interest on a 366-day year to scheduling recurring events, getting this right is non-negotiable in production systems.

Tackling this module from the kodikra Common Lisp learning path is an essential step in moving from understanding syntax to applying logical problem-solving.


How to Implement the Leap Year Algorithm in Common Lisp

Now, let's translate our understanding into working Common Lisp code. We'll start by analyzing the elegant solution provided in the kodikra.com curriculum, breaking it down piece by piece. First, let's visualize the decision process.

Visualizing the Logic Flow

A flowchart is the perfect way to see the decision-making hierarchy we discussed. This diagram shows how an input year is tested against each condition in sequence.

    ● Start: Input `year`
    │
    ▼
  ┌───────────────────────┐
  │ Is `year` divisible   │
  │ by 4?                 │
  └──────────┬────────────┘
             │
   No ╱      ╲ Yes
      │        │
      ▼        ▼
┌───────────┐  ◆ Is `year` divisible by 100?
│ NOT a Leap│
│ Year      │    No ╱      ╲ Yes
└───────────┘       │        │
    │               ▼        ▼
    │         ┌──────────┐   ◆ Is `year` divisible by 400?
    │         │ IS a Leap│
    │         │ Year     │     No ╱      ╲ Yes
    │         └──────────┘        │        │
    │             │               ▼        ▼
    │             │         ┌───────────┐┌──────────┐
    │             │         │ NOT a Leap││ IS a Leap│
    │             │         │ Year      ││ Year     │
    │             │         └───────────┘└──────────┘
    │             │             │            │
    └─────────────┼─────────────┴────────────┘
                  │
                  ▼
              ● End

This visual map is exactly what our Common Lisp code needs to represent.

Setting Up the Lisp Environment: Packages

The provided solution starts with two lines of boilerplate that are fundamental to organizing code in Common Lisp:

(cl:defpackage :leap
  (:use :common-lisp)
  (:export :leap-year-p))

(in-package :leap)
  • (cl:defpackage :leap ...): This defines a new "package" named leap. Think of a package as a namespace in other languages (like a module in Python or a package in Java). It prevents naming conflicts between different parts of a larger program.
  • (:use :common-lisp): This tells our new leap package to inherit all the standard functions and symbols from the core :common-lisp package (like defun, and, =, etc.).
  • (:export :leap-year-p): This is the crucial part for usability. It declares that the function named leap-year-p should be public. Any other code that uses this package can access leap-year-p. Any other functions we define but don't export will be private to this package.
  • (in-package :leap): This command switches the current working namespace to our newly defined leap package. All subsequent code we write will belong to this package.

The Helper Function: divisible-by-p

Good programming is about breaking down complex problems into smaller, manageable ones. The solution wisely starts with a helper function to handle the core mathematical operation: checking for divisibility.

(defun divisible-by-p (n d)
  (= 0 (rem n d)))

Let's dissect this elegant piece of Lisp:

  • (defun divisible-by-p (n d) ...): This defines a function named divisible-by-p. The -p suffix is a common Lisp convention for functions that return a boolean value (true or false). These are called "predicate" functions. It takes two arguments: n (the number) and d (the divisor).
  • (rem n d): This is the core of the logic. The rem function calculates the remainder of dividing n by d. For example, (rem 10 3) would return 1.
  • (= 0 ...): This checks if the result of the inner expression is equal to 0. If the remainder of a division is 0, it means the number is evenly divisible. The function returns T (Lisp's symbol for true) if the remainder is 0, and NIL (Lisp's symbol for false) otherwise.

By creating this function, we've abstracted away the concept of "divisibility," making our main function much cleaner and more readable.

The Main Logic: leap-year-p

Now we arrive at the main function, which beautifully translates the leap year rules into a single, declarative Lisp expression.

(defun leap-year-p (year)
  (and (divisible-by-p year 4)
       (or (not (divisible-by-p year 100))
           (divisible-by-p year 400))))

This looks dense at first, but it maps directly to our rules if we read it from the outside in, remembering Lisp's prefix notation (operator first, then operands).

  1. (and ...): The entire expression is wrapped in an and. This means for the whole thing to be true, every argument inside the and must be true. In this case, there are two arguments.
  2. Argument 1 for and: (divisible-by-p year 4). This directly implements Rule 1. The year must be divisible by 4. If this is false, the and macro "short-circuits" and immediately returns NIL without evaluating the rest.
  3. Argument 2 for and: (or ...). If the first argument was true, the logic proceeds to this part. The or means that for this sub-expression to be true, at least one of its arguments must be true.
  4. Argument 1 for or: (not (divisible-by-p year 100)). This translates to "the year is NOT divisible by 100". This covers the standard case like 2024.
  5. Argument 2 for or: (divisible-by-p year 400). This handles the special exception. Even if the previous part was false (i.e., the year IS divisible by 100), this part gives it a second chance to be true if it's also divisible by 400 (like the year 2000).

Let's trace the year 1900 through this logic:

    ● leap-year-p(1900)
    │
    ├─> (and A B)
    │   │
    │   ├─> A: divisible-by-p(1900, 4)  ───> T
    │   │
    │   └─> B: (or C D)
    │       │
    │       ├─> C: (not (divisible-by-p(1900, 100)))
    │       │   │
    │       │   └─> divisible-by-p(1900, 100) ───> T
    │       │       │
    │       │       └─> (not T) ───> NIL
    │       │
    │       └─> D: divisible-by-p(1900, 400) ───> NIL
    │
    ├─> Evaluate (or NIL NIL) ───> NIL
    │
    └─> Evaluate (and T NIL) ───> NIL (Result: Not a leap year)

Testing the Code in a REPL

You can test this code interactively using a Common Lisp implementation like SBCL (Steel Bank Common Lisp). First, save the code into a file named leap.lisp.

Then, run SBCL from your terminal and load the file:

$ sbcl
* (load "leap.lisp")
T

Now you can call the exported function. Remember to prefix it with the package name.

* (leap:leap-year-p 2024)
T

* (leap:leap-year-p 1997)
NIL

* (leap:leap-year-p 1900)
NIL

* (leap:leap-year-p 2000)
T

The results perfectly match our expected outcomes, confirming the logic is sound.


Alternative Implementations and Code Styles

The solution using and/or is highly idiomatic and efficient. However, for learning purposes, it's valuable to explore other ways to structure the same logic. An alternative approach is using the cond macro, which is similar to a multi-clause if-elseif-else structure.

Using cond for Explicit Readability

The cond macro evaluates a series of clauses. Each clause has a test condition and a result. cond executes the result of the first clause whose test condition evaluates to true.

(defun leap-year-p-cond (year)
  (cond ((not (divisible-by-p year 4)) nil)      ; If not divisible by 4, it's not a leap year.
        ((not (divisible-by-p year 100)) t)     ; If divisible by 4 but not 100, it is.
        ((divisible-by-p year 400) t)           ; If by 4 and 100, check if also by 400.
        (t nil)))                               ; Otherwise (divisible by 100 but not 400), it's not.

This version is more verbose, but some programmers find it easier to read because it follows a more linear, step-by-step decision process. The final clause (t nil) acts as a "default" or "else" case, as t is always true.

Pros and Cons of Each Approach

Choosing a style is often a matter of team convention and personal preference, but it's good to know the trade-offs.

Style Pros Cons
Nested and/or
  • Very concise and compact.
  • Considered highly idiomatic in the Lisp community.
  • Directly mirrors the logical definition of the problem.
  • Can be difficult for beginners to parse due to deep nesting.
  • Requires careful reading to understand the boolean precedence.
cond Macro
  • Extremely readable and explicit.
  • Follows a clear, sequential "if-this, then-that" flow.
  • Easier to debug by evaluating each clause independently.
  • More verbose, requires more lines of code.
  • Can be less efficient if the "true" condition is at the end of a long list of clauses.

For this specific problem, both solutions are excellent. The original solution is a great example of Lisp's expressive power, while the cond version is a fantastic illustration of its clarity.


Frequently Asked Questions (FAQ)

What is the difference between `rem` and `mod` in Common Lisp?

For positive integers, rem (remainder) and mod (modulo) return the same result. The difference appears with negative numbers. rem's result has the same sign as the dividend, while mod's result has the same sign as the divisor. For the leap year problem, which deals with positive years, rem is perfectly sufficient and common.

Why use a helper function like `divisible-by-p`?

Using a helper function follows the DRY (Don't Repeat Yourself) principle. Instead of writing (= 0 (rem year 4)) multiple times, we write (divisible-by-p year 4). This makes the main function's code more abstract and readable. It focuses on the "what" (checking divisibility) rather than the "how" (calculating a remainder and comparing to zero). It also makes the code easier to change; if you ever wanted to modify how divisibility is checked, you'd only need to change it in one place.

Can this logic handle years before the Gregorian calendar?

No. This logic is specific to the Gregorian calendar, which was adopted at different times in different countries (starting in 1582). For dates prior to that, one would need to use the Julian calendar rules, which simply state that any year divisible by 4 is a leap year. For accurate historical date calculations, you would need a more sophisticated library that is aware of these calendar reforms.

How can I test my Common Lisp code from the command line?

Most Common Lisp implementations like SBCL allow you to execute code directly. After loading your file with sbcl --load your-file.lisp, you can use the --eval flag to run a specific form and --quit to exit, making it suitable for scripting. For example: sbcl --load leap.lisp --eval "(format t \"~A~%\" (leap:leap-year-p 2000))" --quit.

What does `defpackage` and `:export` really do?

defpackage creates an isolated namespace for your code's symbols (like function and variable names). This is critical for building large applications without worrying that your function named calculate will clash with another library's function also named calculate. :export acts as the public API for your package, explicitly listing which symbols can be accessed from outside. It enforces a clean boundary between your module's internal implementation and its external interface.

Are there built-in libraries in Common Lisp for date calculations?

While the Common Lisp standard includes functions for handling time (e.g., get-universal-time), it does not have a comprehensive date/time library like Python's `datetime` or Java's `java.time`. However, the ecosystem has excellent third-party libraries available through Quicklisp, such as local-time, which provide robust, full-featured solutions for time zones, date arithmetic, and calendar calculations, including leap years.

Why is the year 2000 a leap year but 1900 is not?

This is the crucial edge case that tests the full set of rules. Both years are divisible by 4 and by 100. According to Rule 2, this would normally make them NOT leap years. However, Rule 3 provides an exception: if the year is also divisible by 400, it becomes a leap year again. The year 2000 is divisible by 400, so it is a leap year. The year 1900 is not divisible by 400, so it remains a non-leap year.


Conclusion: From Logic to Elegant Lisp

The leap year problem, while simple on the surface, provides a perfect lens through which to view the elegance and power of Common Lisp. We've seen how to translate a precise set of rules not into clunky, imperative code, but into a declarative and functional expression that reads like logic itself. By leveraging helper functions, understanding package management, and mastering boolean operators like and, or, and not, you can write code that is not only correct but also clean, idiomatic, and maintainable.

This exercise from the kodikra.com curriculum is more than just a puzzle; it's a foundational step in thinking like a Lisp programmer. The skills you've honed here—decomposing a problem, creating pure functions, and composing them into a larger solution—are universally applicable and will serve you well as you tackle more complex challenges.

Technology Disclaimer: The code and concepts discussed in this article are based on the ANSI Common Lisp standard. All examples have been verified with modern, stable implementations such as Steel Bank Common Lisp (SBCL) version 2.4.x and should be compatible with other compliant Lisp environments.

Ready to continue your journey? Explore the next module in our Common Lisp learning path or dive deeper into the language with our comprehensive collection of Common Lisp guides and tutorials.


Published by Kodikra — Your trusted Common-lisp learning resource.