5.5Formatting strings
This course has been writing IOUs against this lesson since chapter 2, and it's time to pay all of them at once. Lesson 2.4 name-dropped format! as "println!'s sibling that produces the text instead of printing it." Lesson 4.5 pulled {:.17} out of a hat to expose float precision. Lessons 4.4 and 4.11 both promised that {:?} would be explained here. And the chapter 4 quiz left a ball printing its height as 21.599999999999994 meters at a tower whose grammar said "At 1 seconds." Every one of those debts comes due below.
format!
format! works exactly like println!: same placeholders, same argument rules, same inline {name} captures you've used since lesson 1.6. The only difference is the destination. println! sends the finished text to the terminal; format! hands it back to you as a brand-new String:
fn main() {
let name = "Ada";
let score = 97;
let line = format!("{name} scored {score} points");
println!("{line}");
println!("That line is {} bytes long.", line.len());
}Ada scored 97 points
That line is 20 bytes long.
That's the whole macro. Its value is what it unlocks: text you can store, measure, grow with push_str, or pass to a function, instead of text that's already gone to the screen. It's also the promised better concatenator from lesson 5.3: format!("{first} {last}") reads better than a push_str chain and consumes nothing.
(One bit of housekeeping while we're here: to print a literal brace, double it. println!("{{}}") prints {}.)
The mini-language inside the braces
Everything else in this lesson applies to println! and format! equally, because the braces speak a shared formatting mini-language. The part after the : is a spec, and you already know its shape from {:.17}. Here's the tour, one knob at a time.
Width pads a value to a minimum number of characters: {:6} means "at least 6 wide." Numbers pad on the left, text pads on the right, and you can override either with an alignment: < left, > right, ^ center:
fn main() {
println!("[{:>6}]", 42);
println!("[{:<6}]", 42);
println!("[{:^6}]", 42);
println!("[{:>6}]", "hi");
}[ 42]
[42 ]
[ 42 ]
[ hi]
The brackets are just to make the padding visible; width is invisible by design.
Precision, for floats, is the .N you met in 4.5: {:.2} rounds to two decimal places (and pads with zeros if needed). It's the civilized-printing tool the falling ball was promised:
fn main() {
let height: f64 = 100.0 - 9.8 * 4.0 * 4.0 / 2.0;
println!("At 4 seconds, the ball is at height: {height} meters");
println!("At 4 seconds, the ball is at height: {height:.1} meters");
}At 4 seconds, the ball is at height: 21.599999999999994 meters
At 4 seconds, the ball is at height: 21.6 meters
Note {height:.1}: specs ride along happily inside inline captures. And note what precision is not: the float still holds the long ugly value, exactly as lesson 4.5 taught. {:.1} rounds the displayed text, not the number. Cosmetics, not surgery.
The knobs combine, in fixed order (fill and alignment, then sign, then width, then precision). A few useful combinations:
fn main() {
println!("[{:>8.2}]", 3.14159);
println!("[{:08.2}]", 3.14159);
println!("[{:+}]", 42);
println!("[{:->8}]", "hi");
}[ 3.14]
[00003.14]
[+42]
[------hi]
{:>8.2} right-aligns to 8 wide after rounding to 2 places. {:08} pads with zeros instead of spaces (handy for things like 007). {:+} prints the sign even when positive. And {:->8} shows the general fill form: any character before the alignment arrow becomes the padding, dashes included.
Width plus alignment is how you print tables that line up, which is the kind of output real programs are full of:
fn main() {
println!("{:<10} {:>6}", "item", "price");
println!("{:<10} {:>6.2}", "tea", 2.5);
println!("{:<10} {:>6.2}", "biscuits", 12.0);
println!("{:<10} {:>6.2}", "jam", 3.75);
}item price
tea 2.50
biscuits 12.00
jam 3.75
Ten wide for names on the left, six wide for prices on the right, decimals at attention like soldiers.
{:?}, the programmer's view
The last debt is {:?}. The plain {} placeholder asks a value to display itself for end users, and some types decline the job. Tuples, famously, since lesson 4.11: there's no one obviously right way to show (3, 7) to a human, so {} refuses. The alternative spelling {:?} asks for the debug view instead: a representation for programmers, where the only goal is showing the value's structure, warts and all:
fn main() {
let point = (3, 7);
let name = "Ada";
println!("{:?}", point);
println!("{:?}", name);
}(3, 7)
"Ada"
The tuple finally prints. And look closely at the second line: in debug view, text shows its quotes. That's the spirit of {:?} in one detail; a user never cares where text begins and ends, but a programmer chasing a stray-whitespace bug absolutely does ({:?} on "42\n" shows the \n that {} would helpfully render as a line break, which makes it the perfect tool for inspecting exactly the input problems lesson 5.6 is about). This is also why dbg! from lesson 3.4 always shows the debug view: it's a tool for you, not your users.
Why do some types support {} and others only {:?}? A type opts into each view by implementing the matching capability, and the system behind that word is traits, chapter 16's headline. For now the working rule: {} for showing values to people, {:?} when you're debugging, and if {} ever refuses with "doesn't implement std::fmt::Display", switch to {:?} and carry on. (There's also a prettier multi-line form, {:#?}. On a small tuple it's underwhelming; when chapter 10's structs arrive with a dozen fields, it earns its keep.)
Quiz time
Question #1
Predict all five lines:
fn main() {
println!("[{:>5}]", 7);
println!("[{:<5}]", "ok");
println!("[{:^7}]", "mid");
println!("[{:06.2}]", 9.876);
println!("[{:?}]", ("hi", 2));
}Show solution
[ 7]
[ok ]
[ mid ]
[009.88]
[("hi", 2)]
Line by line: right-aligned in 5; left-aligned in 5; centered in 7 (two spaces each side); rounded to 9.88 then zero-padded to 6 wide; and the tuple in debug view, its text element wearing debug quotes.
Question #2
Using one format!, build the string Total: $ 42.50 (two spaces before the number; the amount right-aligned to 7 wide with 2 decimals) from let total = 42.5;, store it in a variable, then print it.
Show solution
fn main() {
let total = 42.5;
let line = format!("Total: ${total:>7.2}");
println!("{line}");
}Total: $ 42.50
>7 reserves seven characters and pushes the value right; .2 makes 42.5 into 42.50 (five characters), leaving two spaces of padding. ${...} is fine as-is: only braces need escaping, dollar signs are just text.
Question #3
The chapter 4 ball-drop printed At 1 seconds, the ball is at height: 95.1 meters, and the grammar pedants were promised satisfaction. Write a function report_time(seconds: f64, height: f64) that prints the same report but says second when the time is exactly 1 and seconds otherwise, with the height shown to one decimal place. (Lesson 4.7 provides the decision tool; lesson 4.5's == warning doesn't apply here, since the quiz values arrive as exact 1.0-style constants, not as arithmetic results.)
Show solution
fn report_time(seconds: f64, height: f64) {
let unit = if seconds == 1.0 { "second" } else { "seconds" };
println!("At {seconds} {unit}, the ball is at height: {height:.1} meters");
}
fn main() {
report_time(1.0, 95.1);
report_time(4.0, 21.599999999999994);
}At 1 second, the ball is at height: 95.1 meters
At 4 seconds, the ball is at height: 21.6 meters
An if expression picks the unit (an arm-typed &str, both arms agreeing, per lesson 4.7), and {height:.1} retires the digit eruption. The pedants may stand down.
You can now turn any value into exactly the text you want. The one remaining gap in your text toolkit runs the other way: turning text the user typed into values you can compute with. That's the incantation you've been copying since lesson 1.12, and next lesson it finally gets taken apart.