Make a cursor advance without falling off the end of a fixed-size buffer.
Imagine a seven-slot worker queue whose next-work cursor points to slot 5. After assigning one task, it advances four slots because several workers completed together. Counting forward gives 6, then 0, 1, then 2. The cursor must cycle through valid slots rather than become 9.
This pattern appears in circular buffers, rotating schedules, and bounded sequence numbers. The cycle has seven positions, numbered 0 through 6. The code should make two rules visible: capacity must be positive, and the starting index must already be in range.
- Capacity
- 7 slots, indexed 0 through 6.
- Current index
- 5, already in the valid range.
- Step
- +4 slots; movement may later be backward too.
- Question
- Which slot is four moves ahead, with wraparound?
Division counts complete cycles and leaves a remainder.
For whole numbers a and positive m, division can be written as a = q × m + r. The quotient q counts cycles of size m; the remainder r is what remains after those cycles. For
non-negative values, 0 ≤ r < m.
9 = 1 × 7 + 2
So 9 items contain one complete group of 7 and two items into the next group. If the group is a circular set of slots, slot 9 maps to slot 2. This equality is a useful audit: the quotient and remainder must reconstruct the original value.
| Unwrapped position | Complete cycles | Remainder / slot |
|---|---|---|
| 5 + 4 = 9 | 1 × 7 | 2 |
| 9 = 1 × 7 + 2 | 1 cycle | slot 2 |
Normalize into the valid interval when the index must be non-negative.
For a positive capacity m, a normalized modulo operation returns a value from
0 through m − 1. A common expression is ((x % m) + m) % m;
because x % m is already less than m in magnitude, adding the positive
modulus once is enough to lift a negative remainder into range. With JavaScript safe integers
near their maximum, a conditional helper avoids an intermediate sum that may no longer be exactly
representable.
For a step of −3 slots with capacity 7, language remainder gives −3. Normalization gives −3 + 7 = 4, so a position three slots backward from 0 lands at slot 4. Check
that 0 ≤ result < 7 before treating it as an array index.
TypeScript and Go agree on remainder sign, but that is not Euclidean modulo.
JavaScript's Number % operator and Go's integer % both use a
quotient truncated toward zero. The nonzero remainder therefore has the dividend's sign.
For example, −11 / 4 truncates to −2, so −11 = (−2 × 4) + (−3) and −11 % 4 = −3.
This differs from floor division: floor(−11 / 4) = −3 and the associated non-negative
remainder would be 1. The language operators have not failed; they implement a different convention.
The distinction matters for timestamps before an epoch, negative hash inputs, or a cursor that
can move backward.
| Value | Language remainder | Normalized result | Reconstruction |
|---|---|---|---|
| 10 | 3 | 3 | 10 = 1 × 7 + 3 |
| −3 | −3 | 4 | −3 = 0 × 7 + (−3) |
| −11 | −4 | 3 | −11 = (−1 × 7) + (−4) |
Change the capacity, position, or signed step and inspect the wrapped result.
Try a step larger than the capacity, then try a negative step. The lab shows how many positions the movement represents after complete cycles are removed. The start index must be a valid physical slot, but the step may be any safe integer.
Exact start + step: 9
Normalized step: 4
Next physical slot: 2
Invariant: 0 ≤ 2 < 7
Put the range and sign rules inside a named helper.
The implementations reject invalid capacities and out-of-range starting indices.
TypeScript also requires safe integer inputs because its ordinary Number representation
cannot exactly represent every integer. Both normalize the step and then wrap without
creating a potentially large index + step intermediate.
Follow the signed remainder first, then verify the final index invariant.
/** Return the Euclidean remainder in [0, modulus) for safe integers. */
export function modulo(value: number, modulus: number): number {
if (!Number.isSafeInteger(value) || !Number.isSafeInteger(modulus)) {
throw new RangeError('Value and modulus must be safe integers.');
}
if (modulus <= 0) throw new RangeError('Modulus must be greater than zero.');
const remainder = value % modulus;
if (remainder === 0) return 0;
return remainder < 0 ? remainder + modulus : remainder;
}
/** Move a ring-buffer index by any safe integer number of slots. */
export function advanceIndex(index: number, step: number, capacity: number): number {
if (
!Number.isSafeInteger(index) ||
!Number.isSafeInteger(step) ||
!Number.isSafeInteger(capacity)
) {
throw new RangeError('Index, step, and capacity must be safe integers.');
}
if (capacity <= 0) throw new RangeError('Capacity must be greater than zero.');
if (index < 0 || index >= capacity) throw new RangeError('Index must be inside the ring.');
const wrappedStep = modulo(step, capacity);
const slotsToEnd = capacity - index;
return wrappedStep >= slotsToEnd ? wrappedStep - slotsToEnd : index + wrappedStep;
}
// Example: advanceIndex(5, 4, 7) returns 2; modulo(-3, 7) returns 4.
package modular
import "errors"
// Example: AdvanceIndex(5, 4, 7) returns 2; Modulo(-3, 7) returns 4.
// Modulo returns the Euclidean remainder in [0, modulus).
func Modulo(value, modulus int) (int, error) {
if modulus <= 0 {
return 0, errors.New("modulus must be greater than zero")
}
remainder := value % modulus
if remainder < 0 {
remainder += modulus
}
return remainder, nil
}
// AdvanceIndex moves a ring-buffer index by any integer number of slots.
func AdvanceIndex(index, step, capacity int) (int, error) {
if capacity <= 0 {
return 0, errors.New("capacity must be greater than zero")
}
if index < 0 || index >= capacity {
return 0, errors.New("index must be inside the ring")
}
wrappedStep, err := Modulo(step, capacity)
if err != nil {
return 0, err
}
slotsToEnd := capacity - index
if wrappedStep >= slotsToEnd {
return wrappedStep - slotsToEnd, nil
}
return index + wrappedStep, nil
}
Use the cycle model, then inspect what the remainder does not guarantee.
- Observed hash
- −29 for one key; another key hashes to 31.
- Partition count
- 12, so valid partition IDs are 0 through 11.
- Your task
- Compute the language remainders, normalize each to a valid partition, then list two reasons the assignment may still be uneven.
- Transfer question
- What additional evidence would tell you whether this hash scheme balances the real key set?
For −29 and 12, truncating division gives quotient −2 and remainder −5; normalized modulo is 7, since −29 = (−3 × 12) + 7 under the Euclidean convention. For 31, the remainder is 7 under either convention. Both keys therefore land in partition 7 in this example. This is not proof that all keys collide there; two samples reveal almost nothing about distribution.
A good diagnosis would inspect the hash function, representative key samples, per-partition counts, and whether the partition count changes over time. Modulo answers “which bucket in this fixed set?” It does not answer “are the buckets balanced?” or “will existing keys move predictably if the set size changes?”
Language references: ECMAScript remainder operator and the Go integer operators specification.