Add @superset-ui/number-format package (#31)

* feat: Add number-format package
This commit is contained in:
Krist Wongsuphasawat 2018-11-16 21:55:46 -08:00 committed by Yongjie Zhao
parent af725ce874
commit 1edfdcf1b5
16 changed files with 498 additions and 1 deletions

View File

@ -15,6 +15,8 @@ applications that leverage a Superset backend :chart_with_upwards_trend:
| [@superset-ui/color](https://github.com/apache-superset/superset-ui/tree/master/packages/superset-ui-color) | [![Version](https://img.shields.io/npm/v/@superset-ui/color.svg?style=flat-square)](https://img.shields.io/npm/v/@superset-ui/color.svg?style=flat-square) |
| [@superset-ui/connection](https://github.com/apache-superset/superset-ui/tree/master/packages/superset-ui-connection) | [![Version](https://img.shields.io/npm/v/@superset-ui/connection.svg?style=flat-square)](https://img.shields.io/npm/v/@superset-ui/connection.svg?style=flat-square) |
| [@superset-ui/core](https://github.com/apache-superset/superset-ui/tree/master/packages/superset-ui-core) | [![Version](https://img.shields.io/npm/v/@superset-ui/core.svg?style=flat-square)](https://img.shields.io/npm/v/@superset-ui/core.svg?style=flat-square) |
| [@superset-ui/generator-superset](https://github.com/apache-superset/superset-ui/tree/master/packages/superset-ui-generator-superset) | [![Version](https://img.shields.io/npm/v/@superset-ui/generator-superset.svg?style=flat-square)](https://img.shields.io/npm/v/@superset-ui/generator-superset.svg?style=flat-square) |
| [@superset-ui/number-format](https://github.com/apache-superset/superset-ui/tree/master/packages/superset-ui-number-format) | [![Version](https://img.shields.io/npm/v/@superset-ui/number-format.svg?style=flat-square)](https://img.shields.io/npm/v/@superset-ui/number-format.svg?style=flat-square) |
| [@superset-ui/translation](https://github.com/apache-superset/superset-ui/tree/master/packages/superset-ui-translation) | [![Version](https://img.shields.io/npm/v/@superset-ui/translation.svg?style=flat-square)](https://img.shields.io/npm/v/@superset-ui/translation.svg?style=flat-square) |
#### Coming :soon:
@ -22,7 +24,6 @@ applications that leverage a Superset backend :chart_with_upwards_trend:
- Data providers
- Embeddable charts
- Chart collections
- Demo storybook package
### Development

View File

@ -0,0 +1,57 @@
## @superset-ui/number-format
[![Version](https://img.shields.io/npm/v/@superset-ui/number-format.svg?style=flat)](https://img.shields.io/npm/v/@superset-ui/number-format.svg?style=flat)
[![David (path)](https://img.shields.io/david/apache-superset/superset-ui.svg?path=packages%2Fsuperset-ui-number-format&style=flat-square)](https://david-dm.org/apache-superset/superset-ui?path=packages/superset-ui-number-format)
Description
#### Example usage
Functions `getNumberFormatter` and `formatNumber` should be used instead of calling `d3.format` directly.
```js
import { getNumberFormatter } from '@superset-ui/number-format';
const formatter = getNumberFormatter('.2f');
console.log(formatter(1000));
```
or
```js
import { formatNumber } from '@superset-ui/number-format';
console.log(formatNumber('.2f', 1000));
```
It is powered by a registry to support registration of custom formatting, with fallback to `d3.format` and handle error for invalid format string.
```js
import { getNumberFormatterRegistry, formatNumber, NumberFormatter } from '@superset-ui/number-format';
getNumberFormatterRegistry().registerValue('my_format', new NumberFormatter({
id: 'my_format',
formatFunc: v => `my special format of ${v}`
});
console.log(formatNumber('my_format', 1000));
// prints 'my special format of 1000'
```
It also define constants for common d3 formats. See the full list of formats in [NumberFormats.js](https://github.com/apache-superset/superset-ui/blob/master/packages/superset-ui-number-format/src/NumberFormats.js).
```js
import { NumberFormats } from '@superset-ui-number-format';
NumberFormats.PERCENT // ,.2%
NumberFormats.PERCENT_3_POINT // ,.3%
```
#### API
`fn(args)`
- Do something
### Development
`@data-ui/build-config` is used to manage the build configuration for this package including babel
builds, jest testing, eslint, and prettier.

View File

@ -0,0 +1,33 @@
{
"name": "@superset-ui/number-format",
"version": "0.0.0",
"description": "Superset UI number format",
"sideEffects": false,
"main": "lib/index.js",
"module": "esm/index.js",
"files": [
"esm",
"lib"
],
"repository": {
"type": "git",
"url": "git+https://github.com/apache-superset/superset-ui.git"
},
"keywords": [
"superset"
],
"author": "Superset",
"license": "Apache-2.0",
"bugs": {
"url": "https://github.com/apache-superset/superset-ui/issues"
},
"homepage": "https://github.com/apache-superset/superset-ui#readme",
"publishConfig": {
"access": "public"
},
"dependencies": {
"@superset-ui/core": "^0.6.0",
"d3-format": "^1.3.2",
"lodash": "^4.17.11"
}
}

View File

@ -0,0 +1,31 @@
export const DOLLAR = '$,.2f';
export const DOLLAR_CHANGE = '+$,.2f';
export const DOLLAR_ROUND = '$,d';
export const DOLLAR_ROUND_CHANGE = '+$,d';
export const FLOAT_1_POINT = ',.1f';
export const FLOAT_2_POINT = ',.2f';
export const FLOAT_3_POINT = ',.3f';
export const FLOAT = FLOAT_2_POINT;
export const FLOAT_CHANGE_1_POINT = '+,.1f';
export const FLOAT_CHANGE_2_POINT = '+,.2f';
export const FLOAT_CHANGE_3_POINT = '+,.3f';
export const FLOAT_CHANGE = FLOAT_CHANGE_2_POINT;
export const INTEGER = ',d';
export const INTEGER_CHANGE = '+,d';
export const PERCENT_1_POINT = ',.1%';
export const PERCENT_2_POINT = ',.2%';
export const PERCENT_3_POINT = ',.3%';
export const PERCENT = PERCENT_2_POINT;
export const PERCENT_CHANGE_1_POINT = '+,.1%';
export const PERCENT_CHANGE_2_POINT = '+,.2%';
export const PERCENT_CHANGE_3_POINT = '+,.3%';
export const PERCENT_CHANGE = PERCENT_CHANGE_2_POINT;
export const SI_1_DIGIT = '.1s';
export const SI_2_DIGIT = '.2s';
export const SI_3_DIGIT = '.3s';

View File

@ -0,0 +1,35 @@
import { ExtensibleFunction, isRequired } from '@superset-ui/core';
export const PREVIEW_VALUE = 12345.432;
export default class NumberFormatter extends ExtensibleFunction {
constructor({
id = isRequired('config.id'),
label,
description = '',
formatFunc = isRequired('config.formatFunc'),
} = {}) {
super((...args) => this.format(...args));
this.id = id;
this.label = label || id;
this.description = description;
this.formatFunc = formatFunc;
}
format(value) {
if (value === null || value === undefined || Number.isNaN(value)) {
return value;
} else if (value === Number.POSITIVE_INFINITY) {
return '∞';
} else if (value === Number.NEGATIVE_INFINITY) {
return '-∞';
}
return this.formatFunc(value);
}
preview(value = PREVIEW_VALUE) {
return `${value} => ${this.format(value)}`;
}
}

View File

@ -0,0 +1,32 @@
import { RegistryWithDefaultKey } from '@superset-ui/core';
import D3Formatter from './formatters/D3Formatter';
import { SI_3_DIGIT } from './NumberFormats';
const DEFAULT_FORMAT = SI_3_DIGIT;
export default class NumberFormatterRegistry extends RegistryWithDefaultKey {
constructor() {
super({
initialDefaultKey: DEFAULT_FORMAT,
name: 'NumberFormatter',
});
}
get(formatterId) {
const targetFormat = formatterId || this.defaultKey;
if (this.has(targetFormat)) {
return super.get(targetFormat);
}
// Create new formatter if does not exist
const formatter = new D3Formatter(targetFormat);
this.registerValue(targetFormat, formatter);
return formatter;
}
format(formatterId, value) {
return this.get(formatterId)(value);
}
}

View File

@ -0,0 +1,14 @@
import { makeSingleton } from '@superset-ui/core';
import NumberFormatterRegistry from './NumberFormatterRegistry';
const getInstance = makeSingleton(NumberFormatterRegistry);
export default getInstance;
export function getNumberFormatter(format) {
return getInstance().get(format);
}
export function formatNumber(format, value) {
return getInstance().format(format, value);
}

View File

@ -0,0 +1,40 @@
import isString from 'lodash/isString';
import { format as d3Format } from 'd3-format';
import { isRequired } from '@superset-ui/core';
import NumberFormatter from '../NumberFormatter';
export default class D3Formatter extends NumberFormatter {
/**
* Pass only the D3 format string to constructor
*
* new D3Formatter('.2f');
*
* or accompany it with human-readable label and description
*
* new D3Formatter({
* id: '.2f',
* label: 'Float with 2 decimal points',
* description: 'lorem ipsum dolor sit amet',
* });
*
* @param {String|Object} configOrFormatString
*/
constructor(configOrFormatString = isRequired('configOrFormatString')) {
const config = isString(configOrFormatString)
? { id: configOrFormatString }
: configOrFormatString;
let formatFunc;
let isInvalid = false;
try {
formatFunc = d3Format(config.id);
} catch (e) {
formatFunc = () => `Invalid format: ${config.id}`;
isInvalid = true;
}
super({ ...config, formatFunc });
this.isInvalid = isInvalid;
}
}

View File

@ -0,0 +1,19 @@
import { format as d3Format } from 'd3-format';
import NumberFormatter from '../NumberFormatter';
export default class SiAtMostNDigitFormatter extends NumberFormatter {
constructor(n = 3) {
const siFormatter = d3Format(`.${n}s`);
super({
formatFunc: value => {
const si = siFormatter(value);
// Removing trailing `.00` if any
return si.slice(-1) < 'A' ? parseFloat(si).toString() : si;
},
id: `si_at_most_${n}_digit`,
label: `SI with at most ${n} significant digits`,
});
}
}

View File

@ -0,0 +1,10 @@
import * as NumberFormats from './NumberFormats';
export {
default as getNumberFormatterRegistry,
formatNumber,
getNumberFormatter,
} from './NumberFormatterRegistrySingleton';
export { default as NumberFormatter, PREVIEW_VALUE } from './NumberFormatter';
export { NumberFormats };

View File

@ -0,0 +1,64 @@
import NumberFormatter from '../src/NumberFormatter';
describe('NumberFormatter', () => {
describe('new NumberFormatter(config)', () => {
it('requires config.id', () => {
expect(() => new NumberFormatter()).toThrow();
});
it('requires config.formatFunc', () => {
expect(
() =>
new NumberFormatter({
id: 'my_format',
}),
).toThrow();
});
});
describe('formatter is also a format function itself', () => {
const formatter = new NumberFormatter({
id: 'fixed_3',
formatFunc: value => value.toFixed(3),
});
it('returns formatted value', () => {
expect(formatter(12345.67)).toEqual('12345.670');
});
it('formatter(value) is the same with formatter.format(value)', () => {
const value = 12345.67;
expect(formatter(value)).toEqual(formatter.format(value));
});
});
describe('.format(value)', () => {
const formatter = new NumberFormatter({
id: 'fixed_3',
formatFunc: value => value.toFixed(3),
});
it('handles null', () => {
expect(formatter.format(null)).toBeNull();
});
it('handles undefined', () => {
expect(formatter.format(undefined)).toBeUndefined();
});
it('handles NaN', () => {
expect(formatter.format(NaN)).toBeNaN();
});
it('handles positive and negative infinity', () => {
expect(formatter.format(Number.POSITIVE_INFINITY)).toEqual('∞');
expect(formatter.format(Number.NEGATIVE_INFINITY)).toEqual('-∞');
});
it('otherwise returns formatted value', () => {
expect(formatter.format(12345.67)).toEqual('12345.670');
});
});
describe('.preview(value)', () => {
const formatter = new NumberFormatter({
id: 'fixed_2',
formatFunc: value => value.toFixed(2),
});
it('returns string comparing value before and after formatting', () => {
expect(formatter.preview(100)).toEqual('100 => 100.00');
});
it('uses the default preview value if not specified', () => {
expect(formatter.preview()).toEqual('12345.432 => 12345.43');
});
});
});

View File

@ -0,0 +1,32 @@
import NumberFormatterRegistry from '../src/NumberFormatterRegistry';
import NumberFormatter from '../src/NumberFormatter';
describe('NumberFormatterRegistry', () => {
let registry;
beforeEach(() => {
registry = new NumberFormatterRegistry();
});
describe('.get(format)', () => {
it('creates and returns a new formatter if does not exist', () => {
const formatter = registry.get('.2f');
expect(formatter).toBeInstanceOf(NumberFormatter);
expect(formatter.format(100)).toEqual('100.00');
});
it('returns an existing formatter if already exists', () => {
const formatter = registry.get('.2f');
const formatter2 = registry.get('.2f');
expect(formatter).toBe(formatter2);
});
it('falls back to default format if format is not specified', () => {
registry.setDefaultKey('.1f');
const formatter = registry.get();
expect(formatter.format(100)).toEqual('100.0');
});
});
describe('.format(format, value)', () => {
it('return the value with the specified format', () => {
expect(registry.format('.2f', 100)).toEqual('100.00');
expect(registry.format(',d', 100)).toEqual('100');
});
});
});

View File

@ -0,0 +1,29 @@
import getNumberFormatterRegistry, {
getNumberFormatter,
formatNumber,
} from '../src/NumberFormatterRegistrySingleton';
import NumberFormatterRegistry from '../src/NumberFormatterRegistry';
describe('NumberFormatterRegistrySingleton', () => {
describe('getNumberFormatterRegistry()', () => {
it('returns a NumberFormatterRegisry', () => {
expect(getNumberFormatterRegistry()).toBeInstanceOf(NumberFormatterRegistry);
});
});
describe('getNumberFormatter(format)', () => {
it('returns a format function', () => {
const format = getNumberFormatter('.3s');
expect(format(12345)).toEqual('12.3k');
});
it('returns a format function even given invalid format', () => {
const format = getNumberFormatter('xkcd');
expect(format(12345)).toEqual('Invalid format: xkcd');
});
});
describe('formatNumber(format, value)', () => {
it('format the given number using the specified format', () => {
const output = formatNumber('.3s', 12345);
expect(output).toEqual('12.3k');
});
});
});

View File

@ -0,0 +1,28 @@
import D3Formatter from '../../src/formatters/D3Formatter';
describe('D3Formatter', () => {
describe('new D3Formatter(config)', () => {
it('requires configOrFormatString', () => {
expect(() => new D3Formatter()).toThrow();
});
describe('if configOrFormatString is string', () => {
it('uses the input as d3.format string', () => {
const formatter = new D3Formatter('.2f');
expect(formatter.format(100)).toEqual('100.00');
});
});
describe('if configOrFormatString is not string', () => {
it('requires field config.id', () => {
expect(() => new D3Formatter({})).toThrow();
});
it('uses d3.format(config.id) as format function', () => {
const formatter = new D3Formatter({ id: ',.4f' });
expect(formatter.format(12345.67)).toEqual('12,345.6700');
});
it('if it is an invalid d3 format, the format function displays error message', () => {
const formatter = new D3Formatter({ id: 'i-am-groot' });
expect(formatter.format(12345.67)).toEqual('Invalid format: i-am-groot');
});
});
});
});

View File

@ -0,0 +1,51 @@
import NumberFormatter from '../../src/NumberFormatter';
import SiAtMostNDigitFormatter from '../../src/formatters/SiAtMostNDigitFormatter';
describe('SiAtMostNDigitFormatter', () => {
describe('new SiAtMostNDigitFormatter(n)', () => {
it('creates an instance of NumberFormatter', () => {
const formatter = new SiAtMostNDigitFormatter(3);
expect(formatter).toBeInstanceOf(NumberFormatter);
});
it('when n is specified, it formats number in SI format with at most n significant digits', () => {
const formatter = new SiAtMostNDigitFormatter(2);
expect(formatter(10)).toBe('10');
expect(formatter(1)).toBe('1');
expect(formatter(1.0)).toBe('1');
expect(formatter(10.0)).toBe('10');
expect(formatter(10001)).toBe('10k');
expect(formatter(10100)).toBe('10k');
expect(formatter(111000000)).toBe('110M');
expect(formatter(0.23)).toBe('230m');
expect(formatter(0)).toBe('0');
expect(formatter(-10)).toBe('-10');
expect(formatter(-1)).toBe('-1');
expect(formatter(-1.0)).toBe('-1');
expect(formatter(-10.0)).toBe('-10');
expect(formatter(-10001)).toBe('-10k');
expect(formatter(-10101)).toBe('-10k');
expect(formatter(-111000000)).toBe('-110M');
expect(formatter(-0.23)).toBe('-230m');
});
it('when n is not specified, it defaults to n=3', () => {
const formatter = new SiAtMostNDigitFormatter(3);
expect(formatter(10)).toBe('10');
expect(formatter(1)).toBe('1');
expect(formatter(1.0)).toBe('1');
expect(formatter(10.0)).toBe('10');
expect(formatter(10001)).toBe('10.0k');
expect(formatter(10100)).toBe('10.1k');
expect(formatter(111000000)).toBe('111M');
expect(formatter(0.23)).toBe('230m');
expect(formatter(0)).toBe('0');
expect(formatter(-10)).toBe('-10');
expect(formatter(-1)).toBe('-1');
expect(formatter(-1.0)).toBe('-1');
expect(formatter(-10.0)).toBe('-10');
expect(formatter(-10001)).toBe('-10.0k');
expect(formatter(-10101)).toBe('-10.1k');
expect(formatter(-111000000)).toBe('-111M');
expect(formatter(-0.23)).toBe('-230m');
});
});
});

View File

@ -0,0 +1,21 @@
import {
formatNumber,
NumberFormats,
getNumberFormatter,
getNumberFormatterRegistry,
NumberFormatter,
PREVIEW_VALUE,
} from '../src/index';
describe('index', () => {
it('exports modules', () => {
[
formatNumber,
NumberFormats,
getNumberFormatter,
getNumberFormatterRegistry,
NumberFormatter,
PREVIEW_VALUE,
].forEach(x => expect(x).toBeDefined());
});
});