Refactor and fix time formatting functions, add tests

Signed-off-by: Julius Volz <julius.volz@gmail.com>
This commit is contained in:
Julius Volz 2024-09-16 15:20:51 +02:00
parent 6acd3dee53
commit 57898c7929
2 changed files with 143 additions and 51 deletions

View file

@ -0,0 +1,104 @@
import { humanizeDuration, formatPrometheusDuration } from "./formatTime";
describe("formatPrometheusDuration", () => {
test('returns "0s" for 0 milliseconds', () => {
expect(formatPrometheusDuration(0)).toBe("0s");
});
test("formats milliseconds correctly", () => {
expect(formatPrometheusDuration(1)).toBe("1ms");
expect(formatPrometheusDuration(999)).toBe("999ms");
});
test("formats seconds correctly", () => {
expect(formatPrometheusDuration(1000)).toBe("1s");
expect(formatPrometheusDuration(1500)).toBe("1s500ms");
expect(formatPrometheusDuration(59999)).toBe("59s999ms");
});
test("formats minutes correctly", () => {
expect(formatPrometheusDuration(60000)).toBe("1m");
expect(formatPrometheusDuration(120000)).toBe("2m");
expect(formatPrometheusDuration(3599999)).toBe("59m59s999ms");
});
test("formats hours correctly", () => {
expect(formatPrometheusDuration(3600000)).toBe("1h");
expect(formatPrometheusDuration(7200000)).toBe("2h");
expect(formatPrometheusDuration(86399999)).toBe("23h59m59s999ms");
});
test("formats days correctly", () => {
expect(formatPrometheusDuration(86400000)).toBe("1d");
expect(formatPrometheusDuration(172800000)).toBe("2d");
expect(formatPrometheusDuration(86400000 * 365 - 1)).toBe(
"364d23h59m59s999ms"
);
});
test("handles negative durations", () => {
expect(formatPrometheusDuration(-1000)).toBe("-1s");
expect(formatPrometheusDuration(-86400000)).toBe("-1d");
});
test("combines multiple units correctly", () => {
expect(
formatPrometheusDuration(86400000 + 3600000 + 60000 + 1000 + 1)
).toBe("1d1h1m1s1ms");
});
test("omits zero values", () => {
expect(formatPrometheusDuration(86400000 + 1000)).toBe("1d1s");
});
});
describe("humanizeDuration", () => {
test('returns "0s" for 0 milliseconds', () => {
expect(humanizeDuration(0)).toBe("0s");
});
test("formats milliseconds correctly", () => {
expect(humanizeDuration(1)).toBe("1ms");
expect(humanizeDuration(999)).toBe("999ms");
});
test("formats seconds correctly", () => {
expect(humanizeDuration(1000)).toBe("1s");
expect(humanizeDuration(1500)).toBe("1.5s");
expect(humanizeDuration(59999)).toBe("59.999s");
});
test("formats minutes correctly", () => {
expect(humanizeDuration(60000)).toBe("1m");
expect(humanizeDuration(120000)).toBe("2m");
expect(humanizeDuration(3599999)).toBe("59m 59.999s");
});
test("formats hours correctly", () => {
expect(humanizeDuration(3600000)).toBe("1h");
expect(humanizeDuration(7200000)).toBe("2h");
expect(humanizeDuration(86399999)).toBe("23h 59m 59.999s");
});
test("formats days correctly", () => {
expect(humanizeDuration(86400000)).toBe("1d");
expect(humanizeDuration(172800000)).toBe("2d");
expect(humanizeDuration(86400000 * 365 - 1)).toBe("364d 23h 59m 59.999s");
expect(humanizeDuration(86400000 * 365 - 1)).toBe("364d 23h 59m 59.999s");
});
test("handles negative durations", () => {
expect(humanizeDuration(-1000)).toBe("-1s");
expect(humanizeDuration(-86400000)).toBe("-1d");
});
test("combines multiple units correctly", () => {
expect(humanizeDuration(86400000 + 3600000 + 60000 + 1000 + 1)).toBe(
"1d 1h 1m 1.001s"
);
});
test("omits zero values", () => {
expect(humanizeDuration(86400000 + 1000)).toBe("1d 1s");
});
});

