Skip to content

Digital-Defiance/brightdate

Repository files navigation

npm Tests

A Universal Decimal Time System for Software Engineers and Scientists

Named in homage to Star Trek's Stardate and reference to BrightChain — a single scalar that resolves the chaos of planetary timezones into one universal quantity. BrightDate does the same for Earth.

BrightDate is a scientifically grounded, timezone-free time representation anchored at J2000.0 — the standard astronomical epoch used by every modern observatory, space agency, and ephemeris. One scalar value. Trivially sortable, diffable, and storable.

Ships with three companion types:

  • BrightDate — Float64 decimal days. Ergonomic, fast, great for math and astronomy. This is what most code should use.
  • BrightInstant — BigInt TAI seconds + integer nanos. 1-nanosecond precision at any magnitude, with no Float64 drift. The rigorous form for distributed systems, GPS engineering, and interplanetary timing.
  • ExactBrightDate — BigInt picoseconds. Bit-exact for every integer-ms input. Use at storage boundaries where lossless round-trips matter.

Why BrightDate?

Problem BrightDate Solution
Timezone confusion Single universal value, no zones
Leap second ambiguity TAI mode for monotonic time (no stutters)
Complex date arithmetic Simple subtraction: b - a = elapsed_days
Sort/compare complexity Float64 sorts natively; v2 sortable-string encoding handles mixed-sign indexes correctly
2038 problem Float64 covers 287,000+ years with sub-µs resolution
Blockchain/archival fidelity ExactBrightDate (BigInt picoseconds) for bit-exact Unix-ms round-trip
Interplanetary coordination Naturally works across the solar system
Deep history / pre-epoch dates Negative scalars are first-class; for full precision indefinitely far from J2000.0, use BrightInstant or ExactBrightDate. Display layer renders negatives as PBD <abs> for readability.

Core Concept

Format: DDDDD.ddddd
         ↑          ↑
         |          Fractional day (decimal time-of-day)
         Integer days since J2000.0 epoch

BrightDate is a count of SI days elapsed since J2000.0.

The J2000.0 Epoch — Astronomically Correct

J2000.0 is defined in Terrestrial Time (TT): the moment 2000-01-01T12:00:00 TT. Converted to the timescales you actually encounter in software:

Timescale Representation Value
TT (definition) 2000-01-01T12:00:00.000 (no zone) Unix s 946_728_000
TAI 2000-01-01T11:59:27.816 (no zone) Unix s 946_727_967.816
UTC label 2000-01-01T11:58:55.816Z Unix ms 946_727_935_816

The UTC label (2000-01-01T11:58:55.816Z) is what you see when you call new Date(946_727_935_816) in JavaScript. It is not UTC noon; the 64.184-second gap is caused by the TT–TAI offset (32.184 s) plus the 32-second TAI–UTC offset at J2000.

BrightDate = 0 at 2000-01-01T11:58:55.816Z. This is the only astronomically correct choice.

TAI Substrate

All BrightDate values are computed on a TAI substrate:

bd = (taiUnixSeconds − J2000_TAI_UNIX_S) / 86400

where J2000_TAI_UNIX_S = 946_727_967.816.

This means:

  • BrightDate ticks in exact SI seconds, with no discontinuities.
  • Leap seconds exist only at UTC boundary conversions (toISO, fromISO, toUnixMs, fromDate, etc.). Internally, they are invisible.
  • Two consecutive UTC wall-clock seconds that straddle a leap second boundary correspond to 2 SI seconds in BrightDate, because TAI advances by 2 during that transition. This is the correct physical behavior.

The BD / PBD Display Convention

BrightDate is anchored at J2000.0, so any moment before 2000-01-01T11:58:55.816Z is mathematically a negative scalar. Negatives are fine for the CPU; they read poorly in user-facing displays. The convention handles that without changing storage or arithmetic:

  • bd ≥ 0 renders as BD <bd>.
  • bd < 0 renders as PBD <abs(bd)>.
  • BD 0 is the canonical label for J2000.0. There is no PBD 0 — formatters never produce it; parsers reject it as invalid input.

The internal scalar is unchanged. PBD is a sign-flipping cosmetic for negative values, nothing more. Round-tripping through the label layer is bit-exact (sign-flip is exact in IEEE 754).

Sort rule for label strings

  1. Any BD is later than any PBD.
  2. Within BD, larger value is later.
  3. Within PBD, smaller value is later (closer to J2000.0).

