From ff7525d3b5226598e1a156fded17f245d24783f7 Mon Sep 17 00:00:00 2001 From: Senrokai Date: Tue, 29 Sep 2026 19:24:29 +0200 Subject: [PATCH] Stop a running clock at the ends of its window, where setDate already refused to go Only setDate held the clock to AD 1 - AD 3000; julianDate did not, so a month a second carried it past either end with nothing to stop it. Past AD 3000 it drew the planets on elements Standish never fitted there, under a note naming "AD 3000"; before AD 1, toISOString writes the six-digit years ECMA-262 uses outside 0000-9999, and the note, the date strip and the date field, which cut it at fixed places, read "... to -000001-10-05 20 UTC.", "-000001-10" and an empty field. julianDate now stops the clock at the end it ran into: re-anchored there, at real time turned back into the window (forwards at AD 1, backwards at AD 3000), as if the reader had set that date. In the running app, run backwards from 0001-01-10 at a month a second for 20 s, the note reads "to 0001-01-01 00:00 UTC." and the strip "0001-01-01", the clock 19.7 s into AD 1 at real time; run on from 2999-12-01 for 8 s, "to 2999-12-31 23:59 UTC." at real time backwards. Control: julianDate unheld fails "stops a running clock at either end of the window". Co-Authored-By: Claude Opus 5.5 (1M context) --- src/app/shared/state/time.store.spec.ts | 16 ++++++++++++++++ src/app/shared/state/time.store.ts | 21 +++++++++++++++++++-- 2 files changed, 35 insertions(+), 2 deletions(-) diff --git a/src/app/shared/state/time.store.spec.ts b/src/app/shared/state/time.store.spec.ts index 762a47f..9895c92 100644 --- a/src/app/shared/state/time.store.spec.ts +++ b/src/app/shared/state/time.store.spec.ts @@ -85,6 +85,22 @@ describe('TimeStore', () => { expect(time.date().toISOString()).toBe('2020-12-21T19:00:01.000Z'); }); + it('stops a running clock at either end of the window, at real time turned back into it', () => { + time.setDate(new Date('0001-01-10T00:00Z')); + time.setRate(-2_629_800); // a month a second, backwards + vi.advanceTimersByTime(60_000); + expect(time.date().toISOString()).toBe('0001-01-01T00:00:00.000Z'); + expect(time.rate()).toBe(1); + vi.advanceTimersByTime(1000); + expect(time.date().toISOString()).toBe('0001-01-01T00:00:01.000Z'); + + time.setDate(new Date('2999-12-01T00:00Z')); + time.setRate(2_629_800); + vi.advanceTimersByTime(60_000); + expect(time.date().toISOString()).toBe('3000-01-01T00:00:00.000Z'); + expect(time.rate()).toBe(-1); + }); + it('refuses a date the planets’ elements were never fitted for, and stays where it was', () => { const before = time.date().toISOString(); diff --git a/src/app/shared/state/time.store.ts b/src/app/shared/state/time.store.ts index ca66ffc..1101974 100644 --- a/src/app/shared/state/time.store.ts +++ b/src/app/shared/state/time.store.ts @@ -40,6 +40,10 @@ const WINDOW_MS = { min: Date.parse(`${CLOCK_WINDOW.min}Z`), max: Date.parse(`${CLOCK_WINDOW.max}Z`), }; +const WINDOW_JD = { + min: WINDOW_MS.min / MS_PER_DAY + JULIAN_DATE_AT_EPOCH, + max: WINDOW_MS.max / MS_PER_DAY + JULIAN_DATE_AT_EPOCH, +}; /** * The date the map is drawn for. @@ -65,9 +69,22 @@ export class TimeStore { private anchorJd = dateToJulianDate(); private anchorWallMs = Date.now(); - /** Julian date for this instant, at the rate the reader chose. */ + /** + * Julian date for this instant, at the rate the reader chose, held to {@link CLOCK_WINDOW}: a + * clock run past either end stops there, at real time turned back into the window, as if the + * reader had set that date. Unheld, a month a second carried it past AD 3000, where the planets' + * elements were never fitted, and before AD 1, where `toISOString` writes a six-digit year the + * date strip, the note and the date field cut in the wrong places ("-000001-12-01 00 UTC"). + */ julianDate(): number { - return this.anchorJd + ((Date.now() - this.anchorWallMs) * this.rate()) / MS_PER_DAY; + const jd = this.anchorJd + ((Date.now() - this.anchorWallMs) * this.rate()) / MS_PER_DAY; + if (jd >= WINDOW_JD.min && jd <= WINDOW_JD.max) { + return jd; + } + this.anchorJd = jd < WINDOW_JD.min ? WINDOW_JD.min : WINDOW_JD.max; + this.anchorWallMs = Date.now(); + this.rate.set(jd < WINDOW_JD.min ? 1 : -1); + return this.anchorJd; } /**