61 lines
2.6 KiB
JavaScript
61 lines
2.6 KiB
JavaScript
// Browser-safe. No node:* imports, no DOM. Imported by both server and browser.
|
||
|
||
const DASHES = /[‒–—−-]/; // -, –, —, minus sign
|
||
// Matches only the non-ASCII members of DASHES — used to fold a leading minus/en/em dash down to
|
||
// an ASCII hyphen before sign detection, so a string like "−0:30" (U+2212, the character every
|
||
// signed number in this app's own reference data — SYMPTOM_FIXES, cultivar profiles — is written
|
||
// with) parses instead of silently returning null.
|
||
const NON_ASCII_DASHES = /[‒–—−]/g;
|
||
|
||
/** "8:45" -> 525, "-0:20" -> -20, "+0:15" -> 15, "0" -> 0, "90" -> 90 (bare seconds). null on junk. */
|
||
export function parseDuration(str) {
|
||
if (str === null || str === undefined) return null;
|
||
const s = String(str).trim().replace(NON_ASCII_DASHES, "-");
|
||
if (s === "") return null;
|
||
const sign = s.startsWith("-") ? -1 : 1;
|
||
const body = s.replace(/^[+-]/, "");
|
||
const m = body.match(/^(\d+):(\d{1,2})$/);
|
||
if (m) {
|
||
const seconds = Number(m[1]) * 60 + Number(m[2]);
|
||
return Number(m[2]) < 60 && Number.isSafeInteger(seconds) ? sign * seconds : null;
|
||
}
|
||
if (/^\d+(\.\d+)?$/.test(body)) {
|
||
const seconds = Math.round(Number(body));
|
||
return Number.isSafeInteger(seconds) ? sign * seconds : null;
|
||
}
|
||
return null;
|
||
}
|
||
|
||
/** "7:30-9:00" / "7:30–9:00" -> midpoint seconds. Single value passes through parseDuration. */
|
||
export function parseRangeMidpoint(str) {
|
||
if (str === null || str === undefined) return null;
|
||
const s = String(str).trim();
|
||
if (s === "") return null;
|
||
const parts = s.split(DASHES).map((p) => p.trim()).filter(Boolean);
|
||
if (parts.length >= 2) {
|
||
const a = parseDuration(parts[0]);
|
||
const b = parseDuration(parts[parts.length - 1]);
|
||
if (a === null || b === null) return null;
|
||
return Math.round((a + b) / 2);
|
||
}
|
||
return parseDuration(s);
|
||
}
|
||
|
||
/** 525 -> "8:45". Negative seconds -> "-0:20". null -> "". */
|
||
export function formatDuration(totalSeconds) {
|
||
if (totalSeconds === null || totalSeconds === undefined || Number.isNaN(totalSeconds)) return "";
|
||
const sign = totalSeconds < 0 ? "-" : "";
|
||
const abs = Math.abs(Math.round(totalSeconds));
|
||
const m = Math.floor(abs / 60);
|
||
const s = abs % 60;
|
||
return `${sign}${m}:${String(s).padStart(2, "0")}`;
|
||
}
|
||
|
||
/** Same as formatDuration but always carries an explicit sign, and "0" for zero. */
|
||
export function formatSigned(totalSeconds) {
|
||
if (totalSeconds === null || totalSeconds === undefined || Number.isNaN(totalSeconds)) return "";
|
||
if (totalSeconds === 0) return "0";
|
||
const out = formatDuration(Math.abs(totalSeconds));
|
||
return totalSeconds < 0 ? `-${out}` : `+${out}`;
|
||
}
|