#227 Jun 2026

227. trim_matches — Strip the Same Char Off Both Ends, However Many There Are

trim() only knows about whitespace, and strip_prefix peels off one occurrence. When you need to shave every leading and trailing . (or 0, or quote) off a string, reach for trim_matches.

The hand-rolled trim

You’ve got a string padded with some character and want it gone from both ends — but only the ends:

1
2
3
4
5
let s = "***heading***";

// strip_prefix only removes one, and only the front
let once = s.strip_prefix('*').unwrap_or(s);
assert_eq!(once, "**heading***");

Looping strip_prefix/strip_suffix until they stop matching works, but it’s a chore. trim_matches does exactly that for you — it removes all consecutive matches from both ends and leaves the middle alone:

1
2
3
4
5
let s = "***heading***";
assert_eq!(s.trim_matches('*'), "heading");

// only the ends — interior matches stay put
assert_eq!("0x00ff00".trim_matches('0'), "x00ff");

One end at a time

There are directional versions when you only care about one side:

1
2
assert_eq!("--verbose".trim_start_matches("--"), "verbose");
assert_eq!("file.txt.bak".trim_end_matches(".bak"), "file.txt");

The pattern can be a closure or a set of chars

The argument is a Pattern, so you’re not limited to a single char. Pass a closure to trim by predicate, or an array of chars to trim any of them:

1
2
3
4
5
// strip leading digits
assert_eq!("12abc34".trim_start_matches(|c: char| c.is_numeric()), "abc34");

// trim any of several characters
assert_eq!("(value)".trim_matches(['(', ')']), "value");

Note trim_end_matches("--") strips the whole substring repeatedly, not a set of chars — that’s the difference between passing "--" and passing ['-'].

#226 Jun 2026

226. unwrap_or_default — Stop Spelling Out the Empty Value

Writing .unwrap_or(0) or .unwrap_or_else(String::new) to fall back to an empty value? If the type already has a Default, unwrap_or_default says it for you.

The fallback you keep typing out

You pull a value out of an Option, and the “missing” case is just the type’s natural zero: 0 for a number, "" for a string, [] for a vec. So you spell it out:

1
2
3
4
5
6
7
let count: Option<u32> = None;
let n = count.unwrap_or(0);
assert_eq!(n, 0);

let name: Option<String> = None;
let s = name.unwrap_or_else(String::new);
assert_eq!(s, "");

Every one of those fallbacks is just Default::default(). unwrap_or_default reaches for it directly — no literal to pick, no closure to write:

1
2
3
4
5
6
7
8
let count: Option<u32> = None;
assert_eq!(count.unwrap_or_default(), 0);

let name: Option<String> = None;
assert_eq!(name.unwrap_or_default(), "");

let items: Option<Vec<i32>> = None;
assert_eq!(items.unwrap_or_default(), Vec::<i32>::new());

When Some, you get the value untouched; when None, you get T::default().

Where it shines: map lookups

Counting with a HashMap is the classic case — a missing key should read as zero:

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

let mut counts: HashMap<&str, u32> = HashMap::new();
counts.insert("hits", 3);

let hits = counts.get("hits").copied().unwrap_or_default();
let misses = counts.get("misses").copied().unwrap_or_default();
assert_eq!(hits, 3);
assert_eq!(misses, 0);

No .unwrap_or(0) sprinkled at every call site, and if the value type changes, the default follows along automatically.

It works on Result too

Result::unwrap_or_default discards the Err and hands back the default — handy when a parse failure should just mean “nothing”:

1
2
3
4
let good = "42".parse::<i32>().unwrap_or_default();
let bad = "oops".parse::<i32>().unwrap_or_default();
assert_eq!(good, 42);
assert_eq!(bad, 0);

And on your own types

Derive Default and the same trick works for your structs — the fallback stays in one place instead of scattered across the codebase:

1
2
3
4
5
6
7
8
#[derive(Default, Debug, PartialEq)]
struct Config {
    retries: u32,
    verbose: bool,
}

let cfg: Option<Config> = None;
assert_eq!(cfg.unwrap_or_default(), Config { retries: 0, verbose: false });

Reach for unwrap_or_default whenever the fallback is the empty value — let the type decide what empty means.

