refactor(format): consolidate duration formatters into format/formatDuration (Phase L, part 2) (#690)

The mm:ss track-time formatter was hand-rolled in 11 places and the
h:mm:ss total-duration formatter in 4 — extract two tested functions:

- formatTrackTime(seconds, fallback='0:00')  — m:ss, used for track /
  playback times. fallback param covers the '–' placeholder rows.
- formatLongDuration(seconds)                — h:mm:ss when >=1h, else
  m:ss, used for album / queue totals.

Behaviour preserved per call site: the unified guard
(!seconds || !isFinite || <0 -> fallback) produces identical output to
every prior variant for all real inputs; SongRow keeps its '–' via the
fallback arg. Removes the formatter exports from 6 componentHelpers
files (playerBarHelpers / fullscreenPlayerHelpers deleted — they only
exported the formatter) and 7 inline component copies.

+ formatDuration.test.ts
This commit is contained in:
Frank Stellmacher
2026-05-14 14:50:10 +02:00
committed by GitHub
parent 7a7a9f5e6b
commit 5231169a71
34 changed files with 139 additions and 138 deletions
@@ -1,13 +1,5 @@
import type { ColDef } from '../useTracklistColumns';
export function formatDuration(seconds: number): string {
const h = Math.floor(seconds / 3600);
const m = Math.floor((seconds % 3600) / 60);
const s = Math.floor(seconds % 60);
if (h > 0) return `${h}:${m.toString().padStart(2, '0')}:${s.toString().padStart(2, '0')}`;
return `${m}:${s.toString().padStart(2, '0')}`;
}
export function codecLabel(song: { suffix?: string; bitRate?: number }, showBitrate: boolean): string {
const parts: string[] = [];
if (song.suffix) parts.push(song.suffix.toUpperCase());
@@ -1,9 +1,3 @@
export function formatDuration(seconds: number): string {
const m = Math.floor(seconds / 60);
const s = seconds % 60;
return `${m}:${s.toString().padStart(2, '0')}`;
}
/** Strip dangerous tags/attributes from server-provided HTML */
export function sanitizeHtml(html: string): string {
const parser = new DOMParser();
@@ -1,6 +0,0 @@
export function formatTime(seconds: number): string {
if (!seconds || isNaN(seconds)) return '0:00';
const m = Math.floor(seconds / 60);
const s = Math.floor(seconds % 60);
return `${m}:${s.toString().padStart(2, '0')}`;
}
@@ -73,10 +73,3 @@ export function initialSnapshot(): MiniSyncPayload {
};
}
}
export function fmt(seconds: number): string {
if (!isFinite(seconds) || seconds < 0) seconds = 0;
const m = Math.floor(seconds / 60);
const s = Math.floor(seconds % 60);
return `${m}:${s.toString().padStart(2, '0')}`;
}
@@ -1,6 +0,0 @@
export function formatTime(seconds: number): string {
if (!seconds || isNaN(seconds)) return '0:00';
const m = Math.floor(seconds / 60);
const s = Math.floor(seconds % 60);
return `${m}:${s.toString().padStart(2, '0')}`;
}
@@ -9,12 +9,6 @@ export function sanitizeFilename(name: string): string {
.substring(0, 200) || 'download';
}
export function formatDuration(seconds: number): string {
const m = Math.floor(seconds / 60);
const s = seconds % 60;
return `${m}:${String(s).padStart(2, '0')}`;
}
export function formatSize(bytes?: number): string {
if (!bytes) return '';
return `${(bytes / 1024 / 1024).toFixed(1)} MB`;
@@ -4,13 +4,6 @@ import type { Track } from '../../store/playerStoreTypes';
export type DurationMode = 'total' | 'remaining' | 'eta';
export function formatTime(seconds: number): string {
if (!seconds || isNaN(seconds)) return '0:00';
const m = Math.floor(seconds / 60);
const s = Math.floor(seconds % 60);
return `${m}:${s.toString().padStart(2, '0')}`;
}
export function formatQueueReplayGainParts(track: Track, t: TFunction): string[] {
const parts: string[] = [];
const fmtDb = (db: number) => `${db >= 0 ? '+' : ''}${db.toFixed(1)}`;
+53
View File
@@ -0,0 +1,53 @@
import { describe, it, expect } from 'vitest';
import { formatTrackTime, formatLongDuration } from './formatDuration';
describe('formatTrackTime', () => {
it('formats m:ss with zero-padded seconds', () => {
expect(formatTrackTime(5)).toBe('0:05');
expect(formatTrackTime(65)).toBe('1:05');
expect(formatTrackTime(599)).toBe('9:59');
});
it('does not roll minutes into hours (used for short track times)', () => {
expect(formatTrackTime(3600)).toBe('60:00');
expect(formatTrackTime(3661)).toBe('61:01');
});
it('floors fractional seconds', () => {
expect(formatTrackTime(90.9)).toBe('1:30');
});
it('returns the fallback for zero / non-finite / negative input', () => {
expect(formatTrackTime(0)).toBe('0:00');
expect(formatTrackTime(NaN)).toBe('0:00');
expect(formatTrackTime(Infinity)).toBe('0:00');
expect(formatTrackTime(-5)).toBe('0:00');
expect(formatTrackTime(undefined as unknown as number)).toBe('0:00');
});
it('honours a custom fallback', () => {
expect(formatTrackTime(0, '')).toBe('');
expect(formatTrackTime(NaN, '')).toBe('');
expect(formatTrackTime(42, '')).toBe('0:42');
});
});
describe('formatLongDuration', () => {
it('formats m:ss below one hour', () => {
expect(formatLongDuration(5)).toBe('0:05');
expect(formatLongDuration(125)).toBe('2:05');
expect(formatLongDuration(3599)).toBe('59:59');
});
it('formats h:mm:ss at or above one hour', () => {
expect(formatLongDuration(3600)).toBe('1:00:00');
expect(formatLongDuration(3661)).toBe('1:01:01');
expect(formatLongDuration(7325)).toBe('2:02:05');
});
it('returns 0:00 for zero / non-finite / negative input', () => {
expect(formatLongDuration(0)).toBe('0:00');
expect(formatLongDuration(NaN)).toBe('0:00');
expect(formatLongDuration(-1)).toBe('0:00');
});
});
+23
View File
@@ -0,0 +1,23 @@
/**
* `m:ss` track time from a seconds value. Non-positive / non-finite input
* returns `fallback` (default `'0:00'`; pass e.g. `''` for placeholder rows).
*/
export function formatTrackTime(seconds: number, fallback = '0:00'): string {
if (!seconds || !isFinite(seconds) || seconds < 0) return fallback;
const m = Math.floor(seconds / 60);
const s = Math.floor(seconds % 60);
return `${m}:${s.toString().padStart(2, '0')}`;
}
/**
* `h:mm:ss` when the duration reaches an hour, otherwise `m:ss`. Used for
* album / queue totals. Non-positive / non-finite input returns `'0:00'`.
*/
export function formatLongDuration(seconds: number): string {
if (!seconds || !isFinite(seconds) || seconds < 0) return '0:00';
const h = Math.floor(seconds / 3600);
const m = Math.floor((seconds % 3600) / 60);
const s = Math.floor(seconds % 60);
if (h > 0) return `${h}:${m.toString().padStart(2, '0')}:${s.toString().padStart(2, '0')}`;
return `${m}:${s.toString().padStart(2, '0')}`;
}