diff --git a/pylib/anki/collection.py b/pylib/anki/collection.py index 463b7d30f..94ec8124a 100644 --- a/pylib/anki/collection.py +++ b/pylib/anki/collection.py @@ -135,14 +135,6 @@ class Collection: self._loadScheduler() - # the sync code uses this to send the local timezone to AnkiWeb - def localOffset(self) -> Optional[int]: - "Minutes west of UTC. Only applies to V2 scheduler." - if isinstance(self.sched, V1Scheduler): - return None - else: - return self.backend.local_minutes_west(intTime()) - # DB-related ########################################################################## diff --git a/pylib/rsbridge/lib.rs b/pylib/rsbridge/lib.rs index 53f324f23..e2a69bbb5 100644 --- a/pylib/rsbridge/lib.rs +++ b/pylib/rsbridge/lib.rs @@ -40,13 +40,11 @@ fn want_release_gil(method: u32) -> bool { | BackendMethod::RenderExistingCard | BackendMethod::RenderUncommittedCard | BackendMethod::StripAVTags - | BackendMethod::LocalMinutesWest | BackendMethod::SchedTimingToday | BackendMethod::AddOrUpdateDeckLegacy | BackendMethod::NewDeckLegacy | BackendMethod::NewDeckConfigLegacy | BackendMethod::GetStockNotetypeLegacy - | BackendMethod::SetLocalMinutesWest | BackendMethod::StudiedToday | BackendMethod::TranslateString | BackendMethod::FormatTimespan diff --git a/rslib/backend.proto b/rslib/backend.proto index d4e3df1e1..0688e1c36 100644 --- a/rslib/backend.proto +++ b/rslib/backend.proto @@ -95,8 +95,6 @@ service BackendService { // scheduling - rpc LocalMinutesWest(Int64) returns (Int32); - rpc SetLocalMinutesWest(Int32) returns (Empty); rpc SchedTimingToday(Empty) returns (SchedTimingTodayOut); rpc StudiedToday(Empty) returns (String); rpc StudiedTodayMessage(StudiedTodayMessageIn) returns (String); diff --git a/rslib/src/backend/http_sync_server.rs b/rslib/src/backend/http_sync_server.rs new file mode 100644 index 000000000..e69de29bb diff --git a/rslib/src/backend/mod.rs b/rslib/src/backend/mod.rs index fa99c7f36..6de2b6b79 100644 --- a/rslib/src/backend/mod.rs +++ b/rslib/src/backend/mod.rs @@ -32,7 +32,6 @@ use crate::{ all_stock_notetypes, CardTemplateSchema11, NoteType, NoteTypeID, NoteTypeSchema11, RenderCardOutput, }, - sched::cutoff::local_minutes_west_for_stamp, sched::new::NewCardSortOrder, sched::timespan::{answer_button_time, time_span}, search::{ @@ -533,20 +532,6 @@ impl BackendService for Backend { }) } - fn local_minutes_west(&self, input: pb::Int64) -> BackendResult { - Ok(pb::Int32 { - val: local_minutes_west_for_stamp(input.val), - }) - } - - fn set_local_minutes_west(&self, input: pb::Int32) -> BackendResult { - self.with_col(|col| { - col.transact(None, |col| { - col.set_local_mins_west(input.val).map(Into::into) - }) - }) - } - /// Fetch data from DB and return rendered string. fn studied_today(&self, _input: pb::Empty) -> BackendResult { self.with_col(|col| col.studied_today().map(Into::into)) diff --git a/rslib/src/config.rs b/rslib/src/config.rs index 70294ef1f..53838e51c 100644 --- a/rslib/src/config.rs +++ b/rslib/src/config.rs @@ -141,11 +141,11 @@ impl Collection { .unwrap_or(DeckID(1)) } - pub(crate) fn get_creation_mins_west(&self) -> Option { + pub(crate) fn get_creation_utc_offset(&self) -> Option { self.get_config_optional(ConfigKey::CreationOffset) } - pub(crate) fn set_creation_mins_west(&self, mins: Option) -> Result<()> { + pub(crate) fn set_creation_utc_offset(&self, mins: Option) -> Result<()> { if let Some(mins) = mins { self.set_config(ConfigKey::CreationOffset, &mins) } else { @@ -153,11 +153,11 @@ impl Collection { } } - pub(crate) fn get_local_mins_west(&self) -> Option { + pub(crate) fn get_configured_utc_offset(&self) -> Option { self.get_config_optional(ConfigKey::LocalOffset) } - pub(crate) fn set_local_mins_west(&self, mins: i32) -> Result<()> { + pub(crate) fn set_configured_utc_offset(&self, mins: i32) -> Result<()> { self.set_config(ConfigKey::LocalOffset, &mins) } diff --git a/rslib/src/preferences.rs b/rslib/src/preferences.rs index 337a61e4c..aea983441 100644 --- a/rslib/src/preferences.rs +++ b/rslib/src/preferences.rs @@ -9,7 +9,6 @@ use crate::{ collection::Collection, err::Result, sched::cutoff::local_minutes_west_for_stamp, - timestamp::TimestampSecs, }; impl Collection { @@ -43,7 +42,7 @@ impl Collection { show_remaining_due_counts: self.get_show_due_counts(), show_intervals_on_buttons: self.get_show_intervals_above_buttons(), time_limit_secs: self.get_answer_time_limit_secs(), - new_timezone: self.get_creation_mins_west().is_some(), + new_timezone: self.get_creation_utc_offset().is_some(), day_learn_first: self.get_day_learn_first(), }) } @@ -73,15 +72,11 @@ impl Collection { } if s.new_timezone { - if self.get_creation_mins_west().is_none() { - self.set_creation_mins_west(Some(local_minutes_west_for_stamp(created.0)))?; + if self.get_creation_utc_offset().is_none() { + self.set_creation_utc_offset(Some(local_minutes_west_for_stamp(created.0)))?; } } else { - self.set_creation_mins_west(None)?; - } - - if s.scheduler_version != 1 { - self.set_local_mins_west(local_minutes_west_for_stamp(TimestampSecs::now().0))?; + self.set_creation_utc_offset(None)?; } // fixme: currently scheduler change unhandled diff --git a/rslib/src/sched/cutoff.rs b/rslib/src/sched/cutoff.rs index 5938984d5..c7727e46c 100644 --- a/rslib/src/sched/cutoff.rs +++ b/rslib/src/sched/cutoff.rs @@ -1,7 +1,7 @@ // Copyright: Ankitects Pty Ltd and contributors // License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html -use crate::timestamp::TimestampSecs; +use crate::prelude::*; use chrono::{Date, Duration, FixedOffset, Local, TimeZone, Timelike}; #[derive(Debug, PartialEq, Clone, Copy)] @@ -13,24 +13,21 @@ pub struct SchedTimingToday { } /// Timing information for the current day. -/// - created_secs is a UNIX timestamp of the collection creation time -/// - created_mins_west is the offset west of UTC at the time of creation -/// (eg UTC+10 hours is -600) -/// - now_secs is a timestamp of the current time -/// - now_mins_west is the current offset west of UTC +/// - creation_secs is a UNIX timestamp of the collection creation time +/// - creation_utc_offset is the UTC offset at collection creation time +/// - current_secs is a timestamp of the current time +/// - current_utc_offset is the current UTC offset /// - rollover_hour is the hour of the day the rollover happens (eg 4 for 4am) pub fn sched_timing_today_v2_new( - created_secs: i64, - created_mins_west: i32, - now_secs: i64, - now_mins_west: i32, + creation_secs: TimestampSecs, + creation_utc_offset: FixedOffset, + current_secs: TimestampSecs, + current_utc_offset: FixedOffset, rollover_hour: u8, ) -> SchedTimingToday { // get date(times) based on timezone offsets - let created_date = fixed_offset_from_minutes(created_mins_west) - .timestamp(created_secs, 0) - .date(); - let now_datetime = fixed_offset_from_minutes(now_mins_west).timestamp(now_secs, 0); + let created_date = creation_secs.datetime(creation_utc_offset).date(); + let now_datetime = current_secs.datetime(current_utc_offset); let today = now_datetime.date(); // rollover @@ -118,9 +115,9 @@ fn v1_creation_date_adjusted_to_hour_inner(crt: i64, hour: u8, offset: FixedOffs .timestamp() } -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; +fn sched_timing_today_v1(crt: TimestampSecs, now: TimestampSecs) -> SchedTimingToday { + let days_elapsed = (now.0 - crt.0) / 86_400; + let next_day_at = crt.0 + (days_elapsed + 1) * 86_400; SchedTimingToday { days_elapsed: days_elapsed as u32, next_day_at, @@ -128,26 +125,24 @@ fn sched_timing_today_v1(crt: i64, now: i64) -> SchedTimingToday { } fn sched_timing_today_v2_legacy( - crt: i64, + crt: TimestampSecs, rollover: u8, - now: i64, - mins_west: i32, + now: TimestampSecs, + current_utc_offset: FixedOffset, ) -> SchedTimingToday { - let offset = fixed_offset_from_minutes(mins_west); - - let crt_at_rollover = offset - .timestamp(crt, 0) + let crt_at_rollover = crt + .datetime(current_utc_offset) .date() .and_hms(rollover as u32, 0, 0) .timestamp(); - let days_elapsed = (now - crt_at_rollover) / 86_400; + let days_elapsed = (now.0 - crt_at_rollover) / 86_400; - let mut next_day_at = offset - .timestamp(now, 0) + let mut next_day_at = now + .datetime(current_utc_offset) .date() .and_hms(rollover as u32, 0, 0) .timestamp(); - if next_day_at < now { + if next_day_at < now.0 { next_day_at += 86_400; } @@ -159,27 +154,33 @@ fn sched_timing_today_v2_legacy( // ---------------------------------- -/// Based on provided input, get timing info from the relevant function. +/// Decide which scheduler timing to use based on the provided input, +/// and return the relevant timing info. pub(crate) fn sched_timing_today( - created_secs: TimestampSecs, - now_secs: TimestampSecs, - created_mins_west: Option, - now_mins_west: Option, + creation_secs: TimestampSecs, + current_secs: TimestampSecs, + creation_utc_offset: Option, + current_utc_offset: FixedOffset, 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) { + match (rollover_hour, creation_utc_offset) { (None, _) => { // if rollover unset, v1 scheduler - sched_timing_today_v1(created_secs.0, now_secs.0) + sched_timing_today_v1(creation_secs, current_secs) } - (Some(roll), None) => { + (Some(rollover), None) => { // if creationOffset unset, v2 scheduler with legacy cutoff handling - sched_timing_today_v2_legacy(created_secs.0, roll, now_secs.0, now_west) + sched_timing_today_v2_legacy(creation_secs, rollover, current_secs, current_utc_offset) } - (Some(roll), Some(crt_west)) => { + (Some(rollover), Some(creation_utc_offset)) => { // v2 scheduler, new cutoff handling - sched_timing_today_v2_new(created_secs.0, crt_west, now_secs.0, now_west, roll) + sched_timing_today_v2_new( + creation_secs, + creation_utc_offset, + current_secs, + current_utc_offset, + rollover, + ) } } } @@ -192,6 +193,10 @@ mod test { // static timezone for tests const AEST_MINS_WEST: i32 = -600; + fn aest_offset() -> FixedOffset { + FixedOffset::west(AEST_MINS_WEST * 60) + } + #[test] fn fixed_offset() { let offset = fixed_offset_from_minutes(AEST_MINS_WEST); @@ -200,6 +205,10 @@ mod test { // helper fn elap(start: i64, end: i64, start_west: i32, end_west: i32, rollhour: u8) -> u32 { + let start = TimestampSecs(start); + let end = TimestampSecs(end); + let start_west = FixedOffset::west(start_west * 60); + let end_west = FixedOffset::west(end_west * 60); let today = sched_timing_today_v2_new(start, start_west, end, end_west, rollhour); today.days_elapsed } @@ -312,10 +321,10 @@ mod test { 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_v2_new( - crt.timestamp(), - crt.offset().utc_minus_local() / 60, - now.timestamp(), - now.offset().utc_minus_local() / 60, + TimestampSecs(crt.timestamp()), + *crt.offset(), + TimestampSecs(now.timestamp()), + *now.offset(), rollhour as u8, ); assert_eq!(today.next_day_at, next_day_at.timestamp()); @@ -324,10 +333,10 @@ mod test { 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_v2_new( - crt.timestamp(), - crt.offset().utc_minus_local() / 60, - now.timestamp(), - now.offset().utc_minus_local() / 60, + TimestampSecs(crt.timestamp()), + *crt.offset(), + TimestampSecs(now.timestamp()), + *now.offset(), rollhour as u8, ); assert_eq!(today.next_day_at, next_day_at.timestamp()); @@ -336,10 +345,10 @@ mod test { 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_v2_new( - crt.timestamp(), - crt.offset().utc_minus_local() / 60, - now.timestamp(), - now.offset().utc_minus_local() / 60, + TimestampSecs(crt.timestamp()), + *crt.offset(), + TimestampSecs(now.timestamp()), + *now.offset(), rollhour as u8, ); assert_eq!(today.next_day_at, next_day_at.timestamp()); @@ -347,10 +356,10 @@ mod test { #[test] fn legacy_timing() { - let now = 1584491078; + let now = TimestampSecs(1584491078); assert_eq!( - sched_timing_today_v1(1575226800, now), + sched_timing_today_v1(TimestampSecs(1575226800), now), SchedTimingToday { days_elapsed: 107, next_day_at: 1584558000 @@ -358,7 +367,7 @@ mod test { ); assert_eq!( - sched_timing_today_v2_legacy(1533564000, 0, now, AEST_MINS_WEST), + sched_timing_today_v2_legacy(TimestampSecs(1533564000), 0, now, aest_offset()), SchedTimingToday { days_elapsed: 589, next_day_at: 1584540000 @@ -366,7 +375,7 @@ mod test { ); assert_eq!( - sched_timing_today_v2_legacy(1524038400, 4, now, AEST_MINS_WEST), + sched_timing_today_v2_legacy(TimestampSecs(1524038400), 4, now, aest_offset()), SchedTimingToday { days_elapsed: 700, next_day_at: 1584554400 diff --git a/rslib/src/sched/mod.rs b/rslib/src/sched/mod.rs index 60554f9ed..4bf5e64fe 100644 --- a/rslib/src/sched/mod.rs +++ b/rslib/src/sched/mod.rs @@ -1,9 +1,7 @@ // 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, -}; +use crate::{collection::Collection, config::SchedulerVersion, err::Result, prelude::*}; pub mod bury_and_suspend; pub(crate) mod congrats; @@ -15,8 +13,8 @@ pub mod timespan; use chrono::FixedOffset; use cutoff::{ - fixed_offset_from_minutes, local_minutes_west_for_stamp, sched_timing_today, - v1_creation_date_adjusted_to_hour, v1_rollover_from_creation_stamp, SchedTimingToday, + sched_timing_today, v1_creation_date_adjusted_to_hour, v1_rollover_from_creation_stamp, + SchedTimingToday, }; impl Collection { @@ -29,37 +27,62 @@ impl Collection { } pub(crate) fn timing_for_timestamp(&self, now: TimestampSecs) -> Result { - let local_offset = if self.server { - self.get_local_mins_west() - } else { - None - }; + let current_utc_offset = self.local_utc_offset_for_user()?; let rollover_hour = match self.sched_ver() { SchedulerVersion::V1 => None, - SchedulerVersion::V2 => self.get_v2_rollover().or(Some(4)), + SchedulerVersion::V2 => { + let configured_rollover = self.get_v2_rollover(); + match configured_rollover { + None => { + // an older Anki version failed to set this; correct + // the issue + self.set_v2_rollover(4)?; + Some(4) + } + val => val, + } + } }; Ok(sched_timing_today( self.storage.creation_stamp()?, now, - self.get_creation_mins_west(), - local_offset, + self.creation_utc_offset(), + current_utc_offset, rollover_hour, )) } - /// Get the local timezone. - /// We could use this to simplify timing_for_timestamp() in the future - pub(crate) fn local_offset(&self) -> FixedOffset { - let local_mins_west = if self.server { - self.get_local_mins_west() + /// In the client case, return the current local timezone offset, + /// ensuring the config reflects the current value. + /// In the server case, return the value set in the config, and + /// fall back on UTC if it's missing/invalid. + pub(crate) fn local_utc_offset_for_user(&self) -> Result { + let config_tz = self + .get_configured_utc_offset() + .and_then(|v| FixedOffset::west_opt(v * 60)) + .unwrap_or_else(|| FixedOffset::west(0)); + + let local_tz = TimestampSecs::now().local_utc_offset(); + + Ok(if self.server { + config_tz } else { - None - }; - let local_mins_west = - local_mins_west.unwrap_or_else(|| local_minutes_west_for_stamp(TimestampSecs::now().0)); - fixed_offset_from_minutes(local_mins_west) + // if the timezone has changed, update the config + if config_tz != local_tz { + self.set_configured_utc_offset(local_tz.utc_minus_local() / 60)?; + } + local_tz + }) + } + + /// Return the timezone offset at collection creation time. This should + /// only be set when the V2 scheduler is active and the new timezone + /// code is enabled. + fn creation_utc_offset(&self) -> Option { + self.get_creation_utc_offset() + .and_then(|v| FixedOffset::west_opt(v * 60)) } pub fn rollover_for_current_scheduler(&self) -> Result { diff --git a/rslib/src/stats/card.rs b/rslib/src/stats/card.rs index 2b0dc4b4f..4b264f29e 100644 --- a/rslib/src/stats/card.rs +++ b/rslib/src/stats/card.rs @@ -58,7 +58,7 @@ struct RevlogText { impl Collection { pub fn card_stats(&mut self, cid: CardID) -> Result { let stats = self.gather_card_stats(cid)?; - Ok(self.card_stats_to_string(stats)) + self.card_stats_to_string(stats) } fn gather_card_stats(&mut self, cid: CardID) -> Result { @@ -126,8 +126,8 @@ impl Collection { }) } - fn card_stats_to_string(&self, cs: CardStats) -> String { - let offset = self.local_offset(); + fn card_stats_to_string(&self, cs: CardStats) -> Result { + let offset = self.local_utc_offset_for_user()?; let i18n = &self.i18n; let mut stats = vec![( @@ -216,13 +216,13 @@ impl Collection { taken_secs: i18n.tr(TR::CardStatsReviewLogTimeTaken).into(), }; - CardStatsTemplate { + Ok(CardStatsTemplate { stats, revlog, revlog_titles, } .render() - .unwrap() + .unwrap()) } } diff --git a/rslib/src/stats/graphs.rs b/rslib/src/stats/graphs.rs index d2afcbee0..f0e8e7163 100644 --- a/rslib/src/stats/graphs.rs +++ b/rslib/src/stats/graphs.rs @@ -22,7 +22,7 @@ impl Collection { 0 }); - let offset = self.local_offset(); + let offset = self.local_utc_offset_for_user()?; let local_offset_secs = offset.local_minus_utc() as i64; let cards = self.storage.all_searched_cards()?; diff --git a/rslib/src/sync/http.rs b/rslib/src/sync/http.rs index 5778f7181..e7f88bbe1 100644 --- a/rslib/src/sync/http.rs +++ b/rslib/src/sync/http.rs @@ -60,8 +60,6 @@ pub struct MetaIn { pub struct StartIn { #[serde(rename = "minUsn")] pub client_usn: Usn, - #[serde(rename = "offset", default)] - pub minutes_west: Option, #[serde(rename = "lnewer")] pub local_is_newer: bool, } diff --git a/rslib/src/sync/http_client.rs b/rslib/src/sync/http_client.rs index a202adbaf..7d779dd95 100644 --- a/rslib/src/sync/http_client.rs +++ b/rslib/src/sync/http_client.rs @@ -73,15 +73,9 @@ impl SyncServer for HTTPSyncClient { self.json_request(&input).await } - async fn start( - &mut self, - client_usn: Usn, - minutes_west: Option, - local_is_newer: bool, - ) -> Result { + async fn start(&mut self, client_usn: Usn, local_is_newer: bool) -> Result { let input = SyncRequest::Start(StartIn { client_usn, - minutes_west, local_is_newer, }); self.json_request(&input).await @@ -370,13 +364,13 @@ mod test { }) )); - let _graves = syncer.start(Usn(1), None, true).await?; + let _graves = syncer.start(Usn(1), true).await?; // aborting should now work syncer.abort().await?; // start again, and continue - let _graves = syncer.start(Usn(1), None, true).await?; + let _graves = syncer.start(Usn(1), true).await?; syncer.apply_graves(Graves::default()).await?; diff --git a/rslib/src/sync/mod.rs b/rslib/src/sync/mod.rs index 5749db3d6..19037e48b 100644 --- a/rslib/src/sync/mod.rs +++ b/rslib/src/sync/mod.rs @@ -404,11 +404,7 @@ where async fn start_and_process_deletions(&mut self, state: &SyncState) -> Result<()> { let remote: Graves = self .remote - .start( - state.usn_at_last_sync, - self.col.get_local_mins_west(), - state.local_is_newer, - ) + .start(state.usn_at_last_sync, state.local_is_newer) .await?; debug!(self.col.log, "removed on remote"; diff --git a/rslib/src/sync/server.rs b/rslib/src/sync/server.rs index 0fc18428b..aca568b80 100644 --- a/rslib/src/sync/server.rs +++ b/rslib/src/sync/server.rs @@ -15,12 +15,7 @@ use super::ChunkableIDs; #[async_trait(?Send)] pub trait SyncServer { async fn meta(&self) -> Result; - async fn start( - &mut self, - client_usn: Usn, - minutes_west: Option, - local_is_newer: bool, - ) -> Result; + async fn start(&mut self, client_usn: Usn, local_is_newer: bool) -> Result; async fn apply_graves(&mut self, client_chunk: Graves) -> Result<()>; async fn apply_changes(&mut self, client_changes: UnchunkedChanges) -> Result; @@ -80,20 +75,12 @@ impl SyncServer for LocalServer { }) } - async fn start( - &mut self, - client_usn: Usn, - minutes_west: Option, - client_is_newer: bool, - ) -> Result { + async fn start(&mut self, client_usn: Usn, client_is_newer: bool) -> Result { self.server_usn = self.col.usn()?; self.client_usn = client_usn; self.client_is_newer = client_is_newer; self.col.storage.begin_rust_trx()?; - if let Some(mins) = minutes_west { - self.col.set_local_mins_west(mins)?; - } self.col.storage.pending_graves(client_usn) } diff --git a/rslib/src/timestamp.rs b/rslib/src/timestamp.rs index 0189c718a..0abcd3872 100644 --- a/rslib/src/timestamp.rs +++ b/rslib/src/timestamp.rs @@ -27,6 +27,14 @@ impl TimestampSecs { pub(crate) fn date_string(self, offset: FixedOffset) -> String { offset.timestamp(self.0, 0).format("%Y-%m-%d").to_string() } + + pub fn local_utc_offset(self) -> FixedOffset { + *Local.timestamp(self.0, 0).offset() + } + + pub fn datetime(self, utc_offset: FixedOffset) -> DateTime { + utc_offset.timestamp(self.0, 0) + } } impl TimestampMillis {