move answer_button_time to the backend, split sched into separate module

This commit is contained in:
Damien Elmes 2020-02-20 14:16:33 +10:00
parent 7b26814922
commit 99c07cfdcb
9 changed files with 171 additions and 27 deletions

View file

@ -44,6 +44,7 @@ message BackendInput {
Empty check_media = 28; Empty check_media = 28;
TrashMediaFilesIn trash_media_files = 29; TrashMediaFilesIn trash_media_files = 29;
TranslateStringIn translate_string = 30; TranslateStringIn translate_string = 30;
FormatTimeSpanIn format_time_span = 31;
} }
} }
@ -64,6 +65,7 @@ message BackendOutput {
MediaCheckOut check_media = 28; MediaCheckOut check_media = 28;
Empty trash_media_files = 29; Empty trash_media_files = 29;
string translate_string = 30; string translate_string = 30;
string format_time_span = 31;
BackendError error = 2047; BackendError error = 2047;
} }
@ -303,3 +305,12 @@ message TranslateArgValue {
double number = 2; double number = 2;
} }
} }
message FormatTimeSpanIn {
enum Context {
ANSWER_BUTTONS = 0;
}
float seconds = 1;
Context context = 2;
}

View file

@ -134,6 +134,8 @@ MediaCheckOutput = pb.MediaCheckOut
StringsGroup = pb.StringsGroup StringsGroup = pb.StringsGroup
FormatTimeSpanContext = pb.FormatTimeSpanIn.Context
@dataclass @dataclass
class ExtractedLatex: class ExtractedLatex:
@ -342,3 +344,10 @@ class RustBackend:
translate_string=pb.TranslateStringIn(group=group, key=key, args=args) translate_string=pb.TranslateStringIn(group=group, key=key, args=args)
) )
).translate_string ).translate_string
def format_time_span(self, seconds: float, context: FormatTimeSpanContext) -> str:
return self._run_command(
pb.BackendInput(
format_time_span=pb.FormatTimeSpanIn(seconds=seconds, context=context)
)
).format_time_span

View file

@ -1,6 +1,8 @@
# 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
from __future__ import annotations
import itertools import itertools
import random import random
import time import time
@ -8,11 +10,13 @@ from heapq import *
from operator import itemgetter from operator import itemgetter
from typing import List, Optional, Set from typing import List, Optional, Set
import anki
from anki import hooks from anki import hooks
from anki.cards import Card from anki.cards import Card
from anki.consts import * from anki.consts import *
from anki.lang import _ from anki.lang import _
from anki.utils import answer_button_time, ids2str, intTime from anki.rsbackend import FormatTimeSpanContext
from anki.utils import ids2str, intTime
# queue types: 0=new/cram, 1=lrn, 2=rev, 3=day lrn, -1=suspended, -2=buried # queue types: 0=new/cram, 1=lrn, 2=rev, 3=day lrn, -1=suspended, -2=buried
# revlog types: 0=lrn, 1=rev, 2=relrn, 3=cram # revlog types: 0=lrn, 1=rev, 2=relrn, 3=cram
@ -25,7 +29,7 @@ class Scheduler:
_spreadRev = True _spreadRev = True
_burySiblingsOnAnswer = True _burySiblingsOnAnswer = True
def __init__(self, col): def __init__(self, col: anki.storage._Collection) -> None:
self.col = col self.col = col
self.queueLimit = 50 self.queueLimit = 50
self.reportLimit = 1000 self.reportLimit = 1000
@ -33,7 +37,7 @@ class Scheduler:
self.lrnCount = 0 self.lrnCount = 0
self.revCount = 0 self.revCount = 0
self.newCount = 0 self.newCount = 0
self.today = None self.today: Optional[int] = None
self._haveQueues = False self._haveQueues = False
self._updateCutoff() self._updateCutoff()
@ -1354,7 +1358,9 @@ To study outside of the normal schedule, click the Custom Study button below."""
ivl_secs = self.nextIvl(card, ease) ivl_secs = self.nextIvl(card, ease)
if not ivl_secs: if not ivl_secs:
return _("(end)") return _("(end)")
s = answer_button_time(self.col, ivl_secs) s = self.col.backend.format_time_span(
ivl_secs, FormatTimeSpanContext.ANSWER_BUTTONS
)
if ivl_secs < self.col.conf["collapseTime"]: if ivl_secs < self.col.conf["collapseTime"]:
s = "<" + s s = "<" + s
return s return s

View file

@ -18,8 +18,8 @@ from anki import hooks
from anki.cards import Card from anki.cards import Card
from anki.consts import * from anki.consts import *
from anki.lang import _ from anki.lang import _
from anki.rsbackend import SchedTimingToday from anki.rsbackend import FormatTimeSpanContext, SchedTimingToday
from anki.utils import answer_button_time, ids2str, intTime from anki.utils import ids2str, intTime
# card types: 0=new, 1=lrn, 2=rev, 3=relrn # card types: 0=new, 1=lrn, 2=rev, 3=relrn
# queue types: 0=new, 1=(re)lrn, 2=rev, 3=day (re)lrn, # queue types: 0=new, 1=(re)lrn, 2=rev, 3=day (re)lrn,
@ -1548,7 +1548,9 @@ To study outside of the normal schedule, click the Custom Study button below."""
ivl_secs = self.nextIvl(card, ease) ivl_secs = self.nextIvl(card, ease)
if not ivl_secs: if not ivl_secs:
return _("(end)") return _("(end)")
s = answer_button_time(self.col, ivl_secs) s = self.col.backend.format_time_span(
ivl_secs, FormatTimeSpanContext.ANSWER_BUTTONS
)
if ivl_secs < self.col.conf["collapseTime"]: if ivl_secs < self.col.conf["collapseTime"]:
s = "<" + s s = "<" + s
return s return s

