Parsing

203. Peekable::next_if_map — Consume a Token Only If It Parses, Transform in One Step

next_if only answers yes/no, so when you also need the converted value you end up peeking, computing, and calling next() by hand. Rust 1.94 stabilized Peekable::next_if_map — conditionally consume the next item and transform it in a single call, putting the item back if it doesn’t match.

The trap: the peek / compute / advance dance

Hand-rolled lexers are full of this pattern — look at the next item, decide whether it’s the kind you want, and only then consume it. With next_if you can express the decide part, but next_if hands you back the original item, so you have to redo the conversion afterward. Most people skip it and drop down to a manual peek() + next():

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
use std::iter::Peekable;
use std::str::Chars;

// Peek, compute the digit, THEN remember to advance. Three steps,
// and it's easy to forget the next() and loop forever.
fn take_digit_manual(it: &mut Peekable<Chars>) -> Option<u32> {
    let &c = it.peek()?;
    let d = c.to_digit(10)?;
    it.next();
    Some(d)
}

The conversion (to_digit) and the consumption (next) are split across separate lines, and the iterator only advances as a side effect. Forget the it.next() and you’ve written an infinite loop.

The fix: decide and transform in one call

next_if_map takes the next item by value and hands it to a closure returning Result<R, Item>. Return Ok(value) and the item is consumed, giving you Some(value); return Err(item) and the item is pushed back, giving you None. The classic conversion-or-give-back is just .ok_or(c):

1
2
3
4
5
6
use std::iter::Peekable;
use std::str::Chars;

fn take_digit(it: &mut Peekable<Chars>) -> Option<u32> {
    it.next_if_map(|c| c.to_digit(10).ok_or(c))
}

One line, no manual next(), and the “advance only on success” rule is enforced by the method instead of by you remembering to call it.

It really does put the item back

When the closure returns Err, the iterator is left exactly where it was — the next read still sees that item:

1
2
3
4
5
6
7
8
9
# use std::iter::Peekable;
# use std::str::Chars;
# fn take_digit(it: &mut Peekable<Chars>) -> Option<u32> {
#     it.next_if_map(|c| c.to_digit(10).ok_or(c))
# }
let mut it = "px".chars().peekable();

assert_eq!(take_digit(&mut it), None); // 'p' isn't a digit...
assert_eq!(it.next(), Some('p'));      // ...so it's still here

That give-it-back guarantee is what makes it safe to chain in a loop: each call either makes progress or leaves the stream untouched for the next rule to try.

Where it shines: tokenizing

A digit-run parser becomes a tight while let that stops cleanly at the first non-digit, leaving the rest of the input for whatever comes next:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
fn parse_number(s: &str) -> (u64, String) {
    let mut it = s.chars().peekable();
    let mut n = 0u64;
    while let Some(d) = it.next_if_map(|c| c.to_digit(10).ok_or(c)) {
        n = n * 10 + d as u64;
    }
    (n, it.collect()) // leftover chars, untouched
}

assert_eq!(parse_number("42px"), (42, "px".to_string()));
assert_eq!(parse_number("2026"), (2026, String::new()));

There’s also next_if_map_mut, which passes &mut Item and takes a closure returning Option<R> — handy when the item is expensive to move or you want to mutate it in place rather than hand it back.

The bottom line

When you only want the next item if it converts to something useful, reach for next_if_map instead of the peek / compute / next shuffle. It folds the test and the transform into one call and guarantees the iterator only advances when the conversion succeeds — exactly the invariant hand-written lexers keep getting wrong.

#186 Jun 2026

186. str::split_inclusive — Split a String and Keep the Separator With Each Chunk

"a\nb\n".split('\n') swallows every newline and hands you a phantom "" at the end. split_inclusive keeps each separator glued to the chunk it belongs to — no ghost element, and you can .concat() straight back to the original.

The split that loses information

The default split is destructive: the matched character is gone, and a trailing separator becomes an empty string.

1
2
3
4
5
let log = "INFO\nWARN\nERROR\n";

let parts: Vec<&str> = log.split('\n').collect();
assert_eq!(parts, ["INFO", "WARN", "ERROR", ""]);
//                                            ^^ phantom empty tail

That phantom empty string is the source of a hundred Stack Overflow questions. You usually paper over it with .filter(|s| !s.is_empty()) or trim_end() before splitting.

split_inclusive keeps the terminator

Each piece keeps the separator that ended it. The trailing newline isn’t an empty string — it’s the end of the last real chunk.

1
2
3
4
5
let log = "INFO\nWARN\nERROR\n";

let parts: Vec<&str> = log.split_inclusive('\n').collect();
assert_eq!(parts, ["INFO\n", "WARN\n", "ERROR\n"]);
assert_eq!(parts.concat(), log); // round-trips exactly

When you actually want this

Reformatting line by line without losing the trailing newline:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
let src = "fn main() {\n    println!(\"hi\");\n}\n";

let numbered: String = src
    .split_inclusive('\n')
    .enumerate()
    .map(|(i, line)| format!("{:>2} | {line}", i + 1))
    .collect();

assert_eq!(
    numbered,
    " 1 | fn main() {\n 2 |     println!(\"hi\");\n 3 | }\n"
);

No newline added, none lost — every \n is already where it should be.

Works on slices too

[T]::split_inclusive exists with the same shape, taking a predicate instead of a pattern. Useful for batching consecutive items up to a delimiter element.

1
2
3
let bytes = [1, 2, 0, 3, 0, 4];
let chunks: Vec<&[u8]> = bytes.split_inclusive(|&b| b == 0).collect();
assert_eq!(chunks, [&[1, 2, 0][..], &[3, 0][..], &[4][..]]);

Reach for split_inclusive whenever the separator is part of the data — line endings, statement terminators, record markers — not noise to throw away.

76. slice::as_chunks — Split Slices into Fixed-Size Arrays

You’re calling .chunks(4) and immediately doing chunk.try_into().unwrap() to get an array. as_chunks gives you &[[T; N]] directly — a slice of properly typed arrays, plus the remainder.

The Problem

When you use .chunks(N), each chunk is a &[T] — a dynamically sized slice. The compiler doesn’t know its length, so you’re stuck converting manually:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
fn sum_pairs(data: &[i32]) -> Vec<i32> {
    data.chunks(2)
        .filter(|c| c.len() == 2) // skip incomplete last chunk
        .map(|c| c[0] + c[1])     // runtime indexing, no guarantees
        .collect()
}

fn main() {
    let values = [1, 2, 3, 4, 5];
    assert_eq!(sum_pairs(&values), vec![3, 7]);
}

That works, but the compiler can’t verify your index access at compile time. You’re also throwing away the last chunk if it’s incomplete, with no easy way to inspect it.

After: as_chunks

Stabilized in Rust 1.88, as_chunks splits a slice into a &[[T; N]] and a remainder &[T] in one call:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
fn sum_pairs(data: &[i32]) -> Vec<i32> {
    let (chunks, _remainder) = data.as_chunks::<2>();
    chunks.iter().map(|[a, b]| a + b).collect()
}

fn main() {
    let values = [1, 2, 3, 4, 5];
    assert_eq!(sum_pairs(&values), vec![3, 7]);

    // The remainder is available too
    let (chunks, remainder) = values.as_chunks::<2>();
    assert_eq!(chunks, &[[1, 2], [3, 4]]);
    assert_eq!(remainder, &[5]);
}

Each chunk is &[i32; 2], so you can pattern-match [a, b] directly. The compiler knows the size — no bounds checks, no panics.

Processing Fixed-Width Records

Parsing binary data with fixed-width fields is where as_chunks shines. Imagine RGB pixel data packed as bytes:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
fn brighten(pixels: &mut [u8], amount: u8) {
    let (chunks, _) = pixels.as_chunks_mut::<3>();
    for [r, g, b] in chunks {
        *r = r.saturating_add(amount);
        *g = g.saturating_add(amount);
        *b = b.saturating_add(amount);
    }
}

