xdd:dateTimeInterval Semantics

Open inAnthropic

xdd:dateTimeInterval Semantics

TerminusDB stores time intervals as xdd:dateTimeInterval values. Every interval is a half-open range [start, end] — the start point is included, the end point is excluded — and every endpoint is stored as a fully qualified UTC datetime.

This page explains the special rule for plain-date endpoints, shows the canonical stored form, and describes how it interacts with xsd:dateTime endpoints and typecasting.

See also: WOQL Time Handling | Allen's Interval Algebra | Data Types


The rule for endpoints of intervals in TerminusDB

If an endpoint is written as a plain xsd:date without a time component, TerminusDB stores it as the start of the next calendar day at 00:00:00Z. The interval remains half-open, so the stored end is the first instant after the intended inclusive end date. This follows the XBRL-JSON standard for financial reporting where periods are represented as start + inclusive end unless fully qualified.

The way to think about the end date follows the XBRL convention (XBRL 2.1 spec): xbrli:endDate in a duration context is inclusive of that whole calendar day that marks the end date. Implementers who treat it as an instant boundary at T00:00:00Z of that date (rather than the following day) undercount the period by a day — this is one of the most common XBRL parsing bugs. The internal storage is precise for the start of the next day, being the end of the previous day.

What you writeWhat TerminusDB stores and retrieves
2019-01-01/2019-12-312019-01-01T00:00:00Z/2020-01-01T00:00:00Z
2020-02-01/2020-02-292020-02-01T00:00:00Z/2020-03-01T00:00:00Z
2020-02-27/2020-02-282020-02-27T00:00:00Z/2020-02-29T00:00:00Z

Fully qualified xsd:dateTime endpoints are stored exactly as given:

What you writeWhat TerminusDB stores and retrieves
2024-03-15T08:00:00Z/2024-03-15T17:00:00Z2024-03-15T08:00:00Z/2024-03-15T17:00:00Z

The rule applies to every place a date-only endpoint can appear:

  • interval(Start, End, Interval) — when End is a xsd:date
  • interval_start_duration(Start, Duration, Interval) — when the computed end is a date
  • interval_duration_end(Duration, End, Interval) — when End is a xsd:date
  • typecast from xsd:string or xdd:dateRange into xdd:dateTimeInterval

The conversion uses the existing day_after logic, so month and year boundaries — including leap days — are handled correctly.


Why Use the Next Day?

Plain dates are ambiguous: does 2025-03-31 mean the end of March 31 or the start of April 1? Storing the date-only end as 2025-04-01T00:00:00Z keeps the half-open model unambiguous and consistent with:

  • XBRL-JSON and other financial-reporting formats that represent periods as start + inclusive end.
  • Allen's Interval Algebra, which requires the end of one interval to equal the start of the next for a meets relation.
  • Clean quarter partitioning: four quarters tile a fiscal year with no gaps and no overlaps.

For example, when a user types Q1 2025 as 2025-01-01/2025-03-31, TerminusDB stores [2025-01-01T00:00:00Z, 2025-04-01T00:00:00Z. When a user types Q2 2025 as 2025-04-01/2025-06-30, TerminusDB stores [2025-04-01T00:00:00Z, 2025-07-01T00:00:00Z]. The two stored intervals meet exactly at 2025-04-01T00:00:00Z.

This is due to the ambiguity of ISO8601 where dates are interpreted as the start of the day, and the end of a period is typically the start of the next period. But the standard is unclear there. TerminusDB instead uses the XBRL standard for financial reporting for guidance on how to interpret ISO8601. The end date, if specified as a date and not a timestamp, the interpretation is to reflect the end of that day. This means the timestamp is set to the start of the next day. An alternative is to use 24:00:00Z which is discouraged.


Constructing Intervals

From two plain dates

Example: JavaScript
let v = Vars("q1");
WOQL.interval(
  literal("2025-01-01", "xsd:date"),
  literal("2025-03-31", "xsd:date"),  // inclusive end as written
  v.q1)
// v.q1 = "2025-01-01T00:00:00Z/2025-04-01T00:00:00Z"^^xdd:dateTimeInterval

From a fully qualified datetime

Example: JavaScript
let v = Vars("shift");
WOQL.interval(
  literal("2025-03-15T08:00:00Z", "xsd:dateTime"),
  literal("2025-03-15T17:00:00Z", "xsd:dateTime"),
  v.shift)
// v.shift = "2025-03-15T08:00:00Z/2025-03-15T17:00:00Z"^^xdd:dateTimeInterval

Typecasting

String → xdd:dateTimeInterval

Example: JavaScript
WOQL.typecast(
  literal("2025-01-01/2025-03-31", "xsd:string"),
  "xdd:dateTimeInterval",
  v.interval)
// v.interval = "2025-01-01T00:00:00Z/2025-04-01T00:00:00Z"^^xdd:dateTimeInterval

xdd:dateTimeIntervalxdd:dateRange

Because the interval is half-open, the exclusive end is moved back one day to make the date range inclusive:

Example: JavaScript
WOQL.typecast(
  literal("2025-01-01T00:00:00Z/2025-04-01T00:00:00Z", "xdd:dateTimeInterval"),
  "xdd:dateRange",
  v.range)
// v.range = "[2025-01-01, 2025-03-31]"^^xdd:dateRange

xdd:dateRangexdd:dateTimeInterval

xdd:dateRange uses inclusive dates, so the inclusive end is advanced by one day:

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

xdd:dateTimeIntervalxsd:string

Example: JavaScript
WOQL.typecast(v.interval, "xsd:string", v.str)
// v.str = "2025-01-01T00:00:00Z/2025-04-02T00:00:00Z"

Quarter Boundaries and Leap Days

The next-day rule works across month and year boundaries, including leap years:

Written intervalStored intervalDuration covered
2024-01-01/2024-03-312024-01-01T00:00:00Z/2024-04-01T00:00:00ZQ1 2024
2024-04-01/2024-06-302024-04-01T00:00:00Z/2024-07-01T00:00:00ZQ2 2024
2024-02-29/2024-03-012024-02-29T00:00:00Z/2024-03-02T00:00:00Zleap day and March 1
2024-12-31/2025-01-012024-12-31T00:00:00Z/2025-01-02T00:00:00ZNew Year's Eve

Because the end date is advanced with day_after, the 29th of February in a leap year is handled correctly: 2024-02-29 becomes 2024-03-01T00:00:00Z, not 2024-02-30.


Deconstructing Intervals

When you decompose an interval with interval(Start, End, Interval), the Start and End variables are always bound to xsd:dateTime values — even if the interval was built from plain xsd:date inputs.

Example: JavaScript
let v = Vars("start", "end");
WOQL.interval(v.start, v.end, literal("2025-01-01/2025-03-31", "xdd:dateTimeInterval"))
// v.start = "2025-01-01T00:00:00Z"^^xsd:dateTime
// v.end   = "2025-04-01T00:00:00Z"^^xsd:dateTime  (stored half-open end)

To convert a single endpoint back to a plain date, use typecast:

Example: JavaScript
WOQL.typecast(v.start, "xsd:date", v.startDate)
// v.startDate = "2025-01-01"^^xsd:date

Displaying Inclusive Dates

When you need to show an interval back to a user as inclusive dates, cast the interval to xdd:dateRange:

Example: JavaScript
WOQL.typecast(v.q1, "xdd:dateRange", v.range)
// v.range = "[2025-01-01, 2025-03-31]"^^xdd:dateRange

This keeps the conversion transparent: inclusive dates go in, half-open intervals are stored and compared, and inclusive dates come back out for display.

Was this helpful?