Char

#229 Jun 2026

229. char::to_digit — Turn a Digit Character Into Its Value, Not Its Byte

Reached for c as u8 - b'0' to turn '7' into 7? It works until the input isn’t a digit — then you get silent garbage. char::to_digit does it safely and handles hex too.

The byte-math trap

The classic trick subtracts the ASCII code of '0':

1
2
3
let c = '7';
let value = c as u8 - b'0';
assert_eq!(value, 7); // fine...

It’s fine for '0'..='9'. But there’s no validation — feed it anything else and the arithmetic just keeps going:

1
2
3
4
5
// 'f' is 102, b'0' is 48 → 54, which is nonsense, not 15
assert_eq!('f' as u8 - b'0', 54);

// 'a' is 97 → 97 - 48 = 49, also wrong
assert_eq!('a' as u8 - b'0', 49);

No panic, no None — just a wrong number flowing downstream.

to_digit returns an Option

char::to_digit(radix) converts a digit character to its numeric value and gives you an Option<u32>, so non-digits become None instead of garbage:

1
2
3
assert_eq!('7'.to_digit(10), Some(7));
assert_eq!('0'.to_digit(10), Some(0));
assert_eq!('x'.to_digit(10), None); // not a digit → None

Pass 16 and it parses hex, letters included and case-insensitive:

1
2
3
4
assert_eq!('f'.to_digit(16), Some(15));
assert_eq!('F'.to_digit(16), Some(15));
assert_eq!('a'.to_digit(16), Some(10));
assert_eq!('9'.to_digit(16), Some(9));

Where it shines

Because it returns an Option, it drops straight into filter_map — sum every digit in a string and skip everything else, no manual bounds check:

1
2
let total: u32 = "a1b2c3".chars().filter_map(|c| c.to_digit(10)).sum();
assert_eq!(total, 6);

The reverse direction

char::from_digit goes the other way — a value plus a radix back to a digit character:

1
2
3
assert_eq!(char::from_digit(15, 16), Some('f'));
assert_eq!(char::from_digit(9, 10), Some('9'));
assert_eq!(char::from_digit(42, 16), None); // out of range → None

Both have been stable since Rust 1.0. Whenever you’re tempted to do char-to-number arithmetic by hand, reach for to_digit — it validates, it does hex, and it never silently lies to you.

#146 May 2026

146. char::MAX_LEN_UTF8 — Size UTF-8 Buffers Without Magic Numbers

Every time you’ve called char::encode_utf8, you’ve written [0u8; 4] from memory. Rust 1.93 stabilises char::MAX_LEN_UTF8 so you don’t have to keep that magic number in your head.

The magic number you keep typing

encode_utf8 writes the UTF-8 bytes of a char into a &mut [u8] and returns a &mut str pointing at the written portion. The slice has to be big enough — which means knowing that the worst-case UTF-8 encoding is 4 bytes:

1
2
3
let mut buf = [0u8; 4]; // why 4? because UTF-8, that's why
let s = '🦀'.encode_utf8(&mut buf);
assert_eq!(s, "🦀");

That 4 is correct but unexplained. Anyone reading your code has to either trust you or go re-derive the UTF-8 spec.

The named version

Rust 1.93 stabilises two constants on char:

1
2
assert_eq!(char::MAX_LEN_UTF8, 4);
assert_eq!(char::MAX_LEN_UTF16, 2);

MAX_LEN_UTF8 is the maximum number of u8s encode_utf8 can ever write. MAX_LEN_UTF16 is the same for encode_utf16 (a surrogate pair = 2 u16s). Drop them straight into your buffer declarations:

1
2
3
4
5
6
7
8
let mut buf = [0u8; char::MAX_LEN_UTF8];
let s = '🦀'.encode_utf8(&mut buf);
assert_eq!(s, "🦀");
assert_eq!(s.len(), 4);

let mut wide = [0u16; char::MAX_LEN_UTF16];
let w = '🦀'.encode_utf16(&mut wide);
assert_eq!(w.len(), 2);

Same behaviour, but the intent is self-documenting — the buffer is sized to hold exactly one char, by definition.

Sizing a buffer for N chars

Where this really pays off is when you’re computing a buffer for several chars on the stack:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
const N: usize = 8;
let mut buf = [0u8; N * char::MAX_LEN_UTF8];

let mut pos = 0;
for c in ['h', 'é', 'l', 'l', 'o'] {
    let s = c.encode_utf8(&mut buf[pos..]);
    pos += s.len();
}

assert_eq!(&buf[..pos], "héllo".as_bytes());

Now if Unicode ever expanded its scalar value range and MAX_LEN_UTF8 grew, your code would still be correct. With a hardcoded 4, you’d have a silent buffer overflow waiting to happen the day someone bumps the constant.

Why bother?

It’s a small change — one constant, no new behaviour. But it kills a real source of off-by-one bugs (people writing [0u8; 3] because they “only handle Latin-1”) and makes UTF-8 buffer code legible at a glance. Available since Rust 1.93 (January 2026).