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 {
            write!(f, "{} + {}i", self.real, self.imag)
        } else {
            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);
}

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;

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: InvalidDigit }) (or Overflow depending on Rust version)
}