122 lines
4.8 KiB
JavaScript
122 lines
4.8 KiB
JavaScript
// Rôle : regroupe les calculs purs de formatage et d'échéance des timers.
|
|
export function getNowMs() {
|
|
return Date.now();
|
|
}
|
|
|
|
export function pad(value) {
|
|
return String(value).padStart(2, "0");
|
|
}
|
|
|
|
export function formatDuration(ms) {
|
|
const safeMs = Math.max(0, Math.floor(ms));
|
|
const totalSeconds = Math.floor(safeMs / 1000);
|
|
const hours = Math.floor(totalSeconds / 3600);
|
|
const minutes = Math.floor((totalSeconds % 3600) / 60);
|
|
const seconds = totalSeconds % 60;
|
|
return `${pad(hours)}:${pad(minutes)}:${pad(seconds)}`;
|
|
}
|
|
|
|
export function getCentiseconds(ms) {
|
|
return pad(Math.floor((Math.max(0, Math.floor(ms)) % 1000) / 10));
|
|
}
|
|
|
|
export function formatDurationWithCentiseconds(ms) {
|
|
return `${formatDuration(ms)}:${getCentiseconds(ms)}`;
|
|
}
|
|
|
|
export function formatTargetTime(ms) {
|
|
if (!Number.isFinite(ms)) return "";
|
|
return new Intl.DateTimeFormat("fr-FR", {
|
|
hour: "2-digit",
|
|
minute: "2-digit",
|
|
second: "2-digit"
|
|
}).format(new Date(ms));
|
|
}
|
|
|
|
export function normalizeTimePart(value, max, allowWildcard = false) {
|
|
const cleanValue = String(value || "").trim().toUpperCase();
|
|
if (allowWildcard && !cleanValue) return "X";
|
|
if (allowWildcard && /^X+$/.test(cleanValue)) return "X";
|
|
const parsed = cleanValue ? Number.parseInt(cleanValue, 10) : 0;
|
|
if (!Number.isFinite(parsed) || parsed < 0 || parsed > max) return "";
|
|
return pad(parsed);
|
|
}
|
|
|
|
export function normalizeTimeParts(parts, { allowWildcard = false, allowLargeHours = false } = {}) {
|
|
const hours = normalizeTimePart(parts?.hours, allowLargeHours ? 999 : 23, allowWildcard);
|
|
const minutes = normalizeTimePart(parts?.minutes, 59, allowWildcard);
|
|
const seconds = normalizeTimePart(parts?.seconds, 59, allowWildcard);
|
|
if (!hours || !minutes || !seconds) return null;
|
|
return { hours, minutes, seconds };
|
|
}
|
|
|
|
export function timePartsToDurationMs(parts) {
|
|
const normalized = normalizeTimeParts(parts, { allowLargeHours: true });
|
|
if (!normalized) return 0;
|
|
return ((Number(normalized.hours) * 3600) + (Number(normalized.minutes) * 60) + Number(normalized.seconds)) * 1000;
|
|
}
|
|
|
|
export function timePartsToString(parts, options) {
|
|
const normalized = normalizeTimeParts(parts, options);
|
|
return normalized ? `${normalized.hours}:${normalized.minutes}:${normalized.seconds}` : "";
|
|
}
|
|
|
|
export function parseTimeString(value, options) {
|
|
const [hours = "", minutes = "", seconds = ""] = String(value || "").split(":");
|
|
return normalizeTimeParts({ hours, minutes, seconds }, options);
|
|
}
|
|
|
|
export function getDailyTargetMs(time, nowMs) {
|
|
const parsed = parseTimeString(time);
|
|
if (!parsed) return 0;
|
|
const target = new Date(nowMs);
|
|
target.setHours(Number(parsed.hours), Number(parsed.minutes), Number(parsed.seconds), 0);
|
|
if (target.getTime() <= nowMs) target.setDate(target.getDate() + 1);
|
|
return target.getTime();
|
|
}
|
|
|
|
export function getTimePatternTargetMs(pattern, nowMs) {
|
|
const parsed = parseTimeString(pattern, { allowWildcard: true });
|
|
if (!parsed) return 0;
|
|
const now = new Date(nowMs);
|
|
const candidate = new Date(nowMs);
|
|
candidate.setMilliseconds(0);
|
|
const hasHour = parsed.hours !== "X";
|
|
const hasMinute = parsed.minutes !== "X";
|
|
|
|
for (let offset = 1; offset <= 86400; offset += 1) {
|
|
candidate.setTime(now.getTime() + offset * 1000);
|
|
const hours = parsed.hours === "X" || Number(parsed.hours) === candidate.getHours();
|
|
const minutes = parsed.minutes === "X"
|
|
? (!hasHour || candidate.getMinutes() === 0)
|
|
: Number(parsed.minutes) === candidate.getMinutes();
|
|
const seconds = parsed.seconds === "X"
|
|
? (!hasHour && !hasMinute) || candidate.getSeconds() === 0
|
|
: Number(parsed.seconds) === candidate.getSeconds();
|
|
if (hours && minutes && seconds) return candidate.getTime();
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
export function getTimePatternRecurrenceMs(pattern) {
|
|
const baseMs = new Date(2026, 0, 1, 0, 0, 0, 0).getTime();
|
|
const firstTargetMs = getTimePatternTargetMs(pattern, baseMs);
|
|
if (!firstTargetMs) return 0;
|
|
const secondTargetMs = getTimePatternTargetMs(pattern, firstTargetMs);
|
|
return secondTargetMs ? secondTargetMs - firstTargetMs : 0;
|
|
}
|
|
|
|
export function getIntervalTargetMs(intervalMs, anchorAt, nowMs) {
|
|
const safeIntervalMs = Math.max(1000, Number(intervalMs) || 0);
|
|
const safeAnchor = Number.isFinite(anchorAt) && anchorAt > 0 ? anchorAt : nowMs;
|
|
if (safeAnchor > nowMs) return safeAnchor;
|
|
return safeAnchor + Math.ceil((nowMs - safeAnchor + 1) / safeIntervalMs) * safeIntervalMs;
|
|
}
|
|
|
|
export function getCountdownTargetMs(countdown, nowMs) {
|
|
if (countdown.type === "duration") return Number(countdown.targetAt) || 0;
|
|
if (countdown.type === "daily_time") return Number(countdown.targetAt) || 0;
|
|
if (countdown.type === "time_pattern") return getTimePatternTargetMs(countdown.pattern, nowMs);
|
|
if (countdown.type === "interval") return getIntervalTargetMs(countdown.intervalMs, countdown.anchorAt, nowMs);
|
|
return 0;
|
|
}
|