#225 Jun 2026

225. Option::map_or — Transform-or-Default in One Call, Skip the match

Reaching for a four-line match just to turn an Option into a plain value? map_or does the transform and the fallback in a single call.

The match you keep rewriting

You have an Option, you want a concrete value: apply a function if it’s Some, fall back to a default if it’s None.

1
2
3
4
5
6
7
let name: Option<&str> = Some("ferris");

let len = match name {
    Some(n) => n.len(),
    None => 0,
};
assert_eq!(len, 6);

map_or(default, f) collapses both arms. The first argument is the None fallback, the second is what to do with the value inside Some:

1
2
3
4
5
let name: Option<&str> = Some("ferris");
assert_eq!(name.map_or(0, |n| n.len()), 6);

let missing: Option<&str> = None;
assert_eq!(missing.map_or(0, |n| n.len()), 0);

It beats the common .map(|n| n.len()).unwrap_or(0) too — same result, but no intermediate Option built just to immediately unwrap it.

The catch: the default is eager

map_or takes the default by value, so it’s computed whether or not you need it. With a cheap literal like 0 that’s free. With anything that allocates or does real work, you pay for it even on the Some path:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
fn expensive_default() -> String {
    // imagine a config read, an allocation, a computation...
    "fallback".to_string()
}

let port: Option<u16> = Some(8080);

// ⚠️ expensive_default() runs even though port is Some
let label = port.map_or(expensive_default(), |p| format!("port {p}"));
assert_eq!(label, "port 8080");

When the fallback isn’t free, switch to map_or_else, which takes a closure and only calls it on None:

1
2
3
4
5
6
7
fn expensive_default() -> String {
    "fallback".to_string()
}

let port: Option<u16> = None;
let label = port.map_or_else(|| expensive_default(), |p| format!("port {p}"));
assert_eq!(label, "fallback");

It works on Result too

Result::map_or follows the same shape — the value goes through f, any Err yields the default:

1
2
3
4
5
let parsed = "42".parse::<i32>();
assert_eq!(parsed.map_or(-1, |n| n * 2), 84);

let bad = "oops".parse::<i32>();
assert_eq!(bad.map_or(-1, |n| n * 2), -1);

Use map_or when the default is a cheap literal, map_or_else when it isn’t, and let the match go.

#224 Jun 2026

224. String::from_utf8_lossy — Returns a Cow, So Valid Bytes Cost Zero

from_utf8_lossy doesn’t always allocate. It hands back a Cow<str> that borrows your bytes when they’re already valid UTF-8 — you only pay for a String when there’s an invalid byte to replace.

The assumption that costs allocations

It’s easy to read this and assume every call builds a fresh String:

1
let text = String::from_utf8_lossy(bytes);

It doesn’t. The return type is Cow<'_, str> — clone-on-write. If bytes is valid UTF-8 (the common case for most files, headers, and protocol fields), you get back a Cow::Borrowed that points straight at your slice. No copy, no heap allocation.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
use std::borrow::Cow;

fn read_name(bytes: &[u8]) -> Cow<'_, str> {
    String::from_utf8_lossy(bytes)
}

let valid = b"hello world";
let s = read_name(valid);
assert!(matches!(s, Cow::Borrowed(_))); // borrowed — nothing allocated
assert_eq!(s, "hello world");

You only pay on the rare path

The allocation happens only when there’s an invalid byte to swap for the replacement character U+FFFD. Then — and only then — it builds an owned String:

1
2
3
4
let invalid = &[b'c', b'a', b'f', 0xFF];
let s2 = read_name(invalid);
assert!(matches!(s2, Cow::Owned(_))); // owned — had to fix a bad byte
assert_eq!(s2, "caf\u{fffd}");

So the cost scales with how messy your input is, not with how often you call it.

Don’t undo it with a reflexive .to_string()

The anti-pattern is forcing an allocation right back on:

1
let owned = String::from_utf8_lossy(bytes).to_string(); // ⚠️ always allocates

Keep the Cow for as long as you’re only reading. If a caller genuinely needs ownership, into_owned() allocates on the borrowed path but reuses the buffer on the owned path — no double allocation:

1
2
let owned: String = read_name(b"abc").into_owned();
assert_eq!(owned, "abc");

