Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

3.2 · Formatting Mastery: fmt, write!, format_args!

Domain 3 — Strings and Text Processing Duration: ~15 minutes Library components: std::fmt, std::fmt::NumBuffer, all formatting traits, std::fmt::Formatter, std::fmt::Arguments, write!, writeln!, format!, format_args!

Introduction

The Rust formatting system is more than println!. The std::fmt module defines a family of traits, and a format specifier selects each one:

  • Display and Debug
  • Binary, Octal, LowerHex, and UpperHex
  • LowerExp and UpperExp
  • Pointer

The Formatter struct gives access to all the options that the caller supplied (width, alignment, precision, fill, sign). When you know these traits and options, you can write custom types that behave exactly like built-in types.

This tutorial explains:

  • How to implement Display and Debug for custom types.
  • How to use the Formatter options: width, alignment, fill, precision, sign, zero-padding.
  • The numeric format traits and the pointer format trait.
  • format_args! for formatting without allocation.
  • {integer}::format_into and std::fmt::NumBuffer for decimal integers without allocation.
  • write! and writeln! for fmt::Write targets and for io::Write targets.
  • The debug builder helpers (DebugStruct, DebugTuple, DebugList, DebugMap, DebugSet).

Display and Debug

Display is for output to the end user. Debug is for developers. A Debug implementation should always give a representation that is similar to syntactically valid Rust.

use std::fmt;

struct Color { r: u8, g: u8, b: u8 }

// Display: the text for the end user, a CSS-style hex color such as #FF0000.
impl fmt::Display for Color {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        // {:02X} writes one u8 as 2 uppercase hex digits, with a leading zero if necessary.
        write!(f, "#{:02X}{:02X}{:02X}", self.r, self.g, self.b)
    }
}

// Debug: the text for developers, with the type name and the name of each field.
impl fmt::Debug for Color {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.debug_struct("Color")     // starts the output with the type name
            .field("r", &self.r)    // adds `r: <Debug text of self.r>`
            .field("g", &self.g)
            .field("b", &self.b)
            .finish()               // completes the output and returns fmt::Result
    }
}

