From 12c60f20fe45516b31fbb569854ab04ac06874ba Mon Sep 17 00:00:00 2001 From: Damien Elmes Date: Fri, 17 Jan 2020 18:52:36 -0700 Subject: [PATCH] _localOffsetForDate() was broken It was including the elapsed time of day when calculating the offset, leading to incorrect results --- proto/backend.proto | 2 ++ pylib/anki/collection.py | 4 ++-- pylib/anki/rsbackend.py | 5 +++++ pylib/anki/schedv2.py | 31 +++++++++++++++---------------- rslib/src/backend.rs | 5 ++++- rslib/src/sched.rs | 23 +++++++++++++++-------- 6 files changed, 43 insertions(+), 27 deletions(-) diff --git a/proto/backend.proto b/proto/backend.proto index eb9892a71..598d7ec89 100644 --- a/proto/backend.proto +++ b/proto/backend.proto @@ -15,6 +15,7 @@ message BackendInput { FindCardsIn find_cards = 19; BrowserRowsIn browser_rows = 20; RenderCardIn render_card = 21; + int64 local_minutes_west = 22; PlusOneIn plus_one = 2046; // temporary, for testing } @@ -28,6 +29,7 @@ message BackendOutput { FindCardsOut find_cards = 19; BrowserRowsOut browser_rows = 20; RenderCardOut render_card = 21; + sint32 local_minutes_west = 22; PlusOneOut plus_one = 2046; // temporary, for testing diff --git a/pylib/anki/collection.py b/pylib/anki/collection.py index fabe871a2..07069ff99 100644 --- a/pylib/anki/collection.py +++ b/pylib/anki/collection.py @@ -132,7 +132,7 @@ class _Collection: elif ver == 2: self.sched = V2Scheduler(self) if not self.server: - self.conf["localOffset"] = self.sched.currentTimezoneOffset() + self.conf["localOffset"] = self.sched._current_timezone_offset() elif self.server.minutes_west is not None: self.conf["localOffset"] = self.server.minutes_west @@ -162,7 +162,7 @@ class _Collection: if isinstance(self.sched, V1Scheduler): return None else: - return self.sched.currentTimezoneOffset() + return self.sched._current_timezone_offset() # DB-related ########################################################################## diff --git a/pylib/anki/rsbackend.py b/pylib/anki/rsbackend.py index 9e5eee8b3..31ce5ca7d 100644 --- a/pylib/anki/rsbackend.py +++ b/pylib/anki/rsbackend.py @@ -143,3 +143,8 @@ class RustBackend: anodes = proto_replacement_list_to_native(out.answer_nodes) # type: ignore return (qnodes, anodes) + + def local_minutes_west(self, stamp: int) -> int: + return self._run_command( + pb.BackendInput(local_minutes_west=stamp) + ).local_minutes_west diff --git a/pylib/anki/schedv2.py b/pylib/anki/schedv2.py index 04ab59dff..f3c02126b 100644 --- a/pylib/anki/schedv2.py +++ b/pylib/anki/schedv2.py @@ -1340,9 +1340,9 @@ where id = ? def _updateCutoff(self) -> None: oldToday = self.today - timing = self._timingToday() + timing = self._timing_today() - if self._newTimezoneEnabled(): + if self._new_timezone_enabled(): self.today = timing.days_elapsed self.dayCutoff = timing.next_day_at else: @@ -1398,41 +1398,40 @@ where id = ? # New timezone handling ########################################################################## - def _newTimezoneEnabled(self) -> bool: + def _new_timezone_enabled(self) -> bool: return self.col.conf.get("creationOffset") is not None - def _timingToday(self) -> SchedTimingToday: + def _timing_today(self) -> SchedTimingToday: return self.col.backend.sched_timing_today( self.col.crt, - self.creationTimezoneOffset(), + self._creation_timezone_offset(), intTime(), - self.currentTimezoneOffset(), + self._current_timezone_offset(), self._rolloverHour(), ) - def currentTimezoneOffset(self) -> int: + def _current_timezone_offset(self) -> int: if self.col.server: return self.col.conf.get("localOffset", 0) else: - return self._localOffsetForDate(datetime.datetime.today()) + return self.col.backend.local_minutes_west(intTime()) - def creationTimezoneOffset(self) -> int: + def _creation_timezone_offset(self) -> int: return self.col.conf.get("creationOffset", 0) - def setCreationOffset(self): + def set_creation_offset(self): """Save the UTC west offset at the time of creation into the DB. Once stored, this activates the new timezone handling code. """ - mins_west = self._localOffsetForDate( - datetime.datetime.fromtimestamp(self.col.crt) - ) + mins_west = self.col.backend.local_minutes_west(self.col.crt) self.col.conf["creationOffset"] = mins_west self.col.setMod() - def _localOffsetForDate(self, date: datetime.datetime) -> int: - "Minutes west of UTC for a given datetime in the local timezone." - return date.astimezone().utcoffset().seconds // -60 + def clear_creation_offset(self): + if "creationOffset" in self.col.conf: + del self.col.conf["creationOffset"] + self.col.setMod() # Deck finished state ########################################################################## diff --git a/rslib/src/backend.rs b/rslib/src/backend.rs index 63d39f960..43c8399d1 100644 --- a/rslib/src/backend.rs +++ b/rslib/src/backend.rs @@ -5,7 +5,7 @@ use crate::backend_proto as pt; use crate::backend_proto::backend_input::Value; use crate::backend_proto::RenderedTemplateReplacement; use crate::err::{AnkiError, Result}; -use crate::sched::sched_timing_today; +use crate::sched::{local_minutes_west_for_stamp, sched_timing_today}; use crate::template::{ render_card, without_legacy_template_directives, FieldMap, FieldRequirements, ParsedTemplate, RenderedNode, @@ -96,6 +96,9 @@ impl Backend { Value::FindCards(_) => todo!(), Value::BrowserRows(_) => todo!(), Value::RenderCard(input) => OValue::RenderCard(self.render_template(input)?), + Value::LocalMinutesWest(stamp) => { + OValue::LocalMinutesWest(local_minutes_west_for_stamp(stamp)) + } }) } diff --git a/rslib/src/sched.rs b/rslib/src/sched.rs index 91ddfcd90..e320e57da 100644 --- a/rslib/src/sched.rs +++ b/rslib/src/sched.rs @@ -82,21 +82,21 @@ fn fixed_offset_from_minutes(minutes_west: i32) -> FixedOffset { FixedOffset::west(bounded_minutes * 60) } -/// Relative to the local timezone, the number of minutes UTC differs by. +/// For the given timestamp, return minutes west of UTC in the +/// local timezone. /// eg, Australia at +10 hours is -600. /// Includes the daylight savings offset if applicable. -#[allow(dead_code)] -fn utc_minus_local_mins() -> i32 { - Local::now().offset().utc_minus_local() / 60 +pub fn local_minutes_west_for_stamp(stamp: i64) -> i32 { + Local.timestamp(stamp, 0).offset().utc_minus_local() / 60 } #[cfg(test)] mod test { use crate::sched::{ - fixed_offset_from_minutes, normalized_rollover_hour, sched_timing_today, - utc_minus_local_mins, + fixed_offset_from_minutes, local_minutes_west_for_stamp, normalized_rollover_hour, + sched_timing_today, }; - use chrono::{FixedOffset, Local, TimeZone}; + use chrono::{FixedOffset, Local, TimeZone, Utc}; #[test] fn test_rollover() { @@ -121,9 +121,16 @@ mod test { today.days_elapsed } + #[test] + fn test_local_minutes_west() { + // -480 throughout the year + std::env::set_var("TZ", "Australia/Perth"); + assert_eq!(local_minutes_west_for_stamp(Utc::now().timestamp()), -480); + } + #[test] fn test_days_elapsed() { - let local_offset = utc_minus_local_mins(); + let local_offset = local_minutes_west_for_stamp(Utc::now().timestamp()); let created_dt = FixedOffset::west(local_offset * 60) .ymd(2019, 12, 1)