#262 Jul 2026

262. String::insert_str — Prepend or Splice Text Without Rebuilding the String

Adding a prefix with format!("{prefix}{s}") builds a brand-new String and throws the old one away. insert_str splices the text into the buffer you already have.

The trap

You have a String and need to stick something in front of it — a log level, a scheme, a marker. The reflex is format!:

1
2
3
4
5
6
let mut msg = String::from("connection lost");

// allocates a second String, copies both halves,
// drops the original
msg = format!("[ERROR] {msg}");
assert_eq!(msg, "[ERROR] connection lost");

That’s a full new allocation and two copies just to add eight bytes at the front. push_str only helps at the end — there’s no push_front for strings.

The fix

String::insert_str shifts the existing bytes over and copies the new text in, reusing the allocation when capacity allows:

1
2
3
4
let mut msg = String::from("connection lost");

msg.insert_str(0, "[ERROR] ");
assert_eq!(msg, "[ERROR] connection lost");

And it’s not just for prepending — the index can be anywhere, which makes splicing into the middle a one-liner:

1
2
3
4
5
6
let mut name = String::from("report_final.txt");

if let Some(dot) = name.rfind('.') {
    name.insert_str(dot, "_v2");
}
assert_eq!(name, "report_final_v2.txt");

For a single character there’s the sibling String::insert(idx, char).

One caveat

The index is a byte offset and must land on a char boundary — mid-emoji it panics, same rule as slicing. And since the tail gets shifted each call, insert_str is O(n): perfect for the occasional splice, wrong for building a string front-to-back in a loop. If you’re prepending repeatedly, collect the pieces and join them once instead.

#261 Jul 2026

261. String::retain — Delete Characters In Place, No New Allocation

Stripping characters with replace("-", "") builds a brand-new String just to throw characters away. retain deletes them in the buffer you already own.

The trap

This morning’s bite (260) covered replacen — but both replace and replacen always return a fresh String, even when the “replacement” is deleting. Same story with the iterator route:

1
2
3
4
5
6
7
let phone = String::from("+49 (0)30 901820");

// both of these allocate a whole new String
let a = phone.replace(|c: char| !c.is_ascii_digit(), "");
let b: String = phone.chars().filter(char::is_ascii_digit).collect();
assert_eq!(a, "49030901820");
assert_eq!(b, a);

If you’re cleaning strings in a loop, that’s one allocation per string, per pass — for data you already had in a perfectly good buffer.

The fix

String::retain keeps every char the closure approves and shifts the rest out, in place, in one O(n) pass:

1
2
3
4
5
6
7
let mut phone = String::from("+49 (0)30 901820");
let cap = phone.capacity();

phone.retain(|c| c.is_ascii_digit());

assert_eq!(phone, "49030901820");
assert_eq!(phone.capacity(), cap); // same buffer

No new allocation, and the capacity stays put — ready for push_str later. It reads as intent, too: “keep digits” instead of “replace non-digits with nothing”.

One caveat

The closure sees chars in order, exactly once, and retain keeps what returns true — it’s a keep-list, not a kill-list. To delete matches, negate:

1
2
3
let mut s = String::from("no_more_underscores");
s.retain(|c| c != '_');
assert_eq!(s, "nomoreunderscores");

Vec<T> and VecDeque<T> have the same method, so the pattern transfers. When you need to substitute text, replace/replacen earn their allocation — but when you’re only deleting, retain does it where the string already lives.

#260 Jul 2026

260. str::replacen — Replace the First N Matches, Not Every Single One

replace is all-or-nothing: it rewrites every occurrence, whether you wanted that or not. replacen lets you say how many.

The trap

str::replace has no off switch — it replaces every match in the string. The moment your pattern appears somewhere you didn’t expect, it happily rewrites that too:

1
2
3
4
5
6
7
let line = "user=admin role=admin";

// demote the user, keep the role... oops
assert_eq!(
    line.replace("admin", "guest"),
    "user=guest role=guest"
);

