diff options
-rw-r--r-- | src/deck.rs | 145 | ||||
-rw-r--r-- | src/model/mod.rs | 2 |
2 files changed, 112 insertions, 35 deletions
diff --git a/src/deck.rs b/src/deck.rs index 0c302e1..4491d9b 100644 --- a/src/deck.rs +++ b/src/deck.rs @@ -23,51 +23,57 @@ impl std::error::Error for ParseError { } } -pub fn read(deck: &str) -> Result<Vec<Line>> { - let file = File::open(deck)?; +pub fn read_file(path: &str) -> Result<Vec<Line>> { + let file = File::open(path)?; let reader = BufReader::new(file); let mut entries: Vec<Line> = Vec::new(); for (index, line) in reader.lines().enumerate() { let line = line?; - let line = line.trim(); - if !line.starts_with('#') && !line.is_empty() { - if !line.starts_with('-') { - return Err(Error::from(ParseError { + if let Some(line) = read_line(index, &line)? { + entries.push(line) + } + } + + Ok(entries) +} + +fn read_line(index: usize, line: &str) -> Result<Option<Line>> { + let line = line.trim(); + + if line.starts_with('#') || line.is_empty() { + Ok(None) + } else if !line.starts_with('-') { + Err(Error::from(ParseError { + line: index + 1, + message: "an entry should starts with “-”.".to_string(), + })) + } else { + let without_minus = line.split('-').skip(1).collect::<Vec<&str>>().join("-"); + let without_comment = without_minus.split('#').collect::<Vec<&str>>()[0].trim(); + let translation = without_comment.split(':').collect::<Vec<&str>>(); + if translation.len() != 2 { + Err(Error::from(ParseError { + line: index + 1, + message: "an entry should contain two parts separated by “:”.".to_string(), + })) + } else { + let t1 = translation[0].trim(); + let t2 = translation[1].trim(); + if t1.is_empty() || t2.is_empty() { + Err(Error::from(ParseError { line: index + 1, - message: "an entry should starts with “-”.".to_string(), - })); + message: "an entry should contain two parts separated by “:”.".to_string(), + })) } else { - let without_minus = line.split('-').skip(1).collect::<Vec<&str>>().join("-"); - let without_comment = without_minus.split('#').collect::<Vec<&str>>()[0].trim(); - let translation = without_comment.split(':').collect::<Vec<&str>>(); - if translation.len() != 2 { - return Err(Error::from(ParseError { - line: index + 1, - message: "an entry should contain two parts separated by “:”.".to_string(), - })); - } else { - let t1 = translation[0].trim(); - let t2 = translation[1].trim(); - if t1.is_empty() || t2.is_empty() { - return Err(Error::from(ParseError { - line: index + 1, - message: "an entry should contain two parts separated by “:”." - .to_string(), - })); - } else { - entries.push(Line { - part_1: serialization::line_to_words(t1), - part_2: serialization::line_to_words(t2), - }) - } - } + Ok(Some(Line { + part_1: serialization::line_to_words(t1), + part_2: serialization::line_to_words(t2), + })) } } } - - Ok(entries) } pub fn pp_from_path(path: &str) -> Option<String> { @@ -83,3 +89,74 @@ fn capitalize(s: &str) -> String { Some(f) => f.to_uppercase().collect::<String>() + c.as_str(), } } + +#[cfg(test)] +pub mod tests { + + use crate::model::Line; + use anyhow::Result; + + #[test] + fn errors() { + is_error("A : a"); + is_error("- A"); + is_error("- A -> a"); + is_error("- A : B : C"); + is_error("- : "); + is_error("- A : a\n-") + } + + #[test] + fn ignored() { + check("", &[]); + check(" ", &[]); + check(" \n \n ", &[]); + check("# 1", &[]); + check("# 1\n\n # 2", &[]); + } + + #[test] + fn card() { + check("- A : a", &[(&["A"], &["a"])]); + } + + #[test] + fn cards() { + check("- A : a\n- B : b", &[(&["A"], &["a"]), (&["B"], &["b"])]); + } + + #[test] + fn alternatives() { + check("- A : a1 | a2", &[(&["A"], &["a1", "a2"])]); + check("- A1 | A2 : a", &[(&["A1", "A2"], &["a"])]); + check("- A1 | A2 : a1 | a2", &[(&["A1", "A2"], &["a1", "a2"])]); + } + + fn is_error(content: &str) { + assert!(read_string(content).is_err()) + } + + fn check(content: &str, res: &[(&[&str], &[&str])]) { + assert_eq!( + read_string(content).unwrap(), + res.iter() + .map(|(part_1, part_2)| Line { + part_1: part_1.iter().map(|x| x.to_string()).collect::<Vec<_>>(), + part_2: part_2.iter().map(|x| x.to_string()).collect::<Vec<_>>() + }) + .collect::<Vec<_>>() + ) + } + + pub fn read_string(content: &str) -> Result<Vec<Line>> { + let mut entries: Vec<Line> = Vec::new(); + + for (index, line) in content.lines().enumerate() { + if let Some(line) = super::read_line(index, line)? { + entries.push(line) + } + } + + Ok(entries) + } +} diff --git a/src/model/mod.rs b/src/model/mod.rs index 2dc1ab5..4df4c49 100644 --- a/src/model/mod.rs +++ b/src/model/mod.rs @@ -2,7 +2,7 @@ use crate::space_repetition; pub mod difficulty; -#[derive(Debug, Clone)] +#[derive(Debug, Clone, PartialEq)] pub struct Line { pub part_1: Vec<String>, pub part_2: Vec<String>, |