move revlog out of sync code, and add enums

and remove type=4, which does not appear to be used
This commit is contained in:
Damien Elmes 2020-06-16 16:31:36 +10:00
parent b51f03085e
commit 26fc6609a7
5 changed files with 121 additions and 88 deletions

View file

@ -21,4 +21,3 @@ card-stats-review-log-type-learn = Learn
card-stats-review-log-type-review = Review card-stats-review-log-type-review = Review
card-stats-review-log-type-relearn = Relearn card-stats-review-log-type-relearn = Relearn
card-stats-review-log-type-filtered = Filtered card-stats-review-log-type-filtered = Filtered
card-stats-review-log-type-rescheduled = Resched

View file

@ -1,6 +1,60 @@
// Copyright: Ankitects Pty Ltd and contributors // Copyright: Ankitects Pty Ltd and contributors
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html // License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
use crate::define_newtype; use crate::{define_newtype, prelude::*};
use num_enum::TryFromPrimitive;
use serde::Deserialize;
use serde_repr::{Deserialize_repr, Serialize_repr};
use serde_tuple::Serialize_tuple;
define_newtype!(RevlogID, i64); define_newtype!(RevlogID, i64);
#[derive(Serialize_tuple, Deserialize, Debug, Default, PartialEq)]
pub struct RevlogEntry {
pub id: TimestampMillis,
pub cid: CardID,
pub usn: Usn,
/// - In the V1 scheduler, 3 represents easy in the learning case.
/// - 0 represents manual rescheduling.
#[serde(rename = "ease")]
pub button_chosen: u8,
/// Positive values are in days, negative values in seconds.
#[serde(rename = "ivl")]
pub interval: i32,
/// Positive values are in days, negative values in seconds.
#[serde(rename = "lastIvl")]
pub last_interval: i32,
/// Card's ease after answering, stored as 10x the %, eg 2500 represents 250%.
#[serde(rename = "factor")]
pub ease_factor: u32,
/// Amount of milliseconds taken to answer the card.
#[serde(rename = "time")]
pub taken_millis: u32,
#[serde(rename = "type")]
pub review_kind: RevlogReviewKind,
}
#[derive(Serialize_repr, Deserialize_repr, Debug, PartialEq, TryFromPrimitive, Clone, Copy)]
#[repr(u8)]
pub enum RevlogReviewKind {
Learning = 0,
Review = 1,
Relearning = 2,
EarlyReview = 3,
}
impl Default for RevlogReviewKind {
fn default() -> Self {
RevlogReviewKind::Learning
}
}
impl RevlogEntry {
pub(crate) fn interval_secs(&self) -> u32 {
(if self.interval > 0 {
self.interval * 86_400
} else {
-self.interval
}) as u32
}
}

View file

