mirror of
https://github.com/ankitects/anki.git
synced 2025-09-18 22:12:21 -04:00
rework new timezone code
We now store the UTC offset that was in effect at creation time, and use that to determine the starting date.
This commit is contained in:
parent
91e215d2c5
commit
a5613523ee
6 changed files with 154 additions and 84 deletions
|
@ -84,10 +84,11 @@ message TemplateRequirementAny {
|
||||||
}
|
}
|
||||||
|
|
||||||
message SchedTimingTodayIn {
|
message SchedTimingTodayIn {
|
||||||
int64 created = 1;
|
int64 created_secs = 1;
|
||||||
int64 now = 2;
|
sint32 created_mins_west = 2;
|
||||||
sint32 minutes_west = 3;
|
int64 now_secs = 3;
|
||||||
sint32 rollover_hour = 4;
|
sint32 now_mins_west = 4;
|
||||||
|
sint32 rollover_hour = 5;
|
||||||
}
|
}
|
||||||
|
|
||||||
message SchedTimingTodayOut {
|
message SchedTimingTodayOut {
|
||||||
|
|
|
@ -132,7 +132,7 @@ class _Collection:
|
||||||
elif ver == 2:
|
elif ver == 2:
|
||||||
self.sched = V2Scheduler(self)
|
self.sched = V2Scheduler(self)
|
||||||
if not self.server:
|
if not self.server:
|
||||||
self.conf["localOffset"] = self.sched.timezoneOffset()
|
self.conf["localOffset"] = self.sched.currentTimezoneOffset()
|
||||||
elif self.server.minutes_west is not None:
|
elif self.server.minutes_west is not None:
|
||||||
self.conf["localOffset"] = self.server.minutes_west
|
self.conf["localOffset"] = self.server.minutes_west
|
||||||
|
|
||||||
|
@ -162,7 +162,7 @@ class _Collection:
|
||||||
if isinstance(self.sched, V1Scheduler):
|
if isinstance(self.sched, V1Scheduler):
|
||||||
return None
|
return None
|
||||||
else:
|
else:
|
||||||
return self.sched.timezoneOffset()
|
return self.sched.currentTimezoneOffset()
|
||||||
|
|
||||||
# DB-related
|
# DB-related
|
||||||
##########################################################################
|
##########################################################################
|
||||||
|
|
|
@ -85,12 +85,21 @@ class RustBackend:
|
||||||
return proto_template_reqs_to_legacy(reqs)
|
return proto_template_reqs_to_legacy(reqs)
|
||||||
|
|
||||||
def sched_timing_today(
|
def sched_timing_today(
|
||||||
self, start: int, end: int, offset: int, rollover: int
|
self,
|
||||||
|
created_secs: int,
|
||||||
|
created_mins_west: int,
|
||||||
|
now_secs: int,
|
||||||
|
now_mins_west: int,
|
||||||
|
rollover: int,
|
||||||
) -> SchedTimingToday:
|
) -> SchedTimingToday:
|
||||||
return self._run_command(
|
return self._run_command(
|
||||||
pb.BackendInput(
|
pb.BackendInput(
|
||||||
sched_timing_today=pb.SchedTimingTodayIn(
|
sched_timing_today=pb.SchedTimingTodayIn(
|
||||||
created=start, now=end, minutes_west=offset, rollover_hour=rollover,
|
created_secs=created_secs,
|
||||||
|
created_mins_west=created_mins_west,
|
||||||
|
now_secs=now_secs,
|
||||||
|
now_mins_west=now_mins_west,
|
||||||
|
rollover_hour=rollover,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
).sched_timing_today
|
).sched_timing_today
|
||||||
|
|
|
@ -1395,22 +1395,44 @@ where id = ?
|
||||||
def _rolloverHour(self) -> int:
|
def _rolloverHour(self) -> int:
|
||||||
return self.col.conf.get("rollover", 4)
|
return self.col.conf.get("rollover", 4)
|
||||||
|
|
||||||
|
# New timezone handling
|
||||||
|
##########################################################################
|
||||||
|
|
||||||
def _newTimezoneEnabled(self) -> bool:
|
def _newTimezoneEnabled(self) -> bool:
|
||||||
return self.col.conf.get("newTimezone", False)
|
return self.col.conf.get("creationOffset") is not None
|
||||||
|
|
||||||
def _timingToday(self) -> SchedTimingToday:
|
def _timingToday(self) -> SchedTimingToday:
|
||||||
return self.col.backend.sched_timing_today(
|
return self.col.backend.sched_timing_today(
|
||||||
self.col.crt, intTime(), self.timezoneOffset(), self._rolloverHour(),
|
self.col.crt,
|
||||||
|
self.creationTimezoneOffset(),
|
||||||
|
intTime(),
|
||||||
|
self.currentTimezoneOffset(),
|
||||||
|
self._rolloverHour(),
|
||||||
)
|
)
|
||||||
|
|
||||||
def timezoneOffset(self) -> int:
|
def currentTimezoneOffset(self) -> int:
|
||||||
if self.col.server:
|
if self.col.server:
|
||||||
return self.col.conf.get("localOffset", 0)
|
return self.col.conf.get("localOffset", 0)
|
||||||
else:
|
else:
|
||||||
if time.localtime().tm_isdst:
|
return self._localOffsetForDate(datetime.datetime.today())
|
||||||
return time.altzone // 60
|
|
||||||
else:
|
def creationTimezoneOffset(self) -> int:
|
||||||
return time.timezone // 60
|
return self.col.conf.get("creationOffset", 0)
|
||||||
|
|
||||||
|
def setCreationOffset(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)
|
||||||
|
)
|
||||||
|
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
|
||||||
|
|
||||||
# Deck finished state
|
# Deck finished state
|
||||||
##########################################################################
|
##########################################################################
|
||||||
|
|
|
@ -145,9 +145,10 @@ impl Backend {
|
||||||
|
|
||||||
fn sched_timing_today(&self, input: pt::SchedTimingTodayIn) -> pt::SchedTimingTodayOut {
|
fn sched_timing_today(&self, input: pt::SchedTimingTodayIn) -> pt::SchedTimingTodayOut {
|
||||||
let today = sched_timing_today(
|
let today = sched_timing_today(
|
||||||
input.created as i64,
|
input.created_secs as i64,
|
||||||
input.now as i64,
|
input.created_mins_west,
|
||||||
input.minutes_west,
|
input.now_secs as i64,
|
||||||
|
input.now_mins_west,
|
||||||
input.rollover_hour as i8,
|
input.rollover_hour as i8,
|
||||||
);
|
);
|
||||||
pt::SchedTimingTodayOut {
|
pt::SchedTimingTodayOut {
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
use chrono::{DateTime, Datelike, FixedOffset, Local, TimeZone};
|
use chrono::{Date, Duration, FixedOffset, Local, TimeZone};
|
||||||
|
|
||||||
pub struct SchedTimingToday {
|
pub struct SchedTimingToday {
|
||||||
/// The number of days that have passed since the collection was created.
|
/// The number of days that have passed since the collection was created.
|
||||||
|
@ -8,49 +8,54 @@ pub struct SchedTimingToday {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Timing information for the current day.
|
/// Timing information for the current day.
|
||||||
/// - created is the collection creation time
|
/// - created_secs is a UNIX timestamp of the collection creation time
|
||||||
/// - now is the current UTC timestamp
|
/// - created_mins_west is the offset west of UTC at the time of creation
|
||||||
/// - minutes_west is relative to the local timezone (eg UTC+10 hours is -600)
|
/// (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
|
||||||
/// - rollover_hour is the hour of the day the rollover happens (eg 4 for 4am)
|
/// - 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(
|
||||||
created: i64,
|
created_secs: i64,
|
||||||
now: i64,
|
created_mins_west: i32,
|
||||||
minutes_west: i32,
|
now_secs: i64,
|
||||||
|
now_mins_west: i32,
|
||||||
rollover_hour: i8,
|
rollover_hour: i8,
|
||||||
) -> SchedTimingToday {
|
) -> SchedTimingToday {
|
||||||
let rollover_today = rollover_for_today(now, minutes_west, rollover_hour).timestamp();
|
// 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 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 = (rollover_today_datetime + Duration::days(1)).timestamp();
|
||||||
|
|
||||||
|
// day count
|
||||||
|
let days_elapsed = days_elapsed(created_date, today, rollover_passed);
|
||||||
|
|
||||||
SchedTimingToday {
|
SchedTimingToday {
|
||||||
days_elapsed: days_elapsed(created, now, rollover_today),
|
days_elapsed,
|
||||||
next_day_at: rollover_today + 86_400,
|
next_day_at,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Convert timestamp to the local timezone, with the provided rollover hour.
|
/// The number of times the day rolled over between two dates.
|
||||||
fn rollover_for_today(
|
fn days_elapsed(
|
||||||
timestamp: i64,
|
start_date: Date<FixedOffset>,
|
||||||
minutes_west: i32,
|
end_date: Date<FixedOffset>,
|
||||||
rollover_hour: i8,
|
rollover_passed: bool,
|
||||||
) -> DateTime<FixedOffset> {
|
) -> u32 {
|
||||||
let local_offset = fixed_offset_from_minutes(minutes_west);
|
let days = (end_date - start_date).num_days();
|
||||||
let rollover_hour = normalized_rollover_hour(rollover_hour);
|
|
||||||
let dt = local_offset.timestamp(timestamp, 0);
|
|
||||||
local_offset
|
|
||||||
.ymd(dt.year(), dt.month(), dt.day())
|
|
||||||
.and_hms(rollover_hour as u32, 0, 0)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The number of times the day rolled over between two timestamps.
|
// current day doesn't count before rollover time
|
||||||
fn days_elapsed(start: i64, end: i64, rollover_today: i64) -> u32 {
|
let days = if rollover_passed { days } else { days - 1 };
|
||||||
let secs = (rollover_today - start).max(0);
|
|
||||||
let days = (secs / 86_400) as u32;
|
|
||||||
|
|
||||||
// minus one if today's cutoff hasn't passed
|
// minimum of 0
|
||||||
if days > 0 && end < rollover_today {
|
days.max(0) as u32
|
||||||
days - 1
|
|
||||||
} else {
|
|
||||||
days
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Negative rollover hours are relative to the next day, eg -1 = 23.
|
/// Negative rollover hours are relative to the next day, eg -1 = 23.
|
||||||
|
@ -81,10 +86,10 @@ fn utc_minus_local_mins() -> i32 {
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod test {
|
mod test {
|
||||||
use crate::sched::{
|
use crate::sched::{
|
||||||
fixed_offset_from_minutes, normalized_rollover_hour, rollover_for_today,
|
fixed_offset_from_minutes, normalized_rollover_hour, sched_timing_today,
|
||||||
sched_timing_today, utc_minus_local_mins,
|
utc_minus_local_mins,
|
||||||
};
|
};
|
||||||
use chrono::{Datelike, FixedOffset, TimeZone, Timelike};
|
use chrono::{FixedOffset, TimeZone};
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_rollover() {
|
fn test_rollover() {
|
||||||
|
@ -95,15 +100,6 @@ mod test {
|
||||||
assert_eq!(normalized_rollover_hour(-2), 22);
|
assert_eq!(normalized_rollover_hour(-2), 22);
|
||||||
assert_eq!(normalized_rollover_hour(-23), 1);
|
assert_eq!(normalized_rollover_hour(-23), 1);
|
||||||
assert_eq!(normalized_rollover_hour(-24), 1);
|
assert_eq!(normalized_rollover_hour(-24), 1);
|
||||||
|
|
||||||
let now_dt = FixedOffset::west(-600).ymd(2019, 12, 1).and_hms(2, 3, 4);
|
|
||||||
let roll_dt = rollover_for_today(now_dt.timestamp(), -600, 4);
|
|
||||||
assert_eq!(roll_dt.year(), 2019);
|
|
||||||
assert_eq!(roll_dt.month(), 12);
|
|
||||||
assert_eq!(roll_dt.day(), 1);
|
|
||||||
assert_eq!(roll_dt.hour(), 4);
|
|
||||||
assert_eq!(roll_dt.minute(), 0);
|
|
||||||
assert_eq!(roll_dt.second(), 0);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
@ -113,56 +109,97 @@ mod test {
|
||||||
}
|
}
|
||||||
|
|
||||||
// helper
|
// helper
|
||||||
fn elap(start: i64, end: i64, west: i32, rollhour: i8) -> u32 {
|
fn elap(start: i64, end: i64, start_west: i32, end_west: i32, rollhour: i8) -> u32 {
|
||||||
let today = sched_timing_today(start, end, west, rollhour);
|
let today = sched_timing_today(start, start_west, end, end_west, rollhour);
|
||||||
today.days_elapsed
|
today.days_elapsed
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_days_elapsed() {
|
fn test_days_elapsed() {
|
||||||
let offset = utc_minus_local_mins();
|
let local_offset = utc_minus_local_mins();
|
||||||
|
|
||||||
let created_dt = FixedOffset::west(offset * 60)
|
let created_dt = FixedOffset::west(local_offset * 60)
|
||||||
.ymd(2019, 12, 1)
|
.ymd(2019, 12, 1)
|
||||||
.and_hms(2, 0, 0);
|
.and_hms(2, 0, 0);
|
||||||
let crt = created_dt.timestamp();
|
let crt = created_dt.timestamp();
|
||||||
|
|
||||||
// days can't be negative
|
// days can't be negative
|
||||||
assert_eq!(elap(crt, crt, offset, 4), 0);
|
assert_eq!(elap(crt, crt, local_offset, local_offset, 4), 0);
|
||||||
assert_eq!(elap(crt, crt - 86_400, offset, 4), 0);
|
assert_eq!(elap(crt, crt - 86_400, local_offset, local_offset, 4), 0);
|
||||||
|
|
||||||
// 2am the next day is still the same day
|
// 2am the next day is still the same day
|
||||||
assert_eq!(elap(crt, crt + 24 * 3600, offset, 4), 0);
|
assert_eq!(elap(crt, crt + 24 * 3600, local_offset, local_offset, 4), 0);
|
||||||
|
|
||||||
// day rolls over at 4am
|
// day rolls over at 4am
|
||||||
assert_eq!(elap(crt, crt + 26 * 3600, offset, 4), 1);
|
assert_eq!(elap(crt, crt + 26 * 3600, local_offset, local_offset, 4), 1);
|
||||||
|
|
||||||
// the longest extra delay is +23, or 19 hours past the 4 hour default
|
// the longest extra delay is +23, or 19 hours past the 4 hour default
|
||||||
assert_eq!(elap(crt, crt + (26 + 18) * 3600, offset, 23), 0);
|
assert_eq!(
|
||||||
assert_eq!(elap(crt, crt + (26 + 19) * 3600, offset, 23), 1);
|
elap(crt, crt + (26 + 18) * 3600, local_offset, local_offset, 23),
|
||||||
|
0
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
elap(crt, crt + (26 + 19) * 3600, local_offset, local_offset, 23),
|
||||||
|
1
|
||||||
|
);
|
||||||
|
|
||||||
|
let mdt = FixedOffset::west(6 * 60 * 60);
|
||||||
|
let mdt_offset = mdt.utc_minus_local() / 60;
|
||||||
|
let mst = FixedOffset::west(7 * 60 * 60);
|
||||||
|
let mst_offset = mst.utc_minus_local() / 60;
|
||||||
|
|
||||||
// a collection created @ midnight in MDT in the past
|
// a collection created @ midnight in MDT in the past
|
||||||
let mdt = FixedOffset::west(6 * 60 * 60);
|
|
||||||
let mst = FixedOffset::west(7 * 60 * 60);
|
|
||||||
let crt = mdt.ymd(2018, 8, 6).and_hms(0, 0, 0).timestamp();
|
let crt = mdt.ymd(2018, 8, 6).and_hms(0, 0, 0).timestamp();
|
||||||
// with the current time being MST
|
// with the current time being MST
|
||||||
let now = mst.ymd(2019, 12, 26).and_hms(20, 0, 0).timestamp();
|
let now = mst.ymd(2019, 12, 26).and_hms(20, 0, 0).timestamp();
|
||||||
let offset = mst.utc_minus_local() / 60;
|
assert_eq!(elap(crt, now, mdt_offset, mst_offset, 4), 507);
|
||||||
assert_eq!(elap(crt, now, offset, 4), 507);
|
|
||||||
// the previous implementation generated a diferent elapsed number of days with a change
|
// the previous implementation generated a diferent elapsed number of days with a change
|
||||||
// to DST, but the number shouldn't change
|
// to DST, but the number shouldn't change
|
||||||
let offset = mdt.utc_minus_local() / 60;
|
assert_eq!(elap(crt, now, mdt_offset, mdt_offset, 4), 507);
|
||||||
assert_eq!(elap(crt, now, offset, 4), 507);
|
|
||||||
|
|
||||||
// collection created at 3am on the 6th, so day 1 starts at 4am on the 7th, and day 3 on the 9th.
|
// collection created at 3am on the 6th, so day 1 starts at 4am on the 7th, and day 3 on the 9th.
|
||||||
let crt = mdt.ymd(2018, 8, 6).and_hms(3, 0, 0).timestamp();
|
let crt = mdt.ymd(2018, 8, 6).and_hms(3, 0, 0).timestamp();
|
||||||
|
|
||||||
let mst_offset = mst.utc_minus_local() / 60;
|
|
||||||
let now = mst.ymd(2018, 8, 9).and_hms(1, 59, 59).timestamp();
|
let now = mst.ymd(2018, 8, 9).and_hms(1, 59, 59).timestamp();
|
||||||
assert_eq!(elap(crt, now, mst_offset, 4), 2);
|
assert_eq!(elap(crt, now, mdt_offset, mst_offset, 4), 2);
|
||||||
let now = mst.ymd(2018, 8, 9).and_hms(3, 59, 59).timestamp();
|
let now = mst.ymd(2018, 8, 9).and_hms(3, 59, 59).timestamp();
|
||||||
assert_eq!(elap(crt, now, mst_offset, 4), 2);
|
assert_eq!(elap(crt, now, mdt_offset, mst_offset, 4), 2);
|
||||||
let now = mst.ymd(2018, 8, 9).and_hms(4, 0, 0).timestamp();
|
let now = mst.ymd(2018, 8, 9).and_hms(4, 0, 0).timestamp();
|
||||||
assert_eq!(elap(crt, now, mst_offset, 4), 3);
|
assert_eq!(elap(crt, now, mdt_offset, mst_offset, 4), 3);
|
||||||
|
|
||||||
|
// try a bunch of combinations of creation time, current time, and rollover hour
|
||||||
|
let hours_of_interest = &[0, 1, 4, 12, 22, 23];
|
||||||
|
for creation_hour in hours_of_interest {
|
||||||
|
let crt_dt = mdt.ymd(2018, 8, 6).and_hms(*creation_hour, 0, 0);
|
||||||
|
let crt_stamp = crt_dt.timestamp();
|
||||||
|
let crt_offset = mdt_offset;
|
||||||
|
|
||||||
|
for current_day in 0..=3 {
|
||||||
|
for current_hour in hours_of_interest {
|
||||||
|
for rollover_hour in hours_of_interest {
|
||||||
|
let end_dt = mdt
|
||||||
|
.ymd(2018, 8, 6 + current_day)
|
||||||
|
.and_hms(*current_hour, 0, 0);
|
||||||
|
let end_stamp = end_dt.timestamp();
|
||||||
|
let end_offset = mdt_offset;
|
||||||
|
let elap_day = if *current_hour < *rollover_hour {
|
||||||
|
current_day.max(1) - 1
|
||||||
|
} else {
|
||||||
|
current_day
|
||||||
|
};
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
elap(
|
||||||
|
crt_stamp,
|
||||||
|
end_stamp,
|
||||||
|
crt_offset,
|
||||||
|
end_offset,
|
||||||
|
*rollover_hour as i8
|
||||||
|
),
|
||||||
|
elap_day
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue