Skip to content

Comparing two numbers of different types

Level: 101 → 201 · working knowledge

One line: < and == take the same number type on both sides, so comparing an i32 with a u16 means converting one of them first. Pick a type that holds every value of both, convert with From, and the comparison can't lie. as compiles for every pair and gives wrong answers for some.

fn main() {
    let a: i32 = 10;
    let b: u16 = 100;
    println!("{}", a < i32::from(b));           // true
    println!("{}", a < b.into());               // true
    println!("{}", i64::from(-1_i32) < i64::from(3_000_000_000_u32));   // true
}

The refusal

Without the conversion, rustc stops at the comparison:

Real rustc 1.98.0 output for mixed_types.rs
error[E0308]: mismatched types
 --> mixed_types.rs:4:24
  |
4 |     println!("{}", a < b);
  |                    -   ^ expected `i32`, found `u16`
  |                    |
  |                    expected because this is `i32`
  |
help: you can convert a `u16` to an `i32`
  |
4 |     println!("{}", a < b.into());
  |                         +++++++

The error is a type mismatch, not a missing trait. < calls PartialOrd::partial_cmp ↗, and the trait does take a type parameter for the right-hand side, PartialOrd<Rhs = Self>. std implements it for i32 with Rhs = i32 and nothing else, so the compiler knows exactly what b should have been. The comparison is refused because std has no i32 < u16 impl. The language has no general rule against comparing different types: std does compare String == &str and Vec<i32> == [i32; 3], because it implements those pairs (the claims page runs both).

The suggested fix is b.into(), not b as i32, and the difference matters. .into() compiles only when a lossless From exists. When it doesn't, you get an error instead of a wrong answer:

Real rustc 1.98.0 output for no_common_from.rs — a: i32, b: u32
error[E0277]: the trait bound `i32: From<u32>` is not satisfied
 --> no_common_from.rs:4:26
  |
4 |     println!("{}", a < b.into());
  |                          ^^^^ the trait `From<u32>` is not implemented for `i32`
  |
  = help: `i32` implements trait `From<T>`:
            From<bool>
            From<i16>
            From<i8>
            From<u16>
            From<u8>
  = note: required for `u32` to implement `Into<i32>`

That list is the lesson in miniature. An i32 can take any u16 or u8, because their whole range fits. It can't take a u32, whose top half doesn't fit, and no amount of casting changes that.

Pick the type that holds both

the two sides compare in write
u16 and i32 i32 a < i32::from(b)
i32 and u32 i64 i64::from(a) < i64::from(b)
i64 and u64 i128 i128::from(a) < i128::from(b)
usize and u64 u64, fallibly u64::try_from(len) — std has no From either way, since usize has no fixed width
i128 and u128 nothing wider a < 0 \|\| a.cast_unsigned() < b
i32 and f64 f64 f64::from(a) < x — every i32 is exact in an f64
i64 and f64 none exists compare the whole part and the fraction separately (section 4 below)

The rule behind every row: the target type must hold every value of both types, sign included. Width alone isn't enough. u64 is wider than i32 and still has no place for -1.

What the program prints

Verified output of comparing_two_number_types.rs — regenerated by tools/run_examples.py, never hand-typed.

1. THE WORKING ANSWER: CONVERT ONE SIDE, LOSSLESSLY
   a: i32 = 10, b: u16 = 100
   a < i32::from(b)        : true
   a < b.into()            : true
   a.cmp(&i32::from(b))    : Less
   i32::from(u16) compiles only because every u16 fits in an i32.

2. PICK THE TYPE THAT HOLDS BOTH RANGES
   u16 and i32   i32     -1 < 65535                  : true
   i32 and u32   i64     -1 < 3000000000             : true
   i64 and u64   i128    -1 < 18446744073709551615   : true
   usize and u64 u64     3 > 2                       : true
                         no From either way, so u64::try_from(len) = Ok(3)
   i128 and u128 none    -1 < 1                      : true
                         no wider type exists: test the sign, then compare as u128

3. THE SAME COMPARISONS WITH `as`, AND WHERE THEY LIE
   -1i32 as u32 < 1u32          : false  -1 became 4294967295
   -1i32 as u64 < 1u64          : false  a WIDER type, and -1 became 18446744073709551615
   -1i32 < 3_000_000_000 as i32 : false  3000000000 became -1294967296
   widening is safe when the TARGET holds every value of the source:
   width is not enough, the sign has to fit too. From only exists
   where it does, so i32::from(b) cannot make this mistake.

4. INTEGER AND FLOAT: f64 HOLDS EVERY i32, NOT EVERY i64
   f64::from(i32::MAX) = 2147483647   exact
   n = 9007199254740993 (2^53 + 1), f = 9007199254740992.0 (2^53)
   n as f64 == f              : true   <- two different numbers, 'equal'
   n as f64 > f               : false
   cmp_i64_f64(n, f)          : Some(Greater)   <- exact
   cmp_i64_f64(3, 3.5)        : Some(Less)
   cmp_i64_f64(-3, -3.5)      : Some(Greater)
   cmp_i64_f64(i64::MAX, 2^63): Some(Less)
   cmp_i64_f64(0, NAN)        : None

