anki/ts/lib/time.ts
Damien Elmes 5004cd332b
Integrate FSRS into Anki (#2654)
* Pack FSRS data into card.data

* Update FSRS card data when preset or weights change

+ Show FSRS stats in card stats

* Show a warning when there's a limited review history

* Add some translations; tweak UI

* Fix default requested retention

* Add browser columns, fix calculation of R

* Property searches

eg prop:d>0.1

* Integrate FSRS into reviewer

* Warn about long learning steps

* Hide minimum interval when FSRS is on

* Don't apply interval multiplier to FSRS intervals

* Expose memory state to Python

* Don't set memory state on new cards

* Port Jarret's new tests; add some helpers to make tests more compact

https://github.com/open-spaced-repetition/fsrs-rs/pull/64

* Fix learning cards not being given memory state

* Require update to v3 scheduler

* Don't exclude single learning step when calculating memory state

* Use relearning step when learning steps unavailable

* Update docstring

* fix single_card_revlog_to_items (#2656)

* not need check the review_kind for unique_dates

* add email address to CONTRIBUTORS

* fix last first learn & keep early review

* cargo fmt

* cargo clippy --fix

* Add Jarrett to about screen

* Fix fsrs_memory_state being initialized to default in get_card()

* Set initial memory state on graduate

* Update to latest FSRS

* Fix experiment.log being empty

* Fix broken colpkg imports

Introduced by "Update FSRS card data when preset or weights change"

* Update memory state during (re)learning; use FSRS for graduating intervals

* Reset memory state when cards are manually rescheduled as new

* Add difficulty graph; hide eases when FSRS enabled

* Add retrievability graph

* Derive memory_state from revlog when it's missing and shouldn't be

---------

Co-authored-by: Jarrett Ye <jarrett.ye@outlook.com>
2023-09-16 16:09:26 +10:00

207 lines
6.0 KiB
TypeScript

// Copyright: Ankitects Pty Ltd and contributors
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
import * as tr from "@tslib/ftl";
export const SECOND = 1.0;
export const MINUTE = 60.0 * SECOND;
export const HOUR = 60.0 * MINUTE;
export const DAY = 24.0 * HOUR;
export const MONTH = 30.0 * DAY;
export const YEAR = 12.0 * MONTH;
export enum TimespanUnit {
Seconds,
Minutes,
Hours,
Days,
Months,
Years,
}
export function unitName(unit: TimespanUnit): string {
switch (unit) {
case TimespanUnit.Seconds:
return "seconds";
case TimespanUnit.Minutes:
return "minutes";
case TimespanUnit.Hours:
return "hours";
case TimespanUnit.Days:
return "days";
case TimespanUnit.Months:
return "months";
case TimespanUnit.Years:
return "years";
}
}
export function naturalUnit(secs: number): TimespanUnit {
secs = Math.abs(secs);
if (secs < MINUTE) {
return TimespanUnit.Seconds;
} else if (secs < HOUR) {
return TimespanUnit.Minutes;
} else if (secs < DAY) {
return TimespanUnit.Hours;
} else if (secs < MONTH) {
return TimespanUnit.Days;
} else if (secs < YEAR) {
return TimespanUnit.Months;
} else {
return TimespanUnit.Years;
}
}
/** Number of seconds in a given unit. */
export function unitSeconds(unit: TimespanUnit): number {
switch (unit) {
case TimespanUnit.Seconds:
return SECOND;
case TimespanUnit.Minutes:
return MINUTE;
case TimespanUnit.Hours:
return HOUR;
case TimespanUnit.Days:
return DAY;
case TimespanUnit.Months:
return MONTH;
case TimespanUnit.Years:
return YEAR;
}
}
export function unitAmount(unit: TimespanUnit, secs: number): number {
return secs / unitSeconds(unit);
}
/** Largest unit provided seconds can be divided by without a remainder. */
export function naturalWholeUnit(secs: number): TimespanUnit {
let unit = naturalUnit(secs);
while (unit != TimespanUnit.Seconds) {
const amount = Math.round(unitAmount(unit, secs));
if (Math.abs(secs - amount * unitSeconds(unit)) < Number.EPSILON) {
return unit;
}
unit -= 1;
}
return unit;
}
export function studiedToday(cards: number, secs: number): string {
const unit = naturalUnit(secs);
const amount = unitAmount(unit, secs);
const name = unitName(unit);
let secsPer = 0;
if (cards > 0) {
secsPer = secs / cards;
}
return tr.statisticsStudiedToday({
unit: name,
secsPerCard: secsPer,
cards,
amount,
});
}
function i18nFuncForUnit(
unit: TimespanUnit,
short: boolean,
): (_: { amount: number }) => string {
if (short) {
switch (unit) {
case TimespanUnit.Seconds:
return tr.statisticsElapsedTimeSeconds;
case TimespanUnit.Minutes:
return tr.statisticsElapsedTimeMinutes;
case TimespanUnit.Hours:
return tr.statisticsElapsedTimeHours;
case TimespanUnit.Days:
return tr.statisticsElapsedTimeDays;
case TimespanUnit.Months:
return tr.statisticsElapsedTimeMonths;
case TimespanUnit.Years:
return tr.statisticsElapsedTimeYears;
}
} else {
switch (unit) {
case TimespanUnit.Seconds:
return tr.schedulingTimeSpanSeconds;
case TimespanUnit.Minutes:
return tr.schedulingTimeSpanMinutes;
case TimespanUnit.Hours:
return tr.schedulingTimeSpanHours;
case TimespanUnit.Days:
return tr.schedulingTimeSpanDays;
case TimespanUnit.Months:
return tr.schedulingTimeSpanMonths;
case TimespanUnit.Years:
return tr.schedulingTimeSpanYears;
}
}
}
/** Describe the given seconds using the largest appropriate unit.
If precise is true, show to two decimal places, eg
eg 70 seconds -> "1.17 minutes"
If false, seconds and days are shown without decimals. */
export function timeSpan(seconds: number, short = false, precise = true): string {
const unit = naturalUnit(seconds);
let amount = unitAmount(unit, seconds);
if (!precise && unit < TimespanUnit.Months) {
amount = Math.round(amount);
}
return i18nFuncForUnit(unit, short)({ amount });
}
export function dayLabel(daysStart: number, daysEnd: number): string {
const larger = Math.max(Math.abs(daysStart), Math.abs(daysEnd));
const smaller = Math.min(Math.abs(daysStart), Math.abs(daysEnd));
if (larger - smaller <= 1) {
// singular
if (daysStart >= 0) {
return tr.statisticsInDaysSingle({ days: daysStart });
} else {
return tr.statisticsDaysAgoSingle({ days: -daysStart });
}
} else {
// range
if (daysStart >= 0) {
return tr.statisticsInDaysRange({
daysStart,
daysEnd: daysEnd - 1,
});
} else {
return tr.statisticsDaysAgoRange({
daysStart: Math.abs(daysEnd - 1),
daysEnd: -daysStart,
});
}
}
}
/** Helper for converting Unix timestamps to date strings. */
export class Timestamp {
private date: Date;
constructor(seconds: number) {
this.date = new Date(seconds * 1000);
}
/** YYYY-MM-DD */
dateString(): string {
const year = this.date.getFullYear();
const month = ("0" + (this.date.getMonth() + 1)).slice(-2);
const date = ("0" + this.date.getDate()).slice(-2);
return `${year}-${month}-${date}`;
}
/** HH:MM */
timeString(): string {
const hours = ("0" + this.date.getHours()).slice(-2);
const minutes = ("0" + this.date.getMinutes()).slice(-2);
return `${hours}:${minutes}`;
}
}