Skip to content

Member (Four-Bound Range)

The fundamental temporal representation used by @edtf-ts/core for precise temporal reasoning.

Overview

All EDTF values are normalized to Members - four-bound ranges that capture temporal uncertainty. This normalization enables precise comparison using Allen's interval algebra.

Type Definition

typescript
interface Member {
  // Four bounds representing range uncertainty
  sMin: bigint | null;  // Earliest possible start
  sMax: bigint | null;  // Latest possible start
  eMin: bigint | null;  // Earliest possible end
  eMax: bigint | null;  // Latest possible end

  // Bound types
  startKind: 'closed' | 'open' | 'unknown';
  endKind: 'closed' | 'open' | 'unknown';

  // Precision level
  precision: 'minute' | 'hour' | 'day' | 'month' | 'year' | 'subyear' | 'mixed' | 'unknown';

  // Optional qualifications
  qualifiers?: {
    uncertain?: boolean;
    approximate?: boolean;
  };
}

Why Four Bounds?

EDTF values have inherent uncertainty that requires four separate bounds:

Simple Date - Collapsed Bounds

typescript
// '1985-04-12'
{
  sMin: 482198400000n,   // 1985-04-12T00:00:00.000Z
  sMax: 482198400000n,   // Same - exact start time
  eMin: 482284799999n,   // 1985-04-12T23:59:59.999Z
  eMax: 482284799999n,   // Same - exact end time
  startKind: 'closed',
  endKind: 'closed',
  precision: 'day'
}

The start is exactly midnight, the end is exactly 23:59:59.999.

Interval - Separated Bounds

typescript
// '1985/1990'
{
  sMin: 473385600000n,   // 1985-01-01T00:00:00.000Z (earliest start)
  sMax: 504921599999n,   // 1985-12-31T23:59:59.999Z (latest start)
  eMin: 631152000000n,   // 1990-01-01T00:00:00.000Z (earliest end)
  eMax: 662687999999n,   // 1990-12-31T23:59:59.999Z (latest end)
  startKind: 'closed',
  endKind: 'closed',
  precision: 'year'
}

The start could be anytime in 1985, the end could be anytime in 1990.

Unspecified Digits - Range Uncertainty

typescript
// '198X' (any year 1980-1989)
{
  sMin: 315532800000n,   // 1980-01-01T00:00:00.000Z
  sMax: 599616000000n,   // 1989-01-01T00:00:00.000Z
  eMin: 347155199999n,   // 1980-12-31T23:59:59.999Z
  eMax: 631151999999n,   // 1989-12-31T23:59:59.999Z
  startKind: 'closed',
  endKind: 'closed',
  precision: 'year'
}

Could start as early as 1980, as late as 1989. Could end as early as 1980, as late as 1989.

Properties

sMin - Earliest Possible Start

typescript
sMin: bigint | null

The earliest time this value could start.

  • For '1985': January 1, 1985 at 00:00:00.000
  • For '198X': January 1, 1980 at 00:00:00.000 (earliest decade year)
  • For '1985/..': January 1, 1985 at 00:00:00.000

sMax - Latest Possible Start

typescript
sMax: bigint | null

The latest time this value could start.

  • For '1985': January 1, 1985 at 00:00:00.000 (same as sMin)
  • For '198X': January 1, 1989 at 00:00:00.000 (latest decade year start)
  • For '1985/1990': December 31, 1985 at 23:59:59.999 (latest start still in 1985)

eMin - Earliest Possible End

typescript
eMin: bigint | null

The earliest time this value could end.

  • For '1985': December 31, 1985 at 23:59:59.999
  • For '198X': December 31, 1980 at 23:59:59.999 (earliest end if it only lasted one year)
  • For '1985/1990': January 1, 1990 at 00:00:00.000 (earliest time in 1990)

eMax - Latest Possible End

typescript
eMax: bigint | null

