diff --git a/site/countdown/main.js b/site/countdown/main.js index 9c99b30..c51aa60 100644 --- a/site/countdown/main.js +++ b/site/countdown/main.js @@ -1,4 +1,4 @@ -import { Countdown, integer } from './timer.mjs'; +import { Countdown, integer, adjustTime } from './timer.mjs'; const timer = new Countdown(); const minutes = document.querySelector('#minutes'); @@ -55,8 +55,10 @@ for (const field of fields) { if (field.readOnly || !['ArrowUp', 'ArrowDown'].includes(event.key)) return; event.preventDefault(); const step = event.shiftKey ? 10n : 1n; - const value = integer(field.value) + (event.key === 'ArrowUp' ? step : -step); - field.value = String(value < 0n ? 0n : value); + const adjusted = adjustTime(minutes.value, seconds.value, field.id, + event.key === 'ArrowUp' ? step : -step); + minutes.value = adjusted.minutes; + seconds.value = adjusted.seconds; edited(); }); field.addEventListener('blur', () => { diff --git a/site/countdown/timer.mjs b/site/countdown/timer.mjs index 10d16eb..44e95c2 100644 --- a/site/countdown/timer.mjs +++ b/site/countdown/timer.mjs @@ -10,6 +10,16 @@ export function splitSeconds(seconds) { }; } +export function adjustTime(minutes, seconds, field, delta) { + const total = integer(minutes) * 60n + integer(seconds); + if (field === 'minutes') { + const adjusted = total / 60n + delta; + return splitSeconds((adjusted < 0n ? 0n : adjusted) * 60n + total % 60n); + } + const adjusted = total + delta; + return splitSeconds(adjusted < 0n ? 0n : adjusted); +} + export class Countdown { constructor(now = Date.now) { this.now = now; diff --git a/site/countdown/timer.test.mjs b/site/countdown/timer.test.mjs index fce24f7..bed739f 100644 --- a/site/countdown/timer.test.mjs +++ b/site/countdown/timer.test.mjs @@ -1,6 +1,38 @@ import { test } from 'node:test'; import assert from 'node:assert/strict'; -import { Countdown, integer, splitSeconds } from './timer.mjs'; +import { Countdown, integer, splitSeconds, adjustTime } from './timer.mjs'; + +for (const [minutes, seconds, delta, expectedMinutes, expectedSeconds] of [ + ['1', '59', 1n, '2', '00'], + ['2', '00', -1n, '1', '59'], + ['2', '05', -10n, '1', '55'], + ['1', '58', 10n, '2', '08'], + ['2', '00', -10n, '1', '50'], + ['0', '90', 1n, '1', '31'], + ['0', '90', -10n, '1', '20'], + ['0', '00', -1n, '0', '00'], + ['0', '05', -10n, '0', '00'], +]) { + test(`seconds adjustment ${minutes}:${seconds} ${delta} normalizes and rolls`, () => { + assert.deepEqual(adjustTime(minutes, seconds, 'seconds', delta), { + minutes: expectedMinutes, + seconds: expectedSeconds, + }); + }); +} + +test('minute adjustments normalize first and keep minutes nonnegative', () => { + assert.deepEqual(adjustTime('0', '90', 'minutes', 1n), { minutes: '2', seconds: '30' }); + assert.deepEqual(adjustTime('0', '90', 'minutes', -1n), { minutes: '0', seconds: '30' }); + assert.deepEqual(adjustTime('1', '90', 'minutes', 10n), { minutes: '12', seconds: '30' }); + assert.deepEqual(adjustTime('1', '90', 'minutes', -10n), { minutes: '0', seconds: '30' }); +}); + +test('keyboard adjustments preserve large integer precision', () => { + assert.deepEqual(adjustTime('12345678901234567890', '59', 'seconds', 1n), { + minutes: '12345678901234567891', seconds: '00', + }); +}); function fixture() { let now = 1000;