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) <noreply@anthropic.com>
This commit is contained in:
2026-09-29 19:24:29 +02:00
co-authored by Claude Opus 5.5
parent 97b8dd9dc8
commit ff7525d3b5
2 changed files with 35 additions and 2 deletions
+16
View File
@@ -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();
+19 -2
View File
@@ -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;
}
/**