Temporal Comparison
Advanced temporal comparison using Allen's interval algebra and four-valued logic for EDTF dates. These functions are included in the main @edtf-ts/core package.
TIP
This functionality was previously in a separate @edtf-ts/compare package. It's now part of the main @edtf-ts/core package.
Overview
The compare package provides:
- Allen's 13 interval relations - Complete temporal relationship algebra
- Four-valued logic - YES/NO/MAYBE/UNKNOWN truth values for precise reasoning
- Four-bound normalization - All EDTF types converted to (sMin, sMax, eMin, eMax) ranges
- BigInt support - Handle extreme years beyond JavaScript Date limits
- Database preparation - Convert EDTF to queryable columns (coming soon)
Quick Start
import { parse } from '@edtf-ts/core';
import { isBefore, during, overlaps } from '@edtf-ts/core';
const a = parse('1985');
const b = parse('1990');
if (a.success && b.success) {
isBefore(a.value, b.value); // 'YES'
during(a.value, b.value); // 'NO'
overlaps(a.value, b.value); // 'NO'
}Truth Values
All comparison functions return one of four truth values:
YES- Definitely true (bounds prove the relation holds)NO- Definitely false (bounds prove the relation cannot hold)MAYBE- Uncertain (bounds allow but don't prove the relation)UNKNOWN- Cannot determine (missing endpoint information)
type Truth = 'YES' | 'NO' | 'MAYBE' | 'UNKNOWN';When You Get Each Value
YES - Definite Truth
// Ranges completely separate
isBefore(parse('1980').value, parse('1990').value); // 'YES'
// Ranges completely nested
during(parse('1985-04').value, parse('1985').value); // 'YES'NO - Definite Falsehood
// Same start time - cannot be "during"
during(parse('2004-06-01/2004-06-20').value, parse('2004-06').value); // 'NO'
// Completely separate - cannot overlap
overlaps(parse('1980').value, parse('1990').value); // 'NO'MAYBE - Uncertain
// Unspecified digits create uncertainty
const decade = parse('198X').value; // 1980-1989
const year = parse('1985').value;
equals(decade, year); // 'MAYBE' - could be 1985 or another year
// Open endpoints create possibility
const ongoing = parse('2020/..').value; // Started 2020, unbounded end
during(parse('2024').value, ongoing); // 'MAYBE' - could still be ongoingUNKNOWN - Missing Information
// Empty endpoint means unknown
const unknown = parse('1985/').value; // Unknown end
overlaps(unknown, parse('1990').value); // 'UNKNOWN'Allen Relations (Simple API)
These functions take two EDTF values and return a truth value.
Temporal Ordering
isBefore()
function isBefore(a: EDTFBase, b: EDTFBase): TruthCheck if A ends before B starts.
isBefore(parse('1980').value, parse('1990').value); // 'YES'
isBefore(parse('1985').value, parse('1985').value); // 'NO'isAfter()
function isAfter(a: EDTFBase, b: EDTFBase): TruthCheck if A starts after B ends. Symmetric to isBefore.
Adjacency
meets()
function meets(a: EDTFBase, b: EDTFBase): TruthCheck if A ends exactly where B starts (adjacent, no gap).
// With day precision, there's a 1ms gap
meets(parse('1985-04').value, parse('1985-05').value); // 'NO'
// Must meet at exact same instant
meets(parse('1985/1990').value, parse('1990/1995').value); // 'NO'Overlap
overlaps()
function overlaps(a: EDTFBase, b: EDTFBase): TruthCheck if A and B overlap (share some time period).
overlaps(parse('1980/1990').value, parse('1985/1995').value); // 'YES'
overlaps(parse('1980').value, parse('1990').value); // 'NO'Containment
starts()
function starts(a: EDTFBase, b: EDTFBase): TruthCheck if A and B start together, but A ends first.
starts(parse('1985-04').value, parse('1985').value); // 'YES'
starts(parse('1985').value, parse('1985').value); // 'NO' (must end first)during()
function during(a: EDTFBase, b: EDTFBase): TruthCheck if A is completely contained within B (starts after, ends before).
during(parse('1985-04').value, parse('1985').value); // 'YES'
during(parse('1985').value, parse('1985').value); // 'NO' (same bounds)contains()
function contains(a: EDTFBase, b: EDTFBase): TruthCheck if A completely contains B. Symmetric to during.
finishes()
function finishes(a: EDTFBase, b: EDTFBase): TruthCheck if A and B end together, but A starts later.
finishes(parse('1985-12').value, parse('1985').value); // 'YES'Equality
equals()
function equals(a: EDTFBase, b: EDTFBase): TruthCheck if A and B have identical bounds.
equals(parse('1985').value, parse('1985').value); // 'YES'
equals(parse('1985').value, parse('1985-01').value); // 'NO'
// Unspecified digits create uncertainty
equals(parse('198X').value, parse('1985').value); // 'MAYBE'
// Sets use "one of" semantics with ANY quantifier
const set = parse('[1667,1668,1670..1672]').value; // 5 years
const year = parse('1671').value;
equals(set, year); // 'YES' - 1671 is one of the values in the set!Set and List Semantics
Sets ([...]) and Lists ({...}) have special comparison semantics:
Sets use
ANYquantifier: Relations check if any member satisfies the relationequals([1667,1668,1671], 1671)→YESbecause 1671 is in the set- Even though the convex hull spans 1667-1671, the relation checks individual members
Lists use
ALLquantifier: Relations check if all members satisfy the relationequals({1985,1990}, 1985)→NObecause the list includes both years
The bounds displayed are the convex hull (min to max), but relations operate on individual members.
Derived Relations
Higher-level relations built from Allen's base relations.
intersects()
function intersects(a: EDTFBase, b: EDTFBase): TruthCheck if A and B share any time period (overlaps, starts, during, finishes, or equals).
intersects(parse('1980/1990').value, parse('1985/1995').value); // 'YES'
intersects(parse('1980').value, parse('1990').value); // 'NO'disjoint()
function disjoint(a: EDTFBase, b: EDTFBase): TruthCheck if A and B do not share any time period (before or after).
touches()
function touches(a: EDTFBase, b: EDTFBase): TruthCheck if A and B are adjacent (meets or metBy).
duringOrEqual()
function duringOrEqual(a: EDTFBase, b: EDTFBase): TruthCheck if A is during B or equals B.
duringOrEqual(parse('1985-04').value, parse('1985').value); // 'YES'
duringOrEqual(parse('1985').value, parse('1985').value); // 'YES'containsOrEqual()
function containsOrEqual(a: EDTFBase, b: EDTFBase): TruthCheck if A contains B or equals B.
Normalization
Convert EDTF values to four-bound ranges for advanced use cases.
normalize()
function normalize(edtf: EDTFBase): ShapeConvert an EDTF value to normalized Member(s).
import { parse } from '@edtf-ts/core';
import { normalize } from '@edtf-ts/core';
const year = parse('1985');
if (year.success) {
const norm = normalize(year.value);
console.log(norm.members[0]);
// {
// sMin: 473385600000n, // 1985-01-01T00:00:00.000Z
// sMax: 473385600000n,
// eMin: 504921599999n, // 1985-12-31T23:59:59.999Z
// eMax: 504921599999n,
// startKind: 'closed',
// endKind: 'closed',
// precision: 'year'
// }
}Member Type
The fundamental four-bound range representation.
interface Member {
sMin: bigint | null; // Earliest possible start
sMax: bigint | null; // Latest possible start
eMin: bigint | null; // Earliest possible end
eMax: bigint | null; // Latest possible end
startKind: 'closed' | 'open' | 'unknown';
endKind: 'closed' | 'open' | 'unknown';
precision: 'minute' | 'hour' | 'day' | 'month' | 'year' | 'subyear' | 'mixed' | 'unknown';
qualifiers?: {
uncertain?: boolean;
approximate?: boolean;
};
}Bound Meanings:
- Closed: Normal range with defined bounds
- Open: Unbounded (e.g.,
1985/..has open end) - Unknown: Missing information (e.g.,
1985/has unknown end)
Normalization Examples
Simple Date
normalize(parse('1985-04-12').value);
// sMin = sMax = 1985-04-12T00:00:00.000Z
// eMin = eMax = 1985-04-12T23:59:59.999ZInterval
normalize(parse('1985/1990').value);
// sMin = sMax = 1985-01-01T00:00:00.000Z
// eMin = eMax = 1990-12-31T23:59:59.999ZUnspecified Digits
normalize(parse('198X').value);
// sMin = 1980-01-01T00:00:00.000Z (earliest possible)
// sMax = 1989-01-01T00:00:00.000Z (latest possible start)
// eMin = 1980-12-31T23:59:59.999Z (earliest possible end)
// eMax = 1989-12-31T23:59:59.999Z (latest possible)Open Endpoints
normalize(parse('1985/..').value);
// sMin = sMax = 1985-01-01T00:00:00.000Z
// eMin = eMax = null (unbounded)
// endKind = 'open'Unknown Endpoints
normalize(parse('1985/').value);
// sMin = sMax = 1985-01-01T00:00:00.000Z
// eMin = eMax = null (unknown)
// endKind = 'unknown'Advanced API
For power users who need direct access to the Member-level functions.
Allen Relations (Member-level)
import { allen } from '@edtf-ts/core';
const a: Member = { sMin: 0n, sMax: 10n, eMin: 20n, eMax: 30n, ... };
const b: Member = { sMin: 25n, sMax: 35n, eMin: 45n, eMax: 55n, ... };
allen.before(a, b); // 'YES'
allen.during(a, b); // 'NO'
allen.overlaps(a, b); // 'YES'Available as allen.*:
before,aftermeets,metByoverlaps,overlappedBystarts,startedByduring,containsfinishes,finishedByequals
Named Exports
Member-level functions are also available as named exports with allen prefix:
import { allenBefore, allenDuring, allenEquals } from '@edtf-ts/core';Derived Relations (Member-level)
import { derived } from '@edtf-ts/core';
derived.intersects(a, b);
derived.disjoint(a, b);
derived.touches(a, b);
derived.duringOrEqual(a, b);
derived.containsOrEqual(a, b);Also available with derived prefix:
import { derivedIntersects, derivedDisjoint } from '@edtf-ts/core';Sets and Lists
EDTF Sets and Lists have special comparison semantics that check individual members.
Set Semantics (One Of)
Sets use "one of" semantics with the ANY quantifier. A Set like [1667,1668,1671] means "one of these years".
import { parse } from '@edtf-ts/core';
import { equals, normalize } from '@edtf-ts/core';
// Set: one of these years
const set = parse('[1667,1668,1670..1672]').value;
// Normalization creates 5 members (years 1667, 1668, 1670, 1671, 1672)
const normalized = normalize(set);
console.log(normalized.members.length); // 5
console.log(normalized.listMode); // 'oneOf'
// Comparing with a single year
const year1671 = parse('1671').value;
equals(set, year1671); // 'YES' - because 1671 is one of the values!
// The comparison checks: "Does ANY member equal 1671?"
// Member 4 (year 1671) equals 1671, so the result is YESImportant: The convex hull spans 1667-1672, but relations check individual members, not the hull.
List Semantics (All Of)
Lists use "all of" semantics with the ALL quantifier. A List like {1985,1990} means "all of these years".
const list = parse('{1985,1990}').value;
const normalized = normalize(list);
console.log(normalized.listMode); // 'allOf'
// Comparing with a single year
equals(list, parse('1985').value); // 'NO' - list is not just 1985
// The comparison checks: "Do ALL members equal 1985?"
// Only member 1 equals 1985, member 2 does not, so result is NOQuantifier Override
You can explicitly override the default quantifier:
import { during } from '@edtf-ts/core';
const set = parse('[1970, 1985, 2010]').value;
const period = parse('1980/2000').value;
// Default for Sets is ANY
during(set, period); // 'YES' - 1985 is during the period
// Explicit quantifiers
during(set, period, 'ANY'); // 'YES' - at least one (1985) is during
during(set, period, 'ALL'); // 'NO' - not all are during (1970, 2010 outside)Why This Matters
This semantic distinction is crucial for correct temporal reasoning:
// Museum artifact dated to "one of these years"
const possibleDates = parse('[1667,1668,1670..1672]').value;
// Exhibition in 1671
const exhibition = parse('1671').value;
// Could the artifact be from the exhibition year?
equals(possibleDates, exhibition); // 'YES' - it could be 1671!
// This is semantically correct: the artifact might be from 1671,
// even though it might also be from 1667, 1668, 1670, or 1672Truth Value Combinators
Combine multiple truth values with quantifiers. These are used internally for Set/List comparison.
combineWithAny()
function combineWithAny(truths: Truth[]): TruthReturns YES if any value is YES, otherwise propagates UNKNOWN/MAYBE/NO. Used for Sets.
import { combineWithAny } from '@edtf-ts/core';
combineWithAny(['YES', 'NO', 'MAYBE']); // 'YES'
combineWithAny(['NO', 'MAYBE', 'UNKNOWN']); // 'UNKNOWN'
combineWithAny(['NO', 'MAYBE']); // 'MAYBE'
combineWithAny(['NO', 'NO']); // 'NO'combineWithAll()
function combineWithAll(truths: Truth[]): TruthReturns NO if any value is NO, otherwise propagates UNKNOWN/MAYBE/YES.
import { combineWithAll } from '@edtf-ts/core';
combineWithAll(['YES', 'YES', 'YES']); // 'YES'
combineWithAll(['YES', 'MAYBE']); // 'MAYBE'
combineWithAll(['YES', 'NO']); // 'NO'negate()
function negate(truth: Truth): TruthLogical negation of a truth value.
import { negate } from '@edtf-ts/core';
negate('YES'); // 'NO'
negate('NO'); // 'YES'
negate('MAYBE'); // 'MAYBE'
negate('UNKNOWN'); // 'UNKNOWN'and() / or()
function and(a: Truth, b: Truth): Truth
function or(a: Truth, b: Truth): TruthLogical AND/OR operations.
Epoch Conversion
Convert dates to BigInt epoch milliseconds.
dateToEpochMs()
function dateToEpochMs(date: DateComponents): bigintimport { dateToEpochMs } from '@edtf-ts/core';
dateToEpochMs({ year: 1985, month: 4, day: 12 });
// 482198400000n
// Supports extreme years
dateToEpochMs({ year: -100000, month: 1, day: 1 });
// Works with BigInt!Helper Functions
function startOfYear(year: number): bigint
function startOfMonth(year: number, month: number): bigint
function startOfDay(year: number, month: number, day: number): bigint
function endOfYear(year: number): bigint
function endOfMonth(year: number, month: number): bigint
function endOfDay(year: number, month: number, day: number): bigintUtilities
BigInt Utilities
function minBigInt(...values: bigint[]): bigint
function maxBigInt(...values: bigint[]): bigint
function clampBigInt(value: bigint, min: bigint, max: bigint): bigint
function bigIntToNumber(value: bigint): number
function isSafeBigInt(value: bigint): booleanCalendar Utilities
function isLeapYear(year: number): boolean
function getDaysInMonth(year: number, month: number): number
function daysSinceEpoch(year: number, month: number, day: number): number
function astronomicalToHistorical(year: number): string
function historicalToAstronomical(yearStr: string): numberSeason Mappings
Configure how seasons map to months.
DEFAULT_SEASON_MAPPINGS
import { DEFAULT_SEASON_MAPPINGS } from '@edtf-ts/core';
console.log(DEFAULT_SEASON_MAPPINGS[21]);
// { start: { month: 3 }, end: { month: 5 } } // Spring: March-MaySeasonMapping Type
interface SeasonMapping {
start: { month: number; day?: number };
end: { month: number; day?: number };
}Examples
Museum Collection Dating
import { parse } from '@edtf-ts/core';
import { during, isBefore } from '@edtf-ts/core';
// Artifact dated to "sometime in the 1800s"
const artifact = parse('18XX').value;
// Exhibition period
const exhibition = parse('1850/1860').value;
// Could the artifact be from the exhibition period?
during(artifact, exhibition); // 'MAYBE' - 18XX could be 1850-1859Historical Event Ordering
// D-Day with approximate date for planning
const planning = parse('1944-06-~01').value; // Approximately early June
const dday = parse('1944-06-06').value;
// Did planning happen before D-Day?
isBefore(planning, dday); // 'MAYBE' - approximate date creates uncertaintyOngoing Projects
// Project with open end date
const project = parse('2020-01/..').value;
const now = parse('2024').value;
// Is the project still ongoing?
during(now, project); // 'MAYBE' - open end means it could still be activeSee Also
- Comparison Guide - Conceptual overview and use cases
- Core Types - EDTF type reference
- Formatting & Utilities - Formatters and simple comparison utilities