2

In (stable) Rust, is there a relatively straightforward method of implementing the following function?

fn mod_euclid(val: i128, modulo: u128) -> u128;

Note the types! That is, 'standard' euclidean modulus (result is always in the range of [0, mod)), avoiding spurious overflow/underflow in the intermediate calculation. Some test cases:

// don't-care, just no panic or UB.
// Mild preference for treating this as though it was mod=1<<128 instead of 0.
assert_dc!(mod_euclid(i128::MAX,         0)); 
assert_dc!(mod_euclid(        0,         0)); 
assert_dc!(mod_euclid(i128::MIN,         0)); 

assert_eq!(mod_euclid(        1,        10),                  1);
assert_eq!(mod_euclid(       -1,        10),                  9);
assert_eq!(mod_euclid(       11,        10),                  1);
assert_eq!(mod_euclid(      -11,        10),                  9);
assert_eq!(mod_euclid(i128::MAX,         1),                  0);
assert_eq!(mod_euclid(        0,         1),                  0);
assert_eq!(mod_euclid(i128::MIN,         1),                  0);
assert_eq!(mod_euclid(i128::MAX, u128::MAX),  i128::MAX as u128);
assert_eq!(mod_euclid(        0, u128::MAX),                  0);
assert_eq!(mod_euclid(i128::MIN, u128::MAX),  i128::MAX as u128);

For signed%signed->signed, or unsigned%unsigned->unsigned, this is relatively straightforward. However, I can't find a good way of calculating signed % unsigned -> unsigned without converting one of the arguments - and as the last example illustrates, this may overflow or underflow no matter which direction you choose.

TLW
  • 1,373
  • 9
  • 22
  • Note when writing tests you could just remove the `assert_dc!()` because if it panics then the test would fail by virtue of how tests work in Rust and UB is impossible in safe Rust (assuming there are no bugs). – cafce25 Dec 25 '22 at 07:32
  • @cafce25 - consider what happens if `mod_euclid` is marked `#[must_use]`. – TLW Dec 25 '22 at 20:17

2 Answers2

2

As far as I can tell, there is no such function in the standard library, but it's not very difficult to write one yourself:

fn mod_euclid(a: i128, b: u128) -> u128 {
    if a >= 0 {
        (a as u128) % b
    } else {
        let r = (!a as u128) % b;
        b - r - 1
    }
}

Playground link

How it works:

  • If a is non-negative then it's straightforward - just use the unsigned remainder operator.
  • Otherwise, the bitwise complement !a is non-negative (because the sign bit is flipped), and numerically equal to -a - 1. This means r is equivalent to b - a - 1 modulo b, and hence b - r - 1 is equivalent to a modulo b. Conveniently, b - r - 1 is in the expected range 0..b.
kaya3
  • 47,440
  • 4
  • 68
  • 97
  • 1
    Panics if `b == 0`, opposed to what the unit test requires. – Finomnis Dec 24 '22 at 21:59
  • 1
    @Finomnis It is trivial to add a condition for `if b == 0` to give it whatever behaviour you want for that case. There is no mathematically sensible option, so I did not choose one. For most use-cases it is preferable to get an error than a nonsense result. – kaya3 Dec 24 '22 at 22:01
  • @kaya3 The mathematically sensible option is to [return the other operand](https://mathoverflow.net/a/359519/157594). – user3840170 Dec 24 '22 at 22:02
  • 1
    @user3840170 Well that sucks in our case, because they aren't the same types :D – Finomnis Dec 24 '22 at 22:04
  • @user3840170 You can make an argument that the remainder modulo 0 should be defined that way, but mathematically it is not defined that way. Doing so violates the basic property that `a mod b` is in the half-open range `0..b`. Either way, if you want your modulo to give some unusual result that happens to be useful to you in the case `b = 0`, you can define that behaviour for yourself. – kaya3 Dec 24 '22 at 22:05
  • In this case it's mildly preferential to treat a modulo of 0 as if it "actually" meant 1<<128, but I don't care too much. – TLW Dec 24 '22 at 22:14
  • In that case you can write `if b == 0 { a as u128 } else ...`, since a straight cast gives the same value modulo 2^128. I doubt there is anyone else who will want that behaviour rather than an error, so I will leave the answer as it is. – kaya3 Dec 24 '22 at 22:16
  • Is there a decent explanation anywhere of what precisely the Rust integer casting rules are? I was under the (obviously-erroneous) assumption that e.g. `-1 as u128` would panic. – TLW Dec 24 '22 at 22:19
  • 1
    @TLW See the [Rust reference](https://doc.rust-lang.org/reference/expressions/operator-expr.html#numeric-cast): *Casting between two integers of the same size (e.g. i32 -> u32) is a no-op (Rust uses 2's complement for negative values of fixed integers)*. – kaya3 Dec 24 '22 at 22:25
  • 2
    @kaya3 Not that real mathematicians ever speak of ‘the modulo operator’ or even of a ‘true definition’ of a mathematical concept, but Knuth defines _a_ mod 0 := _a_. (TAoCP vol. I 1969, §1.2.4, p. 38). – user3840170 Dec 24 '22 at 22:25
  • @user3840170 Your quotes around the phrases "the modulo operator" and "true definition" are not quoting me, as I did not use those terms. The point is not that there is a proper definition that disagrees with the definition you propose, rather that there is no generally accepted definition. You will also find some sources define `0^0 = 1` but that is likewise not generally accepted. Any definition you choose to accept is up to you, but it will violate at least one property that is otherwise normally assumed. – kaya3 Dec 24 '22 at 22:29
1

Maybe a little bit more straight forward, use rem_euclid where possible and else return the positive value equivalent to a:

pub fn mod_euclid(a: i128, b: u128) -> u128 {
    const UPPER: u128 = i128::MAX as u128;
    match b {
        1..=UPPER => a.rem_euclid(b as i128) as u128,
        _ if a >= 0 => a as u128,
        // turn a from two's complement negative into it's
        // equivalent positive value by adding u128::MAX
        // essentialy calculating u128::MAX - |a|
        _ => u128::MAX.wrapping_add_signed(a),
        //_ => a as u128 - (a < 0) as u128,
    }
}

(The parser didn't like my casting in the match hence UPPER)

Playground

Results in a little fewer instructions & jumps on x86_64 as well.

cafce25
  • 15,907
  • 4
  • 25
  • 31
  • "more straightforward", you say, about `a as u128 - (a < 0) as u128` for the large-b case. I hesitate to ask what you consider non-straightforward... – TLW Dec 25 '22 at 20:19
  • I see what it does - I don't understand why it works. – TLW Dec 25 '22 at 20:19
  • 1
    Fair :D, I replaced it with the explicit version which compiles to the same assembly (at least on x86) @TLW – cafce25 Dec 25 '22 at 20:34
  • Much clearer, thank you! – TLW Dec 25 '22 at 20:49