fn main() {
    let mut pixels = [100, 150, 200, 50, 60, 70, 255, 128, 0];
    brighten(&mut pixels, 30);
    assert_eq!(pixels, [130, 180, 230, 80, 90, 100, 255, 158, 30]);
}

No manual stride arithmetic. Each iteration gives you exactly 3 bytes, pattern-matched into r, g, b.

Don’t Lose the Remainder

Unlike chunks_exact() where you call .remainder() on the iterator after consuming it, as_chunks returns the remainder upfront:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
fn main() {
    let data = [10, 20, 30, 40, 50, 60, 70];

    let (fours, rest) = data.as_chunks::<4>();
    assert_eq!(fours.len(), 1);       // one complete chunk: [10, 20, 30, 40]
    assert_eq!(fours[0], [10, 20, 30, 40]);
    assert_eq!(rest, &[50, 60, 70]);  // leftover elements

    // as_rchunks starts from the right instead
    let (rest, fours) = data.as_rchunks::<4>();
    assert_eq!(rest, &[10, 20, 30]);
    assert_eq!(fours[0], [40, 50, 60, 70]);
}

as_rchunks is the mirror — it aligns chunks to the end, putting the remainder at the front. Useful when your trailing data is the structured part (e.g., a checksum or footer).

The Full Family

All stabilized in Rust 1.88, these come in immutable and mutable variants:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
fn main() {
    let data: &[u8] = &[1, 2, 3, 4, 5, 6, 7];

    // as_chunks — align from left, remainder on right
    let (chunks, rem) = data.as_chunks::<3>();
    assert_eq!(chunks, &[[1, 2, 3], [4, 5, 6]]);
    assert_eq!(rem, &[7]);

    // as_rchunks — align from right, remainder on left
    let (rem, chunks) = data.as_rchunks::<3>();
    assert_eq!(rem, &[1]);
    assert_eq!(chunks, &[[2, 3, 4], [5, 6, 7]]);

    // Mutable versions: as_chunks_mut, as_rchunks_mut
    let mut buf = [0u8; 6];
    let (chunks, _) = buf.as_chunks_mut::<2>();
    chunks[0] = [0xCA, 0xFE];
    chunks[1] = [0xBA, 0xBE];
    chunks[2] = [0xDE, 0xAD];
    assert_eq!(buf, [0xCA, 0xFE, 0xBA, 0xBE, 0xDE, 0xAD]);
}

Whenever you’re reaching for .chunks(N) with a compile-time constant, as_chunks::<N>() gives you stronger types, better ergonomics, and the remainder without gymnastics.

#059 Apr 2026

59. split_first_chunk — Destructure Slices into Arrays

Parsing a header from a byte buffer? Extracting the first N elements of a slice? split_first_chunk hands you a fixed-size array and the remainder in one call — no manual indexing, no panics.

The Problem

You have a byte slice and need to pull out a fixed-size prefix — say a 4-byte magic number or a 2-byte length field. The manual approach is fragile:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
fn parse_header(data: &[u8]) -> Option<([u8; 4], &[u8])> {
    if data.len() < 4 {
        return None;
    }
    let header: [u8; 4] = data[..4].try_into().unwrap();
    let rest = &data[4..];
    Some((header, rest))
}

fn main() {
    let packet = b"RUST is awesome";
    let (header, rest) = parse_header(packet).unwrap();
    assert_eq!(&header, b"RUST");
    assert_eq!(rest, b" is awesome");
}

That try_into().unwrap() is ugly, and if you get the index arithmetic wrong, you get a panic at runtime.

After: split_first_chunk

Stabilized in Rust 1.77, split_first_chunk splits a slice into a &[T; N] array reference and the remaining slice — returning None if the slice is too short:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
fn parse_header(data: &[u8]) -> Option<(&[u8; 4], &[u8])> {
    data.split_first_chunk::<4>()
}

