mirror of
https://github.com/ankitects/anki.git
synced 2025-09-18 22:12:21 -04:00
log manual reschedule, but ignore the log entry in the stats
This commit is contained in:
parent
39212a38aa
commit
ce49ca9401
21 changed files with 249 additions and 82 deletions
|
@ -95,7 +95,8 @@ service BackendService {
|
||||||
rpc LocalMinutesWest (Int64) returns (Int32);
|
rpc LocalMinutesWest (Int64) returns (Int32);
|
||||||
rpc SetLocalMinutesWest (Int32) returns (Empty);
|
rpc SetLocalMinutesWest (Int32) returns (Empty);
|
||||||
rpc SchedTimingToday (Empty) returns (SchedTimingTodayOut);
|
rpc SchedTimingToday (Empty) returns (SchedTimingTodayOut);
|
||||||
rpc StudiedToday (StudiedTodayIn) returns (String);
|
rpc StudiedToday (Empty) returns (String);
|
||||||
|
rpc StudiedTodayMessage (StudiedTodayMessageIn) returns (String);
|
||||||
rpc UpdateStats (UpdateStatsIn) returns (Empty);
|
rpc UpdateStats (UpdateStatsIn) returns (Empty);
|
||||||
rpc ExtendLimits (ExtendLimitsIn) returns (Empty);
|
rpc ExtendLimits (ExtendLimitsIn) returns (Empty);
|
||||||
rpc CountsForDeckToday (DeckID) returns (CountsForDeckTodayOut);
|
rpc CountsForDeckToday (DeckID) returns (CountsForDeckTodayOut);
|
||||||
|
@ -685,7 +686,7 @@ message FormatTimespanIn {
|
||||||
Context context = 2;
|
Context context = 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
message StudiedTodayIn {
|
message StudiedTodayMessageIn {
|
||||||
uint32 cards = 1;
|
uint32 cards = 1;
|
||||||
double seconds = 2;
|
double seconds = 2;
|
||||||
}
|
}
|
||||||
|
@ -1016,6 +1017,7 @@ message RevlogEntry {
|
||||||
REVIEW = 1;
|
REVIEW = 1;
|
||||||
RELEARNING = 2;
|
RELEARNING = 2;
|
||||||
EARLY_REVIEW = 3;
|
EARLY_REVIEW = 3;
|
||||||
|
MANUAL = 4;
|
||||||
}
|
}
|
||||||
int64 id = 1;
|
int64 id = 1;
|
||||||
int64 cid = 2;
|
int64 cid = 2;
|
||||||
|
|
|
@ -516,6 +516,9 @@ table.review-log {{ {revlog_style} }}
|
||||||
|
|
||||||
return style + self.backend.card_stats(card_id)
|
return style + self.backend.card_stats(card_id)
|
||||||
|
|
||||||
|
def studied_today(self) -> str:
|
||||||
|
return self.backend.studied_today()
|
||||||
|
|
||||||
# legacy
|
# legacy
|
||||||
|
|
||||||
def cardStats(self, card: Card) -> str:
|
def cardStats(self, card: Card) -> str:
|
||||||
|
|
|
@ -145,7 +145,9 @@ from revlog where id > ? """
|
||||||
return "<b>" + str(s) + "</b>"
|
return "<b>" + str(s) + "</b>"
|
||||||
|
|
||||||
if cards:
|
if cards:
|
||||||
b += self.col.backend.studied_today(cards=cards, seconds=float(thetime))
|
b += self.col.backend.studied_today_message(
|
||||||
|
cards=cards, seconds=float(thetime)
|
||||||
|
)
|
||||||
# again/pass count
|
# again/pass count
|
||||||
b += "<br>" + _("Again count: %s") % bold(failed)
|
b += "<br>" + _("Again count: %s") % bold(failed)
|
||||||
if cards:
|
if cards:
|
||||||
|
|
|
@ -138,16 +138,7 @@ class DeckBrowser:
|
||||||
self.web.eval("$(function() { window.scrollTo(0, %d, 'instant'); });" % offset)
|
self.web.eval("$(function() { window.scrollTo(0, %d, 'instant'); });" % offset)
|
||||||
|
|
||||||
def _renderStats(self):
|
def _renderStats(self):
|
||||||
cards, thetime = self.mw.col.db.first(
|
return self.mw.col.studied_today()
|
||||||
"""
|
|
||||||
select count(), sum(time)/1000 from revlog
|
|
||||||
where id > ?""",
|
|
||||||
(self.mw.col.sched.dayCutoff - 86400) * 1000,
|
|
||||||
)
|
|
||||||
cards = cards or 0
|
|
||||||
thetime = thetime or 0
|
|
||||||
buf = self.mw.col.backend.studied_today(cards=cards, seconds=float(thetime))
|
|
||||||
return buf
|
|
||||||
|
|
||||||
def _renderDeckTree(self, top: DeckTreeNode) -> str:
|
def _renderDeckTree(self, top: DeckTreeNode) -> str:
|
||||||
buf = """
|
buf = """
|
||||||
|
|
|
@ -21,3 +21,5 @@ 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-manual = Manual
|
||||||
|
|
||||||
|
|
|
@ -31,8 +31,9 @@ use crate::{
|
||||||
RenderCardOutput,
|
RenderCardOutput,
|
||||||
},
|
},
|
||||||
sched::cutoff::local_minutes_west_for_stamp,
|
sched::cutoff::local_minutes_west_for_stamp,
|
||||||
sched::timespan::{answer_button_time, studied_today, time_span},
|
sched::timespan::{answer_button_time, time_span},
|
||||||
search::SortMode,
|
search::SortMode,
|
||||||
|
stats::studied_today,
|
||||||
sync::{
|
sync::{
|
||||||
get_remote_sync_meta, sync_abort, sync_login, FullSyncProgress, NormalSyncProgress,
|
get_remote_sync_meta, sync_abort, sync_login, FullSyncProgress, NormalSyncProgress,
|
||||||
SyncActionRequired, SyncAuth, SyncMeta, SyncOutput, SyncStage,
|
SyncActionRequired, SyncAuth, SyncMeta, SyncOutput, SyncStage,
|
||||||
|
@ -472,8 +473,17 @@ impl BackendService for Backend {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn studied_today(&mut self, input: pb::StudiedTodayIn) -> BackendResult<pb::String> {
|
/// Fetch data from DB and return rendered string.
|
||||||
Ok(studied_today(input.cards as usize, input.seconds as f32, &self.i18n).into())
|
fn studied_today(&mut self, _input: pb::Empty) -> BackendResult<pb::String> {
|
||||||
|
self.with_col(|col| col.studied_today().map(Into::into))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Message rendering only, for old graphs.
|
||||||
|
fn studied_today_message(
|
||||||
|
&mut self,
|
||||||
|
input: pb::StudiedTodayMessageIn,
|
||||||
|
) -> BackendResult<pb::String> {
|
||||||
|
Ok(studied_today(input.cards, input.seconds as f32, &self.i18n).into())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn update_stats(&mut self, input: pb::UpdateStatsIn) -> BackendResult<Empty> {
|
fn update_stats(&mut self, input: pb::UpdateStatsIn) -> BackendResult<Empty> {
|
||||||
|
|
|
@ -6,8 +6,8 @@ use crate::define_newtype;
|
||||||
use crate::err::{AnkiError, Result};
|
use crate::err::{AnkiError, Result};
|
||||||
use crate::notes::NoteID;
|
use crate::notes::NoteID;
|
||||||
use crate::{
|
use crate::{
|
||||||
collection::Collection, config::SchedulerVersion, deckconf::INITIAL_EASE_FACTOR,
|
collection::Collection, config::SchedulerVersion, timestamp::TimestampSecs, types::Usn,
|
||||||
timestamp::TimestampSecs, types::Usn, undo::Undoable,
|
undo::Undoable,
|
||||||
};
|
};
|
||||||
use num_enum::TryFromPrimitive;
|
use num_enum::TryFromPrimitive;
|
||||||
use serde_repr::{Deserialize_repr, Serialize_repr};
|
use serde_repr::{Deserialize_repr, Serialize_repr};
|
||||||
|
@ -200,32 +200,6 @@ impl Card {
|
||||||
|
|
||||||
self.original_due = 0;
|
self.original_due = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Remove the card from the (re)learning queue.
|
|
||||||
/// This will reset cards in learning.
|
|
||||||
/// Only used in the V1 scheduler.
|
|
||||||
/// Unlike the legacy Python code, this sets the due# to 0 instead of
|
|
||||||
/// one past the previous max due number.
|
|
||||||
pub(crate) fn remove_from_learning(&mut self) {
|
|
||||||
if !matches!(self.queue, CardQueue::Learn | CardQueue::DayLearn) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if self.ctype == CardType::Review {
|
|
||||||
// reviews are removed from relearning
|
|
||||||
self.due = self.original_due;
|
|
||||||
self.original_due = 0;
|
|
||||||
self.queue = CardQueue::Review;
|
|
||||||
} else {
|
|
||||||
// other cards are reset to new
|
|
||||||
self.ctype = CardType::New;
|
|
||||||
self.queue = CardQueue::New;
|
|
||||||
self.interval = 0;
|
|
||||||
self.due = 0;
|
|
||||||
self.original_due = 0;
|
|
||||||
self.ease_factor = INITIAL_EASE_FACTOR;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub(crate) struct UpdateCardUndo(Card);
|
pub(crate) struct UpdateCardUndo(Card);
|
||||||
|
|
|
@ -42,6 +42,7 @@ pub enum RevlogReviewKind {
|
||||||
Review = 1,
|
Review = 1,
|
||||||
Relearning = 2,
|
Relearning = 2,
|
||||||
EarlyReview = 3,
|
EarlyReview = 3,
|
||||||
|
Manual = 4,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for RevlogReviewKind {
|
impl Default for RevlogReviewKind {
|
||||||
|
@ -59,3 +60,40 @@ impl RevlogEntry {
|
||||||
}) as u32
|
}) as u32
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Card {
|
||||||
|
fn last_interval_for_revlog_todo(&self) -> i32 {
|
||||||
|
self.interval as i32
|
||||||
|
|
||||||
|
// fixme: need to pass in delays for (re)learning
|
||||||
|
// if let Some(delay) = self.current_learning_delay_seconds(&[]) {
|
||||||
|
// -(delay as i32)
|
||||||
|
// } else {
|
||||||
|
// self.interval as i32
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Collection {
|
||||||
|
pub(crate) fn log_manually_scheduled_review(
|
||||||
|
&mut self,
|
||||||
|
card: &Card,
|
||||||
|
usn: Usn,
|
||||||
|
next_interval: u32,
|
||||||
|
) -> Result<()> {
|
||||||
|
println!("fixme: learning last_interval");
|
||||||
|
// let deck = self.get_deck(card.deck_id)?.ok_or(AnkiError::NotFound)?;
|
||||||
|
let entry = RevlogEntry {
|
||||||
|
id: TimestampMillis::now(),
|
||||||
|
cid: card.id,
|
||||||
|
usn,
|
||||||
|
button_chosen: 0,
|
||||||
|
interval: next_interval as i32,
|
||||||
|
last_interval: card.last_interval_for_revlog_todo(),
|
||||||
|
ease_factor: card.ease_factor as u32,
|
||||||
|
taken_millis: 0,
|
||||||
|
review_kind: RevlogReviewKind::Manual,
|
||||||
|
};
|
||||||
|
self.storage.add_revlog_entry(&entry)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
58
rslib/src/sched/learning.rs
Normal file
58
rslib/src/sched/learning.rs
Normal file
|
@ -0,0 +1,58 @@
|
||||||
|
// Copyright: Ankitects Pty Ltd and contributors
|
||||||
|
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
card::{Card, CardQueue, CardType},
|
||||||
|
deckconf::INITIAL_EASE_FACTOR,
|
||||||
|
};
|
||||||
|
|
||||||
|
impl Card {
|
||||||
|
/// Remove the card from the (re)learning queue.
|
||||||
|
/// This will reset cards in learning.
|
||||||
|
/// Only used in the V1 scheduler.
|
||||||
|
/// Unlike the legacy Python code, this sets the due# to 0 instead of
|
||||||
|
/// one past the previous max due number.
|
||||||
|
pub(crate) fn remove_from_learning(&mut self) {
|
||||||
|
if !matches!(self.queue, CardQueue::Learn | CardQueue::DayLearn) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.ctype == CardType::Review {
|
||||||
|
// reviews are removed from relearning
|
||||||
|
self.due = self.original_due;
|
||||||
|
self.original_due = 0;
|
||||||
|
self.queue = CardQueue::Review;
|
||||||
|
} else {
|
||||||
|
// other cards are reset to new
|
||||||
|
self.ctype = CardType::New;
|
||||||
|
self.queue = CardQueue::New;
|
||||||
|
self.interval = 0;
|
||||||
|
self.due = 0;
|
||||||
|
self.original_due = 0;
|
||||||
|
self.ease_factor = INITIAL_EASE_FACTOR;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn all_remaining_steps(&self) -> u32 {
|
||||||
|
self.remaining_steps % 1000
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
|
fn remaining_steps_today(&self) -> u32 {
|
||||||
|
self.remaining_steps / 1000
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
|
pub(crate) fn current_learning_delay_seconds(&self, delays: &[u32]) -> Option<u32> {
|
||||||
|
if self.queue == CardQueue::Learn {
|
||||||
|
let remaining = self.all_remaining_steps();
|
||||||
|
delays
|
||||||
|
.iter()
|
||||||
|
.nth_back(remaining.saturating_sub(0) as usize)
|
||||||
|
.or(Some(&0))
|
||||||
|
.map(|n| n * 60)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -8,6 +8,7 @@ use crate::{
|
||||||
pub mod bury_and_suspend;
|
pub mod bury_and_suspend;
|
||||||
pub(crate) mod congrats;
|
pub(crate) mod congrats;
|
||||||
pub mod cutoff;
|
pub mod cutoff;
|
||||||
|
mod learning;
|
||||||
mod reviews;
|
mod reviews;
|
||||||
pub mod timespan;
|
pub mod timespan;
|
||||||
|
|
||||||
|
|
|
@ -40,6 +40,7 @@ impl Collection {
|
||||||
for mut card in col.storage.all_searched_cards()? {
|
for mut card in col.storage.all_searched_cards()? {
|
||||||
let original = card.clone();
|
let original = card.clone();
|
||||||
let interval = distribution.sample(&mut rng);
|
let interval = distribution.sample(&mut rng);
|
||||||
|
col.log_manually_scheduled_review(&card, usn, interval)?;
|
||||||
card.schedule_as_review(interval, today);
|
card.schedule_as_review(interval, today);
|
||||||
col.update_card(&mut card, &original, usn)?;
|
col.update_card(&mut card, &original, usn)?;
|
||||||
}
|
}
|
||||||
|
|
|
@ -41,21 +41,6 @@ pub fn time_span(seconds: f32, i18n: &I18n, precise: bool) -> String {
|
||||||
i18n.trn(key, args)
|
i18n.trn(key, args)
|
||||||
}
|
}
|
||||||
|
|
||||||
// fixme: this doesn't belong here
|
|
||||||
pub fn studied_today(cards: usize, secs: f32, i18n: &I18n) -> String {
|
|
||||||
let span = Timespan::from_secs(secs).natural_span();
|
|
||||||
let amount = span.as_unit();
|
|
||||||
let unit = span.unit().as_str();
|
|
||||||
let secs_per = if cards > 0 {
|
|
||||||
secs / (cards as f32)
|
|
||||||
} else {
|
|
||||||
0.0
|
|
||||||
};
|
|
||||||
let args = tr_args!["amount" => amount, "unit" => unit,
|
|
||||||
"cards" => cards, "secs-per-card" => secs_per];
|
|
||||||
i18n.trn(TR::StatisticsStudiedToday, args)
|
|
||||||
}
|
|
||||||
|
|
||||||
const SECOND: f32 = 1.0;
|
const SECOND: f32 = 1.0;
|
||||||
const MINUTE: f32 = 60.0 * SECOND;
|
const MINUTE: f32 = 60.0 * SECOND;
|
||||||
const HOUR: f32 = 60.0 * MINUTE;
|
const HOUR: f32 = 60.0 * MINUTE;
|
||||||
|
@ -64,7 +49,7 @@ const MONTH: f32 = 30.0 * DAY;
|
||||||
const YEAR: f32 = 12.0 * MONTH;
|
const YEAR: f32 = 12.0 * MONTH;
|
||||||
|
|
||||||
#[derive(Clone, Copy)]
|
#[derive(Clone, Copy)]
|
||||||
enum TimespanUnit {
|
pub(crate) enum TimespanUnit {
|
||||||
Seconds,
|
Seconds,
|
||||||
Minutes,
|
Minutes,
|
||||||
Hours,
|
Hours,
|
||||||
|
@ -74,7 +59,7 @@ enum TimespanUnit {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TimespanUnit {
|
impl TimespanUnit {
|
||||||
fn as_str(self) -> &'static str {
|
pub fn as_str(self) -> &'static str {
|
||||||
match self {
|
match self {
|
||||||
TimespanUnit::Seconds => "seconds",
|
TimespanUnit::Seconds => "seconds",
|
||||||
TimespanUnit::Minutes => "minutes",
|
TimespanUnit::Minutes => "minutes",
|
||||||
|
@ -87,13 +72,13 @@ impl TimespanUnit {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Copy)]
|
#[derive(Clone, Copy)]
|
||||||
struct Timespan {
|
pub(crate) struct Timespan {
|
||||||
seconds: f32,
|
seconds: f32,
|
||||||
unit: TimespanUnit,
|
unit: TimespanUnit,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Timespan {
|
impl Timespan {
|
||||||
fn from_secs(seconds: f32) -> Self {
|
pub fn from_secs(seconds: f32) -> Self {
|
||||||
Timespan {
|
Timespan {
|
||||||
seconds,
|
seconds,
|
||||||
unit: TimespanUnit::Seconds,
|
unit: TimespanUnit::Seconds,
|
||||||
|
@ -102,7 +87,7 @@ impl Timespan {
|
||||||
|
|
||||||
/// Return the value as the configured unit, eg seconds=70/unit=Minutes
|
/// Return the value as the configured unit, eg seconds=70/unit=Minutes
|
||||||
/// returns 1.17
|
/// returns 1.17
|
||||||
fn as_unit(self) -> f32 {
|
pub fn as_unit(self) -> f32 {
|
||||||
let s = self.seconds;
|
let s = self.seconds;
|
||||||
match self.unit {
|
match self.unit {
|
||||||
TimespanUnit::Seconds => s,
|
TimespanUnit::Seconds => s,
|
||||||
|
@ -116,7 +101,7 @@ impl Timespan {
|
||||||
|
|
||||||
/// Round seconds and days to integers, otherwise
|
/// Round seconds and days to integers, otherwise
|
||||||
/// truncates to one decimal place.
|
/// truncates to one decimal place.
|
||||||
fn as_rounded_unit(self) -> f32 {
|
pub fn as_rounded_unit(self) -> f32 {
|
||||||
match self.unit {
|
match self.unit {
|
||||||
// seconds/days as integer
|
// seconds/days as integer
|
||||||
TimespanUnit::Seconds | TimespanUnit::Days => self.as_unit().round(),
|
TimespanUnit::Seconds | TimespanUnit::Days => self.as_unit().round(),
|
||||||
|
@ -125,13 +110,13 @@ impl Timespan {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn unit(self) -> TimespanUnit {
|
pub fn unit(self) -> TimespanUnit {
|
||||||
self.unit
|
self.unit
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Return a new timespan in the most appropriate unit, eg
|
/// Return a new timespan in the most appropriate unit, eg
|
||||||
/// 70 secs -> timespan in minutes
|
/// 70 secs -> timespan in minutes
|
||||||
fn natural_span(self) -> Timespan {
|
pub fn natural_span(self) -> Timespan {
|
||||||
let secs = self.seconds.abs();
|
let secs = self.seconds.abs();
|
||||||
let unit = if secs < MINUTE {
|
let unit = if secs < MINUTE {
|
||||||
TimespanUnit::Seconds
|
TimespanUnit::Seconds
|
||||||
|
@ -158,7 +143,7 @@ impl Timespan {
|
||||||
mod test {
|
mod test {
|
||||||
use crate::i18n::I18n;
|
use crate::i18n::I18n;
|
||||||
use crate::log;
|
use crate::log;
|
||||||
use crate::sched::timespan::{answer_button_time, studied_today, time_span, MONTH};
|
use crate::sched::timespan::{answer_button_time, time_span, MONTH};
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn answer_buttons() {
|
fn answer_buttons() {
|
||||||
|
@ -180,15 +165,4 @@ mod test {
|
||||||
assert_eq!(time_span(45.0 * 86_400.0, &i18n, false), "1.5 months");
|
assert_eq!(time_span(45.0 * 86_400.0, &i18n, false), "1.5 months");
|
||||||
assert_eq!(time_span(365.0 * 86_400.0 * 1.5, &i18n, false), "1.5 years");
|
assert_eq!(time_span(365.0 * 86_400.0 * 1.5, &i18n, false), "1.5 years");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn combo() {
|
|
||||||
// temporary test of fluent term handling
|
|
||||||
let log = log::terminal();
|
|
||||||
let i18n = I18n::new(&["zz"], "", log);
|
|
||||||
assert_eq!(
|
|
||||||
&studied_today(3, 13.0, &i18n).replace("\n", " "),
|
|
||||||
"Studied 3 cards in 13 seconds today (4.33s/card)"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -229,12 +229,14 @@ fn revlog_to_text(e: RevlogEntry, i18n: &I18n, offset: FixedOffset) -> RevlogTex
|
||||||
RevlogReviewKind::Review => i18n.tr(TR::CardStatsReviewLogTypeReview).into(),
|
RevlogReviewKind::Review => i18n.tr(TR::CardStatsReviewLogTypeReview).into(),
|
||||||
RevlogReviewKind::Relearning => i18n.tr(TR::CardStatsReviewLogTypeRelearn).into(),
|
RevlogReviewKind::Relearning => i18n.tr(TR::CardStatsReviewLogTypeRelearn).into(),
|
||||||
RevlogReviewKind::EarlyReview => i18n.tr(TR::CardStatsReviewLogTypeFiltered).into(),
|
RevlogReviewKind::EarlyReview => i18n.tr(TR::CardStatsReviewLogTypeFiltered).into(),
|
||||||
|
RevlogReviewKind::Manual => i18n.tr(TR::CardStatsReviewLogTypeManual).into(),
|
||||||
};
|
};
|
||||||
let kind_class = match e.review_kind {
|
let kind_class = match e.review_kind {
|
||||||
RevlogReviewKind::Learning => String::from("revlog-learn"),
|
RevlogReviewKind::Learning => String::from("revlog-learn"),
|
||||||
RevlogReviewKind::Review => String::from("revlog-review"),
|
RevlogReviewKind::Review => String::from("revlog-review"),
|
||||||
RevlogReviewKind::Relearning => String::from("revlog-relearn"),
|
RevlogReviewKind::Relearning => String::from("revlog-relearn"),
|
||||||
RevlogReviewKind::EarlyReview => String::from("revlog-filtered"),
|
RevlogReviewKind::EarlyReview => String::from("revlog-filtered"),
|
||||||
|
RevlogReviewKind::Manual => String::from("revlog-manual"),
|
||||||
};
|
};
|
||||||
let rating = e.button_chosen.to_string();
|
let rating = e.button_chosen.to_string();
|
||||||
let interval = if e.interval == 0 {
|
let interval = if e.interval == 0 {
|
||||||
|
|
|
@ -3,3 +3,6 @@
|
||||||
|
|
||||||
mod card;
|
mod card;
|
||||||
mod graphs;
|
mod graphs;
|
||||||
|
mod today;
|
||||||
|
|
||||||
|
pub use today::studied_today;
|
||||||
|
|
45
rslib/src/stats/today.rs
Normal file
45
rslib/src/stats/today.rs
Normal file
|
@ -0,0 +1,45 @@
|
||||||
|
// Copyright: Ankitects Pty Ltd and contributors
|
||||||
|
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
|
|
||||||
|
use crate::{i18n::I18n, prelude::*, sched::timespan::Timespan};
|
||||||
|
|
||||||
|
pub fn studied_today(cards: u32, secs: f32, i18n: &I18n) -> String {
|
||||||
|
let span = Timespan::from_secs(secs).natural_span();
|
||||||
|
let amount = span.as_unit();
|
||||||
|
let unit = span.unit().as_str();
|
||||||
|
let secs_per = if cards > 0 {
|
||||||
|
secs / (cards as f32)
|
||||||
|
} else {
|
||||||
|
0.0
|
||||||
|
};
|
||||||
|
let args = tr_args!["amount" => amount, "unit" => unit,
|
||||||
|
"cards" => cards, "secs-per-card" => secs_per];
|
||||||
|
i18n.trn(TR::StatisticsStudiedToday, args)
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Collection {
|
||||||
|
pub fn studied_today(&self) -> Result<String> {
|
||||||
|
let today = self
|
||||||
|
.storage
|
||||||
|
.studied_today(self.timing_today()?.next_day_at)?;
|
||||||
|
Ok(studied_today(today.cards, today.seconds as f32, &self.i18n))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod test {
|
||||||
|
use super::studied_today;
|
||||||
|
use crate::i18n::I18n;
|
||||||
|
use crate::log;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn today() {
|
||||||
|
// temporary test of fluent term handling
|
||||||
|
let log = log::terminal();
|
||||||
|
let i18n = I18n::new(&["zz"], "", log);
|
||||||
|
assert_eq!(
|
||||||
|
&studied_today(3, 13.0, &i18n).replace("\n", " "),
|
||||||
|
"Studied 3 cards in 13 seconds today (4.33s/card)"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -10,5 +10,25 @@ insert
|
||||||
time,
|
time,
|
||||||
type
|
type
|
||||||
)
|
)
|
||||||
values
|
values (
|
||||||
(?, ?, ?, ?, ?, ?, ?, ?, ?)
|
(
|
||||||
|
case
|
||||||
|
when ?1 in (
|
||||||
|
select id
|
||||||
|
from revlog
|
||||||
|
) then (
|
||||||
|
select max(id) + 1
|
||||||
|
from revlog
|
||||||
|
)
|
||||||
|
else ?1
|
||||||
|
end
|
||||||
|
),
|
||||||
|
?,
|
||||||
|
?,
|
||||||
|
?,
|
||||||
|
?,
|
||||||
|
?,
|
||||||
|
?,
|
||||||
|
?,
|
||||||
|
?
|
||||||
|
)
|
|
@ -15,6 +15,11 @@ use rusqlite::{
|
||||||
};
|
};
|
||||||
use std::convert::TryFrom;
|
use std::convert::TryFrom;
|
||||||
|
|
||||||
|
pub(crate) struct StudiedToday {
|
||||||
|
pub cards: u32,
|
||||||
|
pub seconds: f64,
|
||||||
|
}
|
||||||
|
|
||||||
impl FromSql for RevlogReviewKind {
|
impl FromSql for RevlogReviewKind {
|
||||||
fn column_result(value: ValueRef<'_>) -> std::result::Result<Self, FromSqlError> {
|
fn column_result(value: ValueRef<'_>) -> std::result::Result<Self, FromSqlError> {
|
||||||
if let ValueRef::Integer(i) = value {
|
if let ValueRef::Integer(i) = value {
|
||||||
|
@ -113,4 +118,19 @@ impl SqliteStorage {
|
||||||
})?
|
})?
|
||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) fn studied_today(&self, day_cutoff: i64) -> Result<StudiedToday> {
|
||||||
|
let start = (day_cutoff - 86_400) * 1_000;
|
||||||
|
self.db
|
||||||
|
.prepare_cached(include_str!("studied_today.sql"))?
|
||||||
|
.query_map(&[start, RevlogReviewKind::Manual as i64], |row| {
|
||||||
|
Ok(StudiedToday {
|
||||||
|
cards: row.get(0)?,
|
||||||
|
seconds: row.get(1)?,
|
||||||
|
})
|
||||||
|
})?
|
||||||
|
.next()
|
||||||
|
.unwrap()
|
||||||
|
.map_err(Into::into)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
5
rslib/src/storage/revlog/studied_today.sql
Normal file
5
rslib/src/storage/revlog/studied_today.sql
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
select count(),
|
||||||
|
coalesce(sum(time) / 1000.0, 0.0)
|
||||||
|
from revlog
|
||||||
|
where id > ?
|
||||||
|
and type != ?
|
|
@ -15,6 +15,10 @@ impl TimestampSecs {
|
||||||
Self(elapsed().as_secs() as i64)
|
Self(elapsed().as_secs() as i64)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn zero() -> Self {
|
||||||
|
Self(0)
|
||||||
|
}
|
||||||
|
|
||||||
pub fn elapsed_secs(self) -> u64 {
|
pub fn elapsed_secs(self) -> u64 {
|
||||||
(Self::now().0 - self.0).max(0) as u64
|
(Self::now().0 - self.0).max(0) as u64
|
||||||
}
|
}
|
||||||
|
@ -30,6 +34,10 @@ impl TimestampMillis {
|
||||||
Self(elapsed().as_millis() as i64)
|
Self(elapsed().as_millis() as i64)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn zero() -> Self {
|
||||||
|
Self(0)
|
||||||
|
}
|
||||||
|
|
||||||
pub fn as_secs(self) -> TimestampSecs {
|
pub fn as_secs(self) -> TimestampSecs {
|
||||||
TimestampSecs(self.0 / 1000)
|
TimestampSecs(self.0 / 1000)
|
||||||
}
|
}
|
||||||
|
|
|
@ -47,6 +47,10 @@ export function gatherData(data: pb.BackendProto.GraphsOut): GraphData {
|
||||||
const empty = { mature: 0, young: 0, learn: 0, relearn: 0, early: 0 };
|
const empty = { mature: 0, young: 0, learn: 0, relearn: 0, early: 0 };
|
||||||
|
|
||||||
for (const review of data.revlog as pb.BackendProto.RevlogEntry[]) {
|
for (const review of data.revlog as pb.BackendProto.RevlogEntry[]) {
|
||||||
|
if (review.reviewKind == ReviewKind.MANUAL) {
|
||||||
|
// don't count days with only manual scheduling
|
||||||
|
continue;
|
||||||
|
}
|
||||||
const day = Math.ceil(
|
const day = Math.ceil(
|
||||||
((review.id as number) / 1000 - data.nextDayAtSecs) / 86400
|
((review.id as number) / 1000 - data.nextDayAtSecs) / 86400
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
// 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
|
||||||
|
|
||||||
import pb from "../backend/proto";
|
import pb, { BackendProto } from "../backend/proto";
|
||||||
import { studiedToday } from "../time";
|
import { studiedToday } from "../time";
|
||||||
import { I18n } from "../i18n";
|
import { I18n } from "../i18n";
|
||||||
|
|
||||||
|
@ -30,6 +30,10 @@ export function gatherData(data: pb.BackendProto.GraphsOut, i18n: I18n): TodayDa
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (review.reviewKind == ReviewKind.MANUAL) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
// total
|
// total
|
||||||
answerCount += 1;
|
answerCount += 1;
|
||||||
answerMillis += review.takenMillis;
|
answerMillis += review.takenMillis;
|
||||||
|
|
Loading…
Reference in a new issue