Anki/rslib/src/timestamp.rs
Luc Mcgrady 1af3c58d40
Feat/Desired retention info graphs (#4199)
* backend part

* split memorised and cost

* slapdash frontend

* extract some simulator logic

* Add zoomed version of graph

* ./check

* Fix: Tooltip

* Fix: Simulator/workload transition

* remove "time"

* Update ts/routes/graphs/simulator.ts

Co-authored-by: user1823 <92206575+user1823@users.noreply.github.com>

* Added: Mode toggle

* Disable Dr in workload mode

* keep button order consistant between modes

* dont clear points on mode swap

* add review count graph

* Revert "dont clear points on mode swap"

This reverts commit fc89efb1d9.

* "Help me pick" button

* unrelated title case change

* Add translation strings

* fix: missing translation string

* Fix: Layout shift

* Add: Experimental

* Fix Time / Memorized

* per day values

* set review limit to 9999 on open

* keep default at currently set value

* Do DR calculation in parallel (dae)

Approx 5x faster on my machine

---------

Co-authored-by: user1823 <92206575+user1823@users.noreply.github.com>
Co-authored-by: Damien Elmes <gpg@ankiweb.net>
2025-07-28 18:55:08 +10:00

120 lines
3.1 KiB
Rust

// Copyright: Ankitects Pty Ltd and contributors
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
use std::time;
use chrono::prelude::*;
use crate::define_newtype;
use crate::prelude::*;
define_newtype!(TimestampSecs, i64);
define_newtype!(TimestampMillis, i64);
impl TimestampSecs {
pub fn now() -> Self {
Self(elapsed().as_secs() as i64)
}
pub fn zero() -> Self {
Self(0)
}
pub fn elapsed_secs_since(self, other: TimestampSecs) -> i64 {
self.0 - other.0
}
pub fn elapsed_secs(self) -> u64 {
(Self::now().0 - self.0).max(0) as u64
}
pub fn elapsed_days_since(self, other: TimestampSecs) -> u64 {
(self.0 - other.0).max(0) as u64 / 86_400
}
pub fn as_millis(self) -> TimestampMillis {
TimestampMillis(self.0 * 1000)
}
pub(crate) fn local_datetime(self) -> Result<DateTime<Local>> {
Local
.timestamp_opt(self.0, 0)
.latest()
.or_invalid("invalid timestamp")
}
/// YYYY-mm-dd
pub(crate) fn date_string(self) -> String {
self.local_datetime()
.map(|dt| dt.format("%Y-%m-%d").to_string())
.unwrap_or_else(|_err| "invalid date".to_string())
}
/// HH-MM
pub(crate) fn time_string(self) -> String {
self.local_datetime()
.map(|dt| dt.format("%H:%M").to_string())
.unwrap_or_else(|_err| "invalid date".to_string())
}
pub(crate) fn date_and_time_string(self) -> String {
format!("{} @ {}", self.date_string(), self.time_string())
}
pub fn local_utc_offset(self) -> Result<FixedOffset> {
Ok(*self.local_datetime()?.offset())
}
pub fn datetime(self, utc_offset: FixedOffset) -> Result<DateTime<FixedOffset>> {
utc_offset
.timestamp_opt(self.0, 0)
.latest()
.or_invalid("invalid timestamp")
}
pub fn adding_secs(self, secs: i64) -> Self {
TimestampSecs(self.0 + secs)
}
}
impl TimestampMillis {
pub fn now() -> Self {
Self(elapsed().as_millis() as i64)
}
pub fn zero() -> Self {
Self(0)
}
pub fn as_secs(self) -> TimestampSecs {
TimestampSecs(self.0 / 1000)
}
pub fn adding_secs(self, secs: i64) -> Self {
Self(self.0 + secs * 1000)
}
pub fn elapsed_millis(self) -> u64 {
(Self::now().0 - self.0).max(0) as u64
}
}
fn elapsed() -> time::Duration {
if *crate::PYTHON_UNIT_TESTS {
// shift clock around rollover time to accommodate Python tests that make bad
// assumptions. we should update the tests in the future and remove this
// hack.
let mut elap = time::SystemTime::now()
.duration_since(time::SystemTime::UNIX_EPOCH)
.unwrap();
let now = Local::now();
if now.hour() >= 2 && now.hour() < 4 {
elap -= time::Duration::from_secs(60 * 60 * 2);
}
elap
} else {
time::SystemTime::now()
.duration_since(time::SystemTime::UNIX_EPOCH)
.unwrap()
}
}