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

a close up of a computer screen with code on it

Mastering Date Calculations in Common Lisp: The Ultimate Meetup Scheduler Guide

Learn to calculate specific meetup dates in Common Lisp, such as the "third Tuesday" or "teenth Wednesday" of a given month and year. This guide explores core date functions like encode-universal-time and decode-universal-time to solve complex scheduling logic from scratch.

You’ve been there before. Trying to coordinate a recurring event, like a monthly meetup, can feel like a logistical nightmare. Juggling calendars, time zones, and vague requests like "the last Friday of the month" often leads to confusion and endless back-and-forth emails. What if you could automate this entire process with the precision and elegance of a powerful programming language?

This is where Common Lisp shines. It isn't just an academic language; it's a pragmatic tool with a robust standard library perfectly suited for tackling complex logical problems, including date and time calculations. In this deep dive, we'll deconstruct the "Meetup" scheduling problem from the exclusive kodikra.com learning path. We will build a complete solution from the ground up, giving you the skills to handle any date-based challenge thrown your way.


What is the Meetup Scheduling Problem?

The core task is to write a function that finds the exact date for a meetup. The function will receive a year, a month, a target day of the week (e.g., Monday, Tuesday), and a week descriptor. The challenge lies in correctly interpreting these week descriptors.

The descriptors are:

  • :first - The first instance of the target weekday in the month.
  • :second - The second instance of the target weekday in the month.
  • :third - The third instance of the target weekday in the month.
  • :fourth - The fourth instance of the target weekday in the month.
  • :last - The final instance of the target weekday in the month.
  • :teenth - The only instance of the target weekday that falls on a "teenth" day (13th, 14th, 15th, 16th, 17th, 18th, or 19th).

For example, if asked for the "teenth Wednesday of May 2020," the correct answer is May 13, 2020. If asked for the "last Sunday of July 2021," the answer is July 25, 2021. Our goal is to create a function that reliably produces these dates.


Why Common Lisp is Superb for Date Logic

Before diving into the code, it's worth understanding why Common Lisp is an excellent choice for this kind of problem. While many languages require external libraries for robust date handling, Common Lisp includes powerful, standardized tools right in its core.

The Power of Universal Time

The heart of Common Lisp's date/time functionality is Universal Time. It's a system that represents a specific point in time as a single, non-negative integer. This integer is the number of seconds that have elapsed since midnight, January 1, 1900, in the UTC time zone.

By representing time as a single number, complex calculations become simpler. Adding a day is just adding 86,400 seconds. This integer-based system avoids the complexities and rounding errors associated with other time representations.

Built-in Conversion Functions

Common Lisp provides two critical functions for working with Universal Time:

  • encode-universal-time: Takes human-readable date/time components (second, minute, hour, day, month, year) and converts them into a single Universal Time integer.
  • decode-universal-time: Does the reverse. It takes a Universal Time integer and returns multiple values representing the date and time components, including the day of the week.

These functions are the building blocks of our solution. They allow us to move seamlessly between a human-friendly date format and a machine-friendly integer format, which is perfect for calculation.


How to Approach the Solution: A Strategic Breakdown

Solving this problem requires a clear, step-by-step strategy. We can't just jump into coding. We need to build a logical framework first. The overall flow involves determining a search range of days within the month and then iterating through that range to find our target weekday.

Here is a high-level view of our algorithm:

● Start
│
▼
┌───────────────────────────┐
│ Input: Year, Month, Week, │
│        Weekday            │
└────────────┬──────────────┘
             │
             ▼
  ◆ Analyze 'Week' Descriptor ◆
   ╱            │            ╲
'first'…'fourth' 'teenth'      'last'
   │            │            │
   ▼            ▼            ▼
┌───────────┐ ┌───────────┐ ┌────────────────┐
│ Set Start │ │ Set Start │ │ Find Last Day  │
│ Day Range │ │ Day to 13 │ │ Set Start Day  │
│ (1, 8, etc)│└─────┬─────┘ │ to (End - 6)   │
└─────┬─────┘      │       └────────┬───────┘
      │            │                │
      └────────────┼────────────────┘
                   │
                   ▼