@ -2,7 +2,11 @@
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html // License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
use crate::{ use crate::{
card::CardQueue, i18n::I18n, prelude::*, sched::timespan::time_span, sync::ReviewLogEntry, card::CardQueue,
i18n::I18n,
prelude::*,
revlog::{RevlogEntry, RevlogReviewKind},
sched::timespan::time_span,
}; };
use askama::Template; use askama::Template;
use chrono::prelude::*; use chrono::prelude::*;
@ -23,7 +27,7 @@ struct CardStats {
deck: String, deck: String,
nid: NoteID, nid: NoteID,
cid: CardID, cid: CardID,
revlog: Vec<BasicRevlog>, revlog: Vec<RevlogEntry>,
} }
#[derive(Template)] #[derive(Template)]
@ -40,15 +44,6 @@ enum Due {
Unknown, Unknown,
} }
struct BasicRevlog {
time: TimestampSecs,
kind: u8,
rating: u8,
interval: i32,
ease: u32,
taken_secs: f32,
}
struct RevlogText { struct RevlogText {
time: String, time: String,
kind: String, kind: String,
@ -60,19 +55,6 @@ struct RevlogText {
taken_secs: String, taken_secs: String,
} }
impl From<ReviewLogEntry> for BasicRevlog {
fn from(e: ReviewLogEntry) -> Self {
BasicRevlog {
time: e.id.as_secs(),
kind: e.kind,
rating: e.ease,
interval: e.interval,
ease: e.factor,
taken_secs: (e.time as f32) / 1000.0,
}
}
}
impl Collection { impl Collection {
pub fn card_stats(&mut self, cid: CardID) -> Result<String> { pub fn card_stats(&mut self, cid: CardID) -> Result<String> {
let stats = self.gather_card_stats(cid)?; let stats = self.gather_card_stats(cid)?;
@ -98,7 +80,10 @@ impl Collection {
average_secs = 0.0; average_secs = 0.0;
total_secs = 0.0; total_secs = 0.0;
} else { } else {
total_secs = revlog.iter().map(|e| (e.time as f32) / 1000.0).sum(); total_secs = revlog
.iter()
.map(|e| (e.taken_millis as f32) / 1000.0)
.sum();
average_secs = total_secs / (revlog.len() as f32); average_secs = total_secs / (revlog.len() as f32);
} }
@ -233,49 +218,41 @@ impl Collection {
} }
} }
fn revlog_to_text(e: BasicRevlog, i18n: &I18n, offset: FixedOffset) -> RevlogText { fn revlog_to_text(e: RevlogEntry, i18n: &I18n, offset: FixedOffset) -> RevlogText {
let dt = offset.timestamp(e.time.0, 0); let dt = offset.timestamp(e.id.as_secs().0, 0);
let time = dt.format("<b>%Y-%m-%d</b> @ %H:%M").to_string(); let time = dt.format("<b>%Y-%m-%d</b> @ %H:%M").to_string();
let kind = match e.kind { let kind = match e.review_kind {
0 => i18n.tr(TR::CardStatsReviewLogTypeLearn).into(), RevlogReviewKind::Learning => i18n.tr(TR::CardStatsReviewLogTypeLearn).into(),
1 => i18n.tr(TR::CardStatsReviewLogTypeReview).into(), RevlogReviewKind::Review => i18n.tr(TR::CardStatsReviewLogTypeReview).into(),
2 => i18n.tr(TR::CardStatsReviewLogTypeRelearn).into(), RevlogReviewKind::Relearning => i18n.tr(TR::CardStatsReviewLogTypeRelearn).into(),
3 => i18n.tr(TR::CardStatsReviewLogTypeFiltered).into(), RevlogReviewKind::EarlyReview => i18n.tr(TR::CardStatsReviewLogTypeFiltered).into(),
4 => i18n.tr(TR::CardStatsReviewLogTypeRescheduled).into(),
_ => String::from("?"),
}; };
let kind_class = match e.kind { let kind_class = match e.review_kind {
0 => String::from("revlog-learn"), RevlogReviewKind::Learning => String::from("revlog-learn"),
1 => String::from("revlog-review"), RevlogReviewKind::Review => String::from("revlog-review"),
2 => String::from("revlog-relearn"), RevlogReviewKind::Relearning => String::from("revlog-relearn"),
3 => String::from("revlog-filtered"), RevlogReviewKind::EarlyReview => String::from("revlog-filtered"),
4 => String::from("revlog-rescheduled"),
_ => String::from(""),
}; };
let rating = e.rating.to_string(); let rating = e.button_chosen.to_string();
let interval = if e.interval == 0 { let interval = if e.interval == 0 {
String::from("") String::from("")
} else { } else {
let interval_secs = if e.interval > 0 { let interval_secs = e.interval_secs();
e.interval * 86_400
} else {
e.interval.abs()
};
time_span(interval_secs as f32, i18n, true) time_span(interval_secs as f32, i18n, true)
}; };
let ease = if e.ease > 0 { let ease = if e.ease_factor > 0 {
format!("{:.0}%", (e.ease as f32) / 10.0) format!("{}%", e.ease_factor / 10)
} else { } else {
"".to_string() "".to_string()
}; };
let rating_class = if e.rating == 1 { let rating_class = if e.button_chosen == 1 {
String::from("revlog-ease1") String::from("revlog-ease1")
} else { } else {
"".to_string() "".to_string()
}; };
let taken_secs = i18n.trn( let taken_secs = i18n.trn(
TR::StatisticsSecondsTaken, TR::StatisticsSecondsTaken,
tr_args!["seconds"=>e.taken_secs as i32], tr_args!["seconds"=>(e.taken_millis / 1000) as i32],
); );
RevlogText { RevlogText {

View file

@ -2,21 +2,39 @@
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html // License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
use super::SqliteStorage; use super::SqliteStorage;
use crate::prelude::*; use crate::err::Result;
use crate::{err::Result, sync::ReviewLogEntry}; use crate::{
use rusqlite::{params, Row, NO_PARAMS}; prelude::*,
revlog::{RevlogReviewKind, RevlogEntry},
};
use rusqlite::{
params,
types::{FromSql, FromSqlError, ValueRef},
Row, NO_PARAMS,
};
use std::convert::TryFrom;
fn row_to_revlog_entry(row: &Row) -> Result<ReviewLogEntry> { impl FromSql for RevlogReviewKind {
Ok(ReviewLogEntry { fn column_result(value: ValueRef<'_>) -> std::result::Result<Self, FromSqlError> {
if let ValueRef::Integer(i) = value {
Ok(Self::try_from(i as u8).map_err(|_| FromSqlError::InvalidType)?)
} else {
Err(FromSqlError::InvalidType)
}
}
}
fn row_to_revlog_entry(row: &Row) -> Result<RevlogEntry> {
Ok(RevlogEntry {
id: row.get(0)?, id: row.get(0)?,
cid: row.get(1)?, cid: row.get(1)?,
usn: row.get(2)?, usn: row.get(2)?,
ease: row.get(3)?, button_chosen: row.get(3)?,
interval: row.get(4)?, interval: row.get(4)?,
last_interval: row.get(5)?, last_interval: row.get(5)?,
factor: row.get(6)?, ease_factor: row.get(6)?,
time: row.get(7)?, taken_millis: row.get(7)?,
kind: row.get(8)?, review_kind: row.get(8)?,
}) })
} }
@ -35,24 +53,24 @@ impl SqliteStorage {
Ok(()) Ok(())
} }
pub(crate) fn add_revlog_entry(&self, entry: &ReviewLogEntry) -> Result<()> { pub(crate) fn add_revlog_entry(&self, entry: &RevlogEntry) -> Result<()> {
self.db self.db
.prepare_cached(include_str!("add.sql"))? .prepare_cached(include_str!("add.sql"))?
.execute(params![ .execute(params![
entry.id, entry.id,
entry.cid, entry.cid,
entry.usn, entry.usn,
entry.ease, entry.button_chosen,
entry.interval, entry.interval,
entry.last_interval, entry.last_interval,
entry.factor, entry.ease_factor,
entry.time, entry.taken_millis,
entry.kind entry.review_kind as u8
])?; ])?;
Ok(()) Ok(())
} }
pub(crate) fn get_revlog_entry(&self, id: RevlogID) -> Result<Option<ReviewLogEntry>> { pub(crate) fn get_revlog_entry(&self, id: RevlogID) -> Result<Option<RevlogEntry>> {
self.db self.db
.prepare_cached(concat!(include_str!("get.sql"), " where id=?"))? .prepare_cached(concat!(include_str!("get.sql"), " where id=?"))?
.query_and_then(&[id], row_to_revlog_entry)? .query_and_then(&[id], row_to_revlog_entry)?
@ -60,7 +78,7 @@ impl SqliteStorage {
.transpose() .transpose()
} }
pub(crate) fn get_revlog_entries_for_card(&self, cid: CardID) -> Result<Vec<ReviewLogEntry>> { pub(crate) fn get_revlog_entries_for_card(&self, cid: CardID) -> Result<Vec<RevlogEntry>> {
self.db self.db
.prepare_cached(concat!(include_str!("get.sql"), " where cid=?"))? .prepare_cached(concat!(include_str!("get.sql"), " where cid=?"))?
.query_and_then(&[cid], row_to_revlog_entry)? .query_and_then(&[cid], row_to_revlog_entry)?

View file

@ -12,6 +12,7 @@ use crate::{
notes::{guid, Note}, notes::{guid, Note},
notetype::{NoteType, NoteTypeSchema11}, notetype::{NoteType, NoteTypeSchema11},
prelude::*, prelude::*,
revlog::RevlogEntry,
serde::default_on_invalid, serde::default_on_invalid,
tags::{join_tags, split_tags}, tags::{join_tags, split_tags},
version::sync_client_version, version::sync_client_version,
@ -102,7 +103,7 @@ pub struct UnchunkedChanges {
pub struct Chunk { pub struct Chunk {
done: bool, done: bool,
#[serde(skip_serializing_if = "Vec::is_empty", default)] #[serde(skip_serializing_if = "Vec::is_empty", default)]
revlog: Vec<ReviewLogEntry>, revlog: Vec<RevlogEntry>,
#[serde(skip_serializing_if = "Vec::is_empty", default)] #[serde(skip_serializing_if = "Vec::is_empty", default)]
cards: Vec<CardEntry>, cards: Vec<CardEntry>,
#[serde(skip_serializing_if = "Vec::is_empty", default)] #[serde(skip_serializing_if = "Vec::is_empty", default)]
@ -115,22 +116,6 @@ struct ChunkableIDs {
notes: Vec<NoteID>, notes: Vec<NoteID>,
} }
#[derive(Serialize_tuple, Deserialize, Debug, Default, PartialEq)]
pub struct ReviewLogEntry {
pub id: TimestampMillis,
pub cid: CardID,
pub usn: Usn,
pub ease: u8,
#[serde(rename = "ivl")]
pub interval: i32,
#[serde(rename = "lastIvl")]
pub last_interval: i32,
pub factor: u32,
pub time: u32,
#[serde(rename = "type")]
pub kind: u8,
}
#[derive(Serialize_tuple, Deserialize, Debug)] #[derive(Serialize_tuple, Deserialize, Debug)]
pub struct NoteEntry { pub struct NoteEntry {
pub id: NoteID, pub id: NoteID,
@ -904,7 +889,7 @@ impl Collection {
self.merge_notes(chunk.notes) self.merge_notes(chunk.notes)
} }
fn merge_revlog(&self, entries: Vec<ReviewLogEntry>) -> Result<()> { fn merge_revlog(&self, entries: Vec<RevlogEntry>) -> Result<()> {
for entry in entries { for entry in entries {
self.storage.add_revlog_entry(&entry)?; self.storage.add_revlog_entry(&entry)?;
} }
@ -1275,7 +1260,7 @@ mod test {
col1.add_note(&mut note, deck.id)?; col1.add_note(&mut note, deck.id)?;
// mock revlog entry // mock revlog entry
col1.storage.add_revlog_entry(&ReviewLogEntry { col1.storage.add_revlog_entry(&RevlogEntry {
id: TimestampMillis(123), id: TimestampMillis(123),
cid: CardID(456), cid: CardID(456),
usn: Usn(-1), usn: Usn(-1),