Commit 794fd6b6 authored by Martin Wortschack's avatar Martin Wortschack

Merge branch 'more-helpful-number-formatting' into 'master'

Define easier to use interface for unit formatter

See merge request gitlab-org/gitlab!52668
parents 9ab32a71 3205f3c6
......@@ -2,7 +2,7 @@
import * as Sentry from '@sentry/browser';
import MetricCard from '~/analytics/shared/components/metric_card.vue';
import { deprecatedCreateFlash as createFlash } from '~/flash';
import { SUPPORTED_FORMATS, getFormatter } from '~/lib/utils/unit_format';
import { number } from '~/lib/utils/unit_format';
import { s__ } from '~/locale';
import usageTrendsCountQuery from '../graphql/queries/usage_trends_count.query.graphql';
......@@ -24,8 +24,7 @@ export default {
update(data) {
return Object.entries(data).map(([key, obj]) => {
const label = this.$options.i18n.labels[key];
const formatter = getFormatter(SUPPORTED_FORMATS.number);
const value = obj.nodes?.length ? formatter(obj.nodes[0].count, defaultPrecision) : null;
const value = obj.nodes?.length ? number(obj.nodes[0].count, defaultPrecision) : null;
return {
key,
......
import { formatNumber } from '~/locale';
/**
* Formats a number as string using `toLocaleString`.
* Formats a number as a string using `toLocaleString`.
*
* @param {Number} number to be converted
* @param {params} Parameters
* @param {params.fractionDigits} Number of decimal digits
* to display, defaults to using `toLocaleString` defaults.
* @param {params.maxLength} Max output char lenght at the
*
* @param {options.maxCharLength} Max output char length at the
* expense of precision, if the output is longer than this,
* the formatter switches to using exponential notation.
* @param {params.factor} Value is multiplied by this factor,
* useful for value normalization.
* @returns Formatted value
*
* @param {options.valueFactor} Value is multiplied by this factor,
* useful for value normalization or to alter orders of magnitude.
*
* @param {options} Other options to be passed to
* `formatNumber` such as `valueFactor`, `unit` and `style`.
*
*/
function formatNumber(
value,
{ fractionDigits = undefined, valueFactor = 1, style = undefined, maxLength = undefined },
) {
if (value === null) {
return '';
}
const locale = document.documentElement.lang || undefined;
const num = value * valueFactor;
const formatted = num.toLocaleString(locale, {
minimumFractionDigits: fractionDigits,
maximumFractionDigits: fractionDigits,
style,
});
const formatNumberNormalized = (value, { maxCharLength, valueFactor = 1, ...options }) => {
const formatted = formatNumber(value * valueFactor, options);
if (maxLength !== undefined && formatted.length > maxLength) {
if (maxCharLength !== undefined && formatted.length > maxCharLength) {
// 123456 becomes 1.23e+8
return num.toExponential(2);
return value.toExponential(2);
}
return formatted;
}
};
/**
* Formats a number as a string scaling it up according to units.
......@@ -76,7 +67,10 @@ const scaledFormatter = (units, unitFactor = 1000) => {
const unit = units[scale];
return `${formatNumber(num, { fractionDigits })}${unit}`;
return `${formatNumberNormalized(num, {
maximumFractionDigits: fractionDigits,
minimumFractionDigits: fractionDigits,
})}${unit}`;
};
};
......@@ -84,8 +78,14 @@ const scaledFormatter = (units, unitFactor = 1000) => {
* Returns a function that formats a number as a string.
*/
export const numberFormatter = (style = 'decimal', valueFactor = 1) => {
return (value, fractionDigits, maxLength) => {
return `${formatNumber(value, { fractionDigits, maxLength, valueFactor, style })}`;
return (value, fractionDigits, maxCharLength) => {
return `${formatNumberNormalized(value, {
maxCharLength,
valueFactor,
style,
maximumFractionDigits: fractionDigits,
minimumFractionDigits: fractionDigits,
})}`;
};
};
......@@ -93,9 +93,15 @@ export const numberFormatter = (style = 'decimal', valueFactor = 1) => {
* Returns a function that formats a number as a string with a suffix.
*/
export const suffixFormatter = (unit = '', valueFactor = 1) => {
return (value, fractionDigits, maxLength) => {
const length = maxLength !== undefined ? maxLength - unit.length : undefined;
return `${formatNumber(value, { fractionDigits, maxLength: length, valueFactor })}${unit}`;
return (value, fractionDigits, maxCharLength) => {
const length = maxCharLength !== undefined ? maxCharLength - unit.length : undefined;
return `${formatNumberNormalized(value, {
maxCharLength: length,
valueFactor,
maximumFractionDigits: fractionDigits,
minimumFractionDigits: fractionDigits,
})}${unit}`;
};
};
......
......@@ -58,10 +58,30 @@ const pgettext = (keyOrContext, key) => {
*/
const createDateTimeFormat = (formatOptions) => Intl.DateTimeFormat(languageCode(), formatOptions);
/**
* Formats a number as a string using `toLocaleString`.
*
* @param {Number} value - number to be converted
* @param {options?} options - options to be passed to
* `toLocaleString` such as `unit` and `style`.
* @param {langCode?} langCode - If set, forces a different
* language code from the one currently in the document.
* @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/NumberFormat/NumberFormat
*
* @returns If value is a number, the formatted value as a string
*/
function formatNumber(value, options = {}, langCode = languageCode()) {
if (typeof value !== 'number' && typeof value !== 'bigint') {
return value;
}
return value.toLocaleString(langCode, options);
}
export { languageCode };
export { gettext as __ };
export { ngettext as n__ };
export { pgettext as s__ };
export { sprintf };
export { createDateTimeFormat };
export { formatNumber };
export default locale;
import { setLanguage } from 'helpers/locale_helper';
import { createDateTimeFormat, languageCode } from '~/locale';
import { createDateTimeFormat, formatNumber, languageCode } from '~/locale';
describe('locale', () => {
afterEach(() => setLanguage(null));
......@@ -27,4 +27,68 @@ describe('locale', () => {
expect(dateFormat.format(new Date(2015, 6, 3))).toBe('July 3, 2015');
});
});
describe('formatNumber', () => {
it('formats numbers', () => {
expect(formatNumber(1)).toBe('1');
expect(formatNumber(12345)).toBe('12,345');
});
it('formats bigint numbers', () => {
expect(formatNumber(123456789123456789n)).toBe('123,456,789,123,456,789');
});
it('formats numbers with options', () => {
expect(formatNumber(1, { style: 'percent' })).toBe('100%');
expect(formatNumber(1, { style: 'currency', currency: 'USD' })).toBe('$1.00');
});
it('formats localized numbers', () => {
expect(formatNumber(12345, {}, 'es')).toBe('12.345');
});
it('formats NaN', () => {
expect(formatNumber(NaN)).toBe('NaN');
});
it('formats infinity', () => {
expect(formatNumber(Number.POSITIVE_INFINITY)).toBe('');
});
it('formats negative infinity', () => {
expect(formatNumber(Number.NEGATIVE_INFINITY)).toBe('-∞');
});
it('formats EPSILON', () => {
expect(formatNumber(Number.EPSILON)).toBe('0');
});
describe('non-number values should pass through', () => {
it('undefined', () => {
expect(formatNumber(undefined)).toBe(undefined);
});
it('null', () => {
expect(formatNumber(null)).toBe(null);
});
it('arrays', () => {
expect(formatNumber([])).toEqual([]);
});
it('objects', () => {
expect(formatNumber({ a: 'b' })).toEqual({ a: 'b' });
});
});
describe('when in a different locale', () => {
beforeEach(() => {
setLanguage('es');
});
it('formats localized numbers', () => {
expect(formatNumber(12345)).toBe('12.345');
});
});
});
});
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment