feat: format.js pure helpers with node tests, weather icons

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
2026-09-11 14:42:06 +02:00
co-authored by Claude Fable 5.1
parent dfc28ee6c3
commit 6ef2d3f060
4 changed files with 284 additions and 0 deletions
+124
View File
@@ -0,0 +1,124 @@
/**
* Pure formatteer- en rekenfuncties voor het weerscherm. Geen DOM, geen fetch: los testbaar met node --test.
* Notatie nl-NL met decimale komma; tijden in de tijdzone van het station.
*/
const DASH = '–';
const WMO_ICONS = [
[[0], 'sun'],
[[1, 2], 'partly'],
[[3], 'cloud'],
[[45, 48], 'fog'],
[[51, 52, 53, 54, 55, 56, 57], 'drizzle'],
[[61, 62, 63, 64, 65, 66, 67], 'rain'],
[[80, 81, 82], 'showers'],
[[71, 72, 73, 74, 75, 76, 77, 85, 86], 'snow'],
[[95, 96, 97, 98, 99], 'thunder'],
];
const TRENDS = { up: '↗ stijgend', down: '↘ dalend', stable: '→ stabiel' };
function isNum(v) {
return typeof v === 'number' && Number.isFinite(v);
}
function cap(s) {
return s.charAt(0).toUpperCase() + s.slice(1);
}
/** 16.4 -> "16,4"; null -> "–" */
export function f1(v) {
return isNum(v) ? v.toFixed(1).replace('.', ',') : DASH;
}
/** 18.3 -> "18"; null -> "–" */
export function fInt(v) {
return isNum(v) ? String(Math.round(v)) : DASH;
}
/** 0 -> "0 mm"; 0.4 -> "0,4 mm"; null -> "–" */
export function fmm(v) {
if (!isNum(v)) return DASH;
return v === 0 ? '0 mm' : f1(v) + ' mm';
}
/** Positie van de marker op de min/max-balk, 0–100. Bij max == min of onbekend: 50. */
export function markerPos(temp, min, max) {
if (!isNum(temp) || !isNum(min) || !isNum(max) || max === min) return 50;
const pos = ((temp - min) / (max - min)) * 100;
return Math.min(100, Math.max(0, pos));
}
/** Hoogte per staaf in px: mm / max(waarden) * maxPx, minimaal minPx. Alles 0 of null -> allemaal minPx. */
export function barHeights(values, maxPx = 70, minPx = 2) {
const mm = values.map((v) => (isNum(v) ? v : 0));
const top = Math.max(...mm, 0);
return mm.map((v) => (top > 0 ? Math.max(minPx, (v / top) * maxPx) : minPx));
}
/** WMO-weercode -> icoonnaam. Onbekend -> "cloud". */
export function wmoIcon(code) {
for (const [codes, name] of WMO_ICONS) {
if (codes.includes(code)) return name;
}
return 'cloud';
}
/** "up" -> "↗ stijgend" enz.; onbekend -> "" */
export function trendLabel(trend) {
return TRENDS[trend] || '';
}
/** "Vrijdag 11 september" */
export function formatDate(date, tz) {
const s = new Intl.DateTimeFormat('nl-NL', { weekday: 'long', day: 'numeric', month: 'long', timeZone: tz }).format(date);
return cap(s);
}
/** ISO-tijdstip -> "06:10" in de tijdzone; null -> "–" */
export function formatTime(iso, tz) {
const d = iso instanceof Date ? iso : new Date(iso);
if (!iso || Number.isNaN(d.getTime())) return DASH;
return new Intl.DateTimeFormat('nl-NL', { hour: '2-digit', minute: '2-digit', hourCycle: 'h23', timeZone: tz }).format(d);
}
/** "2026-09-12" -> "Za" */
export function dayAbbr(dateStr, tz) {
const d = new Date(dateStr + 'T12:00:00Z');
if (Number.isNaN(d.getTime())) return DASH;
const s = new Intl.DateTimeFormat('nl-NL', { weekday: 'short', timeZone: 'UTC' }).format(d);
return cap(s.replace('.', '')).slice(0, 2);
}
/** "September" */
export function monthName(date, tz) {
return cap(new Intl.DateTimeFormat('nl-NL', { month: 'long', timeZone: tz }).format(date));
}
/** Is het ISO-tijdstip ouder dan `minutes` minuten ten opzichte van `now`? Onbekend telt als verouderd. */
export function isOlderThan(iso, now, minutes) {
if (!iso) return true;
const t = new Date(iso).getTime();
if (Number.isNaN(t)) return true;
return now.getTime() - t > minutes * 60000;
}
/** Lokale tijd (uur, minuut, seconde) van `now` in de tijdzone. */
function localParts(now, tz) {
const parts = new Intl.DateTimeFormat('en-GB', {
hour: '2-digit', minute: '2-digit', second: '2-digit', hourCycle: 'h23', timeZone: tz,
}).formatToParts(now);
const get = (type) => Number(parts.find((p) => p.type === type).value);
return { h: get('hour'), m: get('minute'), s: get('second') };
}
/** Milliseconden tot de volgende keer dat het `hour`:`minute` is in de tijdzone (minimaal 1 s). */
export function msUntil(now, tz, hour, minute = 0) {
const { h, m, s } = localParts(now, tz);
const target = hour * 3600 + minute * 60;
const current = h * 3600 + m * 60 + s;
let diff = target - current;
if (diff <= 0) diff += 86400;
return Math.max(1000, diff * 1000 - now.getMilliseconds());
}