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.
| 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 |
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.
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.
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.
npm install @brightchain/brightdate
# or
yarn add @brightchain/brightdateA 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.
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 bwatchimport { 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)); // trueBrightDate is honest about what it does and doesn't preserve. This section is the important part — read it before choosing between BrightDate and ExactBrightDate.
| 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) |
✅ 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) |
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-yoctosecond | Full Float64 precision |
| 0.5 (noon boundary) | 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.
// Day-aligned inputs (multiples of 86_400_000 from J2000 anchor) round-trip exactly:
fromUnixMs(0).toUnixMs() === 0; // Unix epoch: ✅
fromUnixMs(946_728_000_000).toUnixMs() === 946_728_000_000; // J2000: ✅
// Off-day inputs gain a bounded error (~2^-52 × 946 728 000 000 ≈ 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.
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)*0is always±0, addition withais 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.
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.
| 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"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();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
:60intoISO(). 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 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();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); // bigintConvert between the two when needed:
const e = ExactBrightDate.fromUnixMs(Date.now());
const bd = BrightDate.fromValue(e.toBrightDateValue()); // lossy to Float64 resolutionimport { 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);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"import { nextOccurrences, INTERVALS, BrightDate } from '@brightchain/brightdate';
const hourly = nextOccurrences(
{
start: BrightDate.now().value,
intervalDays: INTERVALS.HOUR,
maxOccurrences: 5,
},
5,
);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.
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)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)-Nformat is still readable byfromSortableStringfor backward compatibility, but new writes use v2.
| 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 |
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 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 throughLEAP_SECOND_TABLE_VALID_UNTIL_UNIX_Sand was last reviewed onLEAP_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 aLeapSecondTableExpiredError. Update the library to continue.
- One number, one timeline — no zones, no formats, no ambiguity.
- Astronomically correct — TAI substrate, J2000.0 anchor, SI days throughout.
- Engineer-friendly — arithmetic is just addition and subtraction.
- Honest about precision — documented Float64 bounds; exact BigInt companion for when you need it.
- Future-proof — works through at least year 287,000 without losing sub-microsecond precision.
- In homage to Stardate — one universal scalar to rule them all.
MIT © Digital Defiance