// 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; }