// Copyright: Ankitects Pty Ltd and contributors // License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html import * as tr from "@generated/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 YEAR = 365.0 * DAY; export const MONTH = YEAR / 12; 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, maxUnit: TimespanUnit = TimespanUnit.Years): TimespanUnit { secs = Math.abs(secs); const thresholds = [ { unit: TimespanUnit.Years, min: YEAR }, { unit: TimespanUnit.Months, min: MONTH }, { unit: TimespanUnit.Days, min: DAY }, { unit: TimespanUnit.Hours, min: HOUR }, { unit: TimespanUnit.Minutes, min: MINUTE }, { unit: TimespanUnit.Seconds, min: 0 }, ]; for (const { unit, min } of thresholds) { if (unit > maxUnit) { continue; } if (secs >= min) { return unit; } } return TimespanUnit.Seconds; } /** 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, maxUnit: TimespanUnit = TimespanUnit.Years, ): string { const unit = naturalUnit(seconds, maxUnit); 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}`; } }