fn main() {
    let packet = b"RUST is awesome";
    let (magic, rest) = parse_header(packet).unwrap();
    assert_eq!(magic, b"RUST");
    assert_eq!(rest, b" is awesome");

    // Too short — returns None instead of panicking
    let tiny = b"RS";
    assert!(tiny.split_first_chunk::<4>().is_none());
}

One method call. No manual slicing, no try_into, and the const generic N ensures the compiler knows the exact array size.

Chaining Chunks for Protocol Parsing

Real protocols have multiple fields. Chain split_first_chunk calls to peel them off one at a time:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
fn parse_packet(data: &[u8]) -> Option<([u8; 2], [u8; 4], &[u8])> {
    let (version, rest) = data.split_first_chunk::<2>()?;
    let (length, payload) = rest.split_first_chunk::<4>()?;
    Some((*version, *length, payload))
}

fn main() {
    let raw = b"\x01\x02\x00\x00\x00\x05hello";
    let (version, length, payload) = parse_packet(raw).unwrap();

    assert_eq!(version, [0x01, 0x02]);
    assert_eq!(length, [0x00, 0x00, 0x00, 0x05]);
    assert_eq!(payload, b"hello");
}

Each ? short-circuits if the remaining data is too short. No bounds checks scattered across your code.

From the Other End: split_last_chunk

Need to grab a suffix instead — like a trailing checksum? split_last_chunk mirrors the API from the back:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
fn strip_checksum(data: &[u8]) -> Option<(&[u8], &[u8; 2])> {
    data.split_last_chunk::<2>()
}

fn main() {
    let msg = b"payload\xAB\xCD";
    let (body, checksum) = strip_checksum(msg).unwrap();
    assert_eq!(body, b"payload");
    assert_eq!(checksum, &[0xAB, 0xCD]);

    let short = b"\x01";
    assert!(strip_checksum(short).is_none());
}

Same safety, same ergonomics — just peeling from the tail.

The Full Family

These methods come in mutable variants too, all stabilized in 1.77:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
fn main() {
    // Immutable — borrow array refs from a slice
    let data: &[u8] = &[1, 2, 3, 4, 5];
    let (head, tail) = data.split_first_chunk::<2>().unwrap();
    assert_eq!(head, &[1, 2]);
    assert_eq!(tail, &[3, 4, 5]);

    // split_last_chunk — from the back
    let (init, last) = data.split_last_chunk::<2>().unwrap();
    assert_eq!(init, &[1, 2, 3]);
    assert_eq!(last, &[4, 5]);

    // first_chunk / last_chunk — just the array, no remainder
    let first: &[u8; 3] = data.first_chunk::<3>().unwrap();
    assert_eq!(first, &[1, 2, 3]);

    let last: &[u8; 3] = data.last_chunk::<3>().unwrap();
    assert_eq!(last, &[3, 4, 5]);
}

Wherever you reach for &data[..N] and a try_into(), there’s probably a chunk method that does it better. Type-safe, bounds-checked, and zero-cost.

52. Peekable::next_if_map — Peek, Match, and Transform in One Step

Tired of peeking at the next element, checking if it matches, and then consuming and transforming it? next_if_map collapses that entire dance into a single call.

The problem

When writing parsers or processing token streams, you often need to conditionally consume the next element and extract something from it. With next_if and peek, you end up doing the work twice — once to check, once to transform:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
let mut iter = vec![1_i64, 2, -3, 4].into_iter().peekable();

// Clunky: peek, check, consume, transform — separately
let val = if iter.peek().is_some_and(|n| *n > 0) {
    iter.next().map(|n| n * 10)
} else {
    None
};

assert_eq!(val, Some(10));

It works, but you’re expressing the same logic in two places and the code doesn’t clearly convey its intent.

Enter next_if_map

Stabilized in Rust 1.94, Peekable::next_if_map takes a closure that returns Result<R, I::Item>. Return Ok(transformed) to consume the element, or Err(original) to put it back:

1
2
3
4
5
6
7
8
9
let mut iter = vec![1_i64, 2, -3, 4].into_iter().peekable();

