steam-backlog-enforcer/web/src/format.ts
Krzysztof kuhy Rudnicki 0d93539944
Some checks are pending
dependency freshness / dependencies on newest stable (push) Waiting to run
md-naming / markdown naming convention (push) Waiting to run
pre-commit / pre-commit (${{ matrix.group }}) (mypy, mypy, ) (push) Waiting to run
pre-commit / pre-commit (${{ matrix.group }}) (pylint, pylint, ) (push) Waiting to run
pre-commit / pre-commit (${{ matrix.group }}) (rest, , mypy,pylint) (push) Waiting to run
Tests / test (push) Waiting to run
Tests / web-coverage (push) Waiting to run
repo-contract / repo contract (push) Waiting to run
total block: two-unit countdown (2w 2d … 45s), ticking every second
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-23 20:59:41 +02:00

82 lines
3.0 KiB
TypeScript

// Small display-formatting helpers shared across components.
/** Format an hour count with thousands separators, e.g. 67031.1 → "67,031 h". */
export function fmtHours(hours: number): string {
if (hours <= 0) return '—'
return `${Math.round(hours).toLocaleString('en-US')} h`
}
/** Format a possibly-missing per-game hour value, e.g. 44.3 → "44.3". */
export function fmtHoursPrecise(hours: number): string {
if (hours <= 0) return '—'
return hours.toFixed(1)
}
/** Format playtime minutes as hours, e.g. 320 → "5.3 h" (0 → "untouched"). */
export function fmtPlaytime(minutes: number): string {
if (minutes <= 0) return 'untouched'
return `${(minutes / 60).toFixed(1)} h`
}
/** Format a day count and its target date, e.g. (866) → "866 days · 2028-10-11". */
export function fmtEta(days: number | null): string {
if (days === null) return 'N/A'
const target = new Date()
target.setDate(target.getDate() + days)
return `${days.toLocaleString('en-US')} days · ${isoDate(target)}`
}
/** Render a Date as YYYY-MM-DD in local time. */
export function isoDate(date: Date): string {
const y = date.getFullYear()
const m = String(date.getMonth() + 1).padStart(2, '0')
const d = String(date.getDate()).padStart(2, '0')
return `${y}-${m}-${d}`
}
/** Whole days from today (local midnight) until the given ISO date string. */
export function daysUntil(isoDateStr: string): number {
const target = new Date(`${isoDateStr}T00:00:00`)
const today = new Date()
today.setHours(0, 0, 0, 0)
const ms = target.getTime() - today.getTime()
return Math.ceil(ms / (1000 * 60 * 60 * 24))
}
/** Format a second count as "7h 27m" (or "44m" / "38s" when short). */
export function fmtDuration(seconds: number): string {
const total = Math.max(0, Math.round(seconds))
const h = Math.floor(total / 3600)
const m = Math.floor((total % 3600) / 60)
if (h > 0) return `${h}h ${m}m`
if (m > 0) return `${m}m`
return `${total}s`
}
/** Describe how long ago an ISO-8601 timestamp was, e.g. "4m ago". */
export function fmtAgo(iso: string, now: Date = new Date()): string {
if (!iso) return 'unknown'
const then = new Date(iso)
const seconds = (now.getTime() - then.getTime()) / 1000
if (Number.isNaN(seconds)) return 'unknown'
if (seconds < 10) return 'just now'
return `${fmtDuration(seconds)} ago`
}
// Largest first; weeks are the top unit because months vary in length.
const REMAINING_UNITS: [string, number][] = [['w', 604_800], ['d', 86_400], ['h', 3600], ['m', 60], ['s', 1]]
/** Time left as its two largest units: "2w 2d", "2d 14h", "2h 20m", "5m 7s", "45s".
* Rounds up, so it reads "0s" only once the time is actually up. */
export function fmtRemaining(seconds: number): string {
const left = Math.max(0, Math.ceil(seconds))
for (let i = 0; i < REMAINING_UNITS.length - 1; i += 1) {
const [name, size] = REMAINING_UNITS[i]
if (left >= size) {
const [subName, subSize] = REMAINING_UNITS[i + 1]
return `${Math.floor(left / size)}${name} ${Math.floor((left % size) / subSize)}${subName}`
}
}
return `${left}s`
}