음수 나머지 연산: -7 % 3이 언어마다 다른 이유
-7 % 3이 Python에서는 2, JavaScript·Java·C·Go에서는 -1인 이유를 절단·내림·유클리드 나눗셈으로 설명하고, 음수에서도 0 이상의 나머지를 얻는 방법을 정리했습니다.
배열을 원형으로 돌며 인덱스를 줄이는 코드를 (i - 1) % n으로 쓰면, i가 0일 때 Python은 n - 1을 내지만 JavaScript는 -1을 내서 undefined를 읽습니다. 음수가 끼면 %의 결과가 언어마다 다르게 정해져 있기 때문입니다.
몫을 어느 쪽으로 버리느냐
정수 a를 0이 아닌 정수 b로 나누면, 정수 몫 q와 나머지 r은 a = b × q + r을 만족해야 합니다. 여기에 |r| < |b|를 더해도 답은 하나가 아닙니다. -7 ÷ 3이면 (q = -2, r = -1)도, (q = -3, r = 2)도 조건에 맞습니다. 실제 몫 -2.33…을 어느 방향으로 정수로 만드느냐가 나머지를 정합니다.
| 방식 | 몫 q | 나머지 r의 부호 | -7 ÷ 3 | 7 ÷ -3 |
|---|---|---|---|---|
| 절단(truncated) | 0 쪽으로 버림 | 피제수, 즉 왼쪽을 따름 | q -2, r -1 | q -2, r 1 |
| 내림(floored) | 음의 무한대 쪽으로 내림 | 제수, 즉 오른쪽을 따름 | q -3, r 2 | q -3, r -2 |
| 유클리드(Euclidean) | 0 ≤ r < |b|가 되게 정함 | 항상 0 이상 | q -3, r 2 | q -2, r 1 |
세 방식 모두 a = b × q + r을 만족하지만, 나머지가 늘 0 이상인 것은 유클리드 방식뿐입니다. 두 수가 모두 양수면 결과가 같습니다.
언어마다 고른 방식
| 언어 | -7 % 3 |
방식 |
|---|---|---|
| Python | 2 |
내림, //와 짝 |
| JavaScript·Java·C·C++·Go | -1 |
절단 |
| JavaScript BigInt | -1n (-7n % 3n) |
절단 |
정수 몫을 내는 연산자, 즉 Python의 //, Java·C·C++·Go의 정수 /, BigInt의 /가 있으면 그 몫과 %가 위 항등식을 이룹니다. C와 C++은 몫을 자료형으로 표현할 수 있을 때만 그렇습니다.
JavaScript의 일반 숫자(Number)는 다릅니다. -7 / 3은 정수 몫이 아니라 부동소수점 값 -2.33…이므로 (a / b) * b + a % b라는 JavaScript 식은 이 정수 항등식이 아닙니다. %는 명세상 정확한 수학적 몫을 0 쪽으로 버려 따로 계산합니다. 부동소수점 자체는 부동소수점 글에서 다뤘습니다.
자주 걸리는 곳
홀수 판별을 n % 2 == 1로 쓰면 절단 방식 언어에서는 -3의 나머지가 -1이라 거짓이 됩니다. n % 2 != 0으로 쓰면 어느 언어에서나 맞습니다.
원형 인덱스처럼 0 이상 n 미만의 값이 필요하면 나머지가 음수일 때만 n을 더합니다.
// 원형 인덱스용. n은 양의 정수
function wrapIndex(i, n) {
const r = i % n;
return r < 0 ? r + n : r + 0; // + 0으로 -0을 0으로 바꾼다
}
wrapIndex(-1, 5); // 4
-5 % 5처럼 음수가 나누어떨어지면 JavaScript는 -0을 돌려줍니다. 인덱스로는 문제없지만 Object.is(-0, 0)은 false이므로 위 함수에서는 + 0으로 부호를 정리했습니다.
표준 함수도 있습니다. Math.floorMod(-7, 3)과 Rust의 (-7i32).rem_euclid(3)은 둘 다 2입니다. 제수가 음수이면 결과가 갈립니다. floorMod는 내림 방식이라 Math.floorMod(4, -3)이 -2이고, rem_euclid는 유클리드 방식이라 4i32.rem_euclid(-3)이 1입니다.