From 20cfe717065fa53953e9799036a9972880fec801 Mon Sep 17 00:00:00 2001 From: Joris Date: Sun, 6 Feb 2022 19:30:53 +0100 Subject: Replace a card whenever the front or the back changed --- README.md | 7 +++---- src/db/db.rs | 25 ++++++++++++++++--------- src/db/sql/2-primary-key-question-responses.sql | 20 ++++++++++++++++++++ src/deck.rs | 23 ++++++++++++++++------- src/main.rs | 2 +- src/model/deck.rs | 5 ----- src/model/entry.rs | 5 +++++ src/model/mod.rs | 2 +- src/util/serialization.rs | 5 ++++- 9 files changed, 66 insertions(+), 28 deletions(-) create mode 100644 src/db/sql/2-primary-key-question-responses.sql delete mode 100644 src/model/deck.rs create mode 100644 src/model/entry.rs diff --git a/README.md b/README.md index a6b0d05..116c6c5 100644 --- a/README.md +++ b/README.md @@ -17,11 +17,10 @@ Cards are created from a plain text `./deck` file: # TODO -- When changing the back, update the card +- Indications in parenthesis +- Look at last modification of the deck, and omit db sync if last sync was after - Fix crashes on zoom / changing size -## Nice to have +## Maybe -- Indications in parenthesis - Phonetics indication -- Select deck diff --git a/src/db/db.rs b/src/db/db.rs index 86614ec..843d142 100644 --- a/src/db/db.rs +++ b/src/db/db.rs @@ -1,5 +1,5 @@ use crate::{ - model::{card::Card, deck::Entry}, + model::{card::Card, entry::Entry}, space_repetition, util::serialization, }; @@ -11,14 +11,21 @@ use std::time::SystemTime; pub fn init(database: String) -> Result { let mut conn = Connection::open(database)?; - let migrations = Migrations::new(vec![M::up(include_str!("sql/1-init.sql"))]); + let migrations = Migrations::new(vec![ + M::up(include_str!("sql/1-init.sql")), + M::up(include_str!("sql/2-primary-key-question-responses.sql")), + ]); migrations.to_latest(&mut conn)?; Ok(conn) } -pub fn add_missing_deck_entries(conn: &Connection, entries: Vec) -> Result<()> { +/// Synchronize the DB with the deck: +/// +/// - insert new cards, +/// - keep existing cards, +/// - hide unused cards (keep state in case the card is added back afterward). +pub fn synchronize(conn: &Connection, entries: Vec) -> Result<()> { let now = get_current_time()?; - let ready = now; let state = serde_json::to_string(&space_repetition::init())?; let mut rng = rand::thread_rng(); @@ -28,11 +35,11 @@ pub fn add_missing_deck_entries(conn: &Connection, entries: Vec) -> Resul let concat_2 = serialization::words_to_line(&entry.part_2); for w in entry.part_1.iter() { - insert(&conn, &mut rng, now, ready, &w, &concat_2, &state)?; + insert(&conn, &mut rng, now, &w, &concat_2, &state)?; } for w in entry.part_2.iter() { - insert(&conn, &mut rng, now, ready, &w, &concat_1, &state)?; + insert(&conn, &mut rng, now, &w, &concat_1, &state)?; } } @@ -45,11 +52,11 @@ fn insert( conn: &Connection, rng: &mut ThreadRng, now: u64, - ready: u64, question: &String, responses: &String, state: &String, ) -> Result<()> { + println!("insert {} : {}", question, responses); // Subtract a random amount of time so that cards are not initially given in the same order as // in the deck let ready_sub: u64 = rng.gen_range(0..100); @@ -58,9 +65,9 @@ fn insert( " INSERT INTO cards (question, responses, state, created, deck_read, ready) VALUES (?, ?, ?, ?, ?, ?) - ON CONFLICT (question) DO UPDATE SET deck_read = ?, deleted = null + ON CONFLICT (question, responses) DO UPDATE SET deck_read = ?, deleted = null ", - params![question, responses, state, now, now, ready - ready_sub, now], + params![question, responses, state, now, now, now - ready_sub, now], )?; Ok(()) diff --git a/src/db/sql/2-primary-key-question-responses.sql b/src/db/sql/2-primary-key-question-responses.sql new file mode 100644 index 0000000..cb7df21 --- /dev/null +++ b/src/db/sql/2-primary-key-question-responses.sql @@ -0,0 +1,20 @@ +/* Allows to use ON CONFLICT on (question, responses) when inserting a card. */ + +CREATE TABLE IF NOT EXISTS cards_copy ( + question VARCHAR NOT NULL, + responses VARCHAR NOT NULL, + state VARCHAR NOT NULL, + created TIMESTAMP NOT NULL, + updated TIMESTAMP NULL, + deleted TIMESTAMP NULL, + deck_read TIMESTAMP NOT NULL, + ready TIMESTAMP NOT NULL, + PRIMARY KEY (question, responses) +); + +INSERT INTO cards_copy (question, responses, state, created, updated, deleted, deck_read, ready) + SELECT question, responses, state, created, updated, deleted, deck_read, ready FROM cards; + +DROP TABLE cards; + +ALTER TABLE cards_copy RENAME TO cards; diff --git a/src/deck.rs b/src/deck.rs index 0fe8a7b..ce20ee9 100644 --- a/src/deck.rs +++ b/src/deck.rs @@ -1,8 +1,8 @@ -use crate::{model::deck::Entry, util::serialization}; -use anyhow::{Result, Error}; +use crate::{model::entry::Entry, util::serialization}; +use anyhow::{Error, Result}; +use std::fmt; use std::fs::File; use std::io::{prelude::*, BufReader}; -use std::fmt; #[derive(Debug, Clone)] struct ParseError { @@ -22,7 +22,6 @@ impl std::error::Error for ParseError { } } - pub fn read(deck: String) -> Result> { let file = File::open(deck)?; let reader = BufReader::new(file); @@ -34,16 +33,26 @@ pub fn read(deck: String) -> Result> { if !line.starts_with("#") && !line.is_empty() { if !line.starts_with("-") { - return Err(Error::from(ParseError { line: index + 1, message: "an entry should starts with “-”.".to_string() })) + return Err(Error::from(ParseError { + line: index + 1, + message: "an entry should starts with “-”.".to_string(), + })); } else { let translation = line[1..].trim().split(":").collect::>(); if translation.len() != 2 { - return Err(Error::from(ParseError { line: index + 1, message: "an entry should contain two parts separated by “:”.".to_string() })) + 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() })) + return Err(Error::from(ParseError { + line: index + 1, + message: "an entry should contain two parts separated by “:”." + .to_string(), + })); } else { entries.push(Entry { part_1: serialization::line_to_words(&t1.to_string()), diff --git a/src/main.rs b/src/main.rs index 6ef241b..f9d2288 100644 --- a/src/main.rs +++ b/src/main.rs @@ -22,6 +22,6 @@ fn main() -> Result<()> { let opt = Opt::from_args(); let conn = db::db::init(opt.database)?; let entries = deck::read(opt.deck)?; - db::db::add_missing_deck_entries(&conn, entries)?; + db::db::synchronize(&conn, entries)?; gui::gui::start(&conn) } diff --git a/src/model/deck.rs b/src/model/deck.rs deleted file mode 100644 index 769b38c..0000000 --- a/src/model/deck.rs +++ /dev/null @@ -1,5 +0,0 @@ -#[derive(Debug, Clone)] -pub struct Entry { - pub part_1: Vec, - pub part_2: Vec, -} diff --git a/src/model/entry.rs b/src/model/entry.rs new file mode 100644 index 0000000..769b38c --- /dev/null +++ b/src/model/entry.rs @@ -0,0 +1,5 @@ +#[derive(Debug, Clone)] +pub struct Entry { + pub part_1: Vec, + pub part_2: Vec, +} diff --git a/src/model/mod.rs b/src/model/mod.rs index bbd7891..185f401 100644 --- a/src/model/mod.rs +++ b/src/model/mod.rs @@ -1,3 +1,3 @@ pub mod card; -pub mod deck; pub mod difficulty; +pub mod entry; diff --git a/src/util/serialization.rs b/src/util/serialization.rs index fcb3062..cc2899f 100644 --- a/src/util/serialization.rs +++ b/src/util/serialization.rs @@ -1,5 +1,8 @@ pub fn line_to_words(line: &String) -> Vec { - line.split("|").map(|w| w.trim().to_string()).filter(|w| !w.is_empty()).collect() + line.split("|") + .map(|w| w.trim().to_string()) + .filter(|w| !w.is_empty()) + .collect() } pub fn words_to_line(words: &Vec) -> String { -- cgit v1.2.3