I have the following code,
fn parse_web_date(str: &str) -> Result<u64, std::num::ParseIntError> {
let fmt_err = "".parse::<u32>().expect_err("invalid format {str}");
let (_,date) = str.split_once(", ").ok_or(fmt_err.clone())?;
let mut parts = date.split(' ');
let Some(day) = parts.next() else {
return Err(fmt_err)
} ;
let day = day.parse::<u32>()?;
let Some(month) = parts.next() else {
return Err(fmt_err)
} ;
let month:u32 = match month {
"Jan" => 1,
"Feb" => 2,
"Mar" => 3,
"Apr" => 4,
"May" => 5,
"Jun" => 6,
"Jul" => 7,
"Aug" => 8,
"Sep" => 9,
"Oct" => 10,
"Nov" => 11,
"Dec" => 12,
_ => return Err(fmt_err)
};
let Some(year) = parts.next() else {
return Err(fmt_err)
} ;
let year = year.parse::<u32>()?;
let Some(time) = parts.next() else {
return Err(fmt_err)
} ;
let [h,m,s] = *time.splitn(3,':').collect::<Vec<_>>() else { todo!() };
let h = h.parse::<u64>()?;
let m = m.parse::<u64>()?;
let s = s.parse::<u64>()?;
Ok(seconds_from_epoch(year,month,day)+h*60*60+m*60+s)
}
It looks a quite ugly because the fist line. But this error type I can't create other way. Rust is not OO language, so there is no general parse error. So how to manage the situation without introducing tons of map_err calls?
why not create your own error type? I don't think ParseIntError
suits the error case of your parsing procedure.
but you do need some setup boilterplates:
enum DateField {
Unknown, Year, Month, Day, Hour, Minute, Second
}
/// can also use crates like `snafu`, `thiserror`, etc.
enum ParseWebDateError {
Parse(DateField, ParseIntError),
Missing(DateField),
}
// convenient method to convert error type,
// similar to `snafu::ResultExt::context`, etc
// you can use easy-ext to reduce boilerplates
#[ext]
impl<Int> Result<Int, ParseIntError> {
fn parsing(self, field: DateField) -> Result<Int, ParseWebDateError> {
self.map_err(|e| ParseWebDateError::Parse(field, e))
}
}
#[ext]
impl<T> Option<T> {
fn missing(self, field: DateField) -> Result<T, ParsingWebDateError> {
self.ok_or(ParsingWebDateError::Missing(field))
}
}
and use it like this:
fn parse_web_date(str: &str) -> Result<u64, std::num::ParseIntError> {
use DateField::*;
let (_, date) = str.split_once(", ").missing(Unknown)?;
let mut parts = date.split(' ');
let day: u32 = parts.next().missing(Day)?.parse().parsing(Day)?;
//... month, year, hms, etc
Ok(seconds_from_epoch(year,month,day)+h*60*60+m*60+s)
}
4 Likes
It looks quite elegant, thank you.
Why not reach for anyhow
and its bail!
in particular? It'd be my personal first choice here.
Calling parts.next()
several times over feels a bit unnecessary, as well:
// to increase the input tolerance just a little bit
let parts: Vec<_> = date.split_ascii_whitespace().collect();
// match all at once, instead of one `parts.next()` at a time
let [day, month, year, time] = parts.as_slice() else { bail!("invalid format") };
You could also play around with macro_rules!
and type inference to clean up parse::<u32/u64>()
alongside the otherwise redundant let X = Y else { bail!(...)
statements, as in:
let time: Vec<_> = time.splitn(3, ':').collect();
// exercise: try to figure out how do this on your own
or_err!(match time[..] => [h,m,s]);
let [h,m,s]: [u64; 3] = [
h.parse()?,
m.parse()?,
s.parse()?,
];
Solution
macro_rules! or_err {
(match $from:expr => $case:pat) => {
let $case = $from else { or_err!() };
};
() => {
bail!("`{str}` does not match the format `{FORMAT}`")
};
}
They are pretty good suggestions, so my final code was modified accordingly.