What You Should Know About Unsigned, Signed Integers and Casting in Rust | by Shinichi Okada | Jul, 2020 | Towards Data Science | Towards Data Science

[Updated 2021–02–18. Code changed to Gist and added links]
Table of ContentsIntroduction🦀 Unsigned Integer Types🦀 Signed Integer Types🦀 Signed, Ones’ Complement and Two’s Complement🦀 Sign-and-Magnitude🦀 Ones’ Complement🦀 Two’s Complement🦀 4-bit Signed Binary Number Comparison🦀 Rust signed two’s complement integer types🦀 Casting in Rust🦀 Casting to an Unsigned Type🦀 Casting to a Signed Type🦀 Bitwise Negation🦀 Adding a Negative NumberConclusion
Rust has two data type subsets, scalar, and compound. The scalar types are integers, floating numbers, Booleans, and characters. The compound types are arrays and tuples.
In this article we are going to see why the following code fails:
fn main() {    let a: i16 = 2;    let b: u16 = 4;    println!("{}", a+b);}
And why casting 128 to i8 is -128.
Output:
128 as a i8 is : -128
To understand better about the casting, we need to review Signed, Ones’ Complement, and Two’s Complement.
We will also cover adding a negative number, bitwise negation, and converting a binary to an unsigned and signed decimal.
Let’s start with Rust integer types first.
towardsdatascience.com
Unsigned integer types in Rust start with u and it has 8, 16, 32, 64, and 128-bit. The minimum and maximum values are from 0 to 2n-1.
For example u8 has 0 to 28-1, which is 255. The following table shows all the details for unsigned integers.
Output:
u8 has the max value of 255.u16 has the max value of 65535.u32 has the max value of 4294967295.u64 has the max value of 18446744073709551615.u128 has the max value of 340282366920938463463374607431768211455.
If you try to assign a negative number to an unsigned type, it will fail.
fn main() {    let u:u32 = -1;    println!("{} in binary is {:b}", u, u);}error[E0600]: cannot apply unary operator `-` to type `u32` --> main.rs" data-line="2" data-column="17">main.rs:2:17  |2 |     let u:u32 = -1;  |                 ^^ cannot apply unary operator `-`  |  = note: unsigned values cannot be negated
- is one of Rust's unary operators and it is the negation operator for signed integer types and floating-point types.
The default integer type in Rust is i32. Signed integer types in Rust start with i and it has 8, 16, 32, 64, and 128-bit. The minimum and maximum values are from -(2n−1) to 2n−1-1. The -1 in the n-1 is the sign bit (positive or negative), which we cover in the next section.
For example i8 has -(27) to 27-1, which is -128 to 127. The following table shows all the details for signed integers.
Output:
i8 has the min value of -128.i8 has the max value of 127.i16 has the min value of -32768.i16 has the max value of 32767.i32 has the min value of -2147483648.i32 has the max value of 2147483647.i64 has the min value of -9223372036854775808.i64 has the max value of 9223372036854775807.i128 has the min value of -170141183460469231731687303715884105728.i128 has the max value of 170141183460469231731687303715884105727.
In computing, signed number representations are required to encode negative numbers in binary number systems. Let’s examine sign-and-magnitude, ones’ complement, and two’s complement.
Sign-and-Magnitude is also called Signed Magnitude. The first bit (called the most significant bit or MSB) tells if it is positive by 0 or a negative by 1. The rest is called magnitude bits.
As I mentioned it before that signed integer types have the min and the max from -(2n−1) to 2n−1-1 where n stands for the number of bits. Since we use the first bit for the positive and negative signs we have n-1 in the 2n−1.
For 4-bit the min and max are from -(23) to 23–1, which is -8 to +7.
As you see in the diagram above, the positive and the negative have the same digits except for the sign bit.
The problem of the signed magnitude is that there are two zeros, 0000 and 1000.
The first bit (MSB) is the same as the signed magnitude. It tells a positive by 0 or negative by 1. The rest of the bits will take the compliment, which means if it is 1 the complement is 0 and if it is 0, then the complement is 1.
The signed ones’ complement has the same problem as the signed magnitude. There are two zeros, 0000 and 1111.
In the binary numbering system, the radix (base) is two. That’s why the radix complement is called two’s complement and the diminished radix complement is called ones’ complement.
Two’s complement can avoid multiple representations of 0, and it avoids keeping track of carrying bits in case of overflow.
We again use the first bit (MSB) for the + and - signs. We take the complement of the number and we add 1 to get the opposite number. This works from positive to negative and vice versa.
For zero that is 0000 in the binary, the complement is 1111 and adding 1 results in 1 0000. The 1 is called an "overflow" bit. Overflow occurs when the sum of the most significant (left-most) column produces a carry forward. This overflow or carry bit can be ignored.
The following table shows the comparison of three signed number representations.
Now let’s compare negative numbers of signed two’s complement from the above table (from -1 to -7) to Rust’s signed integers.
Output:
-1 in binary is 11111111111111111111111111111111-2 in binary is 11111111111111111111111111111110-3 in binary is 11111111111111111111111111111101-4 in binary is 11111111111111111111111111111100-5 in binary is 11111111111111111111111111111011-6 in binary is 11111111111111111111111111111010-7 in binary is 11111111111111111111111111111001
They are exactly the same except Rust uses the default 32-bit. Rust’s signed integer types are called the signed two’s complement integer types.
Casting means changing the data type of a piece of data from one type to another.
as keyword turns primitive types into other primitive types. We can use as keyword to solve the code in the introduction.
When you cast from a small length to a larger length, for example from 8-bit to 16-bit, there won’t be any problem but when you cast down, you may have a problem.
Method 1
When you cast to an unsigned type, T, T::MAX + 1 is added or subtracted until the value fits into the new type.
Example 1: Casting 1000 from the default, i32 to u8
u8 has the max number of 255, so we subtract 255+1, which is 256 from 1000 until it gets less than 255.
1000 - 256             = 744 1000 - 256 - 256       = 488 1000 - 256 - 256 - 256 = 232
Output:
1000 as a u8 is : 232
Method 2
100010 in decimal is 11 1110 10002 in binary. We can take the last 8 bits, which is 1110 1000 and it is 232 in decimal.
An interesting number is 25610.
Output:
256 as a u8 is : 0
25610 is 1000000002 in binary. If you take the last 8 bits it is 00000000.
Method 1
This is the same as above but you need to be aware that the min/max number of the data type.
i8's min and max numbers are -128 and 127. So the number needs to be between them.
Casting 128 to i8 :
128-256=-128
Casting 1000 to i8:
1000 - 256 = 7441000 - 256 - 256 = 4881000 - 256 - 256 - 256 = 2321000 - 256 - 256 - 256 - 256 = -24
Output:
128 as a i8 is : -1281000 as a i8 is : -24232 as a i8 is : -24257 as a i8 is : 1130 as a i8 is : -126514 as a i8 is : 2
Method 2
100010 in decimal is 11 1110 10002 in binary. The MSB within 8-bit is 1, so it is a negative number. Then use 2’s complement. The complement is 000101112 and we add 12 becomes 000110002. Finally, it is -2410.
When the MSB within 8-bit is 0, it is a positive number. Then take the first 8 least significant bits (LSB).
Rust uses ! for the Bitwise Negation (Bitwise NOT). This produces different results depends on the type.
Output:
u is 2u in binary is 10i is 2i in binary is 10Bitwise negation !u is 4294967293Bitwise negation !i is -3
As you see !2 with the unsigned type returns 4294967293 and with the signed type returns -3.
The bitwise negation on a signed integer returns the two’s complement as we saw previously in Rust signed two’s complement integer types.
The subtraction is the same as adding a negative number.
5 - 2 = 5 + (-2) = 3
This applies to binary as well.
0101 - 0010 = 0101 + (-0010) // (1)= 0101 + 1110    // (2)= 0011           // this is 3 in decimal number.
We find -0010 by finding the two’s complement of 0010 that is 1110.
We covered Rust integer types, three types of signed binary numbers, casting to an unsigned type and a signed type. I hope you have a better idea of what’s going on when you are dealing with Rust integers and casting.
Please stay tuned for the next post.
Get full access to every story on Medium by becoming a member.