/**
* ─────────────────────────────────────────────────────────────────────────────
* Formatage (miroir des helpers Android : formatDose dans DoseDialog.kt,
* formatLabValue dans LabDialog.kt, formatValue dans CurveChart.kt,
* HrtDuration.kt).
* ─────────────────────────────────────────────────────────────────────────────
*/
/**
* Format d'une dose : entier si rond, sinon 1 décimale (miroir formatDose —
* `4.0` → « 4 », `0.5` → « 0.5 »). Point décimal (convention des champs).
*
* @param {number} d
* @returns {string}
*/
export function formatDose(d) {
return Number.isInteger(d) ? String(d) : d.toFixed(1);
}
/**
* Format d'une valeur de lab : préserve les décimales utiles (miroir
* formatLabValue — contrairement à formatDose : un lab 0.45 s'affiche « 0.45 »).
*
* @param {number} d
* @returns {string}
*/
export function formatLabValue(d) {
if (Number.isInteger(d)) return String(d);
return String(d);
}
/**
* Format d'une valeur SUR LE GRAPHIQUE (miroir formatValue de CurveChart.kt) :
* ≥ 10 → 0 décimale ; ≥ 1 → 1 décimale ; sinon 2.
*
* @param {number} v
* @returns {string}
*/
export function formatValue(v) {
if (v >= 10) return v.toFixed(0);
if (v >= 1) return v.toFixed(1);
return v.toFixed(2);
}
/**
* HrtDuration (portage de `util/HrtDuration.kt`, v1.3.1) : estimation du
* temps passé sous THS. Mois = 30 jours (estimation assumée).
*/
export const HrtDuration = {
DAY_MS: 86400000,
/** Jours entiers écoulés depuis la première prise (0 si futur/invalide). */
daysSince(earliestMs, nowMs) {
return nowMs <= earliestMs ? 0 : Math.trunc((nowMs - earliestMs) / this.DAY_MS);
},
/** Décomposition mois(30 j) / jours restants. */
monthsAndDays(totalDays) {
return [Math.trunc(totalDays / 30), totalDays % 30];
},
/**
* Delta EN HEURES → (jours, heures restantes) — v1.4.1, carte « Prochaine
* dose » : au-delà de 24 h, les heures parlent moins que les jours
* (ex. 120 h → 5 j 0 h).
*/
daysAndHours(totalHours) {
return [Math.trunc(totalHours / 24), totalHours % 24];
},
};
// ── Formatage de dates/heures (Intl, fuseau de lecture configurable) ──────────
/** Intl.DateTimeFormat mis en cache par (style, timeZone). */
const fmtCache = new Map();
/** @private */
function fmt(key, optionsFactory, timeZone) {
const cacheKey = `${key}|${timeZone || 'auto'}`;
if (!fmtCache.has(cacheKey)) fmtCache.set(cacheKey, optionsFactory(timeZone));
return fmtCache.get(cacheKey);
}
/**
* « HH:mm » dans le fuseau de lecture (heure des doses, carte prochaine dose).
* @param {number} ms @param {string} [timeZone=null]
*/
export function formatTime(ms, timeZone = null) {
return fmt('time', (tz) => new Intl.DateTimeFormat(undefined, {
timeZone: tz || undefined, hour: '2-digit', minute: '2-digit', hourCycle: 'h23',
}), timeZone).format(new Date(ms));
}
/**
* « dd/MM/yyyy HH:mm » (liste des analyses).
* @param {number} ms @param {string} [timeZone=null]
*/
export function formatDateTime(ms, timeZone = null) {
return fmt('datetime', (tz) => new Intl.DateTimeFormat(undefined, {
timeZone: tz || undefined,
day: '2-digit', month: '2-digit', year: 'numeric',
hour: '2-digit', minute: '2-digit', hourCycle: 'h23',
}), timeZone).format(new Date(ms));
}
/**
* « d MMMM yyyy » (en-tête « temps sous THS »).
* @param {number} ms @param {string} [timeZone=null]
*/
export function formatLongDate(ms, timeZone = null) {
return fmt('longdate', (tz) => new Intl.DateTimeFormat(undefined, {
timeZone: tz || undefined, day: 'numeric', month: 'long', year: 'numeric',
}), timeZone).format(new Date(ms));
}
/**
* « EEE d HH:mm » (carte prochaine dose au-delà de 24 h — jour de semaine
* inclus, cf v1.4.1).
* @param {number} ms @param {string} [timeZone=null]
*/
export function formatWeekdayTime(ms, timeZone = null) {
return fmt('weekdaytime', (tz) => new Intl.DateTimeFormat(undefined, {
timeZone: tz || undefined,
weekday: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit', hourCycle: 'h23',
}), timeZone).format(new Date(ms));
}
/**
* « yyyy-MM-dd » (en-têtes de groupes de l'écran Doses — même clé de groupe
* que le Kotlin `LocalDate.toString()`).
* @param {number} ms @param {string} [timeZone=null]
*/
export function formatISODate(ms, timeZone = null) {
return fmt('isodate', (tz) => new Intl.DateTimeFormat('en-CA', {
timeZone: tz || undefined, year: 'numeric', month: '2-digit', day: '2-digit',
}), timeZone).format(new Date(ms));
}
/** Date du jour « yyyy-MM-dd » (locale) — champ date des dialogs. */
export function todayISO() {
const d = new Date();
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`;
}
/**
* Epoch ms → valeur pour un `` + ``
* (locale) — les dialogs web utilisent ces champs natifs (équivalent
* DatePicker+TimePicker Material3).
*
* @param {number} ms
* @returns {{date:string, time:string}} ex. { date: "2026-09-08", time: "18:30" }
*/
export function msToInputValues(ms) {
const d = new Date(ms);
return {
date: `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`,
time: `${String(d.getHours()).padStart(2, '0')}:${String(d.getMinutes()).padStart(2, '0')}`,
};
}
/**
* Valeurs de champs date/heure (locale) → epoch ms.
* @param {string} dateISO "2026-09-08"
* @param {string} timeHM "18:30"
* @returns {number|null} null si invalide
*/
export function inputValuesToMs(dateISO, timeHM) {
if (!dateISO || !timeHM) return null;
const [y, mo, d] = dateISO.split('-').map(Number);
const [h, mi] = timeHM.split(':').map(Number);
if ([y, mo, d, h, mi].some((n) => Number.isNaN(n))) return null;
const ms = new Date(y, mo - 1, d, h, mi, 0, 0).getTime();
return Number.isFinite(ms) ? ms : null;
}