// Clean: one closure handles the check AND the transformation
let val = iter.next_if_map(|n| {
    if n > 0 { Ok(n * 10) } else { Err(n) }
});

assert_eq!(val, Some(10));
assert_eq!(iter.next(), Some(2)); // iterator continues normally

The element is only consumed when you return Ok. If you return Err, the original value goes back and the iterator is unchanged.

Parsing example: extract leading digits

This is where next_if_map really shines — pulling typed tokens out of a character stream:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
let mut chars = "42abc".chars().peekable();

let mut number = 0u32;
while let Some(digit) = chars.next_if_map(|c| {
    c.to_digit(10).ok_or(c)
}) {
    number = number * 10 + digit;
}

assert_eq!(number, 42);
assert_eq!(chars.next(), Some('a')); // non-digit stays unconsumed

Each character is inspected once: digits are consumed and converted, and the first non-digit stops the loop without being eaten.

Key details

  • Atomic peek + consume + transform: no redundant checks, no repeated logic
  • Non-destructive on rejection: returning Err(item) puts the element back
  • Also available: next_if_map_mut takes FnOnce(&mut I::Item) -> Option<R> for when you don’t need ownership
  • Stable since Rust 1.94

Next time you’re writing a peek-then-consume pattern, reach for next_if_map — your parser will thank you.

#044 Mar 2026

44. split_once — Split a String Exactly Once

When you need to split a string on the first occurrence of a delimiter, split_once is cleaner than anything you’d write by hand. Stable since Rust 1.52.

Parsing key=value pairs, HTTP headers, file paths — almost everywhere you split a string, you only care about the first separator. Before split_once, you’d reach for .find() plus index arithmetic:

The old way

1
2
3
4
5
6
7
8
let s = "Content-Type: application/json; charset=utf-8";

let colon = s.find(':').unwrap();
let header = &s[..colon];
let value = s[colon + 1..].trim();

assert_eq!(header, "Content-Type");
assert_eq!(value, "application/json; charset=utf-8");

Works, but it’s four lines of noise. The index arithmetic is easy to get wrong, and .trim() is a separate step.

With split_once

1
2
3
4
5
6
let s = "Content-Type: application/json; charset=utf-8";

let (header, value) = s.split_once(": ").unwrap();

assert_eq!(header, "Content-Type");
assert_eq!(value, "application/json; charset=utf-8");

One line. The delimiter is consumed, both sides are returned, and you pattern-match directly into named bindings.

Handling missing delimiters

split_once returns Option<(&str, &str)>None if the delimiter isn’t found. This makes it composable with ? or if let:

1
2
3
4
5
6
7
fn parse_env_var(s: &str) -> Option<(&str, &str)> {
    s.split_once('=')
}

assert_eq!(parse_env_var("HOME=/root"), Some(("HOME", "/root")));
assert_eq!(parse_env_var("NOVALUE"), None);
assert_eq!(parse_env_var("KEY=a=b=c"), Some(("KEY", "a=b=c")));

Note the last case: split_once stops at the first =. The rest of the string — a=b=c — is kept intact in the second half. That’s usually exactly what you want.

rsplit_once — split from the right

When you need the last delimiter instead of the first, rsplit_once has you covered:

1
2
3
4
5
6
let path = "/home/martin/projects/rustbites/content/posts/bite-044.md";

let (dir, filename) = path.rsplit_once('/').unwrap();

assert_eq!(dir, "/home/martin/projects/rustbites/content/posts");
assert_eq!(filename, "bite-044.md");

Multi-char delimiters work too

The delimiter can be any pattern — a char, a &str, or even a closure:

1
2
3
4
5
6
7
8
let record = "alice::42::engineer";

let (name, rest) = record.split_once("::").unwrap();
let (age_str, role) = rest.split_once("::").unwrap();

assert_eq!(name, "alice");
assert_eq!(age_str, "42");
assert_eq!(role, "engineer");

Whenever you reach for .splitn(2, ...) just to grab two halves, replace it with split_once — the intent is clearer and the return type is more ergonomic.