MuSProt / frontend /src /utils /format.ts
wenruifan's picture
Deploy MuSProt React and FastAPI application
3993320
Raw
History Blame Contribute Delete
3.53 kB
/**
* 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) + '...';
}