SR.

Temporal Expressions: How Set Theory Can Fix Your Scheduling Nightmares

Back to blog

Temporal Expressions: How Set Theory Can Fix Your Scheduling Nightmares

Apr 6, 2025·14 min read·
Design PatternsSet TheoryTypeScriptScheduling

The Problem: Scheduling Is Secretly Hard

It's a Monday morning, you are in your sprint planning & you've been tasked by your tech lead to build a scheduling feature for events. "How hard can it be?" you think, sipping your coffee with the confidence of someone who has never modeled time professionally. You accept the task...

Then the requirements start rolling in:

  • "We need to be able to set schedules for events every Monday."
  • "Actually, every Monday and Wednesday."
  • "But not during the summer."
  • "Oh, and the first Friday of every month too."
  • "Except holidays."
  • "Can we also add..."

Before you know it, your elegant scheduling function looks like a switch statement that needs therapy. You've got nested if blocks six levels deep, date comparisons scattered across twelve files, and a growing suspicion that the Gregorian calendar was designed by someone who actively hated programmers.

Pressure

There's a better way. It involves math... but the fun kind, called 'Temporal Expression'

Where Does Scheduling Responsibility Live?

Let's think about a concrete example. A community center in Kigali manages several recurring activities. Umuganda (community service) happens on the last Saturday of every month. The local Ikimina (savings group) meets every Wednesday evening. How do we model this?

The naive approach is to give the community center a direct association to dates for each activity: a list of Umuganda dates, a list of Ikimina dates, and so on. But this falls apart quickly. Every time a new activity is added, we have to modify the model. And we'd have to enumerate every individual date, which defeats the whole purpose of a recurring schedule.

A better approach is to pull this responsibility out into a separate Schedule object. The community center doesn't manage dates itself. It just has a schedule, and the schedule tracks which activities occur on which days. Now any object that needs scheduling behavior (a community center, a cooperative, a school) can simply hold a reference to a schedule.

The schedule contains schedule elements, each of which pairs an event (like "Umuganda") with a temporal expression that defines when it occurs. The temporal expression is the piece that knows the date logic: "last Saturday of every month" or "every Wednesday."

This separation is important: the what (which event) is kept apart from the when (which dates). All the date-matching complexity lives inside the temporal expression, and the schedule just delegates to it.

Temporal Expressions: Dates as Sets

So what is a temporal expression? The Temporal Expression pattern, first described by Martin Fowler, reframes the scheduling problem using a simple insight from set theory. Every scheduling rule defines a set of dates, and the only question you ever need to answer is: "Is this date in the set?"

The entire contract for a temporal expression boils down to a single method:

interface TemporalExpression {
  includes(date: Date): boolean;
}

That's it. One method. The schedule asks "is there Umuganda on this date?" and the schedule element matches the event, then asks its temporal expression includes(date). If it returns true, the event is occurring.

This is the key insight: the schedule doesn't contain date logic. The schedule element doesn't contain date logic. All the complexity is pushed down into temporal expressions. Small, focused objects that answer one question about one date.

Building Temporal Expressions

So we have this clean includes(date) interface, but how do we actually build something that implements it? We need concrete objects that can answer the question for specific patterns like "every Wednesday" or "last Saturday of the month."

It turns out we don't need many. A small set of simple expression types can cover most scheduling needs on their own, and for complex cases, we can combine them using set operations. This gives us two categories:

  1. Leaf expressions: simple, single-rule matchers (the building blocks)
  2. Set expressions: combinators that compose leaves into complex schedules (the glue)

We'll start with the leaves, then see how set theory lets us snap them together.

Leaf Expressions: The Atoms

Leaf expressions are the simplest building blocks. Each one matches dates based on a single rule.

Each of these works the same way. You hand it a date, and it tells you whether that date belongs to its set:

  • DayInWeek: "Is this date a Wednesday?" Checks the day of the week.
  • DayInMonth: "Is this the 2nd Friday of the month?" Checks both the weekday and its ordinal position (1st, 2nd, 3rd, last).
  • RangeEveryYear: "Does this date fall between June 1st and August 31st?" Checks whether a date falls within a recurring annual window.

Individually, each of these is about as useful as a single Lego brick. The real power comes when you start combining them.

Set Theory to the Rescue

Here's where things get interesting. If each temporal expression is a set of dates, then we can use set operations to combine them. This isn't just a cute metaphor. It's literally how set theory works, and it maps perfectly onto scheduling logic.

There are three operations that give us everything we need:

Union: "This OR That"

A date matches if it belongs to any of the child sets. Think of it as merging calendars together.

"Available on Mondays, Wednesdays, and Fridays": if the date is in any of those sets, it's a match.

Intersection: "This AND That"

A date matches only if it belongs to all child sets. This is how you layer constraints.

"Every Monday, but only during summer": a date must be both a Monday AND fall within the summer range.

Difference: "This BUT NOT That"

A date matches if it's in the included set but not in the excluded set. This is how you carve out exceptions.

"Every weekday except holidays": take the set of all weekdays, then punch holes in it for holidays.

If you've ever written a SQL WHERE clause, this should feel familiar. You're basically writing WHERE (monday OR wednesday) AND summer AND NOT holiday, but with composable objects instead of string queries.

The Composite Pattern: Trees All the Way Down

What we've built so far is actually a well-known design pattern called the Composite Pattern. The key insight is that composite expressions and leaf expressions share the same interface. They all implement includes(date).

