@burglekitt/gmt
Version:
Temporal-based date and time utilities with timezone support and polyfill integration
49 lines (48 loc) • 2.69 kB
JavaScript
/**
* Compute the wrapped (or rounded) target value for one field of a `cycle*` call.
*
* - `bounds: null` means the field is unbounded (only `year` uses this) — cycling is plain
* addition, and rounding still snaps to a multiple of `amount` with no wrap correction.
* - Non-round path is modulo arithmetic relative to `bounds.min`, so it resolves any `amount` —
* including one bigger than the field's own range (e.g. cycling month by +13) — in one step,
* not by repeatedly correcting a single overflow.
* - Round path is **not** "round to nearest": it steps by `Math.sign(amount)` first, then snaps to
* the next multiple of `Math.abs(amount)` in the direction of that sign (ceiling for positive
* amounts, floor for negative). Multiples are relative to `0`, not `bounds.min`. This matches
* `@internationalized/date`'s `CycleOptions.round` behavior exactly (verified against its
* `manipulation.ts` source) — e.g. minute `22` cycled by `+15` with `round: true` lands on `30`
* (the next multiple of 15 above 22), not `15` (the nearest multiple).
*
* @param current the field's current numeric value
* @param amount signed amount to cycle by
* @param bounds inclusive `{ min, max }` range to wrap within, or `null` for unbounded
* @param round whether to snap to the next/previous multiple of `amount` instead of adding it
* @example cycleFieldValue(12, 1, { min: 1, max: 12 }, false) // 1 (month December +1 wraps to January)
* @example cycleFieldValue(1, 13, { min: 1, max: 12 }, false) // 2 (amount larger than the range)
* @example cycleFieldValue(22, 15, { min: 0, max: 59 }, true) // 30 (next multiple of 15 above 22)
* @example cycleFieldValue(22, -15, { min: 0, max: 59 }, true) // 15 (previous multiple of 15 below 22)
* @example cycleFieldValue(2022, 5, null, true) // 2025 (unbounded field, still rounds)
* @returns the new field value
*/
export function cycleFieldValue(current, amount, bounds, round) {
if (amount === 0)
return current;
if (round) {
let stepped = current + Math.sign(amount);
if (bounds && stepped < bounds.min)
stepped = bounds.max;
const increment = Math.abs(amount);
stepped =
amount > 0
? Math.ceil(stepped / increment) * increment
: Math.floor(stepped / increment) * increment;
if (bounds && stepped > bounds.max)
stepped = bounds.min;
return stepped;
}
if (!bounds)
return current + amount;
const size = bounds.max - bounds.min + 1;
const wrapped = (((current - bounds.min + amount) % size) + size) % size;
return bounds.min + wrapped;
}