diff --git a/rslib/src/backend/mod.rs b/rslib/src/backend/mod.rs index 197004aca..84d40129e 100644 --- a/rslib/src/backend/mod.rs +++ b/rslib/src/backend/mod.rs @@ -12,7 +12,7 @@ use crate::log::{default_logger, Logger}; use crate::media::check::MediaChecker; use crate::media::sync::MediaSyncProgress; use crate::media::MediaManager; -use crate::sched::cutoff::{local_minutes_west_for_stamp, sched_timing_today}; +use crate::sched::cutoff::{local_minutes_west_for_stamp, sched_timing_today_v2_new}; use crate::sched::timespan::{answer_button_time, learning_congrats, studied_today, time_span}; use crate::template::{ render_card, without_legacy_template_directives, FieldMap, FieldRequirements, ParsedTemplate, @@ -348,7 +348,7 @@ impl Backend { } fn sched_timing_today(&self, input: pb::SchedTimingTodayIn) -> pb::SchedTimingTodayOut { - let today = sched_timing_today( + let today = sched_timing_today_v2_new( input.created_secs as i64, input.created_mins_west, input.now_secs as i64, diff --git a/rslib/src/config.rs b/rslib/src/config.rs index 2394819c9..42ae62067 100644 --- a/rslib/src/config.rs +++ b/rslib/src/config.rs @@ -5,7 +5,11 @@ use crate::types::ObjID; use serde_derive::Deserialize; #[derive(Deserialize)] +#[serde(rename_all = "camelCase")] pub struct Config { #[serde(rename = "curDeck")] pub(crate) current_deck_id: ObjID, + pub(crate) rollover: Option, + pub(crate) creation_offset: Option, + pub(crate) local_offset: Option, } diff --git a/rslib/src/sched/cutoff.rs b/rslib/src/sched/cutoff.rs index 5c99b57a5..04bdb60fa 100644 --- a/rslib/src/sched/cutoff.rs +++ b/rslib/src/sched/cutoff.rs @@ -1,8 +1,10 @@ // Copyright: Ankitects Pty Ltd and contributors // License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html +use crate::time::i64_unix_secs; use chrono::{Date, Duration, FixedOffset, Local, TimeZone}; +#[derive(Debug, PartialEq, Clone, Copy)] pub struct SchedTimingToday { /// The number of days that have passed since the collection was created. pub days_elapsed: u32, @@ -17,7 +19,7 @@ pub struct SchedTimingToday { /// - now_secs is a timestamp of the current time /// - now_mins_west is the current offset west of UTC /// - rollover_hour is the hour of the day the rollover happens (eg 4 for 4am) -pub fn sched_timing_today( +pub fn sched_timing_today_v2_new( created_secs: i64, created_mins_west: i32, now_secs: i64, @@ -90,11 +92,86 @@ pub fn local_minutes_west_for_stamp(stamp: i64) -> i32 { Local.timestamp(stamp, 0).offset().utc_minus_local() / 60 } +// Legacy code +// ---------------------------------- + +fn sched_timing_today_v1(crt: i64, now: i64) -> SchedTimingToday { + let days_elapsed = (now - crt) / 86_400; + let next_day_at = crt + (days_elapsed + 1) * 86_400; + SchedTimingToday { + days_elapsed: days_elapsed as u32, + next_day_at, + } +} + +fn sched_timing_today_v2_legacy( + crt: i64, + rollover: i8, + 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) + .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) + .timestamp(); + if next_day_at < now { + next_day_at += 86_400; + } + + SchedTimingToday { + days_elapsed: days_elapsed as u32, + next_day_at, + } +} + +// ---------------------------------- + +/// Based on provided input, get timing info from the relevant function. +pub(crate) fn sched_timing_today( + created_secs: i64, + created_mins_west: Option, + now_mins_west: Option, + rollover_hour: Option, +) -> SchedTimingToday { + let now = i64_unix_secs(); + + match (rollover_hour, created_mins_west) { + (None, _) => { + // if rollover unset, v1 scheduler + sched_timing_today_v1(created_secs, now) + } + (Some(roll), None) => { + // if creation offset unset, v2 legacy cutoff using local timezone + let offset = local_minutes_west_for_stamp(now); + sched_timing_today_v2_legacy(created_secs, roll, now, offset) + } + (Some(roll), Some(crt_west)) => { + // new cutoff code, using provided current timezone, falling back on local timezone + let now_west = now_mins_west.unwrap_or_else(|| local_minutes_west_for_stamp(now)); + sched_timing_today_v2_new(created_secs, crt_west, now, now_west, roll) + } + } +} + #[cfg(test)] mod test { + use super::SchedTimingToday; + use crate::sched::cutoff::sched_timing_today_v1; + use crate::sched::cutoff::sched_timing_today_v2_legacy; use crate::sched::cutoff::{ fixed_offset_from_minutes, local_minutes_west_for_stamp, normalized_rollover_hour, - sched_timing_today, + sched_timing_today_v2_new, }; use chrono::{FixedOffset, Local, TimeZone, Utc}; @@ -117,7 +194,7 @@ mod test { // helper fn elap(start: i64, end: i64, start_west: i32, end_west: i32, rollhour: i8) -> u32 { - let today = sched_timing_today(start, start_west, end, end_west, rollhour); + let today = sched_timing_today_v2_new(start, start_west, end, end_west, rollhour); today.days_elapsed } @@ -228,7 +305,7 @@ mod test { // before the rollover, the next day should be later on the same day let now = Local.ymd(2019, 1, 3).and_hms(2, 0, 0); let next_day_at = Local.ymd(2019, 1, 3).and_hms(rollhour, 0, 0); - let today = sched_timing_today( + let today = sched_timing_today_v2_new( crt.timestamp(), crt.offset().utc_minus_local() / 60, now.timestamp(), @@ -240,7 +317,7 @@ mod test { // after the rollover, the next day should be the next day let now = Local.ymd(2019, 1, 3).and_hms(rollhour, 0, 0); let next_day_at = Local.ymd(2019, 1, 4).and_hms(rollhour, 0, 0); - let today = sched_timing_today( + let today = sched_timing_today_v2_new( crt.timestamp(), crt.offset().utc_minus_local() / 60, now.timestamp(), @@ -252,7 +329,7 @@ mod test { // after the rollover, the next day should be the next day let now = Local.ymd(2019, 1, 3).and_hms(rollhour + 3, 0, 0); let next_day_at = Local.ymd(2019, 1, 4).and_hms(rollhour, 0, 0); - let today = sched_timing_today( + let today = sched_timing_today_v2_new( crt.timestamp(), crt.offset().utc_minus_local() / 60, now.timestamp(), @@ -261,4 +338,34 @@ mod test { ); assert_eq!(today.next_day_at, next_day_at.timestamp()); } + + #[test] + fn legacy_timing() { + let now = 1584491078; + let mins_west = -600; + + assert_eq!( + sched_timing_today_v1(1575226800, now), + SchedTimingToday { + days_elapsed: 107, + next_day_at: 1584558000 + } + ); + + assert_eq!( + sched_timing_today_v2_legacy(1533564000, 0, now, mins_west), + SchedTimingToday { + days_elapsed: 589, + next_day_at: 1584540000 + } + ); + + assert_eq!( + sched_timing_today_v2_legacy(1524038400, 4, now, mins_west), + SchedTimingToday { + days_elapsed: 700, + next_day_at: 1584554400 + } + ); + } } diff --git a/rslib/src/storage/sqlite.rs b/rslib/src/storage/sqlite.rs index a689b34c5..bcf702110 100644 --- a/rslib/src/storage/sqlite.rs +++ b/rslib/src/storage/sqlite.rs @@ -6,7 +6,11 @@ use crate::config::Config; use crate::err::Result; use crate::err::{AnkiError, DBErrorKind}; use crate::time::{i64_unix_millis, i64_unix_secs}; -use crate::{decks::Deck, types::Usn}; +use crate::{ + decks::Deck, + sched::cutoff::{sched_timing_today, SchedTimingToday}, + types::Usn, +}; use rusqlite::{params, Connection, NO_PARAMS}; use std::path::{Path, PathBuf}; @@ -121,6 +125,8 @@ pub(crate) struct StorageContext<'a> { server: bool, #[allow(dead_code)] usn: Option, + + timing_today: Option, } impl StorageContext<'_> { @@ -129,6 +135,7 @@ impl StorageContext<'_> { db, server, usn: None, + timing_today: None, } } @@ -225,4 +232,24 @@ impl StorageContext<'_> { Ok(serde_json::from_str(row.get_raw(0).as_str()?)?) }) } + + #[allow(dead_code)] + pub(crate) fn timing_today(&mut self) -> Result { + if self.timing_today.is_none() { + let crt: i64 = self + .db + .prepare_cached("select crt from col")? + .query_row(NO_PARAMS, |row| row.get(0))?; + let conf = self.all_config()?; + let now_offset = if self.server { conf.local_offset } else { None }; + + self.timing_today = Some(sched_timing_today( + crt, + conf.creation_offset, + now_offset, + conf.rollover, + )); + } + Ok(*self.timing_today.as_ref().unwrap()) + } }