5. try_into() FOR A CONVERSION THAT CANNOT FAIL
   b.try_into() as i32 has type core::result::Result<i32, core::convert::Infallible>
   Infallible has no values, so that unwrap() can never panic.
   the conversion that CAN fail is the other direction:
   u16::try_from(70_000i32)   = Err(TryFromIntError(PosOverflow))
   u16::try_from(-1i32)       = Err(TryFromIntError(NegOverflow))

The as versions, and where they lie

Section 3 is the trap. Each as line compiles without a warning and prints false for a comparison that is true:

  • -1i32 as u32 < 1u32: the same bits read as unsigned, so -1 becomes 4294967295.
  • -1i32 as u64 < 1u64: casting to a larger type doesn't help, because the sign still has nowhere to go. Advice to "cast the smaller type to the larger one" is right only when the larger type also covers the smaller one's sign.
  • 3_000_000_000u32 as i32: the unsigned value doesn't fit, so it wraps to -1294967296 and -1 < it is false.

Casting with as has the full table of what as does. Clippy's pedantic group catches the pattern: cast_lossless says a u16 as i32 "can be expressed infallibly using From", and cast_sign_loss warns that i32 as u32 "may lose the sign of the value". Strict clippy switches them on.

Integers against floats

f64 holds every i32 exactly (f64::from exists), but not every i64: from 2⁵³ up, neighbouring floats are 2 or more apart. So (2⁵³ + 1) as f64 rounds to 2⁵³, and section 4 shows two different numbers comparing equal. No common type holds both, so the program's cmp_i64_f64 splits the float instead: reject NaN, answer directly when the float is beyond i64's range, then compare the whole part as integers and let the fraction break a tie. That's what Python does for every int < float, without being asked.

try_into() when nothing can fail

b.try_into().unwrap() on a u16 headed for an i32 compiles and works, but its error type is Infallible ↗ (section 5). That's an enum with no values, so the unwrap() guards against something that can't happen. Use i32::from(b) when the conversion can't fail, and save try_from for the direction that can: u16::try_from(-1_i32) is Err(TryFromIntError(NegOverflow)).

If you are coming from another language

Python. Python has no refusal here, and needs none. Its integers have no width, and int < float compares the two exact values: 2**53 + 1 == float(2**53 + 1) is False, where Rust's as f64 says equal. So everything section 2 does by hand, Python does for you. What Python keeps, like Rust, is the refusal for unrelated types: 1 < "2" raises TypeError. What transfers back is the idea that a comparison should be between values, with the conversion chosen so no value is lost. See comparing an int with a float ↗ in the Python library.

C. C compiles every mixed comparison and converts first, by rules you have to know. int < unsigned short works, because the unsigned short is promoted to int. That's C's integer promotion, and where the word "promotion" comes from. But -1 < 1u is false: the usual arithmetic conversions turn the int into an unsigned int, and -1 becomes UINT_MAX. That's section 3's first as line, applied by the compiler without being asked, with -Wsign-compare as the only warning. Rust refuses the comparison C gets wrong and makes you choose the type. See comparing signed and unsigned ↗ in the C library.

ABAP. ABAP compares mixed numeric operands in the highest-ranked of their types: decimal floating point, then F, then P, then INT8, then I. So IF lv_int < lv_packed compares in P and is exact, while a P against an F compares in F, where a 31-digit packed number can round. That's the i64 against f64 row above. The habit that transfers is knowing which type the comparison happens in. What changes is that Rust never picks it for you, so the rounding case can't happen without an as you wrote yourself. (Not machine-checked; CI cannot run ABAP. Check the comparison-type rules in your release's documentation.)

See also

Po polsku

Porównanie (comparison) dwóch liczb różnych typów — i32 z u16 — w Ruscie się nie kompiluje: error[E0308]: mismatched types. To nie jest zakaz języka, tylko brak implementacji. Operator < to cecha (trait) PartialOrd<Rhs>, a biblioteka standardowa implementuje ją dla liczb tylko wtedy, gdy po obu stronach stoi ten sam typ. Dla innych par, np. String == &str, implementacje istnieją i porównanie działa.

Rozwiązanie to wybrać typ wspólny, który mieści wszystkie wartości obu typów, łącznie ze znakiem, i przekonwertować bezstratnie przez From: i32::from(b), a dla pary i32 i u32 — i64::from po obu stronach. Rzutowanie as kompiluje się zawsze i czasem kłamie: -1i32 as u64 < 1u64 daje false, bo „większy typ” nie ma miejsca na minus. Polskie materiały o C nazywają to promocją typów (integer promotion), i to w C -1 < 1u też daje false. Python porównuje int z float dokładnie i nie potrzebuje żadnej konwersji.

Szukaj po polsku: porównywanie liczb różnych typów · rzutowanie typów · promocja typów w C · rust E0308 mismatched types · rust compare i32 u32 · rust clippy cast_lossless