Allen's Interval Algebra and ISO 8601 Intervals

Open inAnthropic

Every calendar has two stories to tell. One is the human story: "Q1 runs from January 1 to March 31." The other is the computational story: "Q1 is the half-open interval [January 1, April 1]." These two stories describe the same period, but they use different conventions — and mixing them up causes real bugs in financial systems, scheduling engines, and regulatory reporting.

This page explains the difference, introduces TerminusDB's xdd:dateTimeInterval type for ISO 8601 intervals, and shows how to use Allen's Interval Algebra to reason about temporal relationships in WOQL. Unfortunately the XSD Data Types do not include an explicit interval type, so we include it in the xdd namespace instead.

See also: WOQL Time Handling | Data Types Reference | WOQL Class Reference


The Gap in XSD: No Interval Type

The W3C XML Schema Definition (XSD) provides types for individual time points (xsd:date, xsd:dateTime) and for durations (xsd:duration), but it does not define a type for time intervals — periods anchored to specific dates.

ISO 8601, the international standard for date and time representation, does define time intervals in four forms:

FormExampleDescription
Start/end2025-01-01/2025-04-01Two explicit endpoints
Start/duration2025-01-01/P3MStart date plus a duration
Duration/endP3M/2025-04-01Duration ending at a date
Duration onlyP3MA length of time with no anchor

XSD's xsd:duration only covers the fourth form. The first three — which are the most useful for real-world scheduling and reporting — have no standard XSD type.

TerminusDB fills this gap with xdd:dateTimeInterval.


Inclusive vs Half-Open: The Conversion Problem

How humans express periods

In financial reporting, regulatory filings, and everyday communication, date ranges are inclusive — both the start and end dates are part of the period:

"Q1 2025 runs from January 1 to March 31."

Both January 1 and March 31 are included in Q1. This is the convention used by xdd:dateRange, which stores inclusive ranges as [2025-01-01, 2025-03-31].

How temporal algebra works

Temporal reasoning systems — including Allen's Interval Algebra, SQL's PERIOD type, and most database internals — use half-open intervals where the start is included but the end is excluded:

Q1 2025 = [2025-01-01, 2025-04-01] — includes January 1, excludes April 1.

Half-open intervals have three properties that make them essential for computation:

  • No gaps: Q1 ends at April 1; Q2 starts at April 1. There is no missing day between them.
  • No overlaps: March 31 belongs to Q1 only. April 1 belongs to Q2 only.
  • Clean partitioning: The year [Jan 1, Jan 1 next year] divides exactly into 12 monthly intervals with no gaps or overlaps.

The one-day offset

Converting between inclusive and half-open is straightforward — add one day to the inclusive end:

Example: Text
Inclusive:   [2025-01-01, 2025-03-31]     ← xdd:dateRange
Half-open:   [2025-01-01, 2025-04-01]     ← xdd:dateTimeInterval

                        March 31 + 1 day = April 1

And to convert back, subtract one day from the exclusive end:

Example: Text
Half-open:   [2025-01-01, 2025-04-01]     ← xdd:dateTimeInterval
Inclusive:   [2025-01-01, 2025-03-31]     ← xdd:dateRange

                        April 1 - 1 day = March 31

TerminusDB handles this conversion automatically through typecasting and the interval_inclusive predicate.


The Three Temporal Interval Types

TerminusDB provides three types for temporal extent, each serving a different purpose:

TypeNotationSemanticsUse case
xsd:durationP3M, P1Y2M10DT2H30MA length of time — no anchor to specific datesOffsets, deadlines, durations between events
xdd:dateRange[2025-01-01, 2025-03-31]Inclusive/closed — both endpoints includedHuman-readable reporting periods, regulatory filings
xdd:dateTimeInterval2025-01-01/2025-04-01Half-open — start included, end excluded. ISO 8601 interval notation.Temporal algebra, scheduling, partitioning, Allen's relations

Type hierarchy

xdd:dateTimeInterval inherits from xsd:duration. This means any place that accepts a duration can also accept an interval, and intervals can be typecast to durations to extract the duration component.

Example: Text
xsd:anySimpleType
  └─ xsd:duration              ← pure durations (P3M, P1Y)
       ├─ xsd:yearMonthDuration
       ├─ xsd:dayTimeDuration
       └─ xdd:dateTimeInterval ← ISO 8601 intervals (all 4 forms)

xdd:dateTimeInterval — ISO 8601 Intervals

String notation

The type uses the ISO 8601 solidus (/) separator. All four interval forms are supported:

Example: Text
2025-01-01/2025-04-01              ← Form 1: start/end (dates)
2025-01-01T00:00:00Z/2025-04-01T00:00:00Z  ← Form 1: start/end (dateTimes)
2025-01-01/P3M                     ← Form 2: start/duration
P3M/2025-04-01                     ← Form 3: duration/end
P3M                                ← Form 4: duration only

Extracting dates from intervals

When you unpack an interval to get its start and end dates, the result depends on which form was used:

FormStart dateEnd date
1. Start/endDirectly availableDirectly available
2. Start/durationDirectly availableComputed from start + duration
3. Duration/endComputed from end - durationDirectly available
4. Duration onlyrdf:nil (no anchor)rdf:nil (no anchor)

Using in a schema

Example: JSON
{
  "@type": "Class",
  "@id": "ReportingPeriod",
  "label": { "@type": "xsd:string" },
  "interval": { "@type": "xdd:dateTimeInterval" }
}

Typecasting Between Types

TerminusDB's typecast predicate converts between temporal types. Each conversion direction is shown below.

String ↔ dateTimeInterval

Parse an ISO 8601 interval string, or format an interval back to a string:

Example: JavaScript
// Parse string to interval
WOQL.typecast(
  literal("2025-01-01/2025-04-01", "xsd:string"),
  "xdd:dateTimeInterval",
  v.interval)

// Format interval to string
WOQL.typecast(v.interval, "xsd:string", v.str)

dateRange → dateTimeInterval (inclusive → half-open)

Converts the inclusive end date by adding one day:

Example: JavaScript
// [2025-01-01, 2025-03-31] → 2025-01-01/2025-04-01
WOQL.typecast(v.date_range, "xdd:dateTimeInterval", v.interval)

dateTimeInterval → dateRange (half-open → inclusive)

Converts the exclusive end date by subtracting one day:

Example: JavaScript
// 2025-01-01/2025-04-01 → [2025-01-01, 2025-03-31]
WOQL.typecast(v.interval, "xdd:dateRange", v.date_range)

duration ↔ dateTimeInterval

A plain duration becomes a form-4 interval (duration only). Extracting the duration from an interval returns the duration component:

Example: JavaScript
// Wrap duration as interval
WOQL.typecast(
  literal("P3M", "xsd:duration"),
  "xdd:dateTimeInterval",
  v.interval)

// Extract duration from interval
WOQL.typecast(v.interval, "xsd:duration", v.dur)

Constructing and Unpacking Intervals

Two WOQL predicates provide direct construction and deconstruction of xdd:dateTimeInterval values.

interval — Half-Open Construction

Creates or unpacks an interval using half-open semantics. The end date is exclusive.

Example: JavaScript
// Construct: two dates → interval value
let v = Vars("q1");
WOQL.interval(
  literal("2025-01-01", "xsd:date"),
  literal("2025-04-01", "xsd:date"),
  v.q1)
// v.q1 = "2025-01-01/2025-04-01"^^xdd:dateTimeInterval

// Unpack: interval value → two dates
let v = Vars("start", "end");
WOQL.interval(v.start, v.end, some_interval)
// v.start = "2025-01-01"^^xsd:date
// v.end   = "2025-04-01"^^xsd:date

For a duration-only interval (form 4), both start and end unify with rdf:nil.

interval_inclusive — Inclusive Construction

Creates or unpacks an interval using inclusive semantics. Internally, the inclusive end is converted to an exclusive end by adding one day.

Example: JavaScript
// Construct from inclusive dates (the reporting convention)
let v = Vars("q1");
WOQL.interval_inclusive(
  literal("2025-01-01", "xsd:date"),
  literal("2025-03-31", "xsd:date"),  // inclusive end
  v.q1)