When you’re decoding bytes you’ll mostly just inspect, let from_utf8_lossy stay a Cow. Valid input — the usual case — flows through without touching the heap.

223. count_ones — Count Set Bits Without a Loop

Need to count how many bits are set in an integer — flags in a bitmask, a population count, a Hamming weight? Don’t write a shift-and-mask loop. Every integer type has .count_ones(), and it usually lowers to a single CPU instruction.

The hand-rolled version is a loop that masks the low bit and shifts:

1
2
3
4
5
6
7
8
fn count_set_bits(mut n: u32) -> u32 {
    let mut count = 0;
    while n != 0 {
        count += n & 1;
        n >>= 1;
    }
    count
}

It works, but it’s a loop you have to get right, and it’s slower than the hardware can do the same job.

Enter count_ones

1
2
let flags: u32 = 0b1011_0010;
assert_eq!(flags.count_ones(), 4);

One call. It’s available on every integer type (u8..u128, i8..i128), and on most targets it compiles straight to a popcnt instruction.

Where it earns its keep

Hamming distance — XOR two values, then count the bits that differ:

1
2
3
let a: u8 = 0b1100_1010;
let b: u8 = 0b1001_1011;
assert_eq!((a ^ b).count_ones(), 3);

Power-of-two test — a power of two has exactly one bit set:

1
2
3
4
5
6
fn is_power_of_two(n: u32) -> bool {
    n.count_ones() == 1
}

assert!(is_power_of_two(64));
assert!(!is_power_of_two(48));

The rest of the family

count_zeros, leading_zeros, trailing_zeros, leading_ones, and trailing_ones round it out — all single-instruction on modern CPUs. leading_zeros is the trick behind a fast integer log2; trailing_zeros gives you the index of the lowest set bit:

1
2
assert_eq!(0b0010_1000u8.trailing_zeros(), 3); // lowest set bit at index 3
assert_eq!(0b0000_1111u8.count_zeros(), 4);

Next time you reach for a bit-counting loop, reach for count_ones instead. Stable since Rust 1.0.

#222 Jun 2026

222. HashSet::intersection / union / difference — Set Math Without the Manual Loops

Need the items two collections have in common, or the ones only in one of them? Don’t write a for loop with .contains() inside. HashSet has set algebra built in.

The hand-rolled intersection is a loop, a temp Vec, and a membership check you have to get right:

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

let a: HashSet<i32> = [1, 2, 3, 4].into_iter().collect();
let b: HashSet<i32> = [3, 4, 5, 6].into_iter().collect();

let mut common = Vec::new();
for x in &a {
    if b.contains(x) {
        common.push(*x);
    }
}
common.sort();
assert_eq!(common, vec![3, 4]);

HashSet gives you the four set operations directly. Each returns a lazy iterator that borrows both sets, so nothing is allocated until you collect:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
use std::collections::HashSet;

let a: HashSet<i32> = [1, 2, 3, 4].into_iter().collect();
let b: HashSet<i32> = [3, 4, 5, 6].into_iter().collect();

// In both
let mut common: Vec<i32> = a.intersection(&b).copied().collect();
common.sort();
assert_eq!(common, vec![3, 4]);

// In either
let mut all: Vec<i32> = a.union(&b).copied().collect();
all.sort();
assert_eq!(all, vec![1, 2, 3, 4, 5, 6]);

// In a, not in b
let mut only_a: Vec<i32> = a.difference(&b).copied().collect();
only_a.sort();
assert_eq!(only_a, vec![1, 2]);

// In exactly one
let mut either: Vec<i32> = a.symmetric_difference(&b).copied().collect();
either.sort();
assert_eq!(either, vec![1, 2, 5, 6]);

The iterators yield &T, which is why .copied() shows up before collect — drop it if you’d rather collect references. (The .sort() calls are only there to make the asserts deterministic; set iteration order isn’t fixed.)

Just asking a yes/no question?

If you only need to know the relationship, not materialize it, three predicates answer in one call without building anything:

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

let a: HashSet<i32> = [1, 2, 3, 4].into_iter().collect();
let small: HashSet<i32> = [3, 4].into_iter().collect();
let far: HashSet<i32> = [9, 10].into_iter().collect();

