orrery.js
v0.1.0
Published
Universal metric time for any celestial body. Earth, Mars, Pluto, Io — same format, different constants.
Maintainers
Readme
Why
Human time is a mess. 60 seconds, 60 minutes, 24 hours, 365.25 days — Babylonian legacy baked into every clock on Earth. It doesn't even work on Earth, let alone Mars.
orrery.js replaces all of it with one universal system:
- 1000 ticks per day — metric, clean, no base-60
- Any celestial body — same format, different constants
- Self-describing timestamps — readable without a manual
- SI seconds canonical — constants drift, timestamps don't
T56:039:[email protected] Earth — year 56, day 39, tick 629
T29:556:[email protected] Mars — different constants, same format
T4:07649:[email protected] Jupiter — 5-digit day field, still works
T116:[email protected] Mercury — day > year, canonical fallbackInstall
npm install orrery.jsQuick Start
import orrery from 'orrery.js';
// Earth time — implied, like moment()
const clock = orrery();
clock.format(clock.now()); // T56:039:[email protected]
// Any body
const mars = orrery({
name: 'Mars',
daySeconds: 88775.244, // synodic day in SI seconds
yearSeconds: 687 * 86400, // orbital period in SI seconds
});
mars.format(mars.now()); // T29:556:[email protected]Timestamp Format
T{year}:{day}:{tick}.{subtick}@{tickSeconds}{±longitude}| Field | Meaning |
|-------|---------|
| T | Prefix — identifies an orrery timestamp |
| year | Years since Unix epoch (1970-01-01) |
| day | Day within year (zero-indexed, variable width) |
| tick | Tick within day (000–999) |
| subtick | Thousandths of a tick (000–999) |
| @tickSeconds | SI seconds per tick — makes the timestamp self-describing |
| ±longitude | Meridian offset in degrees (-180 to +180) |
clock.format(ts); // T56:039:[email protected]
clock.format(ts, 'display', { longitude: -74 }); // T56:039:[email protected]
clock.format(ts, 'display', { subtick: false }); // T56:039:[email protected]
clock.format(ts, 'canonical'); // T20493:[email protected]
clock.format(ts, 'full'); // T56:039:[email protected] (T20493:[email protected])Why Two Formats?
Display (Y:DDD:tick) uses year + day-of-year. Human-friendly, but only works when a body has ≥2 days per year.
Canonical (D:tick) uses continuous day count from epoch. Universal — works on every body, including Mercury (where one day is two years).
The library auto-selects. If daysPerYear < 2, display falls back to canonical.
Converting
const clock = orrery();
// Orrery → Unix ms / Date
const ts = clock.now();
clock.toMs(ts); // 1770651038689
clock.toDate(ts); // Date: 2026-02-09T15:30:38.689Z
// Unix ms → Orrery
clock.at(Date.now() / 1000); // Timestamp
// Date → Orrery
clock.fromDate(new Date()); // Timestamp
// Parse string → Timestamp
clock.parse('T56:039:[email protected]'); // Timestamp
clock.toMs(clock.parse('T56:039:[email protected]')); // 1770638400000Body Configuration
A body needs two numbers:
interface Body {
name: string;
daySeconds: number; // synodic day (solar noon → solar noon) in SI seconds
yearSeconds: number; // orbital period in SI seconds
}Synodic day — always. Not sidereal, not orbital period. Solar noon to solar noon, consistently, on every body. This is why Luna's day is 29.53 Earth days (not 27.32), and why tidally locked moons work correctly.
Built-in Bodies
21 bodies ship with the core — 8 planets, 5 dwarf planets, 8 major moons:
import orrery, {
earth, mars, mercury, venus, jupiter, saturn, uranus, neptune,
pluto, ceres, eris, haumea, makemake,
luna, io, europa, ganymede, callisto, titan, enceladus, triton, charon,
} from 'orrery.js';
const m = orrery(mars);
m.format(m.now()); // T29:556:[email protected]Or define your own:
const sedna = orrery({ name: 'Sedna', daySeconds: 36000, yearSeconds: 11400 * 365.25 * 86400 });Reference Table
| Body | Day (Earth hours) | Days/Year | Tick (s) | Notes | |-----------|-------------------|-----------|----------|-------| | Earth | 24.0 | 365 | 86.4 | | | Mars | 24.7 | 669 | 88.8 | | | Mercury | 4222.6 | 0.5 | 15201.4 | Day > year | | Venus | 2802.0 | 1.9 | 10087.2 | Retrograde | | Jupiter | 9.9 | 10476 | 35.7 | | | Saturn | 10.7 | 24160 | 38.4 | | | Uranus | 17.2 | 42700 | 62.1 | 98° tilt | | Neptune | 16.1 | 89800 | 58.0 | | | Pluto | 153.3 | 3.7 | 551.9 | | | Ceres | 9.1 | 4450 | 32.7 | | | Eris | 25.9 | 6543 | 93.2 | | | Haumea | 3.9 | 17745 | 14.0 | Fastest dwarf planet | | Makemake | 22.8 | 3277 | 82.1 | | | Luna | 708.7 | 12.4 | 2551.4 | Tidally locked | | Io | 42.5 | 2447 | 152.9 | Tidally locked | | Europa | 85.2 | 1220 | 306.7 | Tidally locked | | Ganymede | 171.7 | 605 | 618.2 | Tidally locked | | Callisto | 400.5 | 260 | 1441.9 | Tidally locked | | Titan | 382.7 | 780 | 1377.6 | Tidally locked | | Enceladus | 32.9 | 7850 | 118.4 | Tidally locked | | Triton | 141.0 | 2854 | 507.8 | Retrograde orbit | | Charon | 153.3 | 3.7 | 551.9 | Pluto-locked |
Edge Cases
Mercury & Venus — day longer than year. The year:day:tick format breaks (what's "day of year" when there's half a day per year?). The library falls back to canonical format automatically.
Gas giants — thousands of days per year. The day field widens (Jupiter needs 5 digits). The format handles this — day field width adapts to daysPerYear.
Tidally locked moons — day = orbital period around parent. Synodic day differs slightly from orbital period (because the parent also moves around the Sun). The library uses synodic day, consistently.
Tumbling bodies — asteroids like Toutatis, moons like Hyperion — rotate chaotically with no stable axis. Not supported. Time on a tumbling body is genuinely undefined in the rotational sense. orrery.js requires a stable rotation axis.
Constant drift — Earth's day lengthens ~2.3ms/century. The canonical timestamp is SI seconds from epoch. Body constants are projections. When constants are refined, display changes; the underlying timestamp doesn't.
Design Principles
- Day = synodic day. Solar noon to solar noon. No exceptions.
- 1000 ticks per day. Metric. The tick duration varies per body — that's the point.
- SI seconds canonical. The true timestamp. Everything else is a projection.
- Self-describing. The
@suffix tells you the tick duration. No lookup table needed. - Meridian offsets, not time zones. Longitude in degrees, not political boundaries.
- Implied Earth. Call
orrery()with no arguments. Earth isn't special — it's just the default.
API
orrery(body?: Body): Clock
Create a clock. No arguments = Earth.
Clock
| Method | Returns | Description |
|--------|---------|-------------|
| .now() | Timestamp | Current time on this body |
| .at(epochSeconds) | Timestamp | Time at SI seconds since epoch |
| .fromDate(date) | Timestamp | Time from a JS Date |
| .format(ts, fmt?, opts?) | string | Format a timestamp |
| .parse(str) | Timestamp | Parse an orrery format string |
| .toMs(ts) | number | Convert to Unix milliseconds |
| .toDate(ts) | Date | Convert to JS Date |
| .tickSeconds | number | SI seconds per tick |
| .daysPerYear | number | Days per year for this body |
FormatOptions
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| longitude | number | — | Meridian offset in degrees |
| subtick | boolean | true | Include .subtick |
| division | boolean | true | Include @tickSeconds |
License
MIT
