use crate::{ db, deck, model::{DbEntry, Line, Question}, }; use anyhow::Result; use rusqlite::Connection; use std::collections::HashMap; use std::collections::HashSet; pub fn run(conn: &mut Connection, deck_path: &str) -> Result<()> { let db_entries = db::all(conn)?; let lines = deck::read_file(deck_path)?; let Diff { new, deleted, undeleted, } = diff(db_entries, lines); db::insert(conn, &new)?; db::delete(conn, &deleted)?; db::undelete(conn, &undeleted)?; Ok(()) } struct Diff { pub new: Vec, pub deleted: Vec, pub undeleted: Vec, } fn diff(db_entries: Vec, lines: Vec) -> Diff { let mut file_questions = HashMap::>::new(); let mut db_questions_not_deleted = HashSet::::new(); let mut db_questions_deleted = HashSet::::new(); for Line { part_1, part_2 } in lines { insert(&mut file_questions, part_1.clone(), part_2.clone()); insert(&mut file_questions, part_2, part_1); } let file_questions: HashSet = file_questions .iter() .map(|(question, responses)| Question { question: question.to_string(), responses: responses.to_vec(), }) .collect(); for DbEntry { question, mut responses, deleted, } in db_entries { responses.sort(); if deleted.is_some() { db_questions_deleted.insert(Question { question, responses, }); } else { db_questions_not_deleted.insert(Question { question, responses, }); } } let new = file_questions .difference(&db_questions_not_deleted) .cloned() .collect::>() .difference(&db_questions_deleted) .cloned() .collect(); let deleted = db_questions_not_deleted .difference(&file_questions) .cloned() .collect(); let undeleted = file_questions .intersection(&db_questions_deleted) .cloned() .collect(); Diff { new, deleted, undeleted, } } fn insert(map: &mut HashMap>, questions: Vec, responses: Vec) { for question in questions { let mut responses = responses.clone(); responses.sort(); match map.get_mut(&question) { Some(existing_responses) => existing_responses.append(&mut responses), None => { map.insert(question, responses); } }; } } #[cfg(test)] mod tests { use super::{deck, DbEntry, Diff, Question}; use std::collections::HashSet; #[test] fn test_added() { let diff = deck_diff("A : a", "A : a\nB : b"); has_questions(diff.new, vec![("B", vec!["b"]), ("b", vec!["B"])]); assert!(diff.deleted.is_empty()); assert!(diff.undeleted.is_empty()); } #[test] fn test_updated() { let diff = deck_diff("A : a1", "A : a2"); has_questions(diff.new, vec![("A", vec!["a2"]), ("a2", vec!["A"])]); has_questions(diff.deleted, vec![("A", vec!["a1"]), ("a1", vec!["A"])]); assert!(diff.undeleted.is_empty()); } #[test] fn test_deleted() { let diff = deck_diff("A : a", ""); assert!(diff.new.is_empty()); has_questions(diff.deleted, vec![("A", vec!["a"]), ("a", vec!["A"])]); assert!(diff.undeleted.is_empty()); } #[test] fn test_undeleted() { let db_entries = vec![DbEntry { question: "A".to_string(), responses: vec!["a".to_string()], deleted: Some(0), }]; let diff = super::diff(db_entries, deck::tests::read_string("A : a").unwrap()); has_questions(diff.new, vec![("a", vec!["A"])]); assert!(diff.deleted.is_empty()); has_questions(diff.undeleted, vec![("A", vec!["a"])]); } #[test] fn regroup_same_question() { let diff = deck_diff("", "A : a\nA | B : b"); has_questions( diff.new, vec![ ("A", vec!["a", "b"]), ("B", vec!["b"]), ("a", vec!["A"]), ("b", vec!["A", "B"]), ], ); assert!(diff.deleted.is_empty()); assert!(diff.undeleted.is_empty()); } fn deck_diff(from: &str, to: &str) -> Diff { super::diff(db_entries(from), deck::tests::read_string(to).unwrap()) } fn has_questions(questions: Vec, xs: Vec<(&str, Vec<&str>)>) { assert_eq!( to_set(questions), HashSet::from_iter( xs.iter() .map(|(y, ys)| Question { question: y.to_string(), responses: ys.iter().map(|z| z.to_string()).collect::>() }) .collect::>() ) ) } fn to_set(xs: Vec) -> HashSet { xs.iter().cloned().collect() } fn db_entries(deck: &str) -> Vec { let lines = deck::tests::read_string(deck).unwrap(); let diff = super::diff(vec![], lines); diff.new .iter() .map( |Question { question, responses, }| DbEntry { question: question.to_string(), responses: responses.to_vec(), deleted: None, }, ) .collect() } }