Idioms

#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.