┌───────────────────────────────────┐
│ Loop through 7 consecutive days   │
│ starting from the calculated day. │
└───────────────────┬───────────────┘
                    │
                    ▼
      ◆ Day of Week Matches Target? ◆
     ╱                 ╲
   Yes                  No
    │                   │
    ▼                   ▼
┌───────────┐      [Continue Loop]
│ Return    │
│ Date      │
└───────────┘
    │
    ▼
● End

Step 1: Map Weekdays and Descriptors

Computers work with numbers, not words. Our first task is to map the textual inputs like :monday and :first to numerical values that our code can process. Common Lisp's decode-universal-time function conveniently returns the day of the week as an integer from 0 (Monday) to 6 (Sunday). We'll use this standard.

Step 2: Determine the Search Start Day

The key insight is that each week descriptor gives us a starting day for our search. We don't need to check all 31 days of the month.

  • For :first, we start searching from day 1.
  • For :second, we start searching from day 8.
  • For :third, we start searching from day 15.
  • For :fourth, we start searching from day 22.
  • For :teenth, the range is fixed. We start searching from day 13.
  • For :last, this is the trickiest. We need to find the last day of the month first, and then start our search 6 days before it to guarantee we find the last instance of any weekday.

Step 3: Iterate and Check

Once we have a starting day, we can iterate forward for up to 7 days. In each iteration, we calculate the day of the week for the current date. When we find a day whose weekday matches our target, we have found our answer.


Where the Magic Happens: A Deep Code Walkthrough

Now, let's analyze the complete Common Lisp solution provided in the kodikra.com module. We'll break it down function by function to understand every piece of the logic.


(defpackage :meetup
  (:use :common-lisp)
  (:export :meetup))

(in-package :meetup)

(defun day-of-week (day month year)
  "Returns the day of the week (0=Mon, 6=Sun) for a given date."
  (nth 6 (multiple-value-list
           (decode-universal-time
             (encode-universal-time 0 0 0 day month year)))))

(defun last-day-of (month year)
  "Calculates the last day of a given month and year."
  (let ((next-month (if (= month 12) 1 (1+ month)))
        (next-year (if (= month 12) (1+ year) year)))
    (nth 3 (multiple-value-list
             (decode-universal-time
               (1- (encode-universal-time 0 0 0 1 next-month next-year)))))))

(defun find-dow-near-date (start-day month year dow)
  "Finds the date of the first occurrence of a day-of-week (dow)
   on or after a given start-day."
  (loop for day from start-day to (+ start-day 6)
        when (= (day-of-week day month year) dow)
        return day))

(defun meetup (month year dow week)
  "Calculates the date of the meetup."
  (let ((dow-num (case dow
                   (:monday 0) (:tuesday 1) (:wednesday 2) (:thursday 3)
                   (:friday 4) (:saturday 5) (:sunday 6))))
    (let ((day (case week
                 (:first  (find-dow-near-date 1 month year dow-num))
                 (:second (find-dow-near-date 8 month year dow-num))
                 (:third  (find-dow-near-date 15 month year dow-num))
                 (:fourth (find-dow-near-date 22 month year dow-num))
                 (:teenth (find-dow-near-date 13 month year dow-num))
                 (:last   (find-dow-near-date (- (last-day-of month year) 6) month year dow-num)))))
      (list year month day))))

Helper Function: day-of-week

This is a fundamental utility. Its only job is to tell us the day of the week for any given date.


(defun day-of-week (day month year)
  "Returns the day of the week (0=Mon, 6=Sun) for a given date."
  (nth 6 (multiple-value-list
           (decode-universal-time
             (encode-universal-time 0 0 0 day month year)))))
  • (encode-universal-time 0 0 0 day month year): This takes our date components and converts them into a single Universal Time integer. We use 0 for second, minute, and hour as we only care about the date itself.
  • (decode-universal-time ...): This takes the Universal Time integer and returns nine values: second, minute, hour, date, month, year, day-of-week, daylight-saving-time-p, and time-zone.
  • (multiple-value-list ...): Since decode-universal-time returns multiple values, we need to capture them. This function conveniently wraps all returned values into a single list.
  • (nth 6 ...): The day of the week is the 7th value returned (at index 6, since lists are 0-indexed). This extracts that specific value (0 for Monday, ..., 6 for Sunday) and returns it.

Helper Function: last-day-of