View file

@ -23,8 +23,6 @@ from hashlib import sha1
from html.entities import name2codepoint from html.entities import name2codepoint
from typing import Iterable, Iterator, List, Optional, Tuple, Union from typing import Iterable, Iterator, List, Optional, Tuple, Union
import anki
from anki.backend_pb2 import StringsGroup
from anki.db import DB from anki.db import DB
from anki.lang import _, ngettext from anki.lang import _, ngettext
@ -39,22 +37,6 @@ def intTime(scale: int = 1) -> int:
return int(time.time() * scale) return int(time.time() * scale)
# eg 70 seconds -> (1.16, "minutes")
def seconds_to_appropriate_unit(seconds: int) -> Tuple[float, str]:
unit, _ = optimalPeriod(seconds, 0, 99)
amount = convertSecondsTo(seconds, unit)
return (amount, unit)
def answer_button_time(col: anki.storage._Collection, seconds: int) -> str:
(amount, unit) = seconds_to_appropriate_unit(seconds)
if unit not in ("months", "years"):
amount = int(amount)
return col.backend.translate(
StringsGroup.SCHEDULING, f"answer-button-time-{unit}", amount=amount
)
timeTable = { timeTable = {
"years": lambda n: ngettext("%s year", "%s years", n), "years": lambda n: ngettext("%s year", "%s years", n),
"months": lambda n: ngettext("%s month", "%s months", n), "months": lambda n: ngettext("%s month", "%s months", n),

View file

@ -10,7 +10,8 @@ use crate::latex::{extract_latex, ExtractedLatex};
use crate::media::check::MediaChecker; use crate::media::check::MediaChecker;
use crate::media::sync::MediaSyncProgress; use crate::media::sync::MediaSyncProgress;
use crate::media::MediaManager; use crate::media::MediaManager;
use crate::sched::{local_minutes_west_for_stamp, sched_timing_today}; use crate::sched::cutoff::{local_minutes_west_for_stamp, sched_timing_today};
use crate::sched::timespan::answer_button_time;
use crate::template::{ use crate::template::{
render_card, without_legacy_template_directives, FieldMap, FieldRequirements, ParsedTemplate, render_card, without_legacy_template_directives, FieldMap, FieldRequirements, ParsedTemplate,
RenderedNode, RenderedNode,
@ -200,6 +201,7 @@ impl Backend {
OValue::TrashMediaFiles(Empty {}) OValue::TrashMediaFiles(Empty {})
} }
Value::TranslateString(input) => OValue::TranslateString(self.translate_string(input)), Value::TranslateString(input) => OValue::TranslateString(self.translate_string(input)),
Value::FormatTimeSpan(input) => OValue::FormatTimeSpan(self.format_time_span(input)),
}) })
} }
@ -397,6 +399,18 @@ impl Backend {
self.i18n.get(group).trn(&input.key, map) self.i18n.get(group).trn(&input.key, map)
} }
fn format_time_span(&self, input: pb::FormatTimeSpanIn) -> String {
let context = match pb::format_time_span_in::Context::from_i32(input.context) {
Some(context) => context,
None => return "".to_string(),
};
match context {
pb::format_time_span_in::Context::AnswerButtons => {
answer_button_time(input.seconds, &self.i18n)
}
}
}
} }
fn translate_arg_to_fluent_val(arg: &pb::TranslateArgValue) -> FluentValue { fn translate_arg_to_fluent_val(arg: &pb::TranslateArgValue) -> FluentValue {

View file

@ -92,7 +92,7 @@ pub fn local_minutes_west_for_stamp(stamp: i64) -> i32 {
#[cfg(test)] #[cfg(test)]
mod test { mod test {
use crate::sched::{ use crate::sched::cutoff::{
fixed_offset_from_minutes, local_minutes_west_for_stamp, normalized_rollover_hour, fixed_offset_from_minutes, local_minutes_west_for_stamp, normalized_rollover_hour,
sched_timing_today, sched_timing_today,
}; };

2
rslib/src/sched/mod.rs Normal file
View file

@ -0,0 +1,2 @@
pub mod cutoff;
pub mod timespan;

118
rslib/src/sched/timespan.rs Normal file
View file

@ -0,0 +1,118 @@
// Copyright: Ankitects Pty Ltd and contributors
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
use crate::i18n::{tr_args, I18n, StringsGroup};
pub fn answer_button_time(seconds: f32, i18n: &I18n) -> String {
let span = Timespan::from_secs(seconds).natural_span();
let amount = match span.unit() {
TimespanUnit::Months | TimespanUnit::Years => span.as_unit(),
// we don't show fractional value except for months/years
_ => span.as_unit().round(),
};
let unit = span.unit().as_str();
let args = tr_args!["amount" => amount];
i18n.get(StringsGroup::Scheduling)
.trn(&format!("answer-button-time-{}", unit), args)
}
const SECOND: f32 = 1.0;
const MINUTE: f32 = 60.0 * SECOND;
const HOUR: f32 = 60.0 * MINUTE;
const DAY: f32 = 24.0 * HOUR;
const MONTH: f32 = 30.0 * DAY;
const YEAR: f32 = 365.0 * MONTH;
#[derive(Clone, Copy)]
enum TimespanUnit {
Seconds,
Minutes,
Hours,
Days,
Months,
Years,
}
impl TimespanUnit {
fn as_str(self) -> &'static str {
match self {
TimespanUnit::Seconds => "seconds",
TimespanUnit::Minutes => "minutes",
TimespanUnit::Hours => "hours",
TimespanUnit::Days => "days",
TimespanUnit::Months => "months",
TimespanUnit::Years => "years",
}
}
}
#[derive(Clone, Copy)]
struct Timespan {
seconds: f32,
unit: TimespanUnit,
}
impl Timespan {
fn from_secs(seconds: f32) -> Self {
Timespan {
seconds,
unit: TimespanUnit::Seconds,
}
}
/// Return the value as the configured unit, eg seconds=70/unit=Minutes
/// returns 1.17
fn as_unit(self) -> f32 {
let s = self.seconds;
match self.unit {
TimespanUnit::Seconds => s,
TimespanUnit::Minutes => s / MINUTE,
TimespanUnit::Hours => s / HOUR,
TimespanUnit::Days => s / DAY,
TimespanUnit::Months => s / MONTH,
TimespanUnit::Years => s / YEAR,
}
}
fn unit(self) -> TimespanUnit {
self.unit
}
/// Return a new timespan in the most appropriate unit, eg
/// 70 secs -> timespan in minutes
fn natural_span(self) -> Timespan {
let secs = self.seconds.abs();
let unit = if secs < MINUTE {
TimespanUnit::Seconds
} else if secs < HOUR {
TimespanUnit::Minutes
} else if secs < DAY {
TimespanUnit::Hours
} else if secs < MONTH {
TimespanUnit::Days
} else if secs < YEAR {
TimespanUnit::Months
} else {
TimespanUnit::Years
};
Timespan {
seconds: self.seconds,
unit,
}
}
}
#[cfg(test)]
mod test {
use crate::i18n::I18n;
use crate::sched::timespan::{answer_button_time, MONTH};
#[test]
fn answer_buttons() {
let i18n = I18n::new(&["zz"], "");
assert_eq!(answer_button_time(30.0, &i18n), "30s");
assert_eq!(answer_button_time(70.0, &i18n), "1m");
assert_eq!(answer_button_time(1.1 * MONTH, &i18n), "1.10mo");
}
}