mirror of
https://github.com/ankitects/anki.git
synced 2025-09-19 06:22:22 -04:00
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:
parent
b51f03085e
commit
26fc6609a7
5 changed files with 121 additions and 88 deletions
|
@ -21,4 +21,3 @@ card-stats-review-log-type-learn = Learn
|
|||
card-stats-review-log-type-review = Review
|
||||
card-stats-review-log-type-relearn = Relearn
|
||||
card-stats-review-log-type-filtered = Filtered
|
||||
card-stats-review-log-type-rescheduled = Resched
|
||||
|
|
|
@ -1,6 +1,60 @@
|
|||
// Copyright: Ankitects Pty Ltd and contributors
|
||||
// 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);
|
||||
|
||||
#[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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,7 +2,11 @@
|
|||
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
|
||||
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 chrono::prelude::*;
|
||||
|
@ -23,7 +27,7 @@ struct CardStats {
|
|||
deck: String,
|
||||
nid: NoteID,
|
||||
cid: CardID,
|
||||
revlog: Vec<BasicRevlog>,
|
||||
revlog: Vec<RevlogEntry>,
|
||||
}
|
||||
|
||||
#[derive(Template)]
|
||||
|
@ -40,15 +44,6 @@ enum Due {
|
|||
Unknown,
|
||||
}
|
||||
|
||||
struct BasicRevlog {
|
||||
time: TimestampSecs,
|
||||
kind: u8,
|
||||
rating: u8,
|
||||
interval: i32,
|
||||
ease: u32,
|
||||
taken_secs: f32,
|
||||
}
|
||||
|
||||
struct RevlogText {
|
||||
time: String,
|
||||
kind: String,
|
||||
|
@ -60,19 +55,6 @@ struct RevlogText {
|
|||
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 {
|
||||
pub fn card_stats(&mut self, cid: CardID) -> Result<String> {
|
||||
let stats = self.gather_card_stats(cid)?;
|
||||
|
@ -98,7 +80,10 @@ impl Collection {
|
|||
average_secs = 0.0;
|
||||
total_secs = 0.0;
|
||||
} 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);
|
||||
}
|
||||
|
||||
|
@ -233,49 +218,41 @@ impl Collection {
|
|||
}
|
||||
}
|
||||
|
||||
fn revlog_to_text(e: BasicRevlog, i18n: &I18n, offset: FixedOffset) -> RevlogText {
|
||||
let dt = offset.timestamp(e.time.0, 0);
|
||||
fn revlog_to_text(e: RevlogEntry, i18n: &I18n, offset: FixedOffset) -> RevlogText {
|
||||
let dt = offset.timestamp(e.id.as_secs().0, 0);
|
||||
let time = dt.format("<b>%Y-%m-%d</b> @ %H:%M").to_string();
|
||||
let kind = match e.kind {
|
||||
0 => i18n.tr(TR::CardStatsReviewLogTypeLearn).into(),
|
||||
1 => i18n.tr(TR::CardStatsReviewLogTypeReview).into(),
|
||||
2 => i18n.tr(TR::CardStatsReviewLogTypeRelearn).into(),
|
||||
3 => i18n.tr(TR::CardStatsReviewLogTypeFiltered).into(),
|
||||
4 => i18n.tr(TR::CardStatsReviewLogTypeRescheduled).into(),
|
||||
_ => String::from("?"),
|
||||
let kind = match e.review_kind {
|
||||
RevlogReviewKind::Learning => i18n.tr(TR::CardStatsReviewLogTypeLearn).into(),
|
||||
RevlogReviewKind::Review => i18n.tr(TR::CardStatsReviewLogTypeReview).into(),
|
||||
RevlogReviewKind::Relearning => i18n.tr(TR::CardStatsReviewLogTypeRelearn).into(),
|
||||
RevlogReviewKind::EarlyReview => i18n.tr(TR::CardStatsReviewLogTypeFiltered).into(),
|
||||
};
|
||||
let kind_class = match e.kind {
|
||||
0 => String::from("revlog-learn"),
|
||||
1 => String::from("revlog-review"),
|
||||
2 => String::from("revlog-relearn"),
|
||||
3 => String::from("revlog-filtered"),
|
||||
4 => String::from("revlog-rescheduled"),
|
||||
_ => String::from(""),
|
||||
let kind_class = match e.review_kind {
|
||||
RevlogReviewKind::Learning => String::from("revlog-learn"),
|
||||
RevlogReviewKind::Review => String::from("revlog-review"),
|
||||
RevlogReviewKind::Relearning => String::from("revlog-relearn"),
|
||||
RevlogReviewKind::EarlyReview => String::from("revlog-filtered"),
|
||||
};
|
||||
let rating = e.rating.to_string();
|
||||
let rating = e.button_chosen.to_string();
|
||||
let interval = if e.interval == 0 {
|
||||
String::from("")
|
||||
} else {
|
||||
let interval_secs = if e.interval > 0 {
|
||||
e.interval * 86_400
|
||||
} else {
|
||||
e.interval.abs()
|
||||
};
|
||||
let interval_secs = e.interval_secs();
|
||||
time_span(interval_secs as f32, i18n, true)
|
||||
};
|
||||
let ease = if e.ease > 0 {
|
||||
format!("{:.0}%", (e.ease as f32) / 10.0)
|
||||
let ease = if e.ease_factor > 0 {
|
||||
format!("{}%", e.ease_factor / 10)
|
||||
} else {
|
||||
"".to_string()
|
||||
};
|
||||
let rating_class = if e.rating == 1 {
|
||||
let rating_class = if e.button_chosen == 1 {
|
||||
String::from("revlog-ease1")
|
||||
} else {
|
||||
"".to_string()
|
||||
};
|
||||
let taken_secs = i18n.trn(
|
||||
TR::StatisticsSecondsTaken,
|
||||
tr_args!["seconds"=>e.taken_secs as i32],
|
||||
tr_args!["seconds"=>(e.taken_millis / 1000) as i32],
|
||||
);
|
||||
|
||||
RevlogText {
|
||||
|
|
|
@ -2,21 +2,39 @@
|
|||
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
|
||||
use super::SqliteStorage;
|
||||
use crate::prelude::*;
|
||||
use crate::{err::Result, sync::ReviewLogEntry};
|
||||
use rusqlite::{params, Row, NO_PARAMS};
|
||||
use crate::err::Result;
|
||||
use crate::{
|
||||
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> {
|
||||
Ok(ReviewLogEntry {
|
||||
impl FromSql for RevlogReviewKind {
|
||||
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)?,
|
||||
cid: row.get(1)?,
|
||||
usn: row.get(2)?,
|
||||
ease: row.get(3)?,
|
||||
button_chosen: row.get(3)?,
|
||||
interval: row.get(4)?,
|
||||
last_interval: row.get(5)?,
|
||||
factor: row.get(6)?,
|
||||
time: row.get(7)?,
|
||||
kind: row.get(8)?,
|
||||
ease_factor: row.get(6)?,
|
||||
taken_millis: row.get(7)?,
|
||||
review_kind: row.get(8)?,
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -35,24 +53,24 @@ impl SqliteStorage {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn add_revlog_entry(&self, entry: &ReviewLogEntry) -> Result<()> {
|
||||
pub(crate) fn add_revlog_entry(&self, entry: &RevlogEntry) -> Result<()> {
|
||||
self.db
|
||||
.prepare_cached(include_str!("add.sql"))?
|
||||
.execute(params![
|
||||
entry.id,
|
||||
entry.cid,
|
||||
entry.usn,
|
||||
entry.ease,
|
||||
entry.button_chosen,
|
||||
entry.interval,
|
||||
entry.last_interval,
|
||||
entry.factor,
|
||||
entry.time,
|
||||
entry.kind
|
||||
entry.ease_factor,
|
||||
entry.taken_millis,
|
||||
entry.review_kind as u8
|
||||
])?;
|
||||
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
|
||||
.prepare_cached(concat!(include_str!("get.sql"), " where id=?"))?
|
||||
.query_and_then(&[id], row_to_revlog_entry)?
|
||||
|
@ -60,7 +78,7 @@ impl SqliteStorage {
|
|||
.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
|
||||
.prepare_cached(concat!(include_str!("get.sql"), " where cid=?"))?
|
||||
.query_and_then(&[cid], row_to_revlog_entry)?
|
||||
|
|
|
@ -12,6 +12,7 @@ use crate::{
|
|||
notes::{guid, Note},
|
||||
notetype::{NoteType, NoteTypeSchema11},
|
||||
prelude::*,
|
||||
revlog::RevlogEntry,
|
||||
serde::default_on_invalid,
|
||||
tags::{join_tags, split_tags},
|
||||
version::sync_client_version,
|
||||
|
@ -102,7 +103,7 @@ pub struct UnchunkedChanges {
|
|||
pub struct Chunk {
|
||||
done: bool,
|
||||
#[serde(skip_serializing_if = "Vec::is_empty", default)]
|
||||
revlog: Vec<ReviewLogEntry>,
|
||||
revlog: Vec<RevlogEntry>,
|
||||
#[serde(skip_serializing_if = "Vec::is_empty", default)]
|
||||
cards: Vec<CardEntry>,
|
||||
#[serde(skip_serializing_if = "Vec::is_empty", default)]
|
||||
|
@ -115,22 +116,6 @@ struct ChunkableIDs {
|
|||
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)]
|
||||
pub struct NoteEntry {
|
||||
pub id: NoteID,
|
||||
|
@ -904,7 +889,7 @@ impl Collection {
|
|||
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 {
|
||||
self.storage.add_revlog_entry(&entry)?;
|
||||
}
|
||||
|
@ -1275,7 +1260,7 @@ mod test {
|
|||
col1.add_note(&mut note, deck.id)?;
|
||||
|
||||
// mock revlog entry
|
||||
col1.storage.add_revlog_entry(&ReviewLogEntry {
|
||||
col1.storage.add_revlog_entry(&RevlogEntry {
|
||||
id: TimestampMillis(123),
|
||||
cid: CardID(456),
|
||||
usn: Usn(-1),
|
||||
|
|
Loading…
Reference in a new issue