The latest time this value could end.

  • For '1985': December 31, 1985 at 23:59:59.999 (same as eMin)
  • For '198X': December 31, 1989 at 23:59:59.999
  • For '1985/1990': December 31, 1990 at 23:59:59.999

Bound Kinds

startKind / endKind

typescript
type BoundKind = 'closed' | 'open' | 'unknown';
  • 'closed': Normal, bounded value
  • 'open': Unbounded (infinite) - represented by null bounds and .. in EDTF
  • 'unknown': Missing information - represented by null bounds and empty endpoint
typescript
// Closed bounds - normal date
normalize(parse('1985').value).members[0].startKind;  // 'closed'

// Open end - unbounded future
normalize(parse('1985/..').value).members[0].endKind;  // 'open'

// Unknown end - missing data
normalize(parse('1985/').value).members[0].endKind;  // 'unknown'

Precision

typescript
type Precision = 'minute' | 'hour' | 'day' | 'month' | 'year' | 'subyear' | 'mixed' | 'unknown';

The precision level of the temporal value.

typescript
normalize(parse('1985').value).members[0].precision;          // 'year'
normalize(parse('1985-04').value).members[0].precision;       // 'month'
normalize(parse('1985-04-12').value).members[0].precision;    // 'day'
normalize(parse('1985-04-12T10:30').value).members[0].precision;  // 'minute'

Qualifiers

typescript
qualifiers?: {
  uncertain?: boolean;
  approximate?: boolean;
}

Stores uncertainty and approximation markers from EDTF.

typescript
normalize(parse('1985?').value).members[0].qualifiers;
// { uncertain: true }

normalize(parse('1985~').value).members[0].qualifiers;
// { approximate: true }

normalize(parse('1985%').value).members[0].qualifiers;
// { uncertain: true, approximate: true }

Qualifiers Don't Widen Bounds

Qualifiers are metadata only. 1985? has the same numeric bounds as 1985. EDTF doesn't define tolerance values for qualifiers.

BigInt Epoch Milliseconds

All bounds use BigInt to support extreme years beyond JavaScript's Date limits:

typescript
// JavaScript Date limits: ±8.64e15 milliseconds (±273,790 years from 1970)
const jsLimit = new Date(-8640000000000000);  // Sat Apr 19 -271821

// BigInt has no such limits
normalize(parse('-100000').value).members[0].sMin;
// Works! Returns BigInt for 100,001 BC

Converting to JavaScript Date

For display or when you know the year is safe:

typescript
import { bigIntToNumber, isSafeBigInt } from '@edtf-ts/core';

const member = normalize(parse('1985').value).members[0];

if (member.sMin !== null && isSafeBigInt(member.sMin)) {
  const ms = bigIntToNumber(member.sMin);
  const date = new Date(ms);
  console.log(date.toISOString());  // "1985-01-01T00:00:00.000Z"
}

Normalization Examples

Year

typescript
normalize(parse('1985').value).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'
// }

Month

typescript
normalize(parse('1985-04').value).members[0]
// {
//   sMin: 482198400000n,   // 1985-04-01T00:00:00.000Z
//   sMax: 482198400000n,
//   eMin: 484790399999n,   // 1985-04-30T23:59:59.999Z
//   eMax: 484790399999n,
//   startKind: 'closed',
//   endKind: 'closed',
//   precision: 'month'
// }

Interval

typescript
normalize(parse('2004-06-01/2004-06-20').value).members[0]
// {
//   sMin: 1086048000000n,  // 2004-06-01T00:00:00.000Z
//   sMax: 1086048000000n,  // Same - precise day
//   eMin: 1087775999999n,  // 2004-06-20T23:59:59.999Z
//   eMax: 1087775999999n,  // Same - precise day
//   startKind: 'closed',
//   endKind: 'closed',
//   precision: 'day'
// }

Unspecified Decade

