How to avoid map_err?

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}`") 
    };
}

[1]


  1. Full example here. â†Šī¸Ž

They are pretty good suggestions, so my final code was modified accordingly.