File size: 3,529 Bytes
3993320 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 | /**
* 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) + '...';
}
|