The usual workaround is find + manual slicing — index math, an allocation, and an edge case when the pattern is missing.

The fix

replacen takes a third argument: the maximum number of replacements, counted from the left. Everything after the Nth match is left alone:

1
2
3
4
assert_eq!(
    line.replacen("admin", "guest", 1),
    "user=guest role=admin"
);

Fewer matches than n is fine — it just replaces what’s there. And like replace, the pattern can be a char, a &str, or a closure over char:

1
2
3
let csv = "a-b-c-d";
assert_eq!(csv.replacen('-', "+", 2), "a+b+c-d");
assert_eq!(csv.replacen('-', "+", 0), "a-b-c-d"); // n = 0: copy, untouched

One caveat

Counting is strictly left-to-right — there’s no rreplacen for “just the last one”. For that, reach for rfind and slice, or rsplit_once if you’re splitting anyway.

Both replace and replacen return a fresh String and leave the original untouched. If you only need to check the first match, find is cheaper — but when you need “replace the first one and stop”, replacen(pat, to, 1) says exactly that.

#259 Jul 2026

259. str::lines — Split Into Lines Without Dragging \r Along

split('\n') works fine — until a Windows-saved file hands you lines that all end in an invisible \r. lines() was built for exactly this.

The trap

Same story as bite 258: split takes your separator literally. A file saved on Windows uses \r\n line endings, so every “line” keeps a carriage return — and the trailing newline produces a bonus empty string:

1
2
3
4
5
6
7
let text = "alpha\r\nbeta\r\ngamma\r\n";

let naive: Vec<&str> = text.split('\n').collect();
assert_eq!(
    naive,
    ["alpha\r", "beta\r", "gamma\r", ""]
);

That stray \r is invisible in most debug output, so it surfaces as "gamma" != "gamma" mysteries: failed comparisons, HashMap misses, parse errors on the last field.

The fix

lines() splits on \n and strips a trailing \r if one is there — so the same code handles Unix and Windows files:

1
2
let lines: Vec<&str> = text.lines().collect();
assert_eq!(lines, ["alpha", "beta", "gamma"]);

No trailing empty string either — like split_terminator (bite 233), the final newline is treated as a terminator, not a separator.

One caveat

Only \r\n and \n count as line endings. A lone \r (classic Mac OS, some protocol payloads) does not split:

1
2
3
let old_mac = "alpha\rbeta";
let lines: Vec<&str> = old_mac.lines().collect();
assert_eq!(lines, ["alpha\rbeta"]);

And a \r in the middle of a line stays untouched — only one directly before the \n is stripped.

Reading a file? BufRead::lines() gives you the same semantics over owned Strings. Either way: for “give me the lines”, it’s lines() every time — save split('\n') for when you truly mean raw bytes-between-newlines.

#258 Jul 2026

258. split_whitespace — Split on Runs, Not on Every Single Space

split(' ') hands you empty strings for every doubled space — and silently ignores tabs. split_whitespace is what you actually meant.

The trap

User input is messy: leading spaces, double spaces, a stray tab. split(' ') takes all of that literally:

1
2
3
4
5
6
7
let line = "  alpha\tbeta   gamma ";

let naive: Vec<&str> = line.split(' ').collect();
assert_eq!(
    naive,
    ["", "", "alpha\tbeta", "", "", "gamma", ""]
);

Two bugs in one line: every consecutive-space pair produces an empty string, and "alpha\tbeta" sails through as a single “word” because a tab isn’t a space.

The fix

split_whitespace splits on runs of any whitespace and never yields empty strings:

1
2
let words: Vec<&str> = line.split_whitespace().collect();
assert_eq!(words, ["alpha", "beta", "gamma"]);

Leading and trailing whitespace disappear too — no trim() needed first.

Unicode-aware, with an ASCII fast path

“Whitespace” here means the Unicode White_Space property, so a non-breaking space (\u{00A0}) splits words just like a regular one:

1
2
3
let fancy = "alpha\u{00A0}beta";
let words: Vec<&str> = fancy.split_whitespace().collect();
assert_eq!(words, ["alpha", "beta"]);

If your input is guaranteed ASCII (log files, protocol lines), split_ascii_whitespace does the same thing with a cheaper per-byte check — same no-empty-strings guarantee:

1
2
3
let words: Vec<&str> =
    " 42  7\t9 ".split_ascii_whitespace().collect();
assert_eq!(words, ["42", "7", "9"]);

Keep split(' ') for formats where empty fields are meaningful (CSV-like, fixed positions). For “give me the words”, it’s split_whitespace every time.

#257 Jul 2026

257. VecDeque::rotate_left — Where the Ring Buffer Finally Pays Rent

slice::rotate_left touches every element, every time. The same call on a VecDeque moves at most half of them — often far fewer.

Rotation on a slice is O(len)

Bite 133 covered slice::rotate_left: in-place, no allocation, but every rotation is O(len) — all elements physically move through memory.

This morning’s bite 256 showed the cost of VecDeque’s ring buffer: no single slice to hand out. Rotation is where that layout pays you back.

The deque version

VecDeque has its own rotate_left / rotate_right, and the ring buffer turns rotation into pointer arithmetic plus a short move:

1
2
3
4
5
6
7
8
9
use std::collections::VecDeque;

let mut buf: VecDeque<i32> = (0..10).collect();

buf.rotate_left(3);
assert_eq!(buf, [3, 4, 5, 6, 7, 8, 9, 0, 1, 2]);

buf.rotate_right(3);
assert_eq!(buf, [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]);

It’s the loop you’d write by hand — pop one end, push the other — but done as a bulk move:

1
2
3
4
5
6
// rotate_left(3), spelled out:
for _ in 0..3 {
    let x = buf.pop_front().unwrap();
    buf.push_back(x);
}
assert_eq!(buf, [3, 4, 5, 6, 7, 8, 9, 0, 1, 2]);

Because elements may wrap around the allocation’s end, “moving” the front to the back mostly means shifting the head index. The documented bound is O(min(mid, len − mid)) time and no extra space — rotate_left(3) on a million-element deque moves 3 elements, not a million. The same call on a slice moves all of them.

Where it shines: round-robin

A scheduler that cycles through tasks is one rotate_left(1) per turn — O(1):

1
2
3
4
5
let mut tasks: VecDeque<&str> =
    ["fetch", "parse", "render"].into();

tasks.rotate_left(1);
assert_eq!(tasks, ["parse", "render", "fetch"]);

Both methods panic if mid > len(), so a full-cycle rotate_left(len) is legal (and a no-op) but len + 1 is not — use k % len if k can exceed the length.

If you’re rotating a Vec in a hot loop, that’s the signal to switch containers: VecDeque::from(vec) is O(1), and every rotation after that is the cheap kind.

#256 Jul 2026

256. VecDeque::make_contiguous — Turn a Wrapped Ring Buffer Into One Sortable Slice

VecDeque has no .sort(), and any API that wants &[T] rejects it. The catch is the ring buffer underneath — and one call flattens it.

Why a VecDeque isn’t a slice

A VecDeque is a ring buffer: pushes and pops at both ends are O(1) because the contents are allowed to wrap around the end of the allocation. After a few pops and pushes, the elements may sit in memory as two separate runs:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
use std::collections::VecDeque;

let mut buf: VecDeque<i32> = VecDeque::with_capacity(4);
buf.extend([3, 1, 4, 1]);
buf.pop_front();
buf.pop_front();
buf.push_back(5);
buf.push_back(9); // wraps around the buffer's end

let (front, back) = buf.as_slices();
assert_eq!(front, [4, 1]);
assert_eq!(back,  [5, 9]); // two pieces, not one