When the underlying numeric scalars are available, native numeric comparison is preferred — it agrees with the label-string rule without needing the prefix.

Quick example

import { formatBD, parseBD, compareBDLabels } from "@brightchain/brightdate";

formatBD(0);             // "BD 0.000"
formatBD(9622.504);      // "BD 9622.504"
formatBD(-11125.154);    // "PBD 11125.154"   // Apollo 11
formatBD(-1);            // "PBD 1.000"        // one day before J2000.0

parseBD("BD 9622.504");  // 9622.504
parseBD("PBD 11125.154");// -11125.154
parseBD("BD 0");         // 0
parseBD("PBD 0");        // throws — PBD 0 is invalid

Deep-time precision

Float64 BrightDate covers ~287,000 years from J2000.0 with sub-microsecond ULP, in either direction. For full precision indefinitely far from the epoch — geological or cosmological scales — use BrightInstant (BigInt TAI seconds + integer nanoseconds) or ExactBrightDate (BigInt picoseconds). Both are integer-backed and have no magnitude-dependent precision loss. The display convention applies identically: their human-facing labels use the same BD / PBD prefix rule.


Installation

TypeScript / JavaScript (npm)

npm install @brightchain/brightdate
# or
yarn add @brightchain/brightdate

Rust (crates.io)

A Rust port of BrightDate is published as the brightdate crate, with the same J2000.0 / TAI semantics as this library:

# Cargo.toml
[dependencies]
brightdate = "0.1"
use brightdate::BrightDate;
let now = BrightDate::now();
println!("{:.5}", now);

Source: Digital-Defiance/brightdate-rust.

CLI utilities (Homebrew + cargo)

The Rust workspace also ships five drop-in replacements for the classic Unix date/time utilities — bdate, btime, buptime, bcal, bwatch — that emit BrightDate values.

# Homebrew (macOS / Linux)
brew tap digital-defiance/tap
brew install digital-defiance/tap/bdate
brew install digital-defiance/tap/btime
brew install digital-defiance/tap/buptime
brew install digital-defiance/tap/bcal
brew install digital-defiance/tap/bwatch

# Or via cargo
cargo install bdate btime buptime bcal bwatch

Quick Start

import { BrightDate } from '@brightchain/brightdate';

const now = BrightDate.now();
console.log(now.toString());       // "9622.50417"
console.log(now.toLogString());    // "[9622.50417]"

const bd = BrightDate.fromDate(new Date('2025-06-15T10:30:00Z'));
const bd2 = BrightDate.fromISO('2025-01-01T00:00:00Z');

const tomorrow = now.addDays(1);
const elapsed = tomorrow.difference(now);            // 1.0
console.log(now.formatDurationTo(tomorrow));         // "1.000 days"
console.log(now.isBefore(tomorrow));                 // true

Precision & Trade-offs

BrightDate is honest about what it does and doesn't preserve. This section is the important part — read it before choosing between BrightDate and ExactBrightDate.

Summary matrix

Operation BrightDate (Float64) ExactBrightDate (BigInt ps)
fromDate(d).toDate().getTime()d.getTime() ✅ Always bit-exact (Date has ms resolution; fromDate is lossless in that direction) ✅ Always bit-exact
fromUnixMs(ms).toUnixMs()ms (day-aligned ms) ✅ Bit-exact ✅ Bit-exact
fromUnixMs(ms).toUnixMs()ms (arbitrary ms) ⚠️ Error ≤ 0.00012 ms (~120 ns) ✅ Bit-exact
toBinary / fromBinary round-trip ✅ Bit-exact Float64 ✅ Bit-exact 128-bit integer
encode(v, 'utc', 12) / decode ✅ Preserves to 12 decimal places (~86 ps) ✅ Bit-exact (stores full BigInt)
Arithmetic Standard IEEE 754 (1-2 ULP max on composite ops) Bit-exact integer math
Astronomy (GMST, lunar phase, etc.) ✅ Native fit ❌ Not provided (use toBrightDateValue())
Cross-node determinism (same V8 version) ✅ IEEE 754 is deterministic ✅ BigInt is deterministic
Speed Native Float64 ops (~0.5 ns) BigInt ops (~30-300 ns)

What Float64 gives you at different magnitudes

A BrightDate value is a Float64, so the distance to the next representable value (one ULP) grows with the magnitude:

At magnitude ULP in days ULP in seconds What this means
0 (J2000.0) 5e-324 sub-normal IEEE 754 Not engineering-meaningful; treat the 1-day row as the practical floor
0.5 (half a BD day past J2000.0) 1.1e-16 ~9.6 ps Picosecond-clean
1 (one day) 2.2e-16 ~19 ps Picosecond-clean
10,000 (current era, ~2027) 2.2e-12 ~190 ns Sub-microsecond
100,000 (year 2273) 2.2e-11 ~1.9 µs Sub-10-microsecond
1,000,000 (year 4737) 2.2e-10 ~19 µs Still far better than any NTP jitter

Takeaway: for the current era (BrightDate values around 9,000-11,000), BrightDate holds about 190-nanosecond resolution. That's above mechanical clock jitter, CPU cache latency, and well above NTP synchronization accuracy.

When the 0.00012 ms round-trip tax appears

// Several common integer-ms inputs round-trip exactly because the library
// routes them through exact-integer paths:
fromUnixMs(0).toUnixMs()                 === 0;                 // Unix epoch: ✅
fromUnixMs(946_727_935_816).toUnixMs()   === 946_727_935_816;   // J2000.0 UTC label: ✅
fromUnixMs(946_728_000_000).toUnixMs()   === 946_728_000_000;   // TT noon: ✅
fromUnixMs(1_700_000_000_000).toUnixMs() === 1_700_000_000_000; // ✅

// Arbitrary inputs gain a bounded error (~2^-52 × magnitude ≈ 0.00012 ms):
const ms = 1_700_000_000_123;
Math.abs(fromUnixMs(ms).toUnixMs() - ms) < 0.001;               // true (≈1.2e-4)

If your system can tolerate sub-microsecond error at the Unix-ms boundary, use BrightDate. If it cannot — e.g., a blockchain that stores user-supplied Unix ms and must return them byte-identical — use ExactBrightDate, which round-trips bit-exactly for every integer Unix-ms input by construction.

Float64 algebraic identities — limits

Most identities hold with bit-exactness for realistic inputs. A few don't, because IEEE 754 has well-known limits:

  • lerp(a, b, 0) === a — ✅ always (the term (b-a)*0 is always ±0, addition with a is exact).
  • lerp(a, b, 1) === b — ✅ usually, but can differ by 1-2 ULP when |b-a| is comparable to |a| (catastrophic cancellation).
  • (v + d) - d === v — ✅ usually, but can differ by 1-2 ULP in the same cancellation regime.

These are properties of IEEE 754 arithmetic, not BrightDate bugs. The library's property-based tests assert the honest bounds (≤ 2-4 ULP at realistic magnitudes). For workloads that cannot tolerate any ULP drift, use ExactBrightDate and integer picosecond math.

Choosing between BrightDate and ExactBrightDate

Pick BrightDate (Float64) when:

  • You're doing astronomy, scheduling, analytics, logging, display, or interval math.
  • You need fast operator-friendly arithmetic (a - b, a < b, Math.abs(a - b)).
  • You're storing a timestamp that will be compared and diffed, not exactly reconstructed later.

Pick ExactBrightDate (BigInt picoseconds) when:

  • You must round-trip arbitrary Unix milliseconds bit-for-bit.
  • You need blockchain consensus on exact user-supplied timestamps.
  • You're archiving timestamps for century-scale retention and cannot risk any drift.
  • You need sub-picosecond precision at any magnitude.

You can mix them: store ExactBrightDate at boundaries, convert to BrightDate for computation, convert back if needed.


Metric Sub-Units

Unit Value Real-World Equivalent
1 day 1.0 24 hours
1 milliday (md) 0.001 1 min 26.4 s
1 microday (μd) 0.000001 86.4 ms
1 nanoday (nd) 0.000000001 86.4 µs
import { formatDuration } from '@brightchain/brightdate';

formatDuration(0.5);     // "500.000 millidays"
formatDuration(0.00035); // "350.000 microdays"
formatDuration(2.5);     // "2.500 days"

Conversions

const bd = BrightDate.now();

bd.toDate();               // JavaScript Date (ms resolution)
bd.toUnixMs();             // Unix milliseconds (integer; Math.round applied)
bd.toUnixSeconds();        // Unix seconds (Number)
bd.toJulianDate();         // JD = value + 2_451_545.0
bd.toModifiedJulianDate(); // MJD = value + 51_544.5
bd.toISO();                // ISO 8601 string; renders :60 seconds during leap second
bd.toGPSTime();            // { gpsWeek, gpsSeconds }