This means you can nest them arbitrarily deep. A union can contain intersections, which contain differences, which contain other unions. It's turtles all the way down.

This tree represents two schedules: "Every Monday in summer, plus weekends, minus holidays" and "The first Friday of every month, minus holidays."

Every node answers the same question: includes(date)?. The tree evaluates recursively. A parent asks its children, who ask their children, all the way down to the leaves.

This is what makes the pattern so powerful. You never need to modify existing rules to add new ones. Want to add "but not in December"? Wrap the whole thing in a new DifferenceTE. Want to add Tuesdays? Drop a new leaf into the union. The Open/Closed Principle would be proud.

Configuration-Driven Schedules

Hardcoding expression trees is fine for examples, but real systems need schedules defined as data. Something you can store in a database, edit through a UI, or swap between environments without redeploying.

This is where a JSON configuration format shines. Instead of writing code, you describe the schedule structure as data and let a parser build the expression tree at runtime:

{
  "schedule": [
    {
      "type": "INTERSECTION",
      "expressions": [
        { "type": "DAY_IN_WEEK", "day": 1 },
        {
          "type": "RANGE_EVERY_YEAR",
          "of": "START_DAY_TO_END_DAY",
          "startDate": "6-1",
          "endDate": "8-31"
        }
      ],
      "slots": 30
    }
  ]
}

Here, day: 1 represents Monday using ISO 8601 weekday numbering, where Monday = 1 and Sunday = 7. This is different from JavaScript's Date.getDay() where Sunday = 0.

The parser recursively walks this JSON, instantiating the correct expression class for each node. It's essentially a recursive descent parser, but instead of parsing a programming language, it's parsing a schedule.

This separation of data from behavior means:

  • Non-developers can define schedules (with the right UI)
  • Schedules can be stored in a database and modified at runtime
  • Different environments can have different schedules without code changes
  • You can validate schedules before they run

Lazy Evaluation: Don't Compute What You Don't Need

Once you have a schedule, you'll want to answer questions like "What are the next 10 occurrences?" The naive approach would be to generate all dates in some range and filter them. But schedules can be infinite. They recur forever.

This is where lazy evaluation saves the day. Instead of computing all dates upfront, a lazy generator produces dates one at a time, on demand. It checks each day against the schedule, and when it finds a match, it hands it over and pauses. It only resumes when you ask for the next one.

The generator never looks beyond what you need. Ask for 5 dates? It stops after 5. Ask for 1? It stops after 1. Compare that to "generate all dates for the next 10 years and filter", which is the approach that makes your server question its life choices.

In languages like JavaScript/TypeScript, this is natively supported through generator functions using the yield keyword. Python has the same concept, and most modern languages have some form of lazy iteration.

Bringing It All Together

Let's see how all these concepts combine back at our community center in Kigali. The center now has a fuller set of requirements:

  1. Umuganda: Last Saturday of every month
  2. Ikimina: Every Monday and Wednesday evening
  3. No activities during holidays
  4. No Ikimina during the rainy season (March through May)

Here's the thinking process:

  • Step 1: Define the atomic rules: Monday, Wednesday, Last Saturday, Rainy Season, Holidays.
  • Step 2: Combine with set operations. Union the days together, Difference out the rainy season and holidays.
  • Step 3: The tree evaluates itself recursively for any date you throw at it.

Now let's trace what happens when we ask: "Is there an Ikimina meeting on June 9, 2025?" (a Monday, not in rainy season, not a holiday):

Each node just answers its little yes/no question, and the answer bubbles up the tree. No god-function. No tangled logic. Just composable building blocks doing their thing.

Why Not Just Use Cron?

Fair question. Cron expressions are great for "run this job at 2am every Tuesday." But they fall apart when you need:

FeatureCronTemporal Expressions
"Every Monday"0 0 * * 1DayInWeekTE(1)
"Not during summer"❌ Need external logicDifferenceTE(schedule, summer)
"1st Friday of month"0 0 * * 5 + code ⚠️DayInMonthTE(5, 1)
Composability❌ Flat strings✅ Arbitrarily nestable trees
Runtime modification❌ Requires redeploy✅ JSON config swap
"Available slots"❌ Not a concept✅ Built-in capacity

Cron tells you when to run something. Temporal expressions tell you whether a point in time belongs to a set. They solve different problems, and temporal expressions shine when scheduling rules are complex, composable, or user-defined.

Key Takeaways

  1. Model schedules as sets of dates. The includes(date): boolean interface is deceptively powerful. It turns complex scheduling into simple set membership checks.

  2. Use set operations to compose rules. Union (∪), Intersection (∩), and Difference (\) let you combine simple rules into complex ones without if-else chains.

  3. The Composite Pattern enables arbitrary nesting. Because leaf and composite nodes share the same interface, you can build expression trees of any depth.

  4. Lazy evaluation prevents waste. Generators let you work with infinite schedules without generating infinite dates. Only compute what you consume.

  5. Separate data from behavior. A JSON configuration format lets schedules be defined, stored, and modified without touching code.

The next time someone asks you to build a scheduling system and the requirements sound like a logic puzzle wrapped in a calendar, don't reach for nested if statements. Reach for set theory. Your future self (and your code reviewer) will thank you.

Further Reading

Design PatternsSet TheoryTypeScriptScheduling