That’s why there’s no VecDeque::sort, and why you can’t pass one to anything expecting a &[T] — there is no single slice to hand out.

The built-in

make_contiguous rotates the elements back into one run and returns the whole thing as &mut [T]:

1
2
3
let slice = buf.make_contiguous();
slice.sort_unstable();
assert_eq!(slice, [1, 4, 5, 9]);

The deque itself is unchanged as a collection — same elements, same order you left them in — but now it’s backed by a single run:

1
2
3
let (front, back) = buf.as_slices();
assert_eq!(front, [1, 4, 5, 9]);
assert!(back.is_empty()); // one piece

Sorting was the classic motivation, but any slice-only API works after the call: windows, chunks, bite 247’s sort_unstable, or an FFI boundary that wants a pointer and a length.

What it costs

If the deque is already contiguous — which it always is fresh after new() or from(vec) — the call is free. Otherwise it moves elements within the existing buffer: no allocation, worst case O(n) moves. Do it once, then take slices as often as you like.

One nuance: binary_search and friends exist directly on VecDeque, so you don’t need this call just to search. Reach for make_contiguous when the API you’re feeding — or the mutation you want, like an in-place sort — demands one contiguous &mut [T].

#255 Jul 2026

255. rotate_left / rotate_right — Bit Rotation Without the Shift-Overflow Trap

Shifting throws bits away. The manual “wrap them around” idiom panics on n == 0. Rotation has been one method call the whole time.

The problem with shifts

A left shift pushes the top bits off the edge — they’re gone:

1
2
3
let x = 0b1000_0001_u8;

assert_eq!(x << 1, 0b0000_0010); // top bit lost

When you need the bits to wrap around — hashing, checksums, circular counters — the textbook idiom combines two shifts:

1
2
3
let n = 1;
let rotated = (x << n) | (x >> (8 - n));
assert_eq!(rotated, 0b0000_0011);

Which works right up until n == 0: then x >> 8 is a shift by the full bit width — a panic in debug builds, and a masked, silently-wrong result in release. A correct version needs masking both shift amounts, and now you’re writing a code comment again.

The built-in

Every integer type has rotate_left and rotate_right. Bits that fall off one end come back on the other:

1
2
3
4
let x = 0b1000_0001_u8;

assert_eq!(x.rotate_left(1),  0b0000_0011);
assert_eq!(x.rotate_right(1), 0b1100_0000);

No edge cases: the rotation amount is taken modulo the bit width, so n == 0, n == 8, even n == 1000 are all fine —

1
2
3
assert_eq!(x.rotate_left(0), x);
assert_eq!(x.rotate_left(8), x);           // full circle
assert_eq!(x.rotate_left(9), x.rotate_left(1));

— and like bite 254’s isolate_lowest_one, it compiles to a single instruction (ROL/ROR on x86) instead of the three ops the manual idiom costs.

Round trips for free

Rotation never destroys information, so it’s trivially reversible — handy for mixing bits in a hash and for tests:

1
2
3
let v = 0xDEAD_u16;

assert_eq!(v.rotate_left(5).rotate_right(5), v);

Note this rotates the bit pattern, not bytes: for endianness work you want swap_bytes or bite 240’s to_le_bytes. But when the task is “slide bits around a circle”, rotate_left says exactly that — with no (8 - n) waiting to panic.

#254 Jul 2026

254. isolate_lowest_one — The x & x.wrapping_neg() Hack Finally Has a Name

Every bitmask codebase has an unexplained x & x.wrapping_neg() in it somewhere. Rust 1.97 gives the trick a name — and a sibling for the other end.

The folklore version

To keep only the lowest set bit of an integer, the two’s-complement trick is to AND the value with its own negation:

1
2
3
4
let x = 0b0101_0100_u8;

// lowest set bit, the folklore way
assert_eq!(x & x.wrapping_neg(), 0b0000_0100);

