16.6 String Conversions

Converting data to and from string representations is ubiquitous in programming, essential for I/O, serialization, configuration, and user interfaces. Rust provides standard traits for these operations.

16.6.1 Converting To Strings: Display and ToString

The std::fmt::Display trait is the standard way to define a user-friendly string representation for a type. Implementing Display allows a type to be formatted using macros like println! and format!.

Crucially, any type implementing Display automatically gets an implementation of the ToString trait, which provides a to_string(&self) -> String method.

use std::fmt;

struct Complex {
    real: f64,
    imag: f64,
}

// Implement user-facing display format
impl fmt::Display for Complex {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        // Handle sign of imaginary part for nice formatting
        if self.imag >= 0.0 {
            // Use write! macro to write formatted output to the formatter 'f'
            write!(f, "{} + {}i", self.real, self.imag)
        } else {
            // Note: We use -self.imag to display a positive number after the '-' sign
            write!(f, "{} - {}i", self.real, -self.imag)
        }
    }
}

fn main() {
    let c1 = Complex { real: 3.5, imag: -2.1 };
    let c2 = Complex { real: -1.0, imag: 4.0 };

    println!("c1: {}", c1); // Uses Display implicitly
    println!("c2: {}", c2);

    let s1: String = c1.to_string(); // Uses ToString (provided by Display impl)
    let s2 = format!("Complex numbers are {} and {}", c1, c2);
    // format! also uses Display

    println!("String representation of c1: {}", s1);
    println!("{}", s2);
}

Explanation of fmt details:

  • f: &mut fmt::Formatter<'_>: This parameter is a mutable reference to a Formatter. This Formatter is essentially a destination provided by the calling context (like println!, format!, etc.) where the formatted string should be written. It acts as a kind of buffer or writer abstraction. The <'_> indicates an elided lifetime, meaning the Formatter borrows something (like the underlying buffer) for a lifetime determined by the compiler, typically tied to the scope of the formatting operation.
  • fmt::Result: This is the return type of the fmt function. It’s a type alias for Result<(), std::fmt::Error>. If formatting succeeds, the function returns Ok(()). If an error occurs during formatting (e.g., an I/O error if writing to a file), it returns Err(fmt::Error).
  • write! macro: This macro is fundamental to Display implementations. It works similarly to format! or println!, but instead of creating a String or printing to the console, it writes the formatted output directly into the provided Formatter (f in this case). It returns a fmt::Result which is typically propagated using ? or returned directly.

16.6.2 Parsing From Strings: FromStr and parse

The std::str::FromStr trait defines how to parse a string slice (&str) into an instance of a type. Many standard library types, including all primitive numeric types, implement FromStr.

The parse() method available on &str delegates to the FromStr::from_str implementation for the requested target type. Since parsing can fail (e.g., invalid format, non-numeric characters), from_str (and therefore parse()) returns a Result.

use std::num::ParseIntError; // Specific error type for integer parsing

fn main() {
    let s_valid_int = "1024";
    let s_valid_float = "3.14159";
    let s_invalid = "not a number";

    // parse() requires the target type T to be specified or inferred
    // T must implement FromStr
    match s_valid_int.parse::<i32>() {
        Ok(n) => println!("Parsed '{}' as i32: {}", s_valid_int, n),
        Err(e) => println!("Failed to parse '{}': {}", s_valid_int, e),
        // e is ParseIntError
    }

    match s_valid_float.parse::<f64>() {
        Ok(f) => println!("Parsed '{}' as f64: {}", s_valid_float, f),
        Err(e) => println!("Failed to parse '{}': {}", s_valid_float, e),
        // e is ParseFloatError
    }

    match s_invalid.parse::<i32>() {
        Ok(n) => println!("Parsed '{}' as i32: {}", s_invalid, n), // Won't happen
        Err(e) => println!("Failed to parse '{}': {}", s_invalid, e),
        // Failure: invalid digit
    }

    // Using unwrap/expect for concise error handling if failure indicates a bug
    let num: u64 = "1234567890".parse().expect("Valid u64 string expected");
    println!("Parsed u64: {}", num);
}

16.6.3 Implementing FromStr for Custom Types

Implement FromStr for your own types to define their canonical parsing logic from strings.

use std::str::FromStr;
use std::num::ParseIntError;

#[derive(Debug, PartialEq)]
struct RgbColor {
    r: u8,
    g: u8,
    b: u8,
}

// Define a custom error type for parsing failures
#[derive(Debug, PartialEq)]
enum ParseColorError {
    IncorrectFormat(String), // E.g., wrong number of parts
    InvalidComponent(ParseIntError), // Wrap the underlying integer parse error
}

// Implement FromStr to parse "r,g,b" format (e.g., "255, 100, 0")
impl FromStr for RgbColor {
    type Err = ParseColorError; // Associate our custom error type

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        let parts: Vec<&str> = s.trim().split(',').collect();
        if parts.len() != 3 {
            return Err(ParseColorError::IncorrectFormat(format!(
                "Expected 3 comma-separated values, found {}", parts.len()
            )));
        }

        // Helper closure to parse each part and map the error
        let parse_component = |comp_str: &str| {
            comp_str.trim()
                    .parse::<u8>()
                    .map_err(ParseColorError::InvalidComponent)
                    // Convert ParseIntError to our error type
        };

        let r = parse_component(parts[0])?; // Use ? for early return on error
        let g = parse_component(parts[1])?;
        let b = parse_component(parts[2])?;

        Ok(RgbColor { r, g, b })
    }
}

fn main() {
    let input_ok = " 255, 128 , 0 ";
    match input_ok.parse::<RgbColor>() {
        Ok(color) => println!("Parsed '{}': {:?}", input_ok, color),
        Err(e) => println!("Error parsing '{}': {:?}", input_ok, e),
    } // Output: Parsed ' 255, 128 , 0 ': RgbColor { r: 255, g: 128, b: 0 }

    let input_bad_format = "10, 20";
    match input_bad_format.parse::<RgbColor>() {
        Ok(color) => println!("Parsed '{}': {:?}", input_bad_format, color),
        Err(e) => println!("Error parsing '{}': {:?}", input_bad_format, e),
    } // Output: Error parsing '10, 20':
      // IncorrectFormat("Expected 3 comma-separated values, found 2")

    let input_bad_value = "10, 300, 20"; // 300 is out of range for u8
    match input_bad_value.parse::<RgbColor>() {
        Ok(color) => println!("Parsed '{}': {:?}", input_bad_value, color),
        Err(e) => println!("Error parsing '{}': {:?}", input_bad_value, e),
    } // Output: Error parsing '10, 300, 20': InvalidComponent(ParseIntError
      // { kind: NumberOutOfRange }) (Specific error may vary slightly)
}