assert!(small.is_subset(&a));
assert!(a.is_superset(&small));
assert!(a.is_disjoint(&far));

Whenever you catch yourself looping over one collection to test membership in another, reach for these instead — the intent reads straight off the method name.

#221 Jun 2026

221. from_str_radix — Parse Hex, Binary, or Octal Without Hand-Rolling a Loop

Got a "ff" or a "1010" and need the number behind it? Don’t loop over the characters multiplying by 16. Every integer type has from_str_radix, which parses a string in any base from 2 to 36 in one call.

The hand-rolled version is easy to get subtly wrong — overflow, bad digits, off-by-one on the place value:

1
2
3
4
5
6
7
8
fn parse_hex(s: &str) -> u32 {
    let mut n = 0u32;
    for c in s.chars() {
        n = n * 16 + c.to_digit(16).unwrap();
    }
    n
}
let _ = parse_hex("ff"); // works, but silently overflows on long input

from_str_radix does the whole thing, and returns a Result so bad input is an error instead of a panic or a wrong answer:

1
2
3
4
5
6
7
8
let n = u32::from_str_radix("ff", 16).unwrap();
assert_eq!(n, 255);

let b = u8::from_str_radix("1010", 2).unwrap();
assert_eq!(b, 10);

let o = u16::from_str_radix("755", 8).unwrap();
assert_eq!(o, 493);

It validates digits for you — a character outside the chosen base is a clean Err, not garbage:

1
2
assert!(u32::from_str_radix("xyz", 16).is_err());
assert!(u8::from_str_radix("2", 2).is_err()); // '2' isn't a binary digit

A real use: cracking a #RRGGBB color into channels. Slice, parse, done — no manual nibble math:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
fn rgb(hex: &str) -> Option<(u8, u8, u8)> {
    let h = hex.strip_prefix('#').unwrap_or(hex);
    if h.len() != 6 { return None; }
    let r = u8::from_str_radix(&h[0..2], 16).ok()?;
    let g = u8::from_str_radix(&h[2..4], 16).ok()?;
    let b = u8::from_str_radix(&h[4..6], 16).ok()?;
    Some((r, g, b))
}

assert_eq!(rgb("#E8593C"), Some((232, 89, 60)));
assert_eq!(rgb("oops"), None);

Signed types work too, and a leading - is honored:

1
assert_eq!(i32::from_str_radix("-2a", 16), Ok(-42));

For plain base-10 you don’t even need it — "42".parse::<u32>() is the same thing with the radix fixed at 10. Reach for from_str_radix the moment the base isn’t ten.

#220 Jun 2026

220. ilog10 — Count an Integer's Digits Without Formatting It to a String

Need to know how many digits a number has? Reaching for n.to_string().len() allocates a whole String just to measure it. ilog10 answers the same question with one instruction and zero allocation.

The classic way to count digits builds a string and throws it away:

1
2
3
let n: u32 = 4096;
let digits = n.to_string().len(); // heap-allocates a String to count 4 chars
assert_eq!(digits, 4);

ilog10 returns the floor of the base-10 logarithm, so the digit count is just that plus one — no allocation, no formatting:

1
2
3
let n: u32 = 4096;
let digits = n.ilog10() + 1;
assert_eq!(digits, 4);

The one catch: 0 has no logarithm, so 0u32.ilog10() panics. Guard the zero case, since “0” still has one digit:

1
2
3
4
5
6
7
fn digit_count(n: u32) -> u32 {
    if n == 0 { 1 } else { n.ilog10() + 1 }
}

assert_eq!(digit_count(0), 1);
assert_eq!(digit_count(7), 1);
assert_eq!(digit_count(1_000_000), 7);

Prefer no branch? checked_ilog10 returns None for zero instead of panicking, so you can fold the special case into one expression:

1
2
3
let count = |n: u32| n.checked_ilog10().map_or(1, |d| d + 1);
assert_eq!(count(0), 1);
assert_eq!(count(99), 2);

There’s also ilog2 when you want a power-of-two magnitude — the index of the highest set bit:

1
2
assert_eq!(255u32.ilog2(), 7); // 0b1111_1111, top bit is bit 7
assert_eq!(256u32.ilog2(), 8);

