Skip to content

← Ground Rules: Values, Types, Control Flow step 7 of 24

Medium Primitives

Integer division and remainder: the signs will surprise you

A twelve-hour clock face, numbered 0 through 11. The hand starts at start, and each step moves it that many hours — forwards for positive, backwards for negative. Report the position after each step.

pub fn clock_positions(start: i32, steps: &[i32]) -> Vec<i32>

Every value you return must be in 0..=11, including the very first one: start may itself be out of range or negative.

(0, [-1]) gives [11]. (11, [1]) gives [0]. (3, [-15]) gives [0].

The starter compiles, passes clippy, and is wrong on every negative input.

% is a remainder, not a modulus

Ask Python for -7 % 2 and you get 1. Ask Rust and you get -1. Neither is buggy; they have made different choices about what % means, and Rust’s choice is the one C, C++, Java, Go and C# also make.

The rule Rust follows is truncated division: / rounds toward zero, and % takes whatever sign the left operand had, so that the identity (a / b) * b + (a % b) == a holds.

Verified on 1.95:

 7 /  2 ==  3        7 %  2 ==  1
-7 /  2 == -3       -7 %  2 == -1
 7 / -2 == -3        7 % -2 ==  1
-7 / -2 ==  3       -7 % -2 == -1

Python instead uses floored division, which is why its % follows the sign of the right operand. If you arrived from Python, this is a real trap: your wrap-around code, your circular buffers, your hash bucketing and your “every N-th item” logic will all be silently wrong for negative inputs, and your tests will pass because nobody tested a negative.

rem_euclid is the fix almost nobody finds

The standard library has the operation you actually wanted:

(-7i32).rem_euclid(2) == 1

rem_euclid implements Euclidean division, whose defining property is that the remainder is never negative — it is always in 0..b.abs(). That is exactly the contract “give me a position on a clock face”. Its partner div_euclid gives the matching quotient.

It is a plain inherent method on every integer type, it costs about the same as %, and it has been stable since Rust 1.38. There is no lint that will point you at it, which is precisely why it is worth learning deliberately.

The other surprise: division can still panic

Two integer operations panic even at -O, because there is no bit pattern that could be the answer:

  • x / 0 and x % 0 — division by zero;
  • i32::MIN / -1 and i32::MIN % -1 — the true quotient is 2147483648, which is one past i32::MAX.

If either operand comes from outside your function, prefer checked_div and checked_rem, which return Option and hand the decision back to you. This is the same lesson as the previous item, applied to the one arithmetic operation that cannot be made total.

Working it out

Add the step, then reduce into 0..=11. Do the reduction on the starting position too, so that a wild start is normalised before anything else happens — (-5, [0]) must give [7].