1import { getCategories } from './categories'; 2import { DecimalCount } from '../types/displayValue'; 3import { toDateTimeValueFormatter } from './dateTimeFormatters'; 4import { getOffsetFromSIPrefix, SIPrefix, currency } from './symbolFormatters'; 5import { TimeZone } from '../types'; 6 7export interface FormattedValue { 8 text: string; 9 prefix?: string; 10 suffix?: string; 11} 12 13export function formattedValueToString(val: FormattedValue): string { 14 return `${val.prefix ?? ''}${val.text}${val.suffix ?? ''}`; 15} 16 17export type ValueFormatter = ( 18 value: number, 19 decimals?: DecimalCount, 20 scaledDecimals?: DecimalCount, 21 timeZone?: TimeZone, 22 showMs?: boolean 23) => FormattedValue; 24 25export interface ValueFormat { 26 name: string; 27 id: string; 28 fn: ValueFormatter; 29} 30 31export interface ValueFormatCategory { 32 name: string; 33 formats: ValueFormat[]; 34} 35 36export interface ValueFormatterIndex { 37 [id: string]: ValueFormatter; 38} 39 40// Globals & formats cache 41let categories: ValueFormatCategory[] = []; 42const index: ValueFormatterIndex = {}; 43let hasBuiltIndex = false; 44 45export function toFixed(value: number, decimals?: DecimalCount): string { 46 if (value === null) { 47 return ''; 48 } 49 50 if (value === Number.NEGATIVE_INFINITY || value === Number.POSITIVE_INFINITY) { 51 return value.toLocaleString(); 52 } 53 54 if (decimals === null || decimals === undefined) { 55 decimals = getDecimalsForValue(value); 56 } 57 58 const factor = decimals ? Math.pow(10, Math.max(0, decimals)) : 1; 59 const formatted = String(Math.round(value * factor) / factor); 60 61 // if exponent return directly 62 if (formatted.indexOf('e') !== -1 || value === 0) { 63 return formatted; 64 } 65 66 const decimalPos = formatted.indexOf('.'); 67 const precision = decimalPos === -1 ? 0 : formatted.length - decimalPos - 1; 68 if (precision < decimals) { 69 return (precision ? formatted : formatted + '.') + String(factor).substr(1, decimals - precision); 70 } 71 72 return formatted; 73} 74 75function getDecimalsForValue(value: number): number { 76 const log10 = Math.floor(Math.log(Math.abs(value)) / Math.LN10); 77 let dec = -log10 + 1; 78 const magn = Math.pow(10, -dec); 79 const norm = value / magn; // norm is between 1.0 and 10.0 80 81 // special case for 2.5, requires an extra decimal 82 if (norm > 2.25) { 83 ++dec; 84 } 85 86 if (value % 1 === 0) { 87 dec = 0; 88 } 89 90 const decimals = Math.max(0, dec); 91 return decimals; 92} 93 94export function toFixedScaled(value: number, decimals: DecimalCount, ext?: string): FormattedValue { 95 return { 96 text: toFixed(value, decimals), 97 suffix: ext, 98 }; 99} 100 101export function toFixedUnit(unit: string, asPrefix?: boolean): ValueFormatter { 102 return (size: number, decimals?: DecimalCount) => { 103 if (size === null) { 104 return { text: '' }; 105 } 106 const text = toFixed(size, decimals); 107 if (unit) { 108 if (asPrefix) { 109 return { text, prefix: unit }; 110 } 111 return { text, suffix: ' ' + unit }; 112 } 113 return { text }; 114 }; 115} 116 117export function isBooleanUnit(unit?: string) { 118 return unit && unit.startsWith('bool'); 119} 120 121export function booleanValueFormatter(t: string, f: string): ValueFormatter { 122 return (value: any) => { 123 return { text: value ? t : f }; 124 }; 125} 126 127// Formatter which scales the unit string geometrically according to the given 128// numeric factor. Repeatedly scales the value down by the factor until it is 129// less than the factor in magnitude, or the end of the array is reached. 130export function scaledUnits(factor: number, extArray: string[]): ValueFormatter { 131 return (size: number, decimals?: DecimalCount, scaledDecimals?: DecimalCount) => { 132 if (size === null) { 133 return { text: '' }; 134 } 135 if (size === Number.NEGATIVE_INFINITY || size === Number.POSITIVE_INFINITY || isNaN(size)) { 136 return { text: size.toLocaleString() }; 137 } 138 139 let steps = 0; 140 const limit = extArray.length; 141 142 while (Math.abs(size) >= factor) { 143 steps++; 144 size /= factor; 145 146 if (steps >= limit) { 147 return { text: 'NA' }; 148 } 149 } 150 151 return { text: toFixed(size, decimals), suffix: extArray[steps] }; 152 }; 153} 154 155export function locale(value: number, decimals: DecimalCount): FormattedValue { 156 if (value == null) { 157 return { text: '' }; 158 } 159 return { 160 text: value.toLocaleString(undefined, { maximumFractionDigits: decimals as number }), 161 }; 162} 163 164export function simpleCountUnit(symbol: string): ValueFormatter { 165 const units = ['', 'K', 'M', 'B', 'T']; 166 const scaler = scaledUnits(1000, units); 167 return (size: number, decimals?: DecimalCount, scaledDecimals?: DecimalCount) => { 168 if (size === null) { 169 return { text: '' }; 170 } 171 const v = scaler(size, decimals, scaledDecimals); 172 v.suffix += ' ' + symbol; 173 return v; 174 }; 175} 176 177export function stringFormater(value: number): FormattedValue { 178 return { text: `${value}` }; 179} 180 181function buildFormats() { 182 categories = getCategories(); 183 184 for (const cat of categories) { 185 for (const format of cat.formats) { 186 index[format.id] = format.fn; 187 } 188 } 189 190 // Resolve units pointing to old IDs 191 [{ from: 'farenheit', to: 'fahrenheit' }].forEach((alias) => { 192 const f = index[alias.to]; 193 if (f) { 194 index[alias.from] = f; 195 } 196 }); 197 198 hasBuiltIndex = true; 199} 200 201export function getValueFormat(id?: string | null): ValueFormatter { 202 if (!id) { 203 return toFixedUnit(''); 204 } 205 206 if (!hasBuiltIndex) { 207 buildFormats(); 208 } 209 210 const fmt = index[id]; 211 212 if (!fmt && id) { 213 let idx = id.indexOf(':'); 214 215 if (idx > 0) { 216 const key = id.substring(0, idx); 217 const sub = id.substring(idx + 1); 218 219 if (key === 'prefix') { 220 return toFixedUnit(sub, true); 221 } 222 223 if (key === 'suffix') { 224 return toFixedUnit(sub, false); 225 } 226 227 if (key === 'time') { 228 return toDateTimeValueFormatter(sub); 229 } 230 231 if (key === 'si') { 232 const offset = getOffsetFromSIPrefix(sub.charAt(0)); 233 const unit = offset === 0 ? sub : sub.substring(1); 234 return SIPrefix(unit, offset); 235 } 236 237 if (key === 'count') { 238 return simpleCountUnit(sub); 239 } 240 241 if (key === 'currency') { 242 return currency(sub); 243 } 244 245 if (key === 'bool') { 246 idx = sub.indexOf('/'); 247 if (idx >= 0) { 248 const t = sub.substring(0, idx); 249 const f = sub.substring(idx + 1); 250 return booleanValueFormatter(t, f); 251 } 252 return booleanValueFormatter(sub, '-'); 253 } 254 } 255 256 return toFixedUnit(id); 257 } 258 259 return fmt; 260} 261 262export function getValueFormatterIndex(): ValueFormatterIndex { 263 if (!hasBuiltIndex) { 264 buildFormats(); 265 } 266 267 return index; 268} 269 270export function getValueFormats() { 271 if (!hasBuiltIndex) { 272 buildFormats(); 273 } 274 275 return categories.map((cat) => { 276 return { 277 text: cat.name, 278 submenu: cat.formats.map((format) => { 279 return { 280 text: format.name, 281 value: format.id, 282 }; 283 }), 284 }; 285 }); 286} 287