Counter wrap stays on the step grid: it counts in span = top - lo + the finest step rather than the period, so one counter of step 5 up to 100 wraps 100 + 5 to 0 and 0 - 5 to 100; the time picker is unchanged. Check cases and docs formula

This commit is contained in:
Melvin Strobl
2026-10-16 09:00:36 +02:00
parent 21c7961d49
commit bfab6e1d2e
3 changed files with 13 additions and 9 deletions
@@ -47,7 +47,9 @@ assert.equal(stepCounter(time, false, at(0, 30), HOUR, -1), 0)
const single: Counter[] = [{ step: 5, max: 100 }]
assert.equal(rangeOf(single).top, 100)
assert.equal(stepCounter(single, false, 100, 0, 1), 100)
assert.equal(stepCounter(single, true, 100, 0, 1), 4)
// Wrap stays on the step grid: the span is top − lo + 5 = 105, not 101.
assert.equal(stepCounter(single, true, 100, 0, 1), 0)
assert.equal(stepCounter(single, true, 0, 0, -1), 100)
assert.deepEqual(writtenOf(single, 5), ["005"])
// No max: no top, nothing to wrap at, still held at min.
@@ -59,23 +59,25 @@ export function digitsOf(counters: Counter[], value: number): number[] {
/**
* The value's range: `lo`, the highest value the finest step reaches from it
* (`top`), and the `period` a wrap counts in. Without a `max` there is no top
* and nothing to wrap at.
* (`top`), and the `span` a wrap counts in, top − lo + one finest step, which
* keeps a wrap on the step grid. Without a `max` there is no top and nothing
* to wrap at.
*/
export function rangeOf(counters: Counter[]) {
const coarsest = counters[0]
const weight = weightsOf(counters)[0]
const min = coarsest.min ?? 0
const lo = min * weight
if (coarsest.max == null) return { lo, top: Infinity, period: null }
if (coarsest.max == null) return { lo, top: Infinity, span: null }
const period = (coarsest.max - min + 1) * weight
const fine = counters[counters.length - 1].step ?? 1
return { lo, top: lo + Math.floor((period - 1) / fine) * fine, period }
const top = lo + Math.floor((period - 1) / fine) * fine
return { lo, top, span: top - lo + fine }
}
/**
* `value` with counter `index` stepped `by` steps, carrying into and borrowing
* from the others. At the ends it wraps round the period, or stops at `lo` and
* from the others. At the ends it wraps round the span, or stops at `lo` and
* `top`; without a `max` it only stops at `lo`.
*/
export function stepCounter(
@@ -87,8 +89,8 @@ export function stepCounter(
): number {
const next =
value + by * (counters[index].step ?? 1) * weightsOf(counters)[index]
const { lo, top, period } = rangeOf(counters)
if (wrap && period !== null) return lo + mod(next - lo, period)
const { lo, top, span } = rangeOf(counters)
if (wrap && span !== null) return lo + mod(next - lo, span)
return Math.min(top, Math.max(lo, next))
}