/** * Safe formatting utilities for handling nullable/undefined values * Prevents crashes from null/undefined data in metadata */ const PLACEHOLDER = '—'; /** * Safely format a number with decimal places * Returns "—" for null/undefined/NaN values */ export function formatNumber( value: unknown, opts?: { decimals?: number; suffix?: string; prefix?: string } ): string { const { decimals = 2, suffix = '', prefix = '' } = opts || {}; // Handle null/undefined if (value === null || value === undefined) { return PLACEHOLDER; } // Handle empty string if (value === '') { return PLACEHOLDER; } // Try to convert to number let num: number; if (typeof value === 'number') { num = value; } else if (typeof value === 'string') { num = parseFloat(value); } else { return PLACEHOLDER; } // Check if valid number if (!isFinite(num) || isNaN(num)) { return PLACEHOLDER; } // Format with decimals const formatted = num.toFixed(decimals); return `${prefix}${formatted}${suffix}`; } /** * Safely format an array of numbers or a single number * Handles RCSB fields like resolution_combined which can be number or array * Returns first numeric value if array, or formats single number */ export function formatArrayNumber( value: unknown, opts?: { decimals?: number; suffix?: string; prefix?: string; selectMin?: boolean } ): string { const { selectMin = false } = opts || {}; // Handle null/undefined if (value === null || value === undefined) { return PLACEHOLDER; } // Handle array if (Array.isArray(value)) { if (value.length === 0) { return PLACEHOLDER; } // Filter to valid numbers const numbers = value .map(v => (typeof v === 'number' ? v : parseFloat(v))) .filter(n => isFinite(n) && !isNaN(n)); if (numbers.length === 0) { return PLACEHOLDER; } // Select first or minimum const selected = selectMin ? Math.min(...numbers) : numbers[0]; return formatNumber(selected, opts); } // Single value return formatNumber(value, opts); } /** * Safely format text, handling null/undefined/empty */ export function formatText(value: unknown, placeholder: string = PLACEHOLDER): string { if (value === null || value === undefined) { return placeholder; } if (typeof value === 'string') { const trimmed = value.trim(); return trimmed === '' ? placeholder : trimmed; } if (typeof value === 'number') { return String(value); } if (typeof value === 'boolean') { return value ? 'Yes' : 'No'; } // Fallback for objects return placeholder; } /** * Format temperature with Kelvin suffix */ export function formatTemperature(value: unknown): string { return formatNumber(value, { decimals: 1, suffix: ' K' }); } /** * Format pH value */ export function formatPH(value: unknown): string { return formatNumber(value, { decimals: 1 }); } /** * Format resolution (handles array or single value) */ export function formatResolution(value: unknown): string { return formatArrayNumber(value, { decimals: 2, suffix: ' Å', selectMin: true }); } /** * Truncate long text with ellipsis */ export function truncateText(text: string, maxLength: number = 120): string { if (!text || text.length <= maxLength) { return text; } return text.substring(0, maxLength) + '...'; }