// TAI — monotonic, no leap-second discontinuities
const tai = bd.toTAI();
const backToUtc = tai.toUTC();

Leap second boundary behavior

Because BrightDate uses a TAI substrate, a UTC timestamp immediately after a leap second is 2 SI seconds later in BrightDate than the timestamp immediately before. The leap second itself is rendered as :60 in toISO(). This is physically correct: during a positive leap second, the TAI clock advances by 2 while the UTC clock repeats :59. Callers who compute BrightDate differences see the correct SI elapsed time.


BrightInstant (TAI seconds + nanos)

BrightInstant is the rigorous, lossless companion to BrightDate. It stores BigInt TAI seconds + integer nanoseconds since J2000.0 — so you get exact 1-nanosecond precision at any magnitude, with no Float64 drift. Use it when nanosecond fidelity matters indefinitely far from the epoch (distributed systems, GPS, interplanetary mission timing).

import { BrightInstant } from '@brightchain/brightdate';

// J2000.0 itself
const epoch = BrightInstant.J2000;
epoch.taiSecondsSinceJ2000; // 0n
epoch.taiNanos;             // 0

// One SI day later, plus one nanosecond
const later = BrightInstant.fromTaiComponents(86_400n, 1);

// Round-trip the f64 form
const bd = BrightDate.now();
const inst = BrightInstant.fromBrightDate(bd);
const back = inst.toBrightDate(); // lossy to f64 resolution; lossless within range

// UTC / ISO / Unix-ms round-trips honor the leap-second table
BrightInstant.fromUnixMs(Date.now()).toUnixMs();

ExactBrightDate (BigInt picoseconds)

import { ExactBrightDate } from '@brightchain/brightdate';

// Bit-exact Unix ms round-trip (unconditionally, for any integer ms)
const ms = 1_700_000_000_123;
ExactBrightDate.fromUnixMs(ms).toUnixMs() === ms; // true, always

// Integer-unit arithmetic
const e = ExactBrightDate.epoch();
e.addNanoseconds(1n).addMilliseconds(500n).addDays(7n);

// Bit-exact binary / encoded forms
const bin = e.toBinary();           // 16-byte big-endian two's complement
ExactBrightDate.fromBinary(bin);    // bit-exact round-trip

const s = e.encode();               // "EBD1:<picoseconds>"
ExactBrightDate.decode(s);          // bit-exact round-trip

// Differences in any unit
const later = ExactBrightDate.now();
later.differencePicoseconds(e);    // bigint
later.differenceMilliseconds(e);   // bigint

Convert between the two when needed:

const e = ExactBrightDate.fromUnixMs(Date.now());
const bd = BrightDate.fromValue(e.toBrightDateValue()); // lossy to Float64 resolution

Intervals

import { BrightDateInterval, BrightDate } from '@brightchain/brightdate';

const meeting = BrightDateInterval.fromDuration(BrightDate.now(), 0.5 / 24);

meeting.duration;                       // 0.02083...
meeting.formatDuration();               // "20.833 millidays"
meeting.contains(BrightDate.now());     // true

const quarters = meeting.split(4);
const padded = meeting.expand(0.01);

Logging

import {
  BrightDateStopwatch,
  createLogEntry,
  formatLogEntry,
} from '@brightchain/brightdate';

const entry = createLogEntry('info', 'Service started', { port: 3000 });
console.log(formatLogEntry(entry));
// [9622.50417] INFO  Service started {"port":3000}

const sw = new BrightDateStopwatch();
sw.start();
// ... work ...
sw.stop();
console.log(sw.elapsedFormatted); // "2.314 millidays"

Scheduling

import { nextOccurrences, INTERVALS, BrightDate } from '@brightchain/brightdate';

const hourly = nextOccurrences(
  {
    start: BrightDate.now().value,
    intervalDays: INTERVALS.HOUR,
    maxOccurrences: 5,
  },
  5,
);

Astronomy & Interplanetary

import {
  greenwichMeanSiderealTime,
  lunarPhaseName,
  lightDelayTo,
  formatMarsTime,
  BrightDate,
} from '@brightchain/brightdate';

const now = BrightDate.now();

greenwichMeanSiderealTime(now.value); // degrees
lunarPhaseName(now.value);            // "Waxing Gibbous"
lightDelayTo('mars');                 // ~0.00882 days (~12.7 min)
formatMarsTime(now.value);            // "14:23:07 MTC"

