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) + '...';
}