mirror of
https://github.com/ankitects/anki.git
synced 2025-12-01 08:57:12 -05:00
The previous implementation interpreted the creation date as a local time, and applied the rollover to that. If the initial creation date was around midnight local time, even a one hour change due to daylight savings could result in Anki skipping or doubling up on a day. To address this, the rollover is now applied to the current time instead of the creation date. The new code needs the current time passed into it. This makes it easier to unit test, and for AnkiWeb to be able to use the user's local timezone. The new timezone code is currently disabled, as this code needs to be ported to all clients before it can be activated.
149 lines
5 KiB
Rust
149 lines
5 KiB
Rust
use chrono::{DateTime, FixedOffset, Local, TimeZone, Timelike};
|
|
|
|
pub struct SchedTimingToday {
|
|
/// The number of days that have passed since the collection was created.
|
|
pub days_elapsed: u32,
|
|
/// Timestamp of the next day rollover.
|
|
pub next_day_at: i64,
|
|
}
|
|
|
|
/// Timing information for the current day.
|
|
/// - created is the collection creation time
|
|
/// - now is the current UTC timestamp
|
|
/// - minutes_west is relative to the local timezone (eg UTC+10 hours is -600)
|
|
/// - rollover_hour is the hour of the day the rollover happens (eg 4 for 4am)
|
|
pub fn sched_timing_today(
|
|
created: i64,
|
|
now: i64,
|
|
minutes_west: i32,
|
|
rollover_hour: i8,
|
|
) -> SchedTimingToday {
|
|
let rollover_today = rollover_for_today(now, minutes_west, rollover_hour).timestamp();
|
|
|
|
SchedTimingToday {
|
|
days_elapsed: days_elapsed(created, now, rollover_today),
|
|
next_day_at: rollover_today + 86_400,
|
|
}
|
|
}
|
|
|
|
/// Convert timestamp to the local timezone, with the provided rollover hour.
|
|
fn rollover_for_today(
|
|
timestamp: i64,
|
|
minutes_west: i32,
|
|
rollover_hour: i8,
|
|
) -> DateTime<FixedOffset> {
|
|
let local_offset = fixed_offset_from_minutes(minutes_west);
|
|
let rollover_hour = normalized_rollover_hour(rollover_hour);
|
|
local_offset
|
|
.timestamp(timestamp, 0)
|
|
.with_hour(rollover_hour as u32)
|
|
.unwrap()
|
|
}
|
|
|
|
/// The number of times the day rolled over between two timestamps.
|
|
fn days_elapsed(start: i64, end: i64, rollover_today: i64) -> u32 {
|
|
// get the number of full days that have elapsed
|
|
let secs = (end - start).max(0);
|
|
let days = (secs / 86_400) as u32;
|
|
|
|
// minus one if today's cutoff hasn't passed
|
|
if days > 0 && end < rollover_today {
|
|
days - 1
|
|
} else {
|
|
days
|
|
}
|
|
}
|
|
|
|
/// 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);
|
|
FixedOffset::west(bounded_minutes * 60)
|
|
}
|
|
|
|
/// Relative to the local timezone, the number of minutes UTC differs by.
|
|
/// 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
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod test {
|
|
use crate::sched::{
|
|
fixed_offset_from_minutes, normalized_rollover_hour, sched_timing_today,
|
|
utc_minus_local_mins,
|
|
};
|
|
use chrono::{FixedOffset, TimeZone};
|
|
|
|
#[test]
|
|
fn test_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 test_fixed_offset() {
|
|
let offset = fixed_offset_from_minutes(-600);
|
|
assert_eq!(offset.utc_minus_local(), -600 * 60);
|
|
}
|
|
|
|
// helper
|
|
fn elap(start: i64, end: i64, west: i32, rollhour: i8) -> u32 {
|
|
let today = sched_timing_today(start, end, west, rollhour);
|
|
today.days_elapsed
|
|
}
|
|
|
|
#[test]
|
|
fn test_days_elapsed() {
|
|
let offset = utc_minus_local_mins();
|
|
|
|
let created_dt = FixedOffset::west(offset * 60)
|
|
.ymd(2019, 12, 1)
|
|
.and_hms(2, 0, 0);
|
|
let crt = created_dt.timestamp();
|
|
|
|
// days can't be negative
|
|
assert_eq!(elap(crt, crt, offset, 4), 0);
|
|
assert_eq!(elap(crt, crt - 86_400, offset, 4), 0);
|
|
|
|
// 2am the next day is still the same day
|
|
assert_eq!(elap(crt, crt + 24 * 3600, offset, 4), 0);
|
|
|
|
// day rolls over at 4am
|
|
assert_eq!(elap(crt, crt + 26 * 3600, offset, 4), 1);
|
|
|
|
// 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!(elap(crt, crt + (26 + 19) * 3600, offset, 23), 1);
|
|
|
|
// 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();
|
|
// with the current time being MST
|
|
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, offset, 4), 507);
|
|
// the previous implementation generated a diferent elapsed number of days with a change
|
|
// to DST, but the number shouldn't change
|
|
let offset = mdt.utc_minus_local() / 60;
|
|
assert_eq!(elap(crt, now, offset, 4), 507);
|
|
}
|
|
}
|