sokko-g/website/src/features/toolboxes/modules/timerUtils.js
Shinuwa 2e6d8a2c0c
All checks were successful
Deploy Sokko G / deploy (push) Successful in 7s
keepsafe rework & alert ajustments
2026-07-26 17:48:14 +02:00

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