Skip to content

Commit

Permalink
feat: Implement currencies formatter for saved metrics (apache#24517)
Browse files Browse the repository at this point in the history
  • Loading branch information
kgabryje authored Jun 28, 2023
1 parent 1bf8609 commit 8329e82
Show file tree
Hide file tree
Showing 61 changed files with 906 additions and 75 deletions.
9 changes: 5 additions & 4 deletions superset-frontend/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import { Dataset } from './types';

export const TestDataset: Dataset = {
column_formats: {},
currency_formats: {},
columns: [
{
advanced_data_type: undefined,
Expand Down Expand Up @@ -123,6 +124,7 @@ export const TestDataset: Dataset = {
certification_details: null,
certified_by: null,
d3format: null,
currency: null,
description: null,
expression: 'COUNT(*)',
id: 7,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import React, { ReactElement, ReactNode, ReactText } from 'react';
import type {
AdhocColumn,
Column,
Currency,
DatasourceType,
JsonObject,
JsonValue,
Expand Down Expand Up @@ -68,6 +69,7 @@ export interface Dataset {
columns: ColumnMeta[];
metrics: Metric[];
column_formats: Record<string, string>;
currency_formats: Record<string, Currency>;
verbose_map: Record<string, string>;
main_dttm_col: string;
// eg. ['["ds", true]', 'ds [asc]']
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ describe('columnChoices()', () => {
],
verbose_map: {},
column_formats: { fiz: 'NUMERIC', about: 'STRING', foo: 'DATE' },
currency_formats: {},
datasource_name: 'my_datasource',
description: 'this is my datasource',
}),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ describe('defineSavedMetrics', () => {
columns: [],
verbose_map: {},
column_formats: {},
currency_formats: {},
datasource_name: 'my_datasource',
description: 'this is my datasource',
};
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/

import { ExtensibleFunction } from '../models';
import { getNumberFormatter, NumberFormats } from '../number-format';
import { Currency } from '../query';

interface CurrencyFormatterConfig {
d3Format?: string;
currency: Currency;
locale?: string;
}

interface CurrencyFormatter {
(value: number | null | undefined): string;
}

export const getCurrencySymbol = (currency: Currency) =>
new Intl.NumberFormat('en-US', {
style: 'currency',
currency: currency.symbol,
})
.formatToParts(1)
.find(x => x.type === 'currency')?.value;

class CurrencyFormatter extends ExtensibleFunction {
d3Format: string;

locale: string;

currency: Currency;

constructor(config: CurrencyFormatterConfig) {
super((value: number) => this.format(value));
this.d3Format = config.d3Format || NumberFormats.SMART_NUMBER;
this.currency = config.currency;
this.locale = config.locale || 'en-US';
}

hasValidCurrency() {
return Boolean(this.currency?.symbol);
}

getNormalizedD3Format() {
return this.d3Format.replace(/\$|%/g, '');
}

format(value: number) {
const formattedValue = getNumberFormatter(this.getNormalizedD3Format())(
value,
);
if (!this.hasValidCurrency()) {
return formattedValue as string;
}

if (this.currency.symbolPosition === 'prefix') {
return `${getCurrencySymbol(this.currency)} ${formattedValue}`;
}
return `${formattedValue} ${getCurrencySymbol(this.currency)}`;
}
}

export default CurrencyFormatter;
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/

export { default as CurrencyFormatter } from './CurrencyFormatter';
export * from './CurrencyFormatter';
1 change: 1 addition & 0 deletions superset-frontend/packages/superset-ui-core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,3 +36,4 @@ export * from './components';
export * from './math-expression';
export * from './ui-overrides';
export * from './hooks';
export * from './currency-format';
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,11 @@ export enum DatasourceType {
SavedQuery = 'saved_query',
}

export interface Currency {
symbol: string;
symbolPosition: string;
}

/**
* Datasource metadata.
*/
Expand All @@ -41,6 +46,9 @@ export interface Datasource {
columnFormats?: {
[key: string]: string;
};
currencyFormats?: {
[key: string]: Currency;
};
verboseMap?: {
[key: string]: string;
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ export interface Metric {
certification_details?: Maybe<string>;
certified_by?: Maybe<string>;
d3format?: Maybe<string>;
currency?: Maybe<string>;
description?: Maybe<string>;
is_certified?: boolean;
verbose_name?: string;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,15 @@
* specific language governing permissions and limitations
* under the License.
*/
import { NumberFormatter } from '../number-format';
import { CurrencyFormatter } from '../currency-format';

export * from '../query/types';

export type Maybe<T> = T | null;

export type Optional<T> = T | undefined;

export type ValueOf<T> = T[keyof T];

export type ValueFormatter = NumberFormatter | CurrencyFormatter;
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/

import {
CurrencyFormatter,
getCurrencySymbol,
NumberFormats,
} from '@superset-ui/core';

it('getCurrencySymbol', () => {
expect(
getCurrencySymbol({ symbol: 'PLN', symbolPosition: 'prefix' }),
).toEqual('PLN');
expect(
getCurrencySymbol({ symbol: 'USD', symbolPosition: 'prefix' }),
).toEqual('$');

expect(() =>
getCurrencySymbol({ symbol: 'INVALID_CODE', symbolPosition: 'prefix' }),
).toThrow(RangeError);
});

it('CurrencyFormatter object fields', () => {
const defaultCurrencyFormatter = new CurrencyFormatter({
currency: { symbol: 'USD', symbolPosition: 'prefix' },
});
expect(defaultCurrencyFormatter.d3Format).toEqual(NumberFormats.SMART_NUMBER);
expect(defaultCurrencyFormatter.locale).toEqual('en-US');
expect(defaultCurrencyFormatter.currency).toEqual({
symbol: 'USD',
symbolPosition: 'prefix',
});

const currencyFormatter = new CurrencyFormatter({
currency: { symbol: 'PLN', symbolPosition: 'suffix' },
locale: 'pl-PL',
d3Format: ',.1f',
});
expect(currencyFormatter.d3Format).toEqual(',.1f');
expect(currencyFormatter.locale).toEqual('pl-PL');
expect(currencyFormatter.currency).toEqual({
symbol: 'PLN',
symbolPosition: 'suffix',
});
});

it('CurrencyFormatter:hasValidCurrency', () => {
const currencyFormatter = new CurrencyFormatter({
currency: { symbol: 'USD', symbolPosition: 'prefix' },
});
expect(currencyFormatter.hasValidCurrency()).toBe(true);

const currencyFormatterWithoutPosition = new CurrencyFormatter({
// @ts-ignore
currency: { symbol: 'USD' },
});
expect(currencyFormatterWithoutPosition.hasValidCurrency()).toBe(true);

const currencyFormatterWithoutSymbol = new CurrencyFormatter({
// @ts-ignore
currency: { symbolPosition: 'prefix' },
});
expect(currencyFormatterWithoutSymbol.hasValidCurrency()).toBe(false);

// @ts-ignore
const currencyFormatterWithoutCurrency = new CurrencyFormatter({});
expect(currencyFormatterWithoutCurrency.hasValidCurrency()).toBe(false);
});

it('CurrencyFormatter:getNormalizedD3Format', () => {
const currencyFormatter = new CurrencyFormatter({
currency: { symbol: 'USD', symbolPosition: 'prefix' },
});
expect(currencyFormatter.getNormalizedD3Format()).toEqual(
currencyFormatter.d3Format,
);

const currencyFormatter2 = new CurrencyFormatter({
currency: { symbol: 'USD', symbolPosition: 'prefix' },
d3Format: ',.1f',
});
expect(currencyFormatter2.getNormalizedD3Format()).toEqual(
currencyFormatter2.d3Format,
);

const currencyFormatter3 = new CurrencyFormatter({
currency: { symbol: 'USD', symbolPosition: 'prefix' },
d3Format: '$,.1f',
});
expect(currencyFormatter3.getNormalizedD3Format()).toEqual(',.1f');

const currencyFormatter4 = new CurrencyFormatter({
currency: { symbol: 'USD', symbolPosition: 'prefix' },
d3Format: ',.1%',
});
expect(currencyFormatter4.getNormalizedD3Format()).toEqual(',.1');
});

it('CurrencyFormatter:format', () => {
const VALUE = 56100057;
const currencyFormatterWithPrefix = new CurrencyFormatter({
currency: { symbol: 'USD', symbolPosition: 'prefix' },
});

expect(currencyFormatterWithPrefix(VALUE)).toEqual(
currencyFormatterWithPrefix.format(VALUE),
);
expect(currencyFormatterWithPrefix(VALUE)).toEqual('$ 56.1M');

const currencyFormatterWithSuffix = new CurrencyFormatter({
currency: { symbol: 'USD', symbolPosition: 'suffix' },
});
expect(currencyFormatterWithSuffix(VALUE)).toEqual('56.1M $');

const currencyFormatterWithoutPosition = new CurrencyFormatter({
// @ts-ignore
currency: { symbol: 'USD' },
});
expect(currencyFormatterWithoutPosition(VALUE)).toEqual('56.1M $');

// @ts-ignore
const currencyFormatterWithoutCurrency = new CurrencyFormatter({});
expect(currencyFormatterWithoutCurrency(VALUE)).toEqual('56.1M');

const currencyFormatterWithCustomD3 = new CurrencyFormatter({
currency: { symbol: 'USD', symbolPosition: 'prefix' },
d3Format: ',.1f',
});
expect(currencyFormatterWithCustomD3(VALUE)).toEqual('$ 56,100,057.0');

const currencyFormatterWithPercentD3 = new CurrencyFormatter({
currency: { symbol: 'USD', symbolPosition: 'prefix' },
d3Format: ',.1f%',
});
expect(currencyFormatterWithPercentD3(VALUE)).toEqual('$ 56,100,057.0');

const currencyFormatterWithCurrencyD3 = new CurrencyFormatter({
currency: { symbol: 'PLN', symbolPosition: 'suffix' },
d3Format: '$,.1f',
});
expect(currencyFormatterWithCurrencyD3(VALUE)).toEqual('56,100,057.0 PLN');
});
Loading

0 comments on commit 8329e82

Please sign in to comment.