These formulae are intentionally lower-precision than the core time math — they use standard approximations (IAU 1982 for GMST, simplified sinusoidal orbits). For high-precision astronomy, use a dedicated ephemeris library and feed it BrightDate values.


Serialization

import {
  encode, decode,
  toSortableString, fromSortableString,
  toBinary, fromBinary,
} from '@brightchain/brightdate';

// Compact string: "BD1:9622.50417000" (or "BD1:9622.50417:tai")
encode(9622.50417);
decode('BD1:9622.50417000'); // { value: 9622.50417, timescale: 'utc' }

// Sortable string for databases (v2 nine's-complement format):
// lexicographic order MATCHES numeric order across positive and negative values
toSortableString(9622.50417);  // "+0009622.50417000"
toSortableString(-10957.5);    // "!9989042.49999999" (nine's complement)

// Binary — bit-exact Float64
toBinary(9622.50417);          // 8-byte ArrayBuffer (big-endian)
fromBinary(buf);               // 9622.50417 (Object.is bit-exact)

Database index note

The sortable string format uses nine's complement for negative values and a ! prefix (instead of -). This is necessary because the ASCII character '+' (0x2B) sorts before '-' (0x2D), which would invert the expected ordering of mixed-sign timestamps. Both ! (0x21) and the nine's-complement scheme ensure that lexicographic ordering of the strings exactly matches numeric ordering of the values — including the negative-to-positive transition. The older (v1) -N format is still readable by fromSortableString for backward compatibility, but new writes use v2.


Reference Dates

Event ISO 8601 (UTC) BrightDate
J2000.0 (TAI substrate anchor) 2000-01-01T11:58:55.816Z 0.000000000
TT noon (definition moment) 2000-01-01T12:00:00.000Z ≈ 0.000742870
Y2K midnight 2000-01-01T00:00:00Z ≈ −0.499257130
Unix epoch 1970-01-01T00:00:00Z ≈ −10957.499512
GPS epoch 1980-01-06T00:00:00Z ≈ −7300.499408
Apollo 11 landing 1969-07-20T20:17:40Z ≈ −11125.154
Current era (~2027) ≈ 10,000

Key Constants

import {
  J2000_UTC_UNIX_MS,       // 946_727_935_816  — UTC label of J2000.0 in Unix ms
  J2000_TAI_UNIX_S,        // 946_727_967.816  — TAI Unix seconds at J2000.0
  J2000_TT_UNIX_S,         // 946_728_000      — TT (definition) Unix seconds
  J2000_JD,                // 2_451_545.0      — Julian Date at J2000.0
  J2000_MJD,               // 51_544.5         — Modified Julian Date at J2000.0
  TAI_UTC_OFFSET_AT_J2000, // 32               — TAI − UTC at J2000.0 (seconds)
  TT_TAI_OFFSET_SECONDS,   // 32.184           — TT − TAI (seconds, fixed by definition)
  CURRENT_TAI_UTC_OFFSET,  // 37               — current TAI − UTC (seconds, as of 2017)
  GPS_EPOCH_UNIX_TAI,      // 315_964_819      — GPS epoch as TAI Unix seconds
} from '@brightchain/brightdate';

Leap Seconds

Leap seconds are an afterthought in UTC, not a physical phenomenon. BrightDate embraces this:

  • Internally: leap seconds don't exist. BrightDate ticks in strict SI days on a TAI substrate.
  • At UTC boundaries: the leap second table (LEAP_SECOND_TABLE) maps TAI seconds to UTC seconds. The table is valid through LEAP_SECOND_TABLE_VALID_UNTIL_UNIX_S and was last reviewed on LEAP_SECOND_TABLE_REVIEWED_AT.
  • toISO() during a leap second: renders the leap-second moment as :60, e.g. 1998-12-31T23:59:60.000Z.
  • If the table expires: getTaiUtcOffset() throws a LeapSecondTableExpiredError. Update the library to continue.

Design Philosophy

  1. One number, one timeline — no zones, no formats, no ambiguity.
  2. Astronomically correct — TAI substrate, J2000.0 anchor, SI days throughout.
  3. Engineer-friendly — arithmetic is just addition and subtraction.
  4. Honest about precision — documented Float64 bounds; exact BigInt companion for when you need it.
  5. Future-proof — works through at least year 287,000 without losing sub-microsecond precision.
  6. In homage to Stardate — one universal scalar to rule them all.

License

MIT © Digital Defiance

About

The BrightDate time library

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors