/** * Shared formatting utility functions. * * Consolidates duplicated formatters that were previously defined inline in * PropertyCard, ListingDetail, MobileBottomSheet, and StatsBar. */ /** Em-dash placeholder used at render boundaries for missing/invalid values. */ export const EM_DASH = '—'; /** Returns true when v is a finite number — i.e. safe to render directly. */ export function isFiniteNumber(v: unknown): v is number { return typeof v === 'number' && Number.isFinite(v); } /** * Format an integer GBP price with thousands separators, e.g. "£2,500". * Returns the em-dash sentinel for null / undefined / non-finite inputs. * Use at the JSX leaf to distinguish "missing data" from "£0". */ export function formatPrice(value: number | null | undefined): string { if (!isFiniteNumber(value)) return EM_DASH; return `£${Math.round(value).toLocaleString()}`; } /** * Format a price-per-square-metre integer, e.g. "£42/m²". * Returns the em-dash sentinel for null / undefined / non-finite / zero / negative inputs. * (Zero / negative qmprice values come from missing-data rows, not real listings.) */ export function formatPricePerSqmShort(value: number | null | undefined): string { if (!isFiniteNumber(value) || value <= 0) return EM_DASH; return `£${Math.round(value)}/m²`; } /** * Format an integer count (bedrooms, square metres, etc). * Returns the em-dash sentinel for null / undefined / non-finite inputs. */ export function formatInteger(value: number | null | undefined): string { if (!isFiniteNumber(value)) return EM_DASH; return `${Math.round(value)}`; } /** Format a number as a compact GBP string, e.g. "£1.2k" or "£950". */ export function formatCurrency(value: number): string { if (value >= 1000) return `£${(value / 1000).toFixed(1)}k`; return `£${Math.round(value)}`; } /** * Format a duration in seconds as a human-readable string, e.g. "12m" or "1h30m". * * Returns the em-dash sentinel for non-finite or negative inputs (null, NaN, -120…). * Caps absurdly large values (> 24h) as ">24h" to avoid rendering nonsense like * "8760h" when a backend bug produces year-scale values. */ export function formatDuration(seconds: number | null | undefined): string { if (typeof seconds !== 'number' || !Number.isFinite(seconds) || seconds < 0) { return EM_DASH; } // Cap at 24 hours — beyond this it's almost certainly bad data. if (seconds > 24 * 3600) return '>24h'; const minutes = Math.round(seconds / 60); if (minutes < 60) return `${minutes}m`; const hours = Math.floor(minutes / 60); const mins = minutes % 60; return mins > 0 ? `${hours}h${mins}m` : `${hours}h`; } /** Format an ISO date string as a localised short date (e.g. "3 Jan 2025"). */ export function formatDate(value: string): string { const date = new Date(value); if (isNaN(date.getTime())) return value; return date.toLocaleDateString('en-GB', { day: 'numeric', month: 'short', year: 'numeric' }); } /** * Compute the price per square metre as a formatted string, e.g. "£4,500/m²". * Returns null when square metres data is unavailable. */ export function formatPricePerSqm(price: number, sqm: number | null | undefined): string | null { if (sqm == null || sqm <= 0) return null; return `£${Math.round(price / sqm).toLocaleString()}/m²`; }