// v.q1 = "2025-01-01/2025-04-01"^^xdd:dateTimeInterval
// (March 31 + 1 day = April 1 stored as exclusive end)

// Unpack to inclusive dates for display
let v = Vars("start", "incl_end");
WOQL.interval_inclusive(v.start, v.incl_end, some_interval)
// v.start    = "2025-01-01"^^xsd:date
// v.incl_end = "2025-03-31"^^xsd:date

Allen's Interval Algebra — The 13 Relations

James F. Allen introduced his interval algebra in 1983 as a framework for temporal reasoning. Given any two proper intervals (where start < end), exactly one of 13 relations holds between them. This makes the algebra exhaustive (one always applies) and mutually exclusive (only one applies).

The relations

The 13 relations form 6 pairs of inverses plus equals:

RelationInverseX condition (half-open)Description
beforeafterX.end < Y.startX finishes before Y starts (with a gap)
meetsmet_byX.end = Y.startX finishes exactly where Y starts (no gap, no overlap)
overlapsoverlapped_byX.start < Y.start, X.end > Y.start, X.end < Y.endX starts first and partially overlaps Y
startsstarted_byX.start = Y.start, X.end < Y.endX starts at the same time as Y but ends earlier
duringcontainsX.start > Y.start, X.end < Y.endX is entirely within Y
finishesfinished_byX.start > Y.start, X.end = Y.endX starts after Y but they end together
equals(self-inverse)X.start = Y.start, X.end = Y.endIdentical intervals

Timeline visualization

Example: Text
before:        X━━━━━━━┛         ┗━━━━━━━Y
meets:         X━━━━━━━┫━━━━━━━Y
overlaps:      X━━━━━━━╋━━━┛
                       ┗━━━━━━━Y
starts:        ┣━━━X━━━┛
               ┣━━━━━━━━━━━Y━━━┛
during:            ┣━━X━━┛
               ┣━━━━━━━━━━━Y━━━┛
finishes:              ┣━━━X━━━┫
               ┣━━━━━━━━━━━Y━━━┫
equals:        ┣━━━━━━━━━━━━━━━┫  (same interval)

Checking All 13 Relations — Worked Examples

Each example uses the existing interval_relation predicate with four explicit endpoints (half-open). The /2 arity using xdd:dateTimeInterval values will be available after the interval type is implemented.

