worldspawn_message 0.1.5

Parse Quake map worldspawn messages
Documentation
#[derive(Debug, Default, Clone, PartialEq)]
pub struct Message {
    pub lines: Vec<String>,
    pub authors: Vec<String>,
    pub release_year: Option<String>,
}

impl From<&str> for Message {
    fn from(message: &str) -> Self {
        let lines = to_lines(message);

        Self {
            lines: lines.clone(),
            authors: lines.iter().filter_map(|l| get_author(l)).collect(),
            release_year: lines.iter().filter_map(|l| get_release_year(l)).next(),
        }
    }
}

fn to_lines(message: &str) -> Vec<String> {
    let lines: Vec<String> = message
        .split('\n')
        .map(|line| strip(line).trim().to_string())
        .filter(|line| !line.is_empty())
        .collect();
    lines
}

fn strip(value: &str) -> String {
    let result: String = value
        .chars()
        .map(|c| match c.is_ascii_control() {
            true => ' ',
            false => c,
        })
        .collect();
    result
        .replace("   ", " ")
        .replace("  ", " ")
        .trim()
        .to_string()
}

fn is_delimiter(c: char) -> bool {
    c == '(' || c == ')' || c == '[' || c == ']' || c == ',' || c == '-'
}

fn get_author(line: &str) -> Option<String> {
    let line_ = format!(" {}", line);
    let haystack = line_.to_lowercase();
    let index_from = haystack.rfind(" by ").map(|i| i + 4)?;
    let index_to = haystack[index_from..]
        .find(is_delimiter)
        .map(|i| index_from + i)
        .unwrap_or(line_.len());

    let author = &line_[index_from..index_to].trim();
    Some(author.to_string())
}

fn get_release_year(line: &str) -> Option<String> {
    for year in 1996..=2025 {
        if line.contains(&year.to_string()) {
            return Some(year.to_string());
        }
    }

    None
}

#[cfg(test)]
mod tests {
    use std::collections::HashMap;

    use pretty_assertions::assert_eq;

    use super::*;

    #[test]
    fn test_to_lines() {
        assert_eq!(
            to_lines("thefoobar\n\n by Bar\n"),
            vec!["the foo bar".to_string(), "by Bar".to_string(),]
        )
    }

    #[test]
    fn test_strip() {
        assert_eq!(strip("the foo bar"), "the foo bar".to_string());
        assert_eq!(strip(" the   foo  bar  "), "the foo bar".to_string());
        assert_eq!(strip("thefoobar"), "the foo bar".to_string());
    }

    #[test]
    fn test_get_author() {
        let test_cases: HashMap<&str, Option<String>> = HashMap::from([
            ("Foo", None),
            ("Foo BY Bar", Some("Bar".to_string())),
            ("Foo by Bar", Some("Bar".to_string())),
            ("Foo - by Bar", Some("Bar".to_string())),
            ("Foo (abc) - by Bar", Some("Bar".to_string())),
            ("Foo by Bar, dmm4 edition", Some("Bar".to_string())),
            ("Foo by Bar - dmm4 edition", Some("Bar".to_string())),
            ("by Bar", Some("Bar".to_string())),
            ("Foo BY Bar (1996)", Some("Bar".to_string())),
            ("Foo BY Bar [1996]", Some("Bar".to_string())),
            ("Foo (2020 remix by Bar)", Some("Bar".to_string())),
            ("Foo [2020 remix by Bar]", Some("Bar".to_string())),
        ]);

        for (line, expected) in test_cases {
            assert_eq!(get_author(line), expected, "input: {}", line);
        }
    }

    #[test]
    fn test_get_release_date() {
        let test_cases: HashMap<&str, Option<String>> = HashMap::from([
            ("Foo", None),
            ("Foo BY Bar", None),
            ("Foo BY Bar (1996)", Some("1996".to_string())),
            ("Foo BY Bar [1996]", Some("1996".to_string())),
        ]);

        for (line, expected) in test_cases {
            assert_eq!(get_release_year(line), expected, "input: {:?}", line);
        }
    }

    #[test]
    fn test_from() {
        let test_cases: HashMap<&str, Message> = HashMap::from([
            (
                "Foo",
                Message {
                    lines: vec!["Foo".to_string()],
                    authors: vec![],
                    release_year: None,
                },
            ),
            (
                "Foo BY Bar",
                Message {
                    lines: vec!["Foo BY Bar".to_string()],
                    authors: vec!["Bar".to_string()],
                    release_year: None,
                },
            ),
            (
                "Foo BY Bar (1996)",
                Message {
                    lines: vec!["Foo BY Bar (1996)".to_string()],
                    authors: vec!["Bar".to_string()],
                    release_year: Some("1996".to_string()),
                },
            ),
            (
                "(1996)",
                Message {
                    lines: vec!["(1996)".to_string()],
                    authors: vec![],
                    release_year: Some("1996".to_string()),
                },
            ),
        ]);

        for (input, expected) in test_cases {
            assert_eq!(Message::from(input), expected, "input: {}", input);
        }
    }
}