diff --git a/src/benchmark.rs b/src/benchmark.rs index 94a07b8..f90ce34 100644 --- a/src/benchmark.rs +++ b/src/benchmark.rs @@ -447,8 +447,8 @@ mod tests { let anchor = 1_000_000; assert_eq!(Benchmark::session_timestamp(anchor, 0, 1), anchor); assert_eq!(Benchmark::session_timestamp(anchor, 1, 1), anchor + 86400); - assert_eq!(Benchmark::session_timestamp(anchor, 1, 2), anchor + 172800); - assert_eq!(Benchmark::session_timestamp(anchor, 7, 1), anchor + 604800); + assert_eq!(Benchmark::session_timestamp(anchor, 1, 2), anchor + 172_800); + assert_eq!(Benchmark::session_timestamp(anchor, 7, 1), anchor + 604_800); } /// Verifies that exercise timestamps are calculated correctly within a session. diff --git a/src/data.rs b/src/data.rs index b254f28..5b02539 100644 --- a/src/data.rs +++ b/src/data.rs @@ -99,8 +99,11 @@ impl TryFrom for MasteryScore { //@@lp-example-4 /// The delta between the predicted and actual score for a single exercise trial. -#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] +#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize)] pub struct ExerciseDelta { + /// The ID of the exercise to which the delta belongs. + pub exercise_id: Ustr, + /// The delta between the predicted and actual score. pub delta: f32, @@ -1483,6 +1489,7 @@ mod test { #[test] fn exercise_trial_clone() { let trial = ExerciseTrial { + exercise_id: Ustr::from("exercise"), score: 5.0, timestamp: 1, }; diff --git a/src/error.rs b/src/error.rs index 45d6c69..35de9b7 100644 --- a/src/error.rs +++ b/src/error.rs @@ -60,8 +60,8 @@ pub enum PracticeStatsError { #[error("cannot get scores for unit {0}: {1}")] GetScores(Ustr, #[source] anyhow::Error), - #[error("cannot record score for unit {0}: {1}")] - RecordScore(Ustr, #[source] anyhow::Error), + #[error("cannot record scores: {0}")] + RecordScore(#[source] anyhow::Error), #[error("cannot trim scores: {0}")] TrimScores(#[source] anyhow::Error), @@ -77,8 +77,8 @@ pub enum PracticeDeltasError { #[error("cannot get deltas for unit {0}: {1}")] GetDeltas(Ustr, #[source] anyhow::Error), - #[error("cannot record delta for unit {0}: {1}")] - RecordDelta(Ustr, #[source] anyhow::Error), + #[error("cannot record deltas: {0}")] + RecordDelta(#[source] anyhow::Error), #[error("cannot trim deltas: {0}")] TrimDeltas(#[source] anyhow::Error), diff --git a/src/exercise_scorer.rs b/src/exercise_scorer.rs index 41d3d6c..7e1d365 100644 --- a/src/exercise_scorer.rs +++ b/src/exercise_scorer.rs @@ -536,14 +536,17 @@ mod test { ExerciseTrial { score: 5.0, timestamp: generate_timestamp(0), + ..Default::default() }, ExerciseTrial { score: 4.0, timestamp: generate_timestamp(1), + ..Default::default() }, ExerciseTrial { score: 5.0, timestamp: generate_timestamp(2), + ..Default::default() }, ]; let easy_difficulty = PowerLawScorer::estimate_difficulty(&easy_trials); @@ -554,14 +557,17 @@ mod test { ExerciseTrial { score: 1.0, timestamp: generate_timestamp(0), + ..Default::default() }, ExerciseTrial { score: 2.0, timestamp: generate_timestamp(1), + ..Default::default() }, ExerciseTrial { score: 1.0, timestamp: generate_timestamp(2), + ..Default::default() }, ]; let hard_difficulty = PowerLawScorer::estimate_difficulty(&hard_trials); @@ -572,14 +578,17 @@ mod test { ExerciseTrial { score: 3.0, timestamp: generate_timestamp(0), + ..Default::default() }, ExerciseTrial { score: 4.0, timestamp: generate_timestamp(1), + ..Default::default() }, ExerciseTrial { score: 2.0, timestamp: generate_timestamp(2), + ..Default::default() }, ]; let medium_difficulty = PowerLawScorer::estimate_difficulty(&medium_trials); @@ -590,22 +599,27 @@ mod test { ExerciseTrial { score: 3.0, timestamp: generate_timestamp(0), + ..Default::default() }, ExerciseTrial { score: 1.0, timestamp: generate_timestamp(1), + ..Default::default() }, ExerciseTrial { score: 2.0, timestamp: generate_timestamp(2), + ..Default::default() }, ExerciseTrial { score: 5.0, timestamp: generate_timestamp(3), + ..Default::default() }, ExerciseTrial { score: 5.0, timestamp: generate_timestamp(4), + ..Default::default() }, ]; let mixed_difficulty = PowerLawScorer::estimate_difficulty(&mixed_trials); @@ -628,14 +642,17 @@ mod test { ExerciseTrial { score: 3.0, timestamp: generate_timestamp(1), + ..Default::default() }, ExerciseTrial { score: 4.0, timestamp: generate_timestamp(2), + ..Default::default() }, ExerciseTrial { score: 5.0, timestamp: generate_timestamp(3), + ..Default::default() }, ]; @@ -659,6 +676,7 @@ mod test { &[ExerciseTrial { score: 5.0, timestamp: generate_timestamp(1e10 as i64), + ..Default::default() }], &[], Utc::now().timestamp(), @@ -678,10 +696,12 @@ mod test { ExerciseTrial { score: 5.0, timestamp: i64::MAX, + ..Default::default() }, ExerciseTrial { score: 1.0, timestamp: i64::MIN, + ..Default::default() }, ], &[], @@ -700,14 +720,17 @@ mod test { ExerciseTrial { score: 1.0, // Bad: P = -0.5, stability decreases timestamp: generate_timestamp(3), + ..Default::default() }, ExerciseTrial { score: 5.0, // Good: P = 0.5, stability increases timestamp: generate_timestamp(2), + ..Default::default() }, ExerciseTrial { score: 3.0, // Medium: P = 0.0, stability unchanged timestamp: generate_timestamp(1), + ..Default::default() }, ]; let stability = @@ -723,28 +746,34 @@ mod test { ExerciseTrial { score: 4.0, timestamp: generate_timestamp(1), + ..Default::default() }, ExerciseTrial { score: 4.0, timestamp: generate_timestamp(2), + ..Default::default() }, ExerciseTrial { score: 4.0, timestamp: generate_timestamp(3), + ..Default::default() }, ]; let long_spacing_trials = vec![ ExerciseTrial { score: 4.0, timestamp: generate_timestamp(1), + ..Default::default() }, ExerciseTrial { score: 4.0, timestamp: generate_timestamp(10), + ..Default::default() }, ExerciseTrial { score: 4.0, timestamp: generate_timestamp(30), + ..Default::default() }, ]; @@ -769,28 +798,34 @@ mod test { ExerciseTrial { score: 3.0, timestamp: generate_timestamp(1), + ..Default::default() }, ExerciseTrial { score: 3.0, timestamp: generate_timestamp(2), + ..Default::default() }, ExerciseTrial { score: 3.0, timestamp: generate_timestamp(3), + ..Default::default() }, ]; let lapse_trials = vec![ ExerciseTrial { score: 1.0, timestamp: generate_timestamp(1), + ..Default::default() }, ExerciseTrial { score: 3.0, timestamp: generate_timestamp(2), + ..Default::default() }, ExerciseTrial { score: 3.0, timestamp: generate_timestamp(3), + ..Default::default() }, ]; @@ -817,14 +852,17 @@ mod test { ExerciseTrial { score: 1.0, timestamp: generate_timestamp(3), + ..Default::default() }, ExerciseTrial { score: 1.0, timestamp: generate_timestamp(2), + ..Default::default() }, ExerciseTrial { score: 1.0, timestamp: generate_timestamp(1), + ..Default::default() }, ]; @@ -977,6 +1015,7 @@ mod test { let single_trial = vec![ExerciseTrial { score: 5.0, timestamp: generate_timestamp(0), + ..Default::default() }]; let mean = PowerLawScorer::compute_weighted_avg(&single_trial); assert!((mean - 5.0).abs() < 1e-6); @@ -986,14 +1025,17 @@ mod test { ExerciseTrial { score: 5.0, timestamp: generate_timestamp(0), + ..Default::default() }, ExerciseTrial { score: 4.0, timestamp: generate_timestamp(1), + ..Default::default() }, ExerciseTrial { score: 3.0, timestamp: generate_timestamp(2), + ..Default::default() }, ]; let weighted = PowerLawScorer::compute_weighted_avg(&multi_trials); @@ -1004,28 +1046,34 @@ mod test { ExerciseTrial { score: 5.0, timestamp: generate_timestamp(0), + ..Default::default() }, ExerciseTrial { score: 1.0, timestamp: generate_timestamp(1), + ..Default::default() }, ExerciseTrial { score: 1.0, timestamp: generate_timestamp(2), + ..Default::default() }, ]; let sparse_low_tail = vec![ ExerciseTrial { score: 5.0, timestamp: generate_timestamp(0), + ..Default::default() }, ExerciseTrial { score: 1.0, timestamp: generate_timestamp(1), + ..Default::default() }, ExerciseTrial { score: 1.0, timestamp: generate_timestamp(30), + ..Default::default() }, ]; let dense_weighted = PowerLawScorer::compute_weighted_avg(&dense_low_tail); @@ -1037,24 +1085,29 @@ mod test { ExerciseTrial { score: 5.0, timestamp: generate_timestamp(0), + ..Default::default() }, ExerciseTrial { score: 4.0, timestamp: generate_timestamp(1), + ..Default::default() }, ]; let with_ancient = vec![ ExerciseTrial { score: 5.0, timestamp: generate_timestamp(0), + ..Default::default() }, ExerciseTrial { score: 4.0, timestamp: generate_timestamp(1), + ..Default::default() }, ExerciseTrial { score: 1.0, timestamp: generate_timestamp(365), + ..Default::default() }, ]; let compact_weighted = PowerLawScorer::compute_weighted_avg(&compact); @@ -1095,18 +1148,22 @@ mod test { ExerciseTrial { score: 1.0, timestamp: generate_timestamp(3), + ..Default::default() }, ExerciseTrial { score: 3.0, timestamp: generate_timestamp(7), + ..Default::default() }, ExerciseTrial { score: 2.0, timestamp: generate_timestamp(10), + ..Default::default() }, ExerciseTrial { score: 1.0, timestamp: generate_timestamp(13), + ..Default::default() }, ]; let score = score_helper( @@ -1126,42 +1183,52 @@ mod test { ExerciseTrial { score: 3.0, timestamp: generate_timestamp(1), + ..Default::default() }, ExerciseTrial { score: 4.0, timestamp: generate_timestamp(4), + ..Default::default() }, ExerciseTrial { score: 2.0, timestamp: generate_timestamp(5), + ..Default::default() }, ExerciseTrial { score: 5.0, timestamp: generate_timestamp(6), + ..Default::default() }, ExerciseTrial { score: 3.0, timestamp: generate_timestamp(7), + ..Default::default() }, ExerciseTrial { score: 4.0, timestamp: generate_timestamp(10), + ..Default::default() }, ExerciseTrial { score: 2.0, timestamp: generate_timestamp(14), + ..Default::default() }, ExerciseTrial { score: 3.0, timestamp: generate_timestamp(18), + ..Default::default() }, ExerciseTrial { score: 4.0, timestamp: generate_timestamp(21), + ..Default::default() }, ExerciseTrial { score: 3.0, timestamp: generate_timestamp(25), + ..Default::default() }, ]; let score = score_helper( @@ -1183,10 +1250,12 @@ mod test { ExerciseTrial { score: 3.0, timestamp: generate_timestamp(2), + ..Default::default() }, ExerciseTrial { score: 4.0, timestamp: generate_timestamp(1), + ..Default::default() }, ], &[], @@ -1203,6 +1272,7 @@ mod test { &[ExerciseTrial { score: 5.0, timestamp: generate_timestamp(100), + ..Default::default() }], &[], Utc::now().timestamp(), @@ -1218,34 +1288,42 @@ mod test { ExerciseTrial { score: 5.0, timestamp: generate_timestamp(0), + ..Default::default() }, ExerciseTrial { score: 4.0, timestamp: generate_timestamp(1), + ..Default::default() }, ExerciseTrial { score: 5.0, timestamp: generate_timestamp(2), + ..Default::default() }, ExerciseTrial { score: 5.0, timestamp: generate_timestamp(3), + ..Default::default() }, ExerciseTrial { score: 4.0, timestamp: generate_timestamp(4), + ..Default::default() }, ExerciseTrial { score: 5.0, timestamp: generate_timestamp(5), + ..Default::default() }, ExerciseTrial { score: 5.0, timestamp: generate_timestamp(6), + ..Default::default() }, ExerciseTrial { score: 4.0, timestamp: generate_timestamp(7), + ..Default::default() }, ]; let score = score_helper( @@ -1265,34 +1343,42 @@ mod test { ExerciseTrial { score: 1.0, timestamp: generate_timestamp(0), + ..Default::default() }, ExerciseTrial { score: 2.0, timestamp: generate_timestamp(2), + ..Default::default() }, ExerciseTrial { score: 1.0, timestamp: generate_timestamp(4), + ..Default::default() }, ExerciseTrial { score: 4.0, timestamp: generate_timestamp(6), + ..Default::default() }, ExerciseTrial { score: 2.0, timestamp: generate_timestamp(9), + ..Default::default() }, ExerciseTrial { score: 1.0, timestamp: generate_timestamp(15), + ..Default::default() }, ExerciseTrial { score: 1.0, timestamp: generate_timestamp(16), + ..Default::default() }, ExerciseTrial { score: 2.0, timestamp: generate_timestamp(27), + ..Default::default() }, ]; let score = score_helper( @@ -1313,26 +1399,32 @@ mod test { ExerciseTrial { score: 5.0, timestamp: generate_timestamp(200), + ..Default::default() }, ExerciseTrial { score: 4.0, timestamp: generate_timestamp(210), + ..Default::default() }, ExerciseTrial { score: 5.0, timestamp: generate_timestamp(213), + ..Default::default() }, ExerciseTrial { score: 5.0, timestamp: generate_timestamp(248), + ..Default::default() }, ExerciseTrial { score: 4.0, timestamp: generate_timestamp(256), + ..Default::default() }, ExerciseTrial { score: 4.0, timestamp: generate_timestamp(270), + ..Default::default() }, ]; let score = score_helper( @@ -1360,26 +1452,32 @@ mod test { ExerciseTrial { score: 5.0, timestamp: generate_timestamp(400), + ..Default::default() }, ExerciseTrial { score: 4.0, timestamp: generate_timestamp(410), + ..Default::default() }, ExerciseTrial { score: 5.0, timestamp: generate_timestamp(411), + ..Default::default() }, ExerciseTrial { score: 5.0, timestamp: generate_timestamp(420), + ..Default::default() }, ExerciseTrial { score: 5.0, timestamp: generate_timestamp(430), + ..Default::default() }, ExerciseTrial { score: 4.0, timestamp: generate_timestamp(431), + ..Default::default() }, ]; let score = score_helper( @@ -1405,10 +1503,12 @@ mod test { let recent_trials = vec![ExerciseTrial { score: 5.0, timestamp: generate_timestamp(1), + ..Default::default() }]; let old_trials = vec![ExerciseTrial { score: 5.0, timestamp: generate_timestamp(30), + ..Default::default() }]; let recent = score_helper( @@ -1435,6 +1535,7 @@ mod test { let trials = vec![ExerciseTrial { score: 3.0, timestamp: generate_timestamp(0), + ..Default::default() }]; assert_eq!(PowerLawScorer::velocity(&trials), None); } @@ -1447,22 +1548,27 @@ mod test { ExerciseTrial { score: 5.0, timestamp: generate_timestamp(0), + ..Default::default() }, ExerciseTrial { score: 4.0, timestamp: generate_timestamp(1), + ..Default::default() }, ExerciseTrial { score: 3.0, timestamp: generate_timestamp(2), + ..Default::default() }, ExerciseTrial { score: 2.0, timestamp: generate_timestamp(3), + ..Default::default() }, ExerciseTrial { score: 1.0, timestamp: generate_timestamp(4), + ..Default::default() }, ]; let velocity = PowerLawScorer::velocity(&trials).unwrap(); @@ -1477,22 +1583,27 @@ mod test { ExerciseTrial { score: 1.0, timestamp: generate_timestamp(0), + ..Default::default() }, ExerciseTrial { score: 2.0, timestamp: generate_timestamp(1), + ..Default::default() }, ExerciseTrial { score: 3.0, timestamp: generate_timestamp(2), + ..Default::default() }, ExerciseTrial { score: 4.0, timestamp: generate_timestamp(3), + ..Default::default() }, ExerciseTrial { score: 5.0, timestamp: generate_timestamp(4), + ..Default::default() }, ]; let velocity = PowerLawScorer::velocity(&trials).unwrap(); @@ -1506,14 +1617,17 @@ mod test { ExerciseTrial { score: 3.0, timestamp: generate_timestamp(0), + ..Default::default() }, ExerciseTrial { score: 3.0, timestamp: generate_timestamp(1), + ..Default::default() }, ExerciseTrial { score: 3.0, timestamp: generate_timestamp(2), + ..Default::default() }, ]; let velocity = PowerLawScorer::velocity(&trials).unwrap(); @@ -1527,24 +1641,29 @@ mod test { ExerciseTrial { score: 3.0, timestamp: generate_timestamp(0), + ..Default::default() }, ExerciseTrial { score: 4.0, timestamp: generate_timestamp(1), + ..Default::default() }, ExerciseTrial { score: 4.0, timestamp: generate_timestamp(2), + ..Default::default() }, ]; let deltas = vec![ ExerciseDelta { delta: 0.5, timestamp: generate_timestamp(1), + ..Default::default() }, ExerciseDelta { delta: 1.2, timestamp: generate_timestamp(2), + ..Default::default() }, ]; @@ -1571,24 +1690,29 @@ mod test { ExerciseTrial { score: 3.0, timestamp: generate_timestamp(0), + ..Default::default() }, ExerciseTrial { score: 4.0, timestamp: generate_timestamp(1), + ..Default::default() }, ExerciseTrial { score: 4.0, timestamp: generate_timestamp(2), + ..Default::default() }, ]; let deltas = vec![ ExerciseDelta { delta: -0.5, timestamp: generate_timestamp(1), + ..Default::default() }, ExerciseDelta { delta: -0.8, timestamp: generate_timestamp(2), + ..Default::default() }, ]; diff --git a/src/lib.rs b/src/lib.rs index 193e71c..4a02d77 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -534,15 +534,11 @@ impl PracticeDeltas for Trane { .get_deltas(exercise_id, num_deltas) } - fn record_exercise_delta( + fn record_exercise_deltas( &mut self, - exercise_id: Ustr, - delta: f32, - timestamp: i64, + deltas: &[ExerciseDelta], ) -> Result<(), PracticeDeltasError> { - self.practice_deltas - .write() - .record_exercise_delta(exercise_id, delta, timestamp) + self.practice_deltas.write().record_exercise_deltas(deltas) } fn trim_deltas(&mut self, num_deltas: u32) -> Result<(), PracticeDeltasError> { @@ -568,15 +564,11 @@ impl PracticeStats for Trane { .get_scores(exercise_id, num_scores) } - fn record_exercise_score( + fn record_exercise_scores( &mut self, - exercise_id: Ustr, - score: MasteryScore, - timestamp: i64, + trials: &[ExerciseTrial], ) -> Result<(), PracticeStatsError> { - self.practice_stats - .write() - .record_exercise_score(exercise_id, score, timestamp) + self.practice_stats.write().record_exercise_scores(trials) } fn trim_scores(&mut self, num_scores: u32) -> Result<(), PracticeStatsError> { diff --git a/src/practice_deltas.rs b/src/practice_deltas.rs index 213fccd..78c10c9 100644 --- a/src/practice_deltas.rs +++ b/src/practice_deltas.rs @@ -23,13 +23,11 @@ pub trait PracticeDeltas { num_deltas: u32, ) -> Result, PracticeDeltasError>; - /// Records the delta between the student's actual score and the predicted score for a - /// particular exercise. - fn record_exercise_delta( + /// Records the deltas between the student's actual scores and the predicted scores for one or + /// more exercises. + fn record_exercise_deltas( &mut self, - exercise_id: Ustr, - delta: f32, - timestamp: i64, + deltas: &[ExerciseDelta], ) -> Result<(), PracticeDeltasError>; /// Deletes all the exercise trials except for the last `num_deltas` with the aim of keeping the @@ -104,32 +102,36 @@ impl LocalPracticeDeltas { .query_map(params![exercise_id.as_str(), num_deltas], |row| { let delta = row.get(0)?; let timestamp = row.get(1)?; - rusqlite::Result::Ok(ExerciseDelta { delta, timestamp }) + rusqlite::Result::Ok(ExerciseDelta { + exercise_id, + delta, + timestamp, + }) })? .map(|r| r.context("failed to retrieve deltas from practice deltas DB")) .collect::, _>>()?; Ok(rows) } - /// Helper function to record a delta to the database. - fn record_exercise_delta_helper( - &mut self, - exercise_id: Ustr, - delta: f32, - timestamp: i64, - ) -> Result<()> { + /// Helper function to record deltas to the database. + fn record_exercise_deltas_helper(&mut self, deltas: &[ExerciseDelta]) -> Result<()> { let mut connection = self.connection.lock(); let tx = connection.transaction()?; { let mut uid_stmt = tx.prepare_cached("INSERT OR IGNORE INTO uids(unit_id) VALUES ($1);")?; - uid_stmt.execute(params![exercise_id.as_str()])?; - let mut stmt = tx.prepare_cached( "INSERT INTO practice_deltas (unit_uid, delta, timestamp) VALUES ( (SELECT unit_uid FROM uids WHERE unit_id = $1), $2, $3);", )?; - stmt.execute(params![exercise_id.as_str(), delta, timestamp])?; + for delta in deltas { + uid_stmt.execute(params![delta.exercise_id.as_str()])?; + stmt.execute(params![ + delta.exercise_id.as_str(), + delta.delta, + delta.timestamp + ])?; + } } tx.commit()?; Ok(()) @@ -188,14 +190,12 @@ impl PracticeDeltas for LocalPracticeDeltas { .map_err(|e| PracticeDeltasError::GetDeltas(exercise_id, e)) } - fn record_exercise_delta( + fn record_exercise_deltas( &mut self, - exercise_id: Ustr, - delta: f32, - timestamp: i64, + deltas: &[ExerciseDelta], ) -> Result<(), PracticeDeltasError> { - self.record_exercise_delta_helper(exercise_id, delta, timestamp) - .map_err(|e| PracticeDeltasError::RecordDelta(exercise_id, e)) + self.record_exercise_deltas_helper(deltas) + .map_err(PracticeDeltasError::RecordDelta) } fn trim_deltas(&mut self, num_deltas: u32) -> Result<(), PracticeDeltasError> { @@ -221,6 +221,14 @@ mod test { practice_deltas::{LocalPracticeDeltas, PracticeDeltas}, }; + fn delta(exercise_id: Ustr, value: f32, timestamp: i64) -> ExerciseDelta { + ExerciseDelta { + exercise_id, + delta: value, + timestamp, + } + } + fn new_test_deltas() -> Result> { let practice_deltas = LocalPracticeDeltas::new(Connection::open_in_memory()?)?; Ok(Box::new(practice_deltas)) @@ -247,7 +255,7 @@ mod test { fn basic() -> Result<()> { let mut deltas = new_test_deltas()?; let exercise_id = Ustr::from("ex_123"); - deltas.record_exercise_delta(exercise_id, 0.5, 1)?; + deltas.record_exercise_deltas(&[delta(exercise_id, 0.5, 1)])?; let results = deltas.get_deltas(exercise_id, 1)?; assert_deltas(&[0.5], &results); Ok(()) @@ -258,9 +266,11 @@ mod test { fn multiple_records() -> Result<()> { let mut deltas = new_test_deltas()?; let exercise_id = Ustr::from("ex_123"); - deltas.record_exercise_delta(exercise_id, 0.1, 1)?; - deltas.record_exercise_delta(exercise_id, 0.3, 2)?; - deltas.record_exercise_delta(exercise_id, 0.5, 3)?; + deltas.record_exercise_deltas(&[ + delta(exercise_id, 0.1, 1), + delta(exercise_id, 0.3, 2), + delta(exercise_id, 0.5, 3), + ])?; let one_delta = deltas.get_deltas(exercise_id, 1)?; assert_deltas(&[0.5], &one_delta); @@ -287,14 +297,18 @@ mod test { fn trim_deltas_some_removed() -> Result<()> { let mut deltas = new_test_deltas()?; let exercise1_id = Ustr::from("exercise1"); - deltas.record_exercise_delta(exercise1_id, 0.1, 1)?; - deltas.record_exercise_delta(exercise1_id, 0.2, 2)?; - deltas.record_exercise_delta(exercise1_id, 0.3, 3)?; + deltas.record_exercise_deltas(&[ + delta(exercise1_id, 0.1, 1), + delta(exercise1_id, 0.2, 2), + delta(exercise1_id, 0.3, 3), + ])?; let exercise2_id = Ustr::from("exercise2"); - deltas.record_exercise_delta(exercise2_id, -0.1, 1)?; - deltas.record_exercise_delta(exercise2_id, -0.2, 2)?; - deltas.record_exercise_delta(exercise2_id, -0.3, 3)?; + deltas.record_exercise_deltas(&[ + delta(exercise2_id, -0.1, 1), + delta(exercise2_id, -0.2, 2), + delta(exercise2_id, -0.3, 3), + ])?; deltas.trim_deltas(2)?; @@ -310,14 +324,18 @@ mod test { fn trim_deltas_none_removed() -> Result<()> { let mut deltas = new_test_deltas()?; let exercise1_id = Ustr::from("exercise1"); - deltas.record_exercise_delta(exercise1_id, 0.1, 1)?; - deltas.record_exercise_delta(exercise1_id, 0.2, 2)?; - deltas.record_exercise_delta(exercise1_id, 0.3, 3)?; + deltas.record_exercise_deltas(&[ + delta(exercise1_id, 0.1, 1), + delta(exercise1_id, 0.2, 2), + delta(exercise1_id, 0.3, 3), + ])?; let exercise2_id = Ustr::from("exercise2"); - deltas.record_exercise_delta(exercise2_id, -0.1, 1)?; - deltas.record_exercise_delta(exercise2_id, -0.2, 2)?; - deltas.record_exercise_delta(exercise2_id, -0.3, 3)?; + deltas.record_exercise_deltas(&[ + delta(exercise2_id, -0.1, 1), + delta(exercise2_id, -0.2, 2), + delta(exercise2_id, -0.3, 3), + ])?; deltas.trim_deltas(10)?; @@ -333,19 +351,25 @@ mod test { fn remove_deltas_with_prefix() -> Result<()> { let mut deltas = new_test_deltas()?; let exercise1_id = Ustr::from("exercise1"); - deltas.record_exercise_delta(exercise1_id, 0.1, 1)?; - deltas.record_exercise_delta(exercise1_id, 0.2, 2)?; - deltas.record_exercise_delta(exercise1_id, 0.3, 3)?; + deltas.record_exercise_deltas(&[ + delta(exercise1_id, 0.1, 1), + delta(exercise1_id, 0.2, 2), + delta(exercise1_id, 0.3, 3), + ])?; let exercise2_id = Ustr::from("exercise2"); - deltas.record_exercise_delta(exercise2_id, -0.1, 1)?; - deltas.record_exercise_delta(exercise2_id, -0.2, 2)?; - deltas.record_exercise_delta(exercise2_id, -0.3, 3)?; + deltas.record_exercise_deltas(&[ + delta(exercise2_id, -0.1, 1), + delta(exercise2_id, -0.2, 2), + delta(exercise2_id, -0.3, 3), + ])?; let exercise3_id = Ustr::from("exercise3"); - deltas.record_exercise_delta(exercise3_id, 0.4, 1)?; - deltas.record_exercise_delta(exercise3_id, 0.5, 2)?; - deltas.record_exercise_delta(exercise3_id, 0.6, 3)?; + deltas.record_exercise_deltas(&[ + delta(exercise3_id, 0.4, 1), + delta(exercise3_id, 0.5, 2), + delta(exercise3_id, 0.6, 3), + ])?; // Remove the prefix "exercise1". deltas.remove_deltas_with_prefix("exercise1")?; diff --git a/src/practice_stats.rs b/src/practice_stats.rs index 412fe3b..9093a79 100644 --- a/src/practice_stats.rs +++ b/src/practice_stats.rs @@ -10,11 +10,7 @@ use rusqlite::{Connection, params}; use rusqlite_migration::{M, Migrations}; use ustr::Ustr; -use crate::{ - data::{ExerciseTrial, MasteryScore}, - error::PracticeStatsError, - utils, -}; +use crate::{data::ExerciseTrial, error::PracticeStatsError, utils}; /// Contains functions to retrieve and record the scores from each exercise trial. pub trait PracticeStats { @@ -26,15 +22,12 @@ pub trait PracticeStats { num_scores: u32, ) -> Result, PracticeStatsError>; - /// Records the score assigned to the exercise in a particular trial. Therefore, the score is a - /// value of the `MasteryScore` enum instead of a float. Only units of type `UnitType::Exercise` - /// should have scores recorded. However, the enforcement of this requirement is left to the - /// caller. - fn record_exercise_score( + /// Records the scores assigned to exercises in one or more trials. Only units of type + /// `UnitType::Exercise` should have scores recorded. However, the enforcement of this + /// requirement is left to the caller. + fn record_exercise_scores( &mut self, - exercise_id: Ustr, - score: MasteryScore, - timestamp: i64, + trials: &[ExerciseTrial], ) -> Result<(), PracticeStatsError>; /// Deletes all the exercise trials except for the last `num_scores` with the aim of keeping the @@ -126,20 +119,19 @@ impl LocalPracticeStats { .query_map(params![exercise_id.as_str(), num_scores], |row| { let score = row.get(0)?; let timestamp = row.get(1)?; - rusqlite::Result::Ok(ExerciseTrial { score, timestamp }) + rusqlite::Result::Ok(ExerciseTrial { + exercise_id, + score, + timestamp, + }) })? .map(|r| r.context("failed to retrieve scores from practice stats DB")) .collect::, _>>()?; Ok(rows) } - /// Helper function to record a score to the database. - fn record_exercise_score_helper( - &mut self, - exercise_id: Ustr, - score: &MasteryScore, - timestamp: i64, - ) -> Result<()> { + /// Helper function to record scores to the database. + fn record_exercise_scores_helper(&mut self, trials: &[ExerciseTrial]) -> Result<()> { // Update the mapping of unit ID to unique integer ID and add the trial in a single // transaction. let mut connection = self.connection.lock(); @@ -147,17 +139,18 @@ impl LocalPracticeStats { { let mut uid_stmt = tx.prepare_cached("INSERT OR IGNORE INTO uids(unit_id) VALUES ($1);")?; - uid_stmt.execute(params![exercise_id.as_str()])?; - let mut stmt = tx.prepare_cached( "INSERT INTO practice_stats (unit_uid, score, timestamp) VALUES ( (SELECT unit_uid FROM uids WHERE unit_id = $1), $2, $3);", )?; - stmt.execute(params![ - exercise_id.as_str(), - score.float_score(), - timestamp - ])?; + for trial in trials { + uid_stmt.execute(params![trial.exercise_id.as_str()])?; + stmt.execute(params![ + trial.exercise_id.as_str(), + trial.score, + trial.timestamp + ])?; + } } tx.commit()?; Ok(()) @@ -222,14 +215,12 @@ impl PracticeStats for LocalPracticeStats { .map_err(|e| PracticeStatsError::GetScores(exercise_id, e)) } - fn record_exercise_score( + fn record_exercise_scores( &mut self, - exercise_id: Ustr, - score: MasteryScore, - timestamp: i64, + trials: &[ExerciseTrial], ) -> Result<(), PracticeStatsError> { - self.record_exercise_score_helper(exercise_id, &score, timestamp) - .map_err(|e| PracticeStatsError::RecordScore(exercise_id, e)) + self.record_exercise_scores_helper(trials) + .map_err(PracticeStatsError::RecordScore) } fn trim_scores(&mut self, num_scores: u32) -> Result<(), PracticeStatsError> { @@ -251,10 +242,18 @@ mod test { use ustr::Ustr; use crate::{ - data::{ExerciseTrial, MasteryScore}, + data::ExerciseTrial, practice_stats::{LocalPracticeStats, PracticeStats}, }; + fn trial(exercise_id: Ustr, score: f32, timestamp: i64) -> ExerciseTrial { + ExerciseTrial { + exercise_id, + score, + timestamp, + } + } + fn new_tests_stats() -> Result> { let practice_stats = LocalPracticeStats::new(Connection::open_in_memory()?)?; Ok(Box::new(practice_stats)) @@ -281,7 +280,7 @@ mod test { fn basic() -> Result<()> { let mut stats = new_tests_stats()?; let exercise_id = Ustr::from("ex_123"); - stats.record_exercise_score(exercise_id, MasteryScore::Five, 1)?; + stats.record_exercise_scores(&[trial(exercise_id, 5.0, 1)])?; let scores = stats.get_scores(exercise_id, 1)?; assert_scores(&[5.0], &scores); Ok(()) @@ -292,9 +291,11 @@ mod test { fn multiple_records() -> Result<()> { let mut stats = new_tests_stats()?; let exercise_id = Ustr::from("ex_123"); - stats.record_exercise_score(exercise_id, MasteryScore::Three, 1)?; - stats.record_exercise_score(exercise_id, MasteryScore::Four, 2)?; - stats.record_exercise_score(exercise_id, MasteryScore::Five, 3)?; + stats.record_exercise_scores(&[ + trial(exercise_id, 3.0, 1), + trial(exercise_id, 4.0, 2), + trial(exercise_id, 5.0, 3), + ])?; let one_score = stats.get_scores(exercise_id, 1)?; assert_scores(&[5.0], &one_score); @@ -321,14 +322,18 @@ mod test { fn trim_scores_some_scores_removed() -> Result<()> { let mut stats = new_tests_stats()?; let exercise1_id = Ustr::from("exercise1"); - stats.record_exercise_score(exercise1_id, MasteryScore::Three, 1)?; - stats.record_exercise_score(exercise1_id, MasteryScore::Four, 2)?; - stats.record_exercise_score(exercise1_id, MasteryScore::Five, 3)?; + stats.record_exercise_scores(&[ + trial(exercise1_id, 3.0, 1), + trial(exercise1_id, 4.0, 2), + trial(exercise1_id, 5.0, 3), + ])?; let exercise2_id = Ustr::from("exercise2"); - stats.record_exercise_score(exercise2_id, MasteryScore::One, 1)?; - stats.record_exercise_score(exercise2_id, MasteryScore::One, 2)?; - stats.record_exercise_score(exercise2_id, MasteryScore::Three, 3)?; + stats.record_exercise_scores(&[ + trial(exercise2_id, 1.0, 1), + trial(exercise2_id, 1.0, 2), + trial(exercise2_id, 3.0, 3), + ])?; stats.trim_scores(2)?; @@ -344,14 +349,18 @@ mod test { fn trim_scores_no_scores_removed() -> Result<()> { let mut stats = new_tests_stats()?; let exercise1_id = Ustr::from("exercise1"); - stats.record_exercise_score(exercise1_id, MasteryScore::Three, 1)?; - stats.record_exercise_score(exercise1_id, MasteryScore::Four, 2)?; - stats.record_exercise_score(exercise1_id, MasteryScore::Five, 3)?; + stats.record_exercise_scores(&[ + trial(exercise1_id, 3.0, 1), + trial(exercise1_id, 4.0, 2), + trial(exercise1_id, 5.0, 3), + ])?; let exercise2_id = Ustr::from("exercise2"); - stats.record_exercise_score(exercise2_id, MasteryScore::One, 1)?; - stats.record_exercise_score(exercise2_id, MasteryScore::One, 2)?; - stats.record_exercise_score(exercise2_id, MasteryScore::Three, 3)?; + stats.record_exercise_scores(&[ + trial(exercise2_id, 1.0, 1), + trial(exercise2_id, 1.0, 2), + trial(exercise2_id, 3.0, 3), + ])?; stats.trim_scores(10)?; @@ -367,19 +376,25 @@ mod test { fn remove_scores_with_prefix() -> Result<()> { let mut stats = new_tests_stats()?; let exercise1_id = Ustr::from("exercise1"); - stats.record_exercise_score(exercise1_id, MasteryScore::Three, 1)?; - stats.record_exercise_score(exercise1_id, MasteryScore::Four, 2)?; - stats.record_exercise_score(exercise1_id, MasteryScore::Five, 3)?; + stats.record_exercise_scores(&[ + trial(exercise1_id, 3.0, 1), + trial(exercise1_id, 4.0, 2), + trial(exercise1_id, 5.0, 3), + ])?; let exercise2_id = Ustr::from("exercise2"); - stats.record_exercise_score(exercise2_id, MasteryScore::One, 1)?; - stats.record_exercise_score(exercise2_id, MasteryScore::One, 2)?; - stats.record_exercise_score(exercise2_id, MasteryScore::Three, 3)?; + stats.record_exercise_scores(&[ + trial(exercise2_id, 1.0, 1), + trial(exercise2_id, 1.0, 2), + trial(exercise2_id, 3.0, 3), + ])?; let exercise3_id = Ustr::from("exercise3"); - stats.record_exercise_score(exercise3_id, MasteryScore::One, 1)?; - stats.record_exercise_score(exercise3_id, MasteryScore::One, 2)?; - stats.record_exercise_score(exercise3_id, MasteryScore::Three, 3)?; + stats.record_exercise_scores(&[ + trial(exercise3_id, 1.0, 1), + trial(exercise3_id, 1.0, 2), + trial(exercise3_id, 3.0, 3), + ])?; // Remove the prefix "exercise1". stats.remove_scores_with_prefix("exercise1")?; diff --git a/src/reward_scorer.rs b/src/reward_scorer.rs index 5bcce77..8706220 100644 --- a/src/reward_scorer.rs +++ b/src/reward_scorer.rs @@ -360,6 +360,7 @@ mod test { let trials = vec![ExerciseTrial { score: 2.0, timestamp: generate_timestamp(1), + ..Default::default() }]; assert!(!scorer.apply_reward(0.5, &trials)); assert!(!scorer.apply_reward(-1.0, &trials)); @@ -370,14 +371,17 @@ mod test { ExerciseTrial { score: 2.0, timestamp: generate_timestamp(1), + ..Default::default() }, ExerciseTrial { score: 2.0, timestamp: generate_timestamp(8), + ..Default::default() }, ExerciseTrial { score: 3.0, timestamp: generate_timestamp(10), + ..Default::default() }, ]; assert!(!scorer.apply_reward(0.5, &trials)); @@ -389,14 +393,17 @@ mod test { ExerciseTrial { score: 4.0, timestamp: generate_timestamp(1), + ..Default::default() }, ExerciseTrial { score: 5.0, timestamp: generate_timestamp(8), + ..Default::default() }, ExerciseTrial { score: 4.0, timestamp: generate_timestamp(10), + ..Default::default() }, ]; assert!(!scorer.apply_reward(-0.5, &trials)); @@ -407,14 +414,17 @@ mod test { ExerciseTrial { score: 3.0, timestamp: generate_timestamp(1), + ..Default::default() }, ExerciseTrial { score: 3.0, timestamp: generate_timestamp(8), + ..Default::default() }, ExerciseTrial { score: 4.0, timestamp: generate_timestamp(10), + ..Default::default() }, ]; assert!(scorer.apply_reward(0.5, &trials)); @@ -422,14 +432,17 @@ mod test { ExerciseTrial { score: 2.0, timestamp: generate_timestamp(1), + ..Default::default() }, ExerciseTrial { score: 3.0, timestamp: generate_timestamp(8), + ..Default::default() }, ExerciseTrial { score: 2.0, timestamp: generate_timestamp(10), + ..Default::default() }, ]; assert!(scorer.apply_reward(-0.5, &trials)); diff --git a/src/scheduler.rs b/src/scheduler.rs index 9a3b492..34351da 100644 --- a/src/scheduler.rs +++ b/src/scheduler.rs @@ -31,8 +31,8 @@ use ustr::{Ustr, UstrMap, UstrSet}; use crate::{ data::{ - ExerciseManifest, FULL_CANDIDATES_SCORE, MasteryScore, PassingScoreOptions, - SchedulerOptions, UnitType, + ExerciseDelta, ExerciseManifest, ExerciseTrial, FULL_CANDIDATES_SCORE, MasteryScore, + PassingScoreOptions, SchedulerOptions, UnitType, filter::{ExerciseFilter, KeyValueFilter, UnitFilter}, }, error::ExerciseSchedulerError, @@ -1112,7 +1112,11 @@ impl ExerciseScheduler for DepthFirstScheduler { self.data .practice_deltas .write() - .record_exercise_delta(exercise_id, delta, timestamp) + .record_exercise_deltas(&[ExerciseDelta { + exercise_id, + delta, + timestamp, + }]) .map_err(|e| ExerciseSchedulerError::ScoreExercise(e.into()))?; } @@ -1121,7 +1125,11 @@ impl ExerciseScheduler for DepthFirstScheduler { self.data .practice_stats .write() - .record_exercise_score(exercise_id, score.clone(), timestamp) + .record_exercise_scores(&[ExerciseTrial { + exercise_id, + score: score.float_score(), + timestamp, + }]) .map_err(|e| ExerciseSchedulerError::ScoreExercise(e.into()))?; self.unit_scorer.invalidate_cached_score(exercise_id); self.relearn_pile.update(exercise_id, &score);