readformat 1.0.4

Very small format reader
Documentation
const DEBUG: bool = false;

/// The inverse of format!().
/// The format argument is the format string, and the s argument is the string to match the format
/// against.
///
/// Examples:
///  - `readf("Hello, {}!", "Hello, world!")` => `Some(vec!["world"])`
///  - `readf("I hope {} are doing {}!", "I hope you are doing well!")` => `Some(vec!["you", "well"])`
///  - `readf("Goodbye, {}!", "Hello, world!")` => `None`
pub fn readf(format: &str, mut s: &str) -> Option<Vec<String>> {
    let mut patterns = Vec::new();
    {
        let mut format = format;
        while let Some(idx) = format.find("{}") {
            patterns.push(&format[..idx]);
            format = &format[(idx + 2)..];
        }
        // remainder
        patterns.push(format);
    }
    let count = patterns.len() - 1;
    if DEBUG {
        println!("Patterns ({count}): {patterns:?}");
    }

    let mut result = Vec::with_capacity(count);

    if count == 0 {
        if s == format {
            return Some(result);
        } else {
            return None;
        }
    }
    {
        let pat = patterns[0];
        if s.get(..pat.len()).is_none_or(|x| x != pat) {
            return None;
        }
        if DEBUG {
            println!("Shaving off pattern {pat:?} from {s:?}.");
        }
        s = &s[pat.len()..];
    }

    for i in 0..count {
        let next_pat = patterns[i + 1];
        if DEBUG {
            println!("Consuming until next pat: {next_pat:?}");
        }
        let placeholder_end = if i + 1 == count {
            let pat = s.len().wrapping_sub(next_pat.len());
            if s.get(pat..).is_none_or(|x| x != next_pat) {
                None
            } else {
                Some(pat)
            }
        } else {
            s.find(next_pat)
        };
        let Some(placeholder_end) = placeholder_end else {
            if DEBUG {
                println!("! Unable to find next pat.");
            }
            return None;
        };
        result.push(s[..placeholder_end].to_owned());
        if DEBUG {
            println!("Consumed {:?}.", &s[..placeholder_end]);
        }
        s = &s[placeholder_end..];
        if DEBUG {
            println!("Shaving off pattern {next_pat:?} from {s:?}.");
        }
        s = &s[next_pat.len()..];
    }
    if !s.is_empty() {
        unreachable!("String not fully consumed!");
    }

    Some(result)
}

/// Convenience function for single-item format strings. Extra items are dropped.
///
/// Examples:
///  - `readf1("Hello, {}!", "Hello, world!")` => `Some("world")`
///  - `readf1("I hope {} are doing well!", "I hope you are doing well!")` => `Some("you")`
///  - `readf1("Goodbye, {}!", "Hello, world!")` => `None`
pub fn readf1(format: &str, s: &str) -> Option<String> {
    let r = readf(format, s);
    r.map(|x| {
        if x.is_empty() {
            "".into()
        } else {
            x[0].clone()
        }
    })
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn t_readf1() {
        assert_eq!(readf1("hello, {}", "hello, person"), Some("person".into()));
        assert_eq!(
            readf1("hello, {}!", "hello, person!"),
            Some("person".into())
        );
        assert_eq!(readf1("Hello, {}", "hello, person"), None);
        assert_eq!(readf1("Hello, {}!", "hello, person"), None);
        assert_eq!(readf1("hello!", "hello!"), Some("".into()));
        assert_eq!(readf1("Hello!", "hello!"), None);
        assert_eq!(readf1("{}", "person"), Some("person".into()));
    }

    #[test]
    fn t_readf() {
        assert_eq!(
            readf("hello, {} and {}", "hello, person 1 and person 2"),
            Some(vec!["person 1".into(), "person 2".into()])
        );
        assert_eq!(
            readf("hello, {} and {}!", "hello, person 1 and person 2!"),
            Some(vec!["person 1".into(), "person 2".into()])
        );
        assert_eq!(readf("hello, {} and {}", "hello, person"), None);
        assert_eq!(readf("hello, {} and {}!", "hello, person!"), None);
        assert_eq!(
            readf("Hello, {} and {}", "hello, person 1 and person 2"),
            None
        );
        assert_eq!(
            readf("Hello, {} and {}!", "hello, person 1 and person 2!"),
            None
        );
        assert_eq!(readf("hello!", "hello!"), Some(vec![]));
        assert_eq!(readf("Hello!", "hello!"), None);
        assert_eq!(
            readf("{}, {}", "person 1, person 2"),
            Some(vec!["person 1".into(), "person 2".into()])
        );
        assert_eq!(
            readf("{}, {}, {}", "person 1, person 2, person 3"),
            Some(vec![
                "person 1".into(),
                "person 2".into(),
                "person 3".into()
            ])
        );
        assert_eq!(
            readf("{}, {}, {}, ", "person 1, person 2, person 3, "),
            Some(vec![
                "person 1".into(),
                "person 2".into(),
                "person 3".into()
            ])
        );
        assert_eq!(readf("{}, {}, {}, ", "person 1, person 2, person 3"), None);
    }
}