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