View file

@ -45,37 +45,55 @@ export const parsePrometheusDuration = (durationStr: string): number | null => {
return dur;
};
// Format a duration in milliseconds into a Prometheus duration string like "1d2h3m4s".
export const formatPrometheusDuration = (d: number): string => {
let ms = d;
let r = "";
if (ms === 0) {
// Used by:
// - formatPrometheusDuration() => "5d5m2s123ms"
// - humanizeDuration() => "5d 5m 2.123s"
const formatDuration = (
d: number,
componentSeparator?: string,
showFractionalSeconds?: boolean
): string => {
if (d === 0) {
return "0s";
}
const f = (unit: string, mult: number, exact: boolean) => {
const sign = d < 0 ? "-" : "";
let ms = Math.abs(d);
const r: string[] = [];
for (const { unit, mult, exact } of [
// Only format years and weeks if the remainder is zero, as it is often
// easier to read 90d than 12w6d.
{ unit: "y", mult: 1000 * 60 * 60 * 24 * 365, exact: true },
{ unit: "w", mult: 1000 * 60 * 60 * 24 * 7, exact: true },
{ unit: "d", mult: 1000 * 60 * 60 * 24, exact: false },
{ unit: "h", mult: 1000 * 60 * 60, exact: false },
{ unit: "m", mult: 1000 * 60, exact: false },
{ unit: "s", mult: 1000, exact: false },
{ unit: "ms", mult: 1, exact: false },
]) {
if (exact && ms % mult !== 0) {
return;
continue;
}
const v = Math.floor(ms / mult);
if (v > 0) {
r += `${v}${unit}`;
ms -= v * mult;
if (showFractionalSeconds && unit === "s" && ms > 0) {
// Show "2.34s" instead of "2s 340ms".
r.push(`${parseFloat((v + ms / 1000).toFixed(3))}s`);
break;
} else {
r.push(`${v}${unit}`);
}
}
}
return sign + r.join(componentSeparator || "");
};
// Only format years and weeks if the remainder is zero, as it is often
// easier to read 90d than 12w6d.
f("y", 1000 * 60 * 60 * 24 * 365, true);
f("w", 1000 * 60 * 60 * 24 * 7, true);
f("d", 1000 * 60 * 60 * 24, false);
f("h", 1000 * 60 * 60, false);
f("m", 1000 * 60, false);
f("s", 1000, false);
f("ms", 1, false);
return r;
// Format a duration in milliseconds into a Prometheus duration string like "1d2h3m4s".
export const formatPrometheusDuration = (d: number): string => {
return formatDuration(d);
};
export function parseTime(timeText: string): number {
@ -85,37 +103,7 @@ export function parseTime(timeText: string): number {
export const now = (): number => dayjs().valueOf();
export const humanizeDuration = (milliseconds: number): string => {
if (milliseconds === 0) {
return "0s";
}
const sign = milliseconds < 0 ? "-" : "";
const duration = dayjs.duration(Math.abs(milliseconds), "ms");
const ms = Math.floor(duration.milliseconds());
const s = Math.floor(duration.seconds());
const m = Math.floor(duration.minutes());
const h = Math.floor(duration.hours());
const d = Math.floor(duration.asDays());
const parts: string[] = [];
if (d !== 0) {
parts.push(`${d}d`);
}
if (h !== 0) {
parts.push(`${h}h`);
}
if (m !== 0) {
parts.push(`${m}m`);
}
if (s !== 0) {
if (ms !== 0) {
parts.push(`${s}.${ms}s`);
} else {
parts.push(`${s}s`);
}
} else if (milliseconds !== 0) {
parts.push(`${milliseconds.toFixed(3)}ms`);
}
return sign + parts.join(" ");
return formatDuration(milliseconds, " ", true);
};
export const humanizeDurationRelative = (