It works, it compiles to one instruction (BLSI on x86) — and it explains nothing to the next reader. For the highest set bit there isn’t even a one-liner: you shift 1 by leading_zeros arithmetic and special-case zero.

Named, on every integer type

Rust 1.97 stabilizes isolate_lowest_one and isolate_highest_one. They return the isolated bit as a mask — the value with all other bits cleared:

1
2
3
4
5
6
7
8
let x = 0b0101_0100_u8;

assert_eq!(x.isolate_lowest_one(),  0b0000_0100);
assert_eq!(x.isolate_highest_one(), 0b0100_0000);

// zero just stays zero — no panic, no sentinel
assert_eq!(0_u8.isolate_lowest_one(),  0);
assert_eq!(0_u8.isolate_highest_one(), 0);

Where bite 250’s lowest_one / highest_one answer “at which position?” (as an Option), the isolate_ pair answers “which bit?” — same information, shaped for masking instead of indexing.

The pattern: walk the set bits

The mask shape is exactly what you want for iterating over flags — grab the lowest bit, handle it, XOR it away:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
let mut mask = 0b0101_0100_u8;
let mut seen = vec![];

while mask != 0 {
    let bit = mask.isolate_lowest_one();
    seen.push(bit);   // handle one flag
    mask ^= bit;      // clear it
}

assert_eq!(seen, [0b0000_0100,
                  0b0001_0000,
                  0b0100_0000]);

No positions, no shifting back and forth — each iteration hands you a ready-to-use single-bit mask. Signed types work too ((-8_i8).isolate_lowest_one() == 8), since the methods operate on the raw bit pattern.

If your code review comments still include “this ANDs x with its negation to isolate the lowest set bit…”, Rust 1.97 lets the method name say it for you.

#253 Jul 2026

253. overflowing_add — Wrap, But Know It Happened

This morning’s Wrapping<T> (bite 252) wraps silently — but multiword arithmetic needs the carry bit too. overflowing_add returns both: the wrapped result and whether it wrapped.

The sum < a trick

The classic way to detect a carry after unsigned addition compares the result to an input:

1
2
3
4
5
fn add_with_carry(a: u64, b: u64) -> (u64, u64) {
    let sum = a.wrapping_add(b);
    let carry = (sum < a) as u64; // wrapped iff smaller
    (sum, carry)
}

It works — unsigned overflow means the sum came back smaller — but it’s a puzzle for the reader, it’s easy to compare against the wrong operand, and the signed version of the trick is different and wrong in edge cases.

overflowing_add says it directly

Every integer type has overflowing_add (and _sub, _mul, _neg, _shl…): it returns (wrapped_value, overflowed: bool) in one call:

1
2
assert_eq!(u64::MAX.overflowing_add(1), (0, true));
assert_eq!(1_u64.overflowing_add(1), (2, false));

That makes carry chains — the core of any 128-bit-or-wider addition — read like what they are:

1
2
3
4
5
6
7
8
// [lo, hi] little-endian u128 as two u64 limbs
fn add128(a: [u64; 2], b: [u64; 2]) -> [u64; 2] {
    let (lo, carry) = a[0].overflowing_add(b[0]);
    let hi = a[1]
        .wrapping_add(b[1])
        .wrapping_add(carry as u64);
    [lo, hi]
}
1
2
3
let max = [u64::MAX, 0];      // u64::MAX
let one = [1, 0];
assert_eq!(add128(max, one), [0, 1]); // 2^64

One family, four answers to overflow

Where checked_add (and bite 219’s checked_add_signed) bails out with None and Wrapping<T> (bite 252) wraps silently, overflowing_add is the “do both” option: you always get the mod-2^n result, plus the fact you’d otherwise have to reverse-engineer. (The dedicated carrying_add that takes and returns a carry is still nightly-only — on stable, overflowing_add is how the limbs get added.)

Compilers recognize the pattern, too: the carry chain above compiles down to an add + adc pair on x86-64 — the same code the manual trick produces, minus the puzzle.