typescript
normalize(parse('198X').value).members[0]
// {
//   sMin: 315532800000n,   // 1980-01-01T00:00:00.000Z
//   sMax: 599616000000n,   // 1989-01-01T00:00:00.000Z
//   eMin: 347155199999n,   // 1980-12-31T23:59:59.999Z
//   eMax: 631151999999n,   // 1989-12-31T23:59:59.999Z
//   startKind: 'closed',
//   endKind: 'closed',
//   precision: 'year'
// }

Open End

typescript
normalize(parse('1985/..').value).members[0]
// {
//   sMin: 473385600000n,   // 1985-01-01T00:00:00.000Z
//   sMax: 473385600000n,
//   eMin: null,            // Unbounded
//   eMax: null,
//   startKind: 'closed',
//   endKind: 'open',       // Indicates unbounded
//   precision: 'year'
// }

Unknown End

typescript
normalize(parse('1985/').value).members[0]
// {
//   sMin: 473385600000n,   // 1985-01-01T00:00:00.000Z
//   sMax: 473385600000n,
//   eMin: null,            // Unknown
//   eMax: null,
//   startKind: 'closed',
//   endKind: 'unknown',    // Indicates missing data
//   precision: 'year'
// }

Shape Type

The normalize() function returns a Shape that contains one or more Members:

typescript
interface Shape {
  members: Member[];
  listMode?: 'oneOf' | 'allOf';
}

Most EDTF values produce a single Member:

typescript
normalize(parse('1985').value).members.length;  // 1

Sets and Lists produce multiple Members:

typescript
normalize(parse('[1985,1990,1995]').value).members.length;  // 3

Using Members

Direct Comparison

typescript
import { normalize, allen } from '@edtf-ts/core';

const a = normalize(parse('1985').value).members[0];
const b = normalize(parse('1990').value).members[0];

allen.before(a, b);   // 'YES'
allen.during(a, b);   // 'NO'
allen.overlaps(a, b); // 'NO'

Building Custom Relations

typescript
import { normalize } from '@edtf-ts/core';

function startsInSameYear(a: EDTFBase, b: EDTFBase): boolean {
  const normA = normalize(a).members[0];
  const normB = normalize(b).members[0];

  if (!normA || !normB || normA.sMin === null || normB.sMin === null) {
    return false;
  }

  const yearA = new Date(Number(normA.sMin)).getUTCFullYear();
  const yearB = new Date(Number(normB.sMin)).getUTCFullYear();

  return yearA === yearB;
}

Visualizing Bounds

typescript
import { normalize, dateToEpochMs } from '@edtf-ts/core';

function visualizeBounds(edtf: string) {
  const parsed = parse(edtf);
  if (!parsed.success) return;

  const member = normalize(parsed.value).members[0];

  console.log(`EDTF: ${edtf}`);
  console.log(`Start range: ${formatBound(member.sMin)} to ${formatBound(member.sMax)}`);
  console.log(`End range: ${formatBound(member.eMin)} to ${formatBound(member.eMax)}`);
  console.log(`Start kind: ${member.startKind}`);
  console.log(`End kind: ${member.endKind}`);
}

function formatBound(value: bigint | null): string {
  if (value === null) return 'null (unbounded/unknown)';
  try {
    return new Date(Number(value)).toISOString();
  } catch {
    return value.toString();
  }
}

visualizeBounds('1985');
// EDTF: 1985
// Start range: 1985-01-01T00:00:00.000Z to 1985-01-01T00:00:00.000Z
// End range: 1985-12-31T23:59:59.999Z to 1985-12-31T23:59:59.999Z
// Start kind: closed
// End kind: closed

visualizeBounds('198X');
// EDTF: 198X
// Start range: 1980-01-01T00:00:00.000Z to 1989-01-01T00:00:00.000Z
// End range: 1980-12-31T23:59:59.999Z to 1989-12-31T23:59:59.999Z
// Start kind: closed
// End kind: closed

See Also

Released under the MIT License.