This function is clever. Instead of containing complex logic about leap years and the number of days in each month, it uses a brilliant trick with Universal Time.


(defun last-day-of (month year)
  "Calculates the last day of a given month and year."
  (let ((next-month (if (= month 12) 1 (1+ month)))
        (next-year (if (= month 12) (1+ year) year)))
    (nth 3 (multiple-value-list
             (decode-universal-time
               (1- (encode-universal-time 0 0 0 1 next-month next-year)))))))

The logic is as follows:

  1. Calculate the date of the first day of the next month. The let block handles the December-to-January year rollover.
  2. Convert this date (e.g., March 1st) into a Universal Time integer using encode-universal-time.
  3. Subtract 1 from this integer. Since Universal Time is measured in seconds, subtracting 1 second from midnight on the 1st of the next month gives you the last second of the last day of the current month.
  4. decode-universal-time this new, decremented integer.
  5. Extract the 4th value (index 3), which is the day of the month. This will be 28, 29, 30, or 31, depending on the month and year.
This is a highly idiomatic and robust way to find the end of a month without manual checks.

Helper Function: find-dow-near-date

This function encapsulates our core iteration logic. It takes a starting day and finds the first occurrence of the target weekday on or after that day.


(defun find-dow-near-date (start-day month year dow)
  "Finds the date of the first occurrence of a day-of-week (dow)
   on or after a given start-day."
  (loop for day from start-day to (+ start-day 6)
        when (= (day-of-week day month year) dow)
        return day))
  • (loop for day from start-day to (+ start-day 6) ...): This sets up a loop that will run for exactly 7 days, starting from start-day. This is efficient because any day of the week is guaranteed to occur within any 7-day period.
  • when (= (day-of-week day month year) dow): Inside the loop, it calls our day-of-week helper to get the weekday for the current day. It checks if this matches our numeric target dow.
  • return day: As soon as a match is found, the loop immediately stops and returns the matching day number.

The Main Function: meetup

This function orchestrates everything. It ties all the helper functions together to produce the final result.


(defun meetup (month year dow week)
  "Calculates the date of the meetup."
  (let ((dow-num (case dow
                   (:monday 0) (:tuesday 1) (:wednesday 2) (:thursday 3)
                   (:friday 4) (:saturday 5) (:sunday 6))))
    (let ((day (case week
                 (:first  (find-dow-near-date 1 month year dow-num))
                 (:second (find-dow-near-date 8 month year dow-num))
                 (:third  (find-dow-near-date 15 month year dow-num))
                 (:fourth (find-dow-near-date 22 month year dow-num))
                 (:teenth (find-dow-near-date 13 month year dow-num))
                 (:last   (find-dow-near-date (- (last-day-of month year) 6) month year dow-num)))))
      (list year month day))))
  1. Translate Day of Week: The first let block uses a case statement to convert the keyword symbol (e.g., :monday) into its corresponding number (0). This is clean and readable.
  2. Determine Start Day & Find Date: The second, nested let block is the core logic. It uses another case statement on the week descriptor. Based on the descriptor, it calls find-dow-near-date with the correct starting day (1, 8, 13, etc.). The result—the actual day of the month—is bound to the variable day.
  3. Handle the :last Case: This is where our last-day-of helper is used. (- (last-day-of month year) 6) calculates the correct starting point for the final week's search.
  4. Format the Output: Finally, (list year month day) constructs the output in the required format, a list containing the year, month, and the calculated day.

This modular design, with small, focused helper functions, makes the code easy to read, test, and debug. It's a hallmark of good Lisp programming style.


Visualizing the Logic for Special Cases

The logic for :teenth and :last is the most interesting part of the problem. A visual flowchart can help clarify how the algorithm decides where to start its search.

● Input (week descriptor)
│
├─ Is it :first, :second, :third, or :fourth?
│  │
│  └─→ Calculate start day (1, 8, 15, or 22)
│
├─ Is it :teenth?
│  │
│  └─→ Set start day to 13 (fixed)
│      │
│      ▼
│   ┌──────────────────────────┐
│   │ Search days 13, 14, .. 19│
│   └──────────────────────────┘
│
├─ Is it :last?
│  │
│  ├─→ Call last-day-of(month, year) → e.g., 31
│  │
│  └─→ Calculate start day (31 - 6 = 25)
│      │
│      ▼
│   ┌──────────────────────────┐
│   │ Search days 25, 26, .. 31│
│   └──────────────────────────┘
│
▼
● Pass start day to find-dow-near-date

