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 write | What TerminusDB stores and retrieves |
|---|---|
2019-01-01/2019-12-31 | 2019-01-01T00:00:00Z/2020-01-01T00:00:00Z |
2020-02-01/2020-02-29 | 2020-02-01T00:00:00Z/2020-03-01T00:00:00Z |
2020-02-27/2020-02-28 | 2020-02-27T00:00:00Z/2020-02-29T00:00:00Z |
Fully qualified xsd:dateTime endpoints are stored exactly as given:
| What you write | What TerminusDB stores and retrieves |
|---|---|
2024-03-15T08:00:00Z/2024-03-15T17:00:00Z | 2024-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)— whenEndis axsd:dateinterval_start_duration(Start, Duration, Interval)— when the computed end is a dateinterval_duration_end(Duration, End, Interval)— whenEndis axsd:datetypecastfromxsd:stringorxdd:dateRangeintoxdd: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
meetsrelation. - 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
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:dateTimeIntervalFrom a fully qualified datetime
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:dateTimeIntervalTypecasting
String → xdd:dateTimeInterval
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:dateTimeIntervalxdd:dateTimeInterval → xdd:dateRange
Because the interval is half-open, the exclusive end is moved back one day to make the date range inclusive:
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:dateRangexdd:dateRange → xdd:dateTimeInterval
xdd:dateRange uses inclusive dates, so the inclusive end is advanced by one day:
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:dateTimeIntervalxdd:dateTimeInterval → xsd:string
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 interval | Stored interval | Duration covered |
|---|---|---|
2024-01-01/2024-03-31 | 2024-01-01T00:00:00Z/2024-04-01T00:00:00Z | Q1 2024 |
2024-04-01/2024-06-30 | 2024-04-01T00:00:00Z/2024-07-01T00:00:00Z | Q2 2024 |
2024-02-29/2024-03-01 | 2024-02-29T00:00:00Z/2024-03-02T00:00:00Z | leap day and March 1 |
2024-12-31/2025-01-01 | 2024-12-31T00:00:00Z/2025-01-02T00:00:00Z | New 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.
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:
WOQL.typecast(v.start, "xsd:date", v.startDate)
// v.startDate = "2025-01-01"^^xsd:dateDisplaying Inclusive Dates
When you need to show an interval back to a user as inclusive dates, cast the interval to xdd:dateRange:
WOQL.typecast(v.q1, "xdd:dateRange", v.range)
// v.range = "[2025-01-01, 2025-03-31]"^^xdd:dateRangeThis keeps the conversion transparent: inclusive dates go in, half-open intervals are stored and compared, and inclusive dates come back out for display.