TC-AIR-01: before — Q1 ends before Q3 starts {#tc-air-01}

Example: JavaScript
// Q1 [Jan 1, Apr 1) is before Q3 [Jul 1, Oct 1) — there's a gap (Q2)
WOQL.interval_relation("before",
  literal("2025-01-01", "xsd:date"), literal("2025-04-01", "xsd:date"),
  literal("2025-07-01", "xsd:date"), literal("2025-10-01", "xsd:date"))
// Succeeds

TC-AIR-02: after — Q3 is after Q1 {#tc-air-02}

Example: JavaScript
// Q3 [Jul 1, Oct 1) is after Q1 [Jan 1, Apr 1)
WOQL.interval_relation("after",
  literal("2025-07-01", "xsd:date"), literal("2025-10-01", "xsd:date"),
  literal("2025-01-01", "xsd:date"), literal("2025-04-01", "xsd:date"))
// Succeeds — "after" is the inverse of "before"

TC-AIR-03: meets — Q1 meets Q2 {#tc-air-03}

Example: JavaScript
// Q1 [Jan 1, Apr 1) meets Q2 [Apr 1, Jul 1) — no gap, no overlap
WOQL.interval_relation("meets",
  literal("2025-01-01", "xsd:date"), literal("2025-04-01", "xsd:date"),
  literal("2025-04-01", "xsd:date"), literal("2025-07-01", "xsd:date"))
// Succeeds — Q1's exclusive end equals Q2's start

TC-AIR-04: met_by — Q2 is met by Q1 {#tc-air-04}

Example: JavaScript
// Q2 [Apr 1, Jul 1) is met_by Q1 [Jan 1, Apr 1)
WOQL.interval_relation("met_by",
  literal("2025-04-01", "xsd:date"), literal("2025-07-01", "xsd:date"),
  literal("2025-01-01", "xsd:date"), literal("2025-04-01", "xsd:date"))
// Succeeds — inverse of "meets"

TC-AIR-05: overlaps — Overlapping project phases {#tc-air-05}

Example: JavaScript
// Design phase [Jan, Jun) overlaps with Development phase [Apr, Oct)
WOQL.interval_relation("overlaps",
  literal("2025-01-01", "xsd:date"), literal("2025-06-01", "xsd:date"),
  literal("2025-04-01", "xsd:date"), literal("2025-10-01", "xsd:date"))
// Succeeds — Design starts first, ends during Development

TC-AIR-06: overlapped_by — Reverse overlap {#tc-air-06}

Example: JavaScript
// Development [Apr, Oct) is overlapped_by Design [Jan, Jun)
WOQL.interval_relation("overlapped_by",
  literal("2025-04-01", "xsd:date"), literal("2025-10-01", "xsd:date"),
  literal("2025-01-01", "xsd:date"), literal("2025-06-01", "xsd:date"))
// Succeeds — inverse of "overlaps"

TC-AIR-07: starts — First week starts with January {#tc-air-07}

Example: JavaScript
// First week of January [Jan 1, Jan 8) starts with January [Jan 1, Feb 1]
WOQL.interval_relation("starts",
  literal("2025-01-01", "xsd:date"), literal("2025-01-08", "xsd:date"),
  literal("2025-01-01", "xsd:date"), literal("2025-02-01", "xsd:date"))
// Succeeds — same start, week ends before month ends

TC-AIR-08: started_by — January started by first week {#tc-air-08}

Example: JavaScript
// January [Jan 1, Feb 1] is started_by the first week [Jan 1, Jan 8)
WOQL.interval_relation("started_by",
  literal("2025-01-01", "xsd:date"), literal("2025-02-01", "xsd:date"),
  literal("2025-01-01", "xsd:date"), literal("2025-01-08", "xsd:date"))
// Succeeds — inverse of "starts"

TC-AIR-09: during — Meeting during a work day {#tc-air-09}

Example: JavaScript
// A 1-hour meeting [10:00, 11:00) during a work day [09:00, 17:00)
WOQL.interval_relation("during",
  literal("2025-03-15T10:00:00Z", "xsd:dateTime"),
  literal("2025-03-15T11:00:00Z", "xsd:dateTime"),
  literal("2025-03-15T09:00:00Z", "xsd:dateTime"),
  literal("2025-03-15T17:00:00Z", "xsd:dateTime"))
// Succeeds — meeting is entirely within the work day

TC-AIR-10: contains — Work day contains meeting {#tc-air-10}

Example: JavaScript
// Work day [09:00, 17:00) contains the meeting [10:00, 11:00)
WOQL.interval_relation("contains",
  literal("2025-03-15T09:00:00Z", "xsd:dateTime"),
  literal("2025-03-15T17:00:00Z", "xsd:dateTime"),
  literal("2025-03-15T10:00:00Z", "xsd:dateTime"),
  literal("2025-03-15T11:00:00Z", "xsd:dateTime"))
// Succeeds — inverse of "during"

TC-AIR-11: finishes — Last week finishes with January {#tc-air-11}

Example: JavaScript
// Last week of Jan [Jan 25, Feb 1] finishes with January [Jan 1, Feb 1]
WOQL.interval_relation("finishes",
  literal("2025-01-25", "xsd:date"), literal("2025-02-01", "xsd:date"),
  literal("2025-01-01", "xsd:date"), literal("2025-02-01", "xsd:date"))
// Succeeds — same end, week starts after month starts

TC-AIR-12: finished_by — January finished by last week {#tc-air-12}

Example: JavaScript
// January [Jan 1, Feb 1] is finished_by last week [Jan 25, Feb 1]
WOQL.interval_relation("finished_by",
  literal("2025-01-01", "xsd:date"), literal("2025-02-01", "xsd:date"),
  literal("2025-01-25", "xsd:date"), literal("2025-02-01", "xsd:date"))
// Succeeds — inverse of "finishes"

TC-AIR-13: equals — Same interval {#tc-air-13}

Example: JavaScript
// Two references to the same fiscal year
WOQL.interval_relation("equals",
  literal("2025-01-01", "xsd:date"), literal("2026-01-01", "xsd:date"),
  literal("2025-01-01", "xsd:date"), literal("2026-01-01", "xsd:date"))
// Succeeds — identical start and end

Classifying an Interval Relation

When you do not know which relation holds between two intervals, leave the relation as an unbound variable. TerminusDB will determine the unique relation:

Example: JavaScript
let v = Vars("rel");
WOQL.interval_relation(v.rel,
  literal("2025-01-01", "xsd:date"), literal("2025-04-01", "xsd:date"),
  literal("2025-04-01", "xsd:date"), literal("2025-07-01", "xsd:date"))
// v.rel = "meets"

This is useful for auditing temporal data:

Example: JavaScript
// Classify the relationship between every pair of reporting periods
let v = Vars("p1", "p2", "s1", "e1", "s2", "e2", "rel");
WOQL.and(
  WOQL.triple(v.p1, "period_start", v.s1),
  WOQL.triple(v.p1, "period_end", v.e1),
  WOQL.triple(v.p2, "period_start", v.s2),
  WOQL.triple(v.p2, "period_end", v.e2),
  WOQL.not_equals(v.p1, v.p2),
  WOQL.interval_relation(v.rel, v.s1, v.e1, v.s2, v.e2)
)
// Returns one row per pair with the Allen relation

Producing Intervals from Dates and Durations

From two explicit dates (half-open)

Example: JavaScript
let v = Vars("interval");
WOQL.interval(
  literal("2025-01-01", "xsd:date"),
  literal("2025-04-01", "xsd:date"),
  v.interval)
// v.interval = "2025-01-01/2025-04-01"^^xdd:dateTimeInterval

From two inclusive dates (reporting convention)

Example: JavaScript
let v = Vars("interval");
WOQL.interval_inclusive(
  literal("2025-01-01", "xsd:date"),
  literal("2025-03-31", "xsd:date"),
  v.interval)
// v.interval = "2025-01-01/2025-04-01"^^xdd:dateTimeInterval
// (internally: March 31 + 1 day = April 1)

From a dateRange value via typecast

Example: JavaScript
let v = Vars("interval");
WOQL.typecast(
  literal("[2025-01-01, 2025-03-31]", "xdd:dateRange"),
  "xdd:dateTimeInterval",
  v.interval)
// v.interval = "2025-01-01/2025-04-01"^^xdd:dateTimeInterval

From a start date and duration (form 2)

Example: JavaScript
let v = Vars("interval");
WOQL.typecast(
  literal("2025-01-01/P3M", "xsd:string"),
  "xdd:dateTimeInterval",
  v.interval)
// v.interval = "2025-01-01/P3M"^^xdd:dateTimeInterval

From a duration and end date (form 3)

Example: JavaScript
let v = Vars("interval");
WOQL.typecast(
  literal("P3M/2025-04-01", "xsd:string"),
  "xdd:dateTimeInterval",
  v.interval)
// v.interval = "P3M/2025-04-01"^^xdd:dateTimeInterval

interval_relation_typed — Allen's on Interval Values {#interval-relation-typed}

The interval_relation predicate requires four separate endpoint arguments. When your intervals are already stored or constructed as xdd:dateTimeInterval values, interval_relation_typed is more convenient — it takes the interval values directly.

PredicateSignatureDescription
interval_relation_typedinterval_relation_typed(Rel, X, Y)Allen's algebra on two xdd:dateTimeInterval values

All 13 Allen relations work identically. The predicate internally decomposes each interval into start/end endpoints and delegates to the same logic as interval_relation.

Example: Typed Validation — Q1 meets Q2

Example: JavaScript
WOQL.interval_relation_typed(
  literal("meets", "xsd:string"),
  literal("2025-01-01/2025-04-01", "xdd:dateTimeInterval"),
  literal("2025-04-01/2025-07-01", "xdd:dateTimeInterval"))
// Succeeds

Example: Typed Classification

Example: JavaScript
let v = Vars("rel");
WOQL.interval_relation_typed(v.rel,
  literal("2025-01-01/2025-04-01", "xdd:dateTimeInterval"),
  literal("2025-04-01/2025-07-01", "xdd:dateTimeInterval"))
// v.rel = "meets"

Example: DateTime Intervals (Sub-Day Precision)

Example: JavaScript
let v = Vars("rel");
WOQL.interval_relation_typed(v.rel,
  literal("2025-03-15T08:00:00Z/2025-03-15T12:00:00Z", "xdd:dateTimeInterval"),
  literal("2025-03-15T12:00:00Z/2025-03-15T17:00:00Z", "xdd:dateTimeInterval"))
// v.rel = "meets" — morning shift meets afternoon shift

Python Example

Example: Python
from terminusdb_client.woqlquery import WOQLQuery as WOQL

q1 = {"@type": "xdd:dateTimeInterval", "@value": "2025-01-01/2025-04-01"}
q2 = {"@type": "xdd:dateTimeInterval", "@value": "2025-04-01/2025-07-01"}

result = client.query(WOQL().interval_relation_typed("meets", q1, q2))
assert len(result["bindings"]) == 1

When to Use Which Predicate

ScenarioPredicateWhy
Endpoints already in separate variablesinterval_relation(rel, xs, xe, ys, ye)No need to construct intervals first
Intervals stored as xdd:dateTimeIntervalinterval_relation_typed(rel, x, y)Cleaner — no unpacking needed
Intervals constructed from inclusive datesinterval_relation_typed(rel, x, y)Pair with interval_inclusive

Complete Workflow — Financial Quarterly Reporting

This example demonstrates the full cycle: reporting periods expressed as inclusive date ranges, converted to half-open intervals for Allen's algebra, then converted back for display. It uses interval_relation_typed for the temporal checks.

Example: JavaScript
let v = Vars("q1", "q2", "q3", "q4", "rel_q1_q2",
             "q1_start", "q1_end_incl", "q2_start", "q2_end_incl");

WOQL.and(
  // Step 1: Construct intervals from inclusive reporting dates
  WOQL.interval_inclusive(
    literal("2025-01-01", "xsd:date"),
    literal("2025-03-31", "xsd:date"), v.q1),
  WOQL.interval_inclusive(
    literal("2025-04-01", "xsd:date"),
    literal("2025-06-30", "xsd:date"), v.q2),
  WOQL.interval_inclusive(
    literal("2025-07-01", "xsd:date"),
    literal("2025-09-30", "xsd:date"), v.q3),
  WOQL.interval_inclusive(
    literal("2025-10-01", "xsd:date"),
    literal("2025-12-31", "xsd:date"), v.q4),

  // Step 2: Verify temporal relationships using typed intervals
  WOQL.interval_relation_typed("meets", v.q1, v.q2),   // Q1 meets Q2
  WOQL.interval_relation_typed("meets", v.q2, v.q3),   // Q2 meets Q3
  WOQL.interval_relation_typed("meets", v.q3, v.q4),   // Q3 meets Q4
  WOQL.interval_relation_typed("before", v.q1, v.q3),  // Q1 is before Q3

  // Step 3: Classify the relationship between Q1 and Q2
  WOQL.interval_relation_typed(v.rel_q1_q2, v.q1, v.q2),
  // v.rel_q1_q2 = "meets"

  // Step 4: Unpack back to inclusive dates for display
  WOQL.interval_inclusive(v.q1_start, v.q1_end_incl, v.q1),
  WOQL.interval_inclusive(v.q2_start, v.q2_end_incl, v.q2)
  // v.q1_start = "2025-01-01", v.q1_end_incl = "2025-03-31"
  // v.q2_start = "2025-04-01", v.q2_end_incl = "2025-06-30"
)

The key insight: inclusive dates go in, Allen's algebra verifies the temporal structure, and inclusive dates come back out. The half-open conversion happens transparently inside the interval type.

Was this helpful?