Pros & Cons: Built-in Functions vs. External Libraries

While the standard library is powerful, it's important to know when it's sufficient and when you might need more. For most standalone applications, the built-in functions are perfect. However, for complex applications involving time zones, localization, and duration arithmetic, a dedicated library might be better.

Aspect Common Lisp Standard (encode/decode-universal-time) External Library (e.g., local-time)
Dependencies Pro: Zero. Included in every CL implementation. Con: Requires Quicklisp or manual installation. Adds a dependency to your project.
Time Zone Handling Con: Basic. Relies on the host system's time zone settings. Can be tricky to manage explicitly. Pro: Rich, explicit time zone support. Can load and work with the full IANA time zone database.
Ease of Use Pro: Simple for basic date-to-integer conversions. The logic is straightforward. Pro: Provides a higher-level, object-oriented API that can be more intuitive for complex operations (e.g., (timestamp+ now 5 :day)).
Performance Pro: Highly optimized and implemented in the core system. Very fast. Con: May have slightly more overhead due to the more complex object system, but generally very performant.
Functionality Con: Lacks advanced features like date formatting/parsing from strings and duration arithmetic. Pro: Comprehensive feature set including robust parsing, formatting, and arithmetic with time units.

For the meetup problem, the standard library is more than sufficient and provides an elegant, dependency-free solution. To learn more about the breadth of the language, check out our complete guide to Common Lisp.


Frequently Asked Questions (FAQ)

What exactly is Universal Time in Common Lisp?

Universal Time is a standardized system in Common Lisp for representing time as a single integer. This integer counts the number of seconds elapsed since midnight on January 1, 1900, in the UTC (Coordinated Universal Time) time zone. This makes date arithmetic (like adding or subtracting days) as simple as integer arithmetic.

How does decode-universal-time handle time zones?

By default, decode-universal-time decodes the Universal Time integer into the current time zone of the operating system where the Lisp environment is running. You can provide an optional argument to specify a time zone offset from UTC if you need to work with a different zone.

Why does the day-of-week mapping start with Monday=0?

This is a convention defined by the Common Lisp standard (ANSI Common Lisp). It aligns with the ISO 8601 standard, which also treats Monday as the first day of the week. While other systems might use Sunday=0, sticking to the Lisp standard (Monday=0, Sunday=6) ensures consistency when using the built-in functions.

How does this code handle leap years?

It handles them perfectly and automatically! This is the beauty of using the encode-universal-time and decode-universal-time functions. The underlying Common Lisp implementation is responsible for all the complex calendar rules, including leap years. Our code doesn't need any special `if` statements for February 29th; the system takes care of it.

Could this logic be adapted for other recurring schedules, like "the second-to-last Friday"?

Absolutely. You could adapt the logic for the :last case. For "second-to-last," you would find the last occurrence and then subtract 7 days from that date. For more complex schedules, you might build upon these foundational functions to create a more general-purpose scheduling engine.

Is there a more efficient way to find the last day of a month?

The method used in the solution (finding the first day of the next month and subtracting one second) is considered the most robust and idiomatic way in Common Lisp. It's computationally very fast and avoids the need to maintain tables of month lengths or write custom leap year logic, which is error-prone. It leverages the heavily optimized core system for maximum reliability.


Conclusion: Elegance and Power Combined

We've successfully dissected a non-trivial date calculation problem and built a robust, readable, and efficient solution in Common Lisp. This exercise from the kodikra curriculum demonstrates that with a solid understanding of core language features—like Universal Time—you can solve complex real-world problems without reaching for external dependencies.

The key takeaways are the power of abstraction through helper functions (day-of-week, last-day-of) and the clever use of the built-in time conversion utilities to bypass manual, error-prone calendar logic. This approach is not just a solution to one problem; it's a template for tackling any task that involves date manipulation in Common Lisp.

Ready to continue your journey? Explore our Common Lisp learning path to tackle more challenges and deepen your understanding of this powerful language.


Disclaimer: All code snippets and explanations are based on the ANSI Common Lisp standard. Behavior should be consistent across modern implementations like SBCL, CCL, and ECL.


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