diff --git a/proto/backend.proto b/proto/backend.proto index 0ecd55226..c383d9d72 100644 --- a/proto/backend.proto +++ b/proto/backend.proto @@ -29,7 +29,7 @@ message I18nBackendInit { message BackendInput { oneof value { - SchedTimingTodayIn sched_timing_today = 17; + Empty sched_timing_today = 17; DeckTreeIn deck_tree = 18; SearchCardsIn search_cards = 19; SearchNotesIn search_notes = 20; @@ -101,7 +101,6 @@ message BackendInput { message BackendOutput { oneof value { // infallible commands - SchedTimingTodayOut sched_timing_today = 17; sint32 local_minutes_west = 22; string strip_av_tags = 23; ExtractAVTagsOut extract_av_tags = 24; @@ -114,6 +113,7 @@ message BackendOutput { AllStockNotetypesOut all_stock_notetypes = 60; // fallible commands + SchedTimingTodayOut sched_timing_today = 17; DeckTreeNode deck_tree = 18; SearchCardsOut search_cards = 19; SearchNotesOut search_notes = 20; @@ -237,14 +237,6 @@ message MediaSyncUploadProgress { uint32 deletions = 2; } -message SchedTimingTodayIn { - int64 created_secs = 1; - int64 now_secs = 2; - OptionalInt32 created_mins_west = 3; - OptionalInt32 now_mins_west = 4; - OptionalInt32 rollover_hour = 5; -} - message SchedTimingTodayOut { uint32 days_elapsed = 1; int64 next_day_at = 2; diff --git a/pylib/anki/rsbackend.py b/pylib/anki/rsbackend.py index a57e750c6..7c5efeb9d 100644 --- a/pylib/anki/rsbackend.py +++ b/pylib/anki/rsbackend.py @@ -287,38 +287,9 @@ class RustBackend: release_gil=True, ) - def sched_timing_today( - self, - created_secs: int, - created_mins_west: Optional[int], - now_mins_west: Optional[int], - rollover: Optional[int], - ) -> SchedTimingToday: - if created_mins_west is not None: - crt_west = pb.OptionalInt32(val=created_mins_west) - else: - crt_west = None - - if now_mins_west is not None: - now_west = pb.OptionalInt32(val=now_mins_west) - else: - now_west = None - - if rollover is not None: - roll = pb.OptionalInt32(val=rollover) - else: - roll = None - + def sched_timing_today(self,) -> SchedTimingToday: return self._run_command( - pb.BackendInput( - sched_timing_today=pb.SchedTimingTodayIn( - created_secs=created_secs, - now_secs=intTime(), - created_mins_west=crt_west, - now_mins_west=now_west, - rollover_hour=roll, - ) - ) + pb.BackendInput(sched_timing_today=pb.Empty()) ).sched_timing_today def render_card( diff --git a/pylib/anki/schedv2.py b/pylib/anki/schedv2.py index ad209b88c..edba3ab37 100644 --- a/pylib/anki/schedv2.py +++ b/pylib/anki/schedv2.py @@ -1374,28 +1374,8 @@ where id = ? if time.time() > self.dayCutoff: self.reset() - def _rolloverHour(self) -> int: - return self.col.conf.get("rollover", 4) - def _timing_today(self) -> SchedTimingToday: - roll: Optional[int] = None - if self.col.schedVer() > 1: - roll = self._rolloverHour() - return self.col.backend.sched_timing_today( - self.col.crt, - self._creation_timezone_offset(), - self._current_timezone_offset(), - roll, - ) - - def _current_timezone_offset(self) -> Optional[int]: - if self.col.server: - return self.col.conf.get("localOffset", 0) - else: - return None - - def _creation_timezone_offset(self) -> Optional[int]: - return self.col.conf.get("creationOffset", None) + return self.col.backend.sched_timing_today() # New timezone handling - GUI helpers ########################################################################## diff --git a/pylib/tests/test_exporting.py b/pylib/tests/test_exporting.py index e2afcb8c7..83f6ca93e 100644 --- a/pylib/tests/test_exporting.py +++ b/pylib/tests/test_exporting.py @@ -106,6 +106,7 @@ def test_export_anki_due(): f["Front"] = "foo" deck.addNote(f) deck.crt -= 86400 * 10 + deck.flush() deck.sched.reset() c = deck.sched.getCard() deck.sched.answerCard(c, 3) diff --git a/rslib/src/backend/mod.rs b/rslib/src/backend/mod.rs index 06cfaaacf..bacd8ff9c 100644 --- a/rslib/src/backend/mod.rs +++ b/rslib/src/backend/mod.rs @@ -22,7 +22,7 @@ use crate::{ media::MediaManager, notes::{Note, NoteID}, notetype::{all_stock_notetypes, NoteType, NoteTypeID, NoteTypeSchema11}, - sched::cutoff::{local_minutes_west_for_stamp, sched_timing_today}, + sched::cutoff::local_minutes_west_for_stamp, sched::timespan::{answer_button_time, learning_congrats, studied_today, time_span}, search::SortMode, template::{render_card, RenderedNode}, @@ -214,9 +214,7 @@ impl Backend { ) -> Result { use pb::backend_output::Value as OValue; Ok(match ival { - Value::SchedTimingToday(input) => { - OValue::SchedTimingToday(self.sched_timing_today(input)) - } + Value::SchedTimingToday(_) => OValue::SchedTimingToday(self.sched_timing_today()?), Value::DeckTree(input) => OValue::DeckTree(self.deck_tree(input)?), Value::RenderCard(input) => OValue::RenderCard(self.render_template(input)?), Value::LocalMinutesWest(stamp) => { @@ -436,18 +434,8 @@ impl Backend { self.progress_callback = progress_cb; } - fn sched_timing_today(&self, input: pb::SchedTimingTodayIn) -> pb::SchedTimingTodayOut { - let today = sched_timing_today( - TimestampSecs(input.created_secs), - TimestampSecs(input.now_secs), - input.created_mins_west.map(|v| v.val), - input.now_mins_west.map(|v| v.val), - input.rollover_hour.map(|v| v.val as i8), - ); - pb::SchedTimingTodayOut { - days_elapsed: today.days_elapsed, - next_day_at: today.next_day_at, - } + fn sched_timing_today(&self) -> Result { + self.with_col(|col| col.timing_today().map(Into::into)) } fn deck_tree(&self, input: pb::DeckTreeIn) -> Result { @@ -1269,3 +1257,12 @@ fn pbcard_to_native(c: pb::Card) -> Result { data: c.data, }) } + +impl From for pb::SchedTimingTodayOut { + fn from(t: crate::sched::cutoff::SchedTimingToday) -> pb::SchedTimingTodayOut { + pb::SchedTimingTodayOut { + days_elapsed: t.days_elapsed, + next_day_at: t.next_day_at, + } + } +} diff --git a/rslib/src/collection.rs b/rslib/src/collection.rs index e22437063..c52ba7733 100644 --- a/rslib/src/collection.rs +++ b/rslib/src/collection.rs @@ -4,12 +4,10 @@ use crate::err::{AnkiError, Result}; use crate::i18n::I18n; use crate::log::Logger; -use crate::timestamp::TimestampSecs; use crate::types::Usn; use crate::{ decks::{Deck, DeckID}, notetype::{NoteType, NoteTypeID}, - sched::cutoff::{sched_timing_today, SchedTimingToday}, storage::SqliteStorage, undo::UndoManager, }; @@ -51,7 +49,6 @@ pub fn open_test_collection() -> Collection { pub struct CollectionState { task_state: CollectionTaskState, pub(crate) undo: UndoManager, - timing_today: Option, pub(crate) notetype_cache: HashMap>, pub(crate) deck_cache: HashMap>, } @@ -142,37 +139,6 @@ impl Collection { self.storage.close(downgrade) } - // fixme: invalidate when config changes - pub fn timing_today(&mut self) -> Result { - if let Some(timing) = &self.state.timing_today { - if timing.next_day_at > TimestampSecs::now().0 { - return Ok(*timing); - } - } - - let local_offset = if self.server { - self.get_local_mins_west() - } else { - None - }; - - let timing = sched_timing_today( - self.storage.creation_stamp()?, - TimestampSecs::now(), - self.get_creation_mins_west(), - local_offset, - self.get_rollover(), - ); - - self.state.timing_today = Some(timing); - - Ok(timing) - } - - pub(crate) fn learn_cutoff(&self) -> u32 { - TimestampSecs::now().0 as u32 + self.learn_ahead_secs() - } - pub(crate) fn usn(&self) -> Result { // if we cache this in the future, must make sure to invalidate cache when usn bumped in sync.finish() self.storage.usn(self.server) diff --git a/rslib/src/config.rs b/rslib/src/config.rs index 8bf455abf..992612d21 100644 --- a/rslib/src/config.rs +++ b/rslib/src/config.rs @@ -138,8 +138,9 @@ impl Collection { self.set_config(ConfigKey::LocalOffset, &mins) } - pub(crate) fn get_rollover(&self) -> Option { - self.get_config_optional(ConfigKey::Rollover) + pub(crate) fn get_rollover(&self) -> Option { + self.get_config_optional::(ConfigKey::Rollover) + .map(|r| r.min(23)) } #[allow(dead_code)] diff --git a/rslib/src/sched/cutoff.rs b/rslib/src/sched/cutoff.rs index 1da332d63..233a78b45 100644 --- a/rslib/src/sched/cutoff.rs +++ b/rslib/src/sched/cutoff.rs @@ -2,7 +2,7 @@ // License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html use crate::timestamp::TimestampSecs; -use chrono::{Date, Duration, FixedOffset, Local, TimeZone}; +use chrono::{Date, Duration, FixedOffset, Local, TimeZone, Timelike}; #[derive(Debug, PartialEq, Clone, Copy)] pub struct SchedTimingToday { @@ -24,7 +24,7 @@ pub fn sched_timing_today_v2_new( created_mins_west: i32, now_secs: i64, now_mins_west: i32, - rollover_hour: i8, + rollover_hour: u8, ) -> SchedTimingToday { // get date(times) based on timezone offsets let created_date = fixed_offset_from_minutes(created_mins_west) @@ -34,7 +34,6 @@ pub fn sched_timing_today_v2_new( let today = now_datetime.date(); // rollover - let rollover_hour = normalized_rollover_hour(rollover_hour); let rollover_today_datetime = today.and_hms(rollover_hour as u32, 0, 0); let rollover_passed = rollover_today_datetime <= now_datetime; let next_day_at = if rollover_passed { @@ -67,17 +66,6 @@ fn days_elapsed( days.max(0) as u32 } -/// Negative rollover hours are relative to the next day, eg -1 = 23. -/// Cap hour to 23. -fn normalized_rollover_hour(hour: i8) -> u8 { - let capped_hour = hour.max(-23).min(23); - if capped_hour < 0 { - (24 + capped_hour) as u8 - } else { - capped_hour as u8 - } -} - /// Build a FixedOffset struct, capping minutes_west if out of bounds. fn fixed_offset_from_minutes(minutes_west: i32) -> FixedOffset { let bounded_minutes = minutes_west.max(-23 * 60).min(23 * 60); @@ -95,6 +83,10 @@ pub fn local_minutes_west_for_stamp(stamp: i64) -> i32 { // Legacy code // ---------------------------------- +pub(crate) fn v1_rollover_from_creation_stamp(crt: i64) -> u8 { + Local.timestamp(crt, 0).hour() as u8 +} + pub(crate) fn v1_creation_date() -> i64 { let now = TimestampSecs::now(); v1_creation_date_inner(now, local_minutes_west_for_stamp(now.0)) @@ -124,24 +116,23 @@ fn sched_timing_today_v1(crt: i64, now: i64) -> SchedTimingToday { fn sched_timing_today_v2_legacy( crt: i64, - rollover: i8, + rollover: u8, now: i64, mins_west: i32, ) -> SchedTimingToday { - let normalized_rollover = normalized_rollover_hour(rollover); let offset = fixed_offset_from_minutes(mins_west); let crt_at_rollover = offset .timestamp(crt, 0) .date() - .and_hms(normalized_rollover as u32, 0, 0) + .and_hms(rollover as u32, 0, 0) .timestamp(); let days_elapsed = (now - crt_at_rollover) / 86_400; let mut next_day_at = offset .timestamp(now, 0) .date() - .and_hms(normalized_rollover as u32, 0, 0) + .and_hms(rollover as u32, 0, 0) .timestamp(); if next_day_at < now { next_day_at += 86_400; @@ -161,7 +152,7 @@ pub(crate) fn sched_timing_today( now_secs: TimestampSecs, created_mins_west: Option, now_mins_west: Option, - rollover_hour: Option, + rollover_hour: Option, ) -> SchedTimingToday { let now_west = now_mins_west.unwrap_or_else(|| local_minutes_west_for_stamp(now_secs.0)); match (rollover_hour, created_mins_west) { @@ -188,17 +179,6 @@ mod test { // static timezone for tests const AEST_MINS_WEST: i32 = -600; - #[test] - fn rollover() { - assert_eq!(normalized_rollover_hour(4), 4); - assert_eq!(normalized_rollover_hour(23), 23); - assert_eq!(normalized_rollover_hour(24), 23); - assert_eq!(normalized_rollover_hour(-1), 23); - assert_eq!(normalized_rollover_hour(-2), 22); - assert_eq!(normalized_rollover_hour(-23), 1); - assert_eq!(normalized_rollover_hour(-24), 1); - } - #[test] fn fixed_offset() { let offset = fixed_offset_from_minutes(AEST_MINS_WEST); @@ -206,7 +186,7 @@ mod test { } // helper - fn elap(start: i64, end: i64, start_west: i32, end_west: i32, rollhour: i8) -> u32 { + fn elap(start: i64, end: i64, start_west: i32, end_west: i32, rollhour: u8) -> u32 { let today = sched_timing_today_v2_new(start, start_west, end, end_west, rollhour); today.days_elapsed } @@ -300,7 +280,7 @@ mod test { end_stamp, crt_offset, end_offset, - *rollover_hour as i8 + *rollover_hour as u8 ), elap_day ); @@ -323,7 +303,7 @@ mod test { crt.offset().utc_minus_local() / 60, now.timestamp(), now.offset().utc_minus_local() / 60, - rollhour as i8, + rollhour as u8, ); assert_eq!(today.next_day_at, next_day_at.timestamp()); @@ -335,7 +315,7 @@ mod test { crt.offset().utc_minus_local() / 60, now.timestamp(), now.offset().utc_minus_local() / 60, - rollhour as i8, + rollhour as u8, ); assert_eq!(today.next_day_at, next_day_at.timestamp()); @@ -347,7 +327,7 @@ mod test { crt.offset().utc_minus_local() / 60, now.timestamp(), now.offset().utc_minus_local() / 60, - rollhour as i8, + rollhour as u8, ); assert_eq!(today.next_day_at, next_day_at.timestamp()); } diff --git a/rslib/src/sched/mod.rs b/rslib/src/sched/mod.rs index 84889c7aa..d8571d19b 100644 --- a/rslib/src/sched/mod.rs +++ b/rslib/src/sched/mod.rs @@ -1,2 +1,46 @@ +// Copyright: Ankitects Pty Ltd and contributors +// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html + +use crate::{ + collection::Collection, config::SchedulerVersion, err::Result, timestamp::TimestampSecs, +}; + pub mod cutoff; pub mod timespan; + +use cutoff::{sched_timing_today, v1_rollover_from_creation_stamp, SchedTimingToday}; + +impl Collection { + pub fn timing_today(&mut self) -> Result { + self.timing_for_timestamp(TimestampSecs::now()) + } + + pub(crate) fn timing_for_timestamp(&mut self, now: TimestampSecs) -> Result { + let local_offset = if self.server { + self.get_local_mins_west() + } else { + None + }; + + Ok(sched_timing_today( + self.storage.creation_stamp()?, + now, + self.get_creation_mins_west(), + local_offset, + self.get_rollover(), + )) + } + + pub fn rollover_for_current_scheduler(&self) -> Result { + match self.sched_ver() { + SchedulerVersion::V1 => Ok(v1_rollover_from_creation_stamp( + self.storage.creation_stamp()?.0, + )), + SchedulerVersion::V2 => Ok(self.get_rollover().unwrap_or(4)), + } + } + + pub(crate) fn learn_cutoff(&self) -> u32 { + TimestampSecs::now().0 as u32 + self.learn_ahead_secs() + } +}