FromStr Trait & .parse()
FromStr Trait & .parse()
Level 4 — Error Handling & Generics The trait behind
str::parse::<T>(); the standard way to turn text into a typed value.
1. Prerequisites
- String vs &str — The text you are converting from.
Result<T, E>— Parsing is fallible, so it always returns aResult.From/IntoTraits —FromStris the text-specific sibling of this conversion-trait family.
2. Term Category
Standard Library Trait (the text-to-type gateway): FromStr is the trait that powers .parse(). Any type that implements it can be produced from a string slice, and the compiler figures out which implementation to use based on how you annotate or turbofish the call. It's the standard, idiomatic answer to "how do I turn user input into a number?"
3. Explanation
(1) Design Motivation — "Why did we design this?"
Reading a number from a CLI argument, a config file, or stdin means starting with a &str. But &str and i32 are utterly different in memory — there's no as cast that can bridge them (unlike, say, i64 as i32), because turning "42" into 42 requires actual parsing logic, and turning "abc" into a number should fail, not produce garbage. FromStr formalizes this: it's a trait with one method, from_str(s: &str) -> Result<Self, Self::Err>, that every parseable type implements. .parse::<T>() on &str is just a convenience method that calls T::from_str() for you.
(2) Reality Metaphor
Imagine a customs officer at a border crossing who only accepts typed, verified forms — never raw, unverified paperwork.
- The raw string (
"42") is a handwritten note someone hands the officer. .parse::<i32>()is the officer's specialized "Numbers Department" stamp: they carefully verify the note really is a valid number, and issue you an officiali32passport (Ok(42)).- If the note says
"forty-two", the officer can't process it. They don't guess or crash the whole checkpoint — they hand you back a rejection slip explaining exactly what went wrong (Err(ParseIntError)), and you decide what to do next.
(3) Rust Code Examples
Short Snippet (The Basic Parse)
fn main() {
let input = "42";
// Turbofish tells .parse() WHICH type to build.
let number = input.parse::<i32>().unwrap();
println!("{}", number + 8); // 50
// A bad input returns Err instead of panicking or garbage data.
let bad_input = "not a number";
let result: Result<i32, _> = bad_input.parse();
println!("{:?}", result); // Err(ParseIntError { kind: InvalidDigit })
}
Fuller Example (Implementing FromStr for Your Own Type)
use std::str::FromStr;
#[derive(Debug)]
struct Point { x: i32, y: i32 }
impl FromStr for Point {
type Err = String; // The error type returned on failure.
fn from_str(s: &str) -> Result<Self, Self::Err> {
// Expects input like "3,4"
let (x_str, y_str) = s.split_once(',').ok_or("missing comma")?;
let x = x_str.trim().parse::<i32>().map_err(|e| e.to_string())?;
let y = y_str.trim().parse::<i32>().map_err(|e| e.to_string())?;
Ok(Point { x, y })
}
}
fn main() {
// Because we implemented FromStr, ".parse::<Point>()" now works for free!
let p: Point = "3, 4".parse().unwrap();
println!("{:?}", p); // Point { x: 3, y: 4 }
let bad: Result<Point, String> = "not a point".parse();
println!("{:?}", bad); // Err("missing comma")
}
4. Common Mistakes & Pitfalls
Mistake 1: Misunderstanding Fromstr Parse Scoping and Lifecycle Rules
The mistake: Assuming Fromstr Parse instances remain valid beyond their declaring scope block or across asynchronous boundaries without explicit lifetime tracking.
Why it's wrong: Rust strictly enforces lexical scope boundaries and non-lexical lifetimes (NLL) at compile time. Accessing dropped values or failing to handle variable drop order results in compiler errors such as E0597 or E0382.
Incorrect:
fn get_ref() -> &str {
let s = String::from("fromstr_parse_data");
&s // ❌ Error E0106/E0515: returns a reference to data owned by the current function
}
Fix:
fn get_string() -> String {
let s = String::from("fromstr_parse_data");
s // Ownership of the String is transferred directly to the caller
}
Mistake 2: Mutating Fromstr Parse State Without Exclusive Ownership or mut Borrowing
The mistake: Attempting to mutate data associated with Fromstr Parse through an immutable reference &T or without specifying mut in variable declarations.
Why it's wrong: Rust's aliasing XOR mutability rule (&T for shared immutable access, &mut T for exclusive mutable access) prohibits mutating state through shared references unless interior mutability patterns (e.g. RefCell, Mutex) are explicitly used.
Incorrect:
fn update_val(data: &i32) {
// *data += 1; // ❌ Error E0594: cannot assign to `*data`, which is behind a `&` reference
}
Fix:
fn update_val(data: &mut i32) {
*data += 1; // Correct: exclusive mutable reference permits mutation
}
Mistake 3: Concurrent Access to Fromstr Parse Across Threads Without Send / Sync Guards
The mistake: Sharing non-thread-safe Fromstr Parse instances across OS threads via std::thread::spawn.
Why it's wrong: Types that do not implement Send or Sync marker traits cannot safely cross thread boundaries. The compiler prevents data races by raising compile errors E0277 (trait Send is not implemented).
Incorrect:
use std::rc::Rc;
use std::thread;
let rc = Rc::new(42);
// thread::spawn(move || { println!("{}", rc); }); // ❌ Error E0277: `Rc` cannot be sent between threads safely
Fix:
use std::sync::Arc;
use std::thread;
let arc = Arc::new(42);
thread::spawn(move || {
println!("{}", arc); // Correct: `Arc` implements `Send` and `Sync`
});
5. Practice Exercises
Exercise 1: Multi-Protocol Network Endpoint URI Parser with Custom Error Hierarchies
Scenario: Problem Statement:
In distributed microservices, network configuration parameters are frequently ingested from environment variables or remote config servers as raw strings (e.g., "https://api.internal.v1:8443", "grpc://10.0.0.1:50051", or "ws://localhost:8080").
Design and implement a zero-allocation-focused custom string parsing pipeline by implementing FromStr for a domain struct NetworkEndpoint.
Requirements: Requirements:
- Define an enum
Protocolwith variants:Http,Https,Grpc,Ws,Wss. ImplementFromStrforProtocol(case-insensitive). - Define a struct
NetworkEndpointwith fields:protocol: Protocol,host: String,port: u16. - Define a custom error enum
EndpointParseErrorwith variants:MissingProtocolUnsupportedProtocol(String)InvalidHost(String)MissingPortInvalidPort(std::num::ParseIntError)MalformedUri(String)Implementstd::fmt::Displayandstd::error::ErrorforEndpointParseError.
- Implement
FromStrforNetworkEndpointwithtype Err = EndpointParseError. Parse URI format"<protocol>://<host>:<port>".- Validate that scheme separator
://is present. - Validate host (must not be empty, must not contain spaces or slashes).
- If port is missing or invalid, return the corresponding
EndpointParseErrorvariant.
- Validate that scheme separator
- Provide a helper method
NetworkEndpoint::is_secure(&self) -> boolreturningtrueforHttpsandWss. - Include a complete test module
#[cfg(test)] mod testswith explicit assertions (assert_eq!,assert!,assert_ne!,matches!).
Answer
Implementation
use std::fmt;
use std::str::FromStr;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Protocol {
Http,
Https,
Grpc,
Ws,
Wss,
}
#[derive(Debug, PartialEq, Eq)]
pub enum EndpointParseError {
MissingProtocol,
UnsupportedProtocol(String),
InvalidHost(String),
MissingPort,
InvalidPort(std::num::ParseIntError),
MalformedUri(String),
}
impl fmt::Display for EndpointParseError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
EndpointParseError::MissingProtocol => write!(f, "URI scheme/protocol separator '://' is missing"),
EndpointParseError::UnsupportedProtocol(p) => write!(f, "Unsupported network protocol: '{}'", p),
EndpointParseError::InvalidHost(h) => write!(f, "Invalid hostname/IP specified: '{}'", h),
EndpointParseError::MissingPort => write!(f, "Port number was not specified in endpoint URI"),
EndpointParseError::InvalidPort(err) => write!(f, "Failed to parse port number: {}", err),
EndpointParseError::MalformedUri(uri) => write!(f, "Malformed URI format: '{}'", uri),
}
}
}
impl std::error::Error for EndpointParseError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
EndpointParseError::InvalidPort(err) => Some(err),
_ => None,
}
}
}
impl FromStr for Protocol {
type Err = EndpointParseError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.to_ascii_lowercase().as_str() {
"http" => Ok(Protocol::Http),
"https" => Ok(Protocol::Https),
"grpc" => Ok(Protocol::Grpc),
"ws" => Ok(Protocol::Ws),
"wss" => Ok(Protocol::Wss),
other => Err(EndpointParseError::UnsupportedProtocol(other.to_string())),
}
}
}
#[derive(Debug, PartialEq, Eq)]
pub struct NetworkEndpoint {
pub protocol: Protocol,
pub host: String,
pub port: u16,
}
impl NetworkEndpoint {
pub fn is_secure(&self) -> bool {
matches!(self.protocol, Protocol::Https | Protocol::Wss)
}
}
impl FromStr for NetworkEndpoint {
type Err = EndpointParseError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let (proto_str, rest) = s
.split_once("://")
.ok_or(EndpointParseError::MissingProtocol)?;
if proto_str.is_empty() {
return Err(EndpointParseError::MissingProtocol);
}
let protocol: Protocol = proto_str.parse()?;
let (host_str, port_str) = rest
.rsplit_once(':')
.ok_or(EndpointParseError::MissingPort)?;
if host_str.is_empty() || host_str.contains(' ') || host_str.contains('/') {
return Err(EndpointParseError::InvalidHost(host_str.to_string()));
}
if port_str.is_empty() {
return Err(EndpointParseError::MissingPort);
}
let port: u16 = port_str
.parse()
.map_err(EndpointParseError::InvalidPort)?;
Ok(NetworkEndpoint {
protocol,
host: host_str.to_string(),
port,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_valid_endpoints() {
let ep: NetworkEndpoint = "https://api.internal.v1:8443".parse().unwrap();
assert_eq!(ep.protocol, Protocol::Https);
assert_eq!(ep.host, "api.internal.v1");
assert_eq!(ep.port, 8443);
assert!(ep.is_secure());
let ep_grpc: NetworkEndpoint = "grpc://10.0.0.1:50051".parse().unwrap();
assert_eq!(ep_grpc.protocol, Protocol::Grpc);
assert_eq!(ep_grpc.host, "10.0.0.1");
assert_eq!(ep_grpc.port, 50051);
assert!(!ep_grpc.is_secure());
}
#[test]
fn test_parse_invalid_cases() {
let res: Result<NetworkEndpoint, _> = "localhost:8080".parse();
assert!(matches!(res, Err(EndpointParseError::MissingProtocol)));
let res_proto: Result<NetworkEndpoint, _> = "ftp://localhost:21".parse();
assert!(matches!(res_proto, Err(EndpointParseError::UnsupportedProtocol(ref p)) if p == "ftp"));
let res_no_port: Result<NetworkEndpoint, _> = "http://localhost".parse();
assert!(matches!(res_no_port, Err(EndpointParseError::MissingPort)));
let res_bad_port: Result<NetworkEndpoint, _> = "http://localhost:99999".parse();
assert!(matches!(res_bad_port, Err(EndpointParseError::InvalidPort(_))));
let res_bad_host: Result<NetworkEndpoint, _> = "http://invalid host:8080".parse();
assert!(matches!(res_bad_host, Err(EndpointParseError::InvalidHost(_))));
assert_ne!(Protocol::Http, Protocol::Https);
}
}
Technical Explanation
- Trait Composition & Method Chaining: Implementing
FromStrforProtocolallowsproto_str.parse()?insideNetworkEndpoint::from_strto automatically invokeProtocol::from_str. The compiler uses monomorphization to generate direct function calls toProtocol::from_strwithout dynamic dispatch (dyn). - Zero-Copy Subslice Splitting:
split_onceandrsplit_onceoperate directly on string slices (&str), returning borrowing tuples ((&str, &str)). No heap allocations occur until string slices are explicitly converted to ownedStringinstances (host_str.to_string()) upon constructingNetworkEndpoint. - Error Wrappers & Ownership: The
std::error::Errorimplementation forEndpointParseErroroptionally returns an underlying cause viasource(). By wrappingstd::num::ParseIntErrorinsideEndpointParseError::InvalidPort, lower-level parsing errors retain their full diagnostic stack traces while adhering to domain-specific error protocols.
Exercise 2: Generic Configuration Extraction with Rich Error Context
Scenario: Problem Statement:
Production services read raw string key-value configurations (e.g., from std::env::vars() or .env files stored in a HashMap<String, String>).
Implement a generic parsing pipeline function parse_config_entry<T: FromStr> that safely extracts and parses configuration keys into typed Rust domain values while producing detailed, contextual error reports when parsing fails.
Requirements: Requirements:
- Define a domain enum
LogLevel(Debug,Info,Warn,Error). ImplementFromStrforLogLevel. - Define a domain struct
DatabaseConfigwith fieldshost: String,port: u16,max_connections: u32,log_level: LogLevel. - Define a custom error type
ConfigError:MissingKey { key: String }InvalidValue { key: String, raw_value: String, cause: String }ImplementDisplayandstd::error::ErrorforConfigError.
- Implement a generic function
parse_config_entry<T>(map: &HashMap<String, String>, key: &str) -> Result<T, ConfigError> where T: FromStr, T::Err: std::fmt::Display. - Implement
DatabaseConfig::from_map(map: &HashMap<String, String>) -> Result<Self, ConfigError>extracting keys"DB_HOST","DB_PORT","DB_MAX_CONN", and"DB_LOG_LEVEL". - Include a complete test module
#[cfg(test)] mod testswith explicit assertions (assert_eq!,assert!,assert_ne!,matches!).
Answer
Implementation
use std::collections::HashMap;
use std::fmt;
use std::str::FromStr;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum LogLevel {
Debug,
Info,
Warn,
Error,
}
impl FromStr for LogLevel {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.trim().to_uppercase().as_str() {
"DEBUG" => Ok(LogLevel::Debug),
"INFO" => Ok(LogLevel::Info),
"WARN" | "WARNING" => Ok(LogLevel::Warn),
"ERROR" => Ok(LogLevel::Error),
_ => Err(format!("Unknown log level: '{}'", s)),
}
}
}
#[derive(Debug, PartialEq, Eq)]
pub enum ConfigError {
MissingKey { key: String },
InvalidValue {
key: String,
raw_value: String,
cause: String,
},
}
impl fmt::Display for ConfigError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
ConfigError::MissingKey { key } => write!(f, "Configuration key '{}' is missing", key),
ConfigError::InvalidValue { key, raw_value, cause } => {
write!(f, "Failed to parse key '{}' with value '{}': {}", key, raw_value, cause)
}
}
}
}
impl std::error::Error for ConfigError {}
pub fn parse_config_entry<T>(map: &HashMap<String, String>, key: &str) -> Result<T, ConfigError>
where
T: FromStr,
T::Err: fmt::Display,
{
let raw = map
.get(key)
.ok_or_else(|| ConfigError::MissingKey { key: key.to_string() })?;
raw.parse::<T>().map_err(|err| ConfigError::InvalidValue {
key: key.to_string(),
raw_value: raw.clone(),
cause: err.to_string(),
})
}
#[derive(Debug, PartialEq, Eq)]
pub struct DatabaseConfig {
pub host: String,
pub port: u16,
pub max_connections: u32,
pub log_level: LogLevel,
}
impl DatabaseConfig {
pub fn from_map(map: &HashMap<String, String>) -> Result<Self, ConfigError> {
let host: String = parse_config_entry(map, "DB_HOST")?;
let port: u16 = parse_config_entry(map, "DB_PORT")?;
let max_connections: u32 = parse_config_entry(map, "DB_MAX_CONN")?;
let log_level: LogLevel = parse_config_entry(map, "DB_LOG_LEVEL")?;
Ok(DatabaseConfig {
host,
port,
max_connections,
log_level,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_valid_config_parsing() {
let mut map = HashMap::new();
map.insert("DB_HOST".to_string(), "postgres.internal".to_string());
map.insert("DB_PORT".to_string(), "5432".to_string());
map.insert("DB_MAX_CONN".to_string(), "100".to_string());
map.insert("DB_LOG_LEVEL".to_string(), "info".to_string());
let cfg = DatabaseConfig::from_map(&map).unwrap();
assert_eq!(cfg.host, "postgres.internal");
assert_eq!(cfg.port, 5432);
assert_eq!(cfg.max_connections, 100);
assert_eq!(cfg.log_level, LogLevel::Info);
assert_ne!(cfg.log_level, LogLevel::Debug);
}
#[test]
fn test_missing_key_error() {
let mut map = HashMap::new();
map.insert("DB_HOST".to_string(), "localhost".to_string());
let res = DatabaseConfig::from_map(&map);
assert!(matches!(res, Err(ConfigError::MissingKey { ref key }) if key == "DB_PORT"));
}
#[test]
fn test_invalid_value_error() {
let mut map = HashMap::new();
map.insert("DB_HOST".to_string(), "localhost".to_string());
map.insert("DB_PORT".to_string(), "not_a_number".to_string());
map.insert("DB_MAX_CONN".to_string(), "10".to_string());
map.insert("DB_LOG_LEVEL".to_string(), "DEBUG".to_string());
let res = DatabaseConfig::from_map(&map);
assert!(matches!(
res,
Err(ConfigError::InvalidValue { ref key, ref raw_value, .. })
if key == "DB_PORT" && raw_value == "not_a_number"
));
}
}
Technical Explanation
- Generic Monomorphization:
parse_config_entry<T>uses Rust's static generics (T: FromStr). At compile time, the Rust compiler monomorphizes distinct specialized instantiations forT = String,T = u16,T = u32, andT = LogLevel. This eliminates runtime trait object dispatch overhead while preserving complete type safety. - Contextual Error Enrichment: Low-level parse errors (
ParseIntErrororString) often lack context regarding which configuration key caused the failure. By capturingkeyandraw_valueinsideConfigError::InvalidValue, the function converts anonymous errors into actionable diagnostic signals for operators. - Trait Bound
T::Err: Display: ConstrainingT::Errwithstd::fmt::Displayguarantees that any error produced byT::from_strcan be serialized into a user-readableStringviaerr.to_string(), permitting arbitrary domain types to plug seamlessly intoparse_config_entry.
Exercise 3: High-Precision Fixed-Point Financial Monetary Parser
Scenario: Problem Statement:
In financial ledger software, binary floating-point types (f32 / f64) are forbidden because representation issues cause rounding errors (e.g. 0.1 + 0.2 != 0.3). Monetary values are stored as fixed-point integers representing sub-units (e.g., cents or Yen).
Implement FromStr for a Money type that parses strings like "$1,234.56 USD", "EUR 99.00", "-¥500 JPY", or "100.50 CAD".
Requirements: Requirements:
- Define a
Currencyenum:USD,EUR,GBP,JPY,CAD. ImplementFromStrforCurrency.JPYhas 0 decimal sub-units.USD,EUR,GBP,CADhave 2 decimal sub-units (cents).
- Define a
Moneystruct containing:amount_subunits: i64,currency: Currency. - Define a custom error type
MoneyParseErrorwith variants:InvalidFormatUnknownCurrency(String)InvalidAmountSubunitOverflowImplementDisplayandstd::error::ErrorforMoneyParseError.
- Implement
FromStrforMoney:- Strip whitespace, currency symbols (
$,€,£,¥), and thousand separators (,). - Determine currency via ISO code (
"USD") or symbol ("$"). - Validate decimal places based on currency specifications (reject extra decimals or decimals for
JPY). - Safely convert decimal components to integer sub-units using checked integer arithmetic (
checked_mul,checked_add).
- Strip whitespace, currency symbols (
- Provide helper
Money::to_formatted_string(&self) -> Stringto re-format parsed values. - Include a complete test module
#[cfg(test)] mod testswith explicit assertions (assert_eq!,assert!,assert_ne!,matches!).
Answer
Implementation
use std::fmt;
use std::str::FromStr;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Currency {
USD,
EUR,
GBP,
JPY,
CAD,
}
impl Currency {
pub fn decimal_places(&self) -> u32 {
match self {
Currency::JPY => 0,
Currency::USD | Currency::EUR | Currency::GBP | Currency::CAD => 2,
}
}
pub fn symbol(&self) -> &'static str {
match self {
Currency::USD | Currency::CAD => "$",
Currency::EUR => "€",
Currency::GBP => "£",
Currency::JPY => "¥",
}
}
}
impl FromStr for Currency {
type Err = MoneyParseError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.trim().to_uppercase().as_str() {
"USD" | "$" => Ok(Currency::USD),
"EUR" | "€" => Ok(Currency::EUR),
"GBP" | "£" => Ok(Currency::GBP),
"JPY" | "¥" => Ok(Currency::JPY),
"CAD" => Ok(Currency::CAD),
other => Err(MoneyParseError::UnknownCurrency(other.to_string())),
}
}
}
#[derive(Debug, PartialEq, Eq)]
pub enum MoneyParseError {
InvalidFormat,
UnknownCurrency(String),
InvalidAmount,
SubunitOverflow,
}
impl fmt::Display for MoneyParseError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
MoneyParseError::InvalidFormat => write!(f, "Invalid monetary string format"),
MoneyParseError::UnknownCurrency(c) => write!(f, "Unrecognized currency indicator: '{}'", c),
MoneyParseError::InvalidAmount => write!(f, "Failed to parse numeric monetary amount"),
MoneyParseError::SubunitOverflow => write!(f, "Monetary amount exceeds integer storage limit"),
}
}
}
impl std::error::Error for MoneyParseError {}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Money {
pub amount_subunits: i64,
pub currency: Currency,
}
impl Money {
pub fn new(amount_subunits: i64, currency: Currency) -> Self {
Money { amount_subunits, currency }
}
pub fn to_formatted_string(&self) -> String {
let decimals = self.currency.decimal_places();
if decimals == 0 {
format!("{} {}", self.amount_subunits, self.currency.symbol())
} else {
let divisor = 10i64.pow(decimals);
let units = self.amount_subunits / divisor;
let subs = (self.amount_subunits % divisor).abs();
format!("{}{}.{:02} {:?}", self.currency.symbol(), units, subs, self.currency)
}
}
}
impl FromStr for Money {
type Err = MoneyParseError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let trimmed = s.trim();
if trimmed.is_empty() {
return Err(MoneyParseError::InvalidFormat);
}
let mut currency: Option<Currency> = None;
let mut clean_str = String::new();
for token in trimmed.split_whitespace() {
if let Ok(c) = token.parse::<Currency>() {
if currency.is_none() {
currency = Some(c);
continue;
}
}
clean_str.push_str(token);
}
if currency.is_none() {
if clean_str.starts_with('$') || clean_str.starts_with('€') || clean_str.starts_with('£') || clean_str.starts_with('¥') {
let symbol = &clean_str[..clean_str.chars().next().unwrap().len_utf8()];
currency = symbol.parse::<Currency>().ok();
clean_str = clean_str[symbol.len()..].to_string();
}
}
let currency = currency.ok_or(MoneyParseError::InvalidFormat)?;
let number_str = clean_str.replace(',', "");
if number_str.is_empty() {
return Err(MoneyParseError::InvalidAmount);
}
let is_negative = number_str.starts_with('-');
let abs_str = if is_negative || number_str.starts_with('+') {
&number_str[1..]
} else {
&number_str[..]
};
let decimals = currency.decimal_places();
let (units_str, frac_str) = match abs_str.split_once('.') {
Some((u, f)) => (u, f),
None => (abs_str, ""),
};
if decimals == 0 && !frac_str.is_empty() {
return Err(MoneyParseError::InvalidAmount);
}
if frac_str.len() > decimals as usize {
return Err(MoneyParseError::InvalidAmount);
}
let units: i64 = if units_str.is_empty() {
0
} else {
units_str.parse().map_err(|_| MoneyParseError::InvalidAmount)?
};
let padded_frac = format!("{:0<width$}", frac_str, width = decimals as usize);
let frac: i64 = if decimals == 0 {
0
} else {
padded_frac.parse().map_err(|_| MoneyParseError::InvalidAmount)?
};
let scale = 10i64.pow(decimals);
let total_subunits = units
.checked_mul(scale)
.and_then(|u| u.checked_add(frac))
.ok_or(MoneyParseError::SubunitOverflow)?;
let final_subunits = if is_negative {
-total_subunits
} else {
total_subunits
};
Ok(Money {
amount_subunits: final_subunits,
currency,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_valid_money() {
let m1: Money = "$1,234.56 USD".parse().unwrap();
assert_eq!(m1.amount_subunits, 123456);
assert_eq!(m1.currency, Currency::USD);
let m2: Money = "EUR 99.00".parse().unwrap();
assert_eq!(m2.amount_subunits, 9900);
assert_eq!(m2.currency, Currency::EUR);
let m3: Money = "100 JPY".parse().unwrap();
assert_eq!(m3.amount_subunits, 100);
assert_eq!(m3.currency, Currency::JPY);
let m4: Money = "-$50.25 USD".parse().unwrap();
assert_eq!(m4.amount_subunits, -5025);
}
#[test]
fn test_parse_invalid_money() {
let res1: Result<Money, _> = "100.50.25 USD".parse();
assert!(matches!(res1, Err(MoneyParseError::InvalidAmount)));
let res2: Result<Money, _> = "$10.999 USD".parse();
assert!(matches!(res2, Err(MoneyParseError::InvalidAmount)));
let res3: Result<Money, _> = "100.50 JPY".parse();
assert!(matches!(res3, Err(MoneyParseError::InvalidAmount)));
let res4: Result<Money, _> = "$100 XYZ".parse();
assert!(matches!(res4, Err(MoneyParseError::UnknownCurrency(_))));
assert_ne!(Currency::USD, Currency::EUR);
}
}
Technical Explanation
- Fixed-Point Decimal Parsing: To prevent IEEE 754 floating-point inaccuracies, string components (
units_strandfrac_str) are split on decimal boundaries (.split_once('.')) and converted directly into integer sub-units. Arithmetic operations utilizechecked_mulandchecked_addto protect againsti64integer overflow vulnerabilities. - Multi-Stage Token & Symbol Resolution:
FromStr::from_strparses both currency symbols ($,€,£,¥) and ISO 4217 alphabetic codes (USD,EUR) by leveraging sub-parsing viatoken.parse::<Currency>(). UTF-8 multibyte boundary awareness (symbol.chars().next().unwrap().len_utf8()) ensures slice bounds remain valid when stripping unicode symbols like€(3 bytes) or¥(2 bytes). - Currency Domain Invariants: The parser dynamically queries
currency.decimal_places()to enforce scale precision. Currencies without subunits (e.g.JPY) reject fractional strings, whereas sub-cent decimals (e.g.$10.999) triggerMoneyParseError::InvalidAmountto maintain accounting invariants.
6. Related Terms
?Operator — The idiomatic way to propagate a.parse()failure out of a function.Result<T, E>— The type everyFromStr::from_strimplementation must return.From/IntoTraits — The infallible-conversion sibling family;FromStris specifically for the fallible, text-parsing case.TryFromandTryIntoTraits — The general-purpose fallible-conversion trait;FromStris effectivelyTryFrom<&str>with a dedicated name and.parse()sugar.
7. Key Takeaways
.parse::<T>()is sugar forT::from_str(s), and works for anyTthat implementsFromStr.- Parsing is fallible by design — it always returns a
Result, never panics or silently produces wrong data. - Because
.parse()is generic, the compiler needs a type hint: use the turbofish (::<T>) or a variable type annotation. - You can implement
FromStrfor your own types to get free, idiomatic"text".parse::<MyType>()support.