All three (ilog10, ilog2, checked_ilog10) work on every integer type. Skip the string round-trip — the math is right there.

#219 Jun 2026

219. checked_add_signed — Move an Unsigned Index by a Signed Delta, No Cast

You have a usize index and a delta: isize that might be negative. idx + delta won’t even compile, and casting your way around it wraps silently on underflow.

The naive fixes are both wrong in their own way:

1
2
3
4
5
let idx: usize = 3;
let delta: isize = -2;

// let next = idx + delta;          // error: cannot add `isize` to `usize`
let next = (idx as isize + delta) as usize; // compiles, wraps on underflow

That cast dance hides bugs: subtract past zero and you get a gigantic index instead of an error.

checked_add_signed adds a signed offset to an unsigned integer and hands back an OptionNone exactly when the result would underflow below zero or overflow the type:

1
2
3
4
5
let idx: usize = 3;

assert_eq!(idx.checked_add_signed(2),  Some(5));
assert_eq!(idx.checked_add_signed(-2), Some(1));
assert_eq!(idx.checked_add_signed(-4), None);   // would go below 0

So moving a cursor inside bounds becomes one honest expression — no as, no manual if delta < 0 branch:

1
2
3
4
5
6
7
fn move_cursor(pos: usize, delta: isize, len: usize) -> Option<usize> {
    pos.checked_add_signed(delta).filter(|&p| p < len)
}

assert_eq!(move_cursor(2, 1, 5), Some(3));
assert_eq!(move_cursor(0, -1, 5), None); // off the front
assert_eq!(move_cursor(4, 1, 5), None);  // off the back

It’s available on every unsigned type with its matching signed offset (u32 takes i32, usize takes isize, and so on). If you’d rather clamp than reject, the saturating_add_signed sibling pins the result to the type’s bounds instead of returning None. And as of Rust 1.90 the _sub_signed variants round out the set for subtracting a signed amount.

#218 Jun 2026

218. str::match_indices — Find Every Match and Its Position in One Pass

Hand-rolling a find loop to locate every occurrence of a substring means juggling a running offset and remembering to skip past each match. match_indices hands you each hit and its byte position as an iterator — no bookkeeping.

The classic way to collect every position of a needle is a loop over find, slicing the remainder each time and adding the offset back by hand:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
let log = "ERROR x ERROR y WARN z ERROR";

let mut positions = Vec::new();
let mut start = 0;
while let Some(i) = log[start..].find("ERROR") {
    let idx = start + i;
    positions.push(idx);
    start = idx + "ERROR".len(); // easy to get this advance wrong
}

assert_eq!(positions, vec![0, 8, 23]);

It works, but the offset arithmetic is exactly the kind of thing you fumble at 5pm. match_indices walks the string for you and yields (byte_index, matched_str) pairs:

1
2
3
4
5
let log = "ERROR x ERROR y WARN z ERROR";

let positions: Vec<(usize, &str)> = log.match_indices("ERROR").collect();

assert_eq!(positions, vec![(0, "ERROR"), (8, "ERROR"), (23, "ERROR")]);

It’s lazy and composes like any other iterator, so you can map down to just the indices, or count without allocating at all:

1
2
3
4
let log = "ERROR x ERROR y WARN z ERROR";

let count = log.matches("ERROR").count();
assert_eq!(count, 3);

The pattern argument is the full Pattern family — a &str, a char, a closure, or a slice of chars — so you can find runs of a class of characters without a regex:

1
2
3
4
let s = "a1b22c333";

let digits: Vec<(usize, &str)> = s.match_indices(char::is_numeric).collect();
assert_eq!(digits, vec![(1, "1"), (3, "2"), (4, "2"), (6, "3"), (7, "3"), (8, "3")]);

Matches are non-overlapping and scanned left to right; when you need the last hit first, rmatch_indices walks right to left:

1
2
3
4
let s = "aXbXc";

let last = s.rmatch_indices('X').next();
assert_eq!(last, Some((3, "X")));

The byte indices are real &str offsets, so they slot straight into slicing and replacement logic without any conversion. When you only care about whether something matches, reach for contains; when you want each location, match_indices already did the bookkeeping.