{} calls Display. {:?} calls Debug. {:#?} enables pretty-printing, and the debug builder helpers do the pretty-printing automatically.

An implementation of Display also gives the type a to_string() method at no cost. The blanket impl<T: fmt::Display> ToString for T supplies the method.

03_05_display_debug_traits.rs also formats a Point type and a Meters type. It prints:

Display: #FF0000
Display: #008080

Debug: Color { r: 255, g: 0, b: 0 }
Debug pretty: Color {
    r: 0,
    g: 128,
    b: 128,
}

Point Display: (3, -1.5)
Point Debug:   Point { x: 3.0, y: -1.5 }

Meters Display: 12.35m
Meters Debug:   Meters(12.345678)

formatted: The color is #FF0000 and the point is (3, -1.5)

All assertions passed.

Formatter Options

The format string {:fill<align><sign><#><0><width>.<precision>type} maps to fields on the Formatter struct.

Figure: Format Specifier Syntax

SpecifierMeaningExample
< > ^Left, right, or center alignment{:<10}
fillFill char, written before the alignment character{:*>10}
+Always print the sign{:+}
0Zero-padding{:08}
widthMinimum field width{:10}
.precDecimal precision, or string truncation{:.2} / {:.4}
width$Width from an argument{:>width$}
// Alignment in a field that is 10 characters wide
format!("{:<10}", "left")   // "left      "
format!("{:>10}", "right")  // "     right"
format!("{:^10}", "center") // "  center  "

// Fill character: it goes before the alignment character
format!("{:*<10}", "fill")  // "fill******"
format!("{:0>6}", 42)       // "000042"

// Precision
format!("{:.2}", 3.14159)   // "3.14": 2 digits after the decimal point
format!("{:.4}", "hello")   // "hell": for a string, the precision is the maximum length

// Sign
format!("{:+}", 42_i32)     // "+42": a positive number also gets its sign

// Combination: fill `*`, right alignment, sign, width 10
format!("{:*>+10}", 42_i32) // "*******+42"

// Width at run time: `width$` reads the width from the argument with the name `width`
let w = 12_usize;
format!("{:>width$}", "dynamic", width = w) // "     dynamic"

To obey the options of the caller in a custom Display, call f.pad(&s) with your formatted string. pad applies the alignment and the fill automatically.

03_06_formatter_options.rs prints each formatted value on a line of its own. The last three lines come from a Padded(i64) type that calls f.pad:

left         # (6 spaces follow the text)
     right
  center     # (2 spaces follow the text)
fill******
000042
----hi-----

pi = 3.14
pi = 3.14159
pi = 3
truncated: truncate
truncated: trun

{:+} of 42 = +42
{:+} of -42 = -42
00000042
00002.50

*******+42
     dynamic

Padded left:   [42      ]
Padded right:  [      42]
Padded center: [   42   ]

All assertions passed.

Numeric and Pointer Format Traits

The fmt module defines more traits than Display and Debug. A type character in the format string selects each one:

TraitSpecifierExample
Binary{:b}"10101100"
Octal{:o}"755"
LowerHex{:x}"deadbeef"
UpperHex{:X}"DEADBEEF"
LowerExp{:e}"1.235e6"
UpperExp{:E}"1.235E6"
Pointer{:p}"0x7ff..."

The # flag adds the usual prefix (0b, 0o, 0x).

let n: u32 = 0b1010_1100;             // 172 in decimal
format!("{:b}", n)                    // "10101100"
format!("{:#b}", n)                   // "0b10101100": `#` adds the 0b prefix
format!("{:#X}", 0xDEAD_BEEFu32)      // "0xDEADBEEF": the prefix stays lowercase
format!("{:.3e}", 1_234_567.89_f64)   // "1.235e6": 3 digits after the decimal point

let x = 42u32;
format!("{:p}", &x)                   // the address of x, for example "0x16ee01754" (varies)

You can implement each of these traits on your own types. A common pattern is to implement LowerHex and UpperHex on a newtype for a byte buffer. Then the type formats as a hex string.

03_07_numeric_format_traits.rs prints the lines below. The Rgb lines come from a newtype with custom LowerHex and UpperHex implementations:

Binary:    10101100
Binary 0b: 0b10101100
Binary 8w: 10101100

Octal:    755
Octal 0o: 0o755

LowerHex:    deadbeef
UpperHex:    DEADBEEF
LowerHex 0x: 0xdeadbeef
UpperHex 0X: 0xDEADBEEF
LowerHex 10: 00deadbeef

Rgb lower: ff7f00
Rgb upper: FF7F00

LowerExp: 1.23456789e6
UpperExp: 1.23456789E6
Prec 3:   1.235e6

Pointer: 0x16ee01754   # (varies)

All assertions passed.

format_args! and write!

format! always allocates a String. format_args! captures the format string and its arguments as a fmt::Arguments<'_> value and does not allocate. You can pass the value to any type that implements fmt::Write or io::Write.

// The two traits have the same name. The aliases let you import them together.
use std::fmt::Write as FmtWrite;
use std::io::Write as IoWrite;

// Write to a String (String implements fmt::Write)
let mut buf = String::new();
write!(buf, "Hello, world!").unwrap();         // buf is "Hello, world!"
writeln!(buf, " Next line.").unwrap();         // appends the text and a '\n'

// Write to a Vec<u8> (Vec<u8> implements io::Write)
let mut bytes: Vec<u8> = Vec::new();
write!(bytes, "bytes: {}", 255_u8).unwrap();   // bytes is b"bytes: 255"

// A logger with no intermediate String: `args` goes directly to the sink
fn log_to(sink: &mut dyn FmtWrite, args: std::fmt::Arguments<'_>) {
    sink.write_fmt(args).unwrap();
}
let mut output = String::new();
log_to(&mut output, format_args!("event={} level={}", "login", "INFO"));
// output is "event=login level=INFO"

fmt::Write and io::Write are different traits. write! works with the two traits. The macro calls write_fmt from the trait that is in scope. To prevent ambiguity, import only the trait that you need, or import the two traits with aliases.

03_08_format_args.rs prints:

value = 42
format_args passthrough: x = 1, y = 2
write! to String: "Hello, world! Next line.\n"
report:
  item 0: value=0
  item 1: value=10
  item 2: value=20

write! to Vec<u8>: [98, 121, 116, 101, 115, 58, 32, 50, 53, 53]
log_to output: "event=login level=INFO"

Named args: Alice scored 99 points
Positional: yes and no and yes

All assertions passed.

format_into (1.98) is the fast path for integers only. It writes the decimal text into a NumBuffer on the stack and returns a &str that borrows from the buffer. It uses no heap and no dyn Write. You can use the same buffer again: each call overwrites the previous text.

Rust 1.98 supplied the buffer type only as core::fmt::NumBuffer. Since Rust 1.99, std::fmt and alloc::fmt re-export it, so the usual std::fmt import path is valid.

use std::fmt::NumBuffer; // 1.99 re-export. On 1.98 the path is core::fmt::NumBuffer.

// The buffer is a stack value. Its size comes from the integer type (u32 here).
let mut buf = NumBuffer::new();   // the compiler infers NumBuffer<u32> from the next line
assert_eq!(1972u32.format_into(&mut buf), "1972");   // the &str borrows from `buf`
// A negative number includes its sign. This call uses a temporary NumBuffer<i32>.
assert_eq!((-1972i32).format_into(&mut NumBuffer::new()), "-1972");

Debug Builder Helpers

Manual formatting of {:?} output is tedious, and the code must also handle the {:#?} pretty-print flag. The builder helpers do this work automatically:

HelperCreated byUse for
DebugStructf.debug_struct("Name")Struct with named fields
DebugTuplef.debug_tuple("Name")Tuple struct
DebugListf.debug_list()Sequence of values
DebugSetf.debug_set()Unordered set of values
DebugMapf.debug_map()Key–value pairs
// QueryNode has the fields table: &'static str, limit: usize, columns: Vec<&'static str>.
impl fmt::Debug for QueryNode {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.debug_struct("QueryNode")            // the type name in the output
            .field("table", &self.table)       // field name, then a value that implements Debug
            .field("limit", &self.limit)
            .field("columns", &self.columns)   // the Vec uses its own Debug implementation
            .finish()                          // completes the output and returns fmt::Result
    }
}

The same Debug implementation gives compact output with {:?} and indented output with {:#?}.

03_09_debug_builders.rs has one type for each helper. It prints:

DebugStruct compact:
QueryNode { table: "users", limit: 10, columns: ["id", "name", "email"] }

DebugStruct pretty:
QueryNode {
    table: "users",
    limit: 10,
    columns: [
        "id",
        "name",
        "email",
    ],
}

DebugTuple: Pair(3, 7)
DebugList: ["parse", "validate", "execute"]
DebugSet: {"async", "rust", "web"}
DebugMap: {"host": "localhost", "port": "8080"}

All assertions passed.

Summary

ToolPurpose
Display ({})String representation for users
Debug ({:?})Representation for developers, which you can derive
Formatter optionsWidth, alignment, fill, precision, sign, zero-padding
Binary/Octal/Hex/ExpNumeric format traits for {:b}, {:o}, {:x}, {:e}
Pointer ({:p})Raw memory address
format_args!Argument capture without allocation
format_into + NumBuffer (1.98, std::fmt path since 1.99)Decimal integers into a stack buffer. The &str borrows from the buffer.
write! / writeln!Format into any fmt::Write or io::Write target
Debug buildersAutomatic support for the {:#?} pretty-print flag

Code Examples

FileDescription
03_05_display_debug_traits.rsCustom Display and Debug, to_string(), format!
03_06_formatter_options.rsWidth, alignment, fill, precision, sign, zero-padding, run-time width
03_07_numeric_format_traits.rsBinary, Octal, LowerHex, UpperHex, LowerExp, UpperExp, Pointer
03_08_format_args.rsformat_args!, write!, writeln!, a logger with no intermediate String
03_09_debug_builders.rsDebugStruct, DebugTuple, DebugList, DebugSet, DebugMap
03_22_format_into.rs{integer}::format_into and std::fmt::NumBuffer (1.98, std::fmt path since 1.99)