Skip to content

Commit

Permalink
feat(formatters): add Currency Formatter and GroupTotalFormatter (#850)
Browse files Browse the repository at this point in the history
  • Loading branch information
ghiscoding committed Dec 19, 2022
1 parent 05402e5 commit ad373ab
Show file tree
Hide file tree
Showing 26 changed files with 981 additions and 106 deletions.
2 changes: 1 addition & 1 deletion README.md
Expand Up @@ -47,7 +47,7 @@ The GitHub [live demo](https://ghiscoding.github.io/slickgrid-universal) shows 2
The Vanilla Implementation (which is not associated to any framework) was built with [WebPack](https://webpack.js.org/) and is also used to run and test all the UI functionalities [Cypress](https://www.cypress.io/) (E2E tests). The [Vanilla-force-bundle](https://github.com/ghiscoding/slickgrid-universal/tree/master/packages/vanilla-bundle), which extends the `vanilla-bundle` package is what we use in our SalesForce implementation (with Lightning Web Component), hence the creation of this monorepo library.

### Fully Tested with [Jest](https://jestjs.io/) (Unit Tests) - [Cypress](https://www.cypress.io/) (E2E Tests)
Slickgrid-Universal has **100%** Unit Test Coverage, we are talking about +15,000 lines of code (+3,700 unit tests) that are fully tested with [Jest](https://jestjs.io/). There are also +450 Cypress E2E tests to cover all [Examples](https://ghiscoding.github.io/slickgrid-universal/) and most UI functionalities (there's also an additional +500 tests in Angular/Aurelia-Slickgrid)
Slickgrid-Universal has **100%** Unit Test Coverage, we are talking about +15,000 lines of code (~4,000 unit tests) that are fully tested with [Jest](https://jestjs.io/). There are also +450 Cypress E2E tests to cover all [Examples](https://ghiscoding.github.io/slickgrid-universal/) and most UI functionalities (there's also an additional +500 tests in Angular/Aurelia-Slickgrid)

### Available Public Packages

Expand Down
4 changes: 2 additions & 2 deletions packages/common/src/constants.ts
Expand Up @@ -80,8 +80,8 @@ export class Constants {
};
static readonly DEFAULT_FORMATTER_NUMBER_MIN_DECIMAL = 2;
static readonly DEFAULT_FORMATTER_NUMBER_MAX_DECIMAL = 2;
static readonly DEFAULT_FORMATTER_DOLLAR_MIN_DECIMAL = 2;
static readonly DEFAULT_FORMATTER_DOLLAR_MAX_DECIMAL = 4;
static readonly DEFAULT_FORMATTER_CURRENCY_MIN_DECIMAL = 2;
static readonly DEFAULT_FORMATTER_CURRENCY_MAX_DECIMAL = 4;
static readonly DEFAULT_FORMATTER_PERCENT_MIN_DECIMAL = undefined;
static readonly DEFAULT_FORMATTER_PERCENT_MAX_DECIMAL = undefined;
static readonly DEFAULT_NUMBER_DECIMAL_SEPARATOR = '.';
Expand Down
158 changes: 158 additions & 0 deletions packages/common/src/formatters/__tests__/currencyFormatter.spec.ts
@@ -0,0 +1,158 @@
import { Column, GridOption, SlickGrid } from '../../interfaces/index';
import { currencyFormatter } from '../currencyFormatter';

describe('the Currency Formatter', () => {
const gridStub = {
getOptions: jest.fn()
} as unknown as SlickGrid;

beforeEach(() => {
jest.spyOn(global.console, 'warn').mockReturnValue();
});

it('should display an empty string when no value is provided', () => {
const output = currencyFormatter(1, 1, '', {} as Column, {}, {} as any);
expect(output).toBe('');
});

it('should display original string when non-numeric value is provided', () => {
const output = currencyFormatter(1, 1, 'hello', {} as Column, {}, {} as any);
expect(output).toBe('hello');
});

it('should display €0 when number 0 is provided', () => {
const input = 0;
const output = currencyFormatter(1, 1, input, {} as Column, {}, {} as any);
expect(output).toBe('0.00');
});

it('should display a decimal number with negative when a negative number is provided', () => {
const input = -15;
const output = currencyFormatter(1, 1, input, {} as Column, {}, {} as any);
expect(output).toBe('-15.00');
});

it('should display a decimal number with when a number is provided', () => {
const input = 99;
const output = currencyFormatter(1, 1, input, {} as Column, {}, {} as any);
expect(output).toBe('99.00');
});

it('should display a decimal number with a prefix when numberPrefix is provided', () => {
const input = 99;
const output = currencyFormatter(1, 1, input, { params: { numberPrefix: 'USD ' } } as Column, {}, {} as any);
expect(output).toBe('USD 99.00');
});

it('should display a negative decimal number with a prefix when numberPrefix is provided', () => {
const input = -99;
const output = currencyFormatter(1, 1, input, { params: { currencyPrefix: '€' } } as Column, {}, {} as any);
expect(output).toBe('-€99.00');
});

it('should display a negative decimal number with a prefix when numberPrefix is provided', () => {
const input = -99;
const output = currencyFormatter(1, 1, input, { params: { numberPrefix: '€' } } as Column, {}, {} as any);
expect(output).toBe('€-99.00');
});

it('should display a negative decimal number with a prefix when currencyPrefix and numberPrefix are provided', () => {
const input = -99;
const output = currencyFormatter(1, 1, input, { params: { currencyPrefix: '€', numberPrefix: 'Price ' } } as Column, {}, {} as any);
expect(output).toBe('Price -€99.00');
});

it('should display a negative decimal number with a prefix when numberPrefix is provided', () => {
const input = -99;
const output = currencyFormatter(1, 1, input, { params: { displayNegativeNumberWithParentheses: true, currencyPrefix: '€' } } as Column, {}, {} as any);
expect(output).toBe('(€99.00)');
});

it('should display a negative decimal number with a prefix when numberPrefix is provided', () => {
const input = -99;
const output = currencyFormatter(1, 1, input, { params: { displayNegativeNumberWithParentheses: true, numberPrefix: '€' } } as Column, {}, {} as any);
expect(output).toBe('€(99.00)');
});

it('should display a decimal number with a prefix when numberSuffix is provided', () => {
const input = 99;
const output = currencyFormatter(1, 1, input, { params: { numberSuffix: ' USD' } } as Column, {}, {} as any);
expect(output).toBe('99.00 USD');
});

it('should display a decimal number with a prefix when numberSuffix is provided', () => {
const input = -99;
const output = currencyFormatter(1, 1, input, { params: { numberSuffix: ' USD' } } as Column, {}, {} as any);
expect(output).toBe('-99.00 USD');
});

it('should display a negative decimal number with a suffix when currencySuffix and numberSuffix are provided', () => {
const input = -99;
const output = currencyFormatter(1, 1, input, { params: { currencySuffix: '€', numberSuffix: ' EUR' } } as Column, {}, {} as any);
expect(output).toBe('-99.00€ EUR');
});

it('should display a decimal number with when a string number is provided', () => {
const input = '99';
const output = currencyFormatter(1, 1, input, {} as Column, {}, {} as any);
expect(output).toBe('99.00');
});

it('should display a decimal number with and use "minDecimal" params', () => {
const input = 99.1;
const output = currencyFormatter(1, 1, input, { params: { minDecimal: 2 } } as Column, {}, {} as any);
expect(output).toBe('99.10');
});

it('should display a decimal number with and use "minDecimal" params', () => {
const input = 12345678.1;

const output1 = currencyFormatter(1, 1, input, { params: { minDecimal: 2 } } as Column, {}, {} as any);
const output2 = currencyFormatter(1, 1, input, { params: { decimalPlaces: 2 } } as Column, {}, {} as any);
const output3 = currencyFormatter(1, 1, input, { params: { decimalPlaces: 2, thousandSeparator: ',' } } as Column, {}, {} as any);
const output4 = currencyFormatter(1, 1, input, { params: { decimalPlaces: 2, decimalSeparator: ',', thousandSeparator: ' ' } } as Column, {}, {} as any);

expect(output1).toBe('12345678.10');
expect(output2).toBe('12345678.10');
expect(output3).toBe('12,345,678.10');
expect(output4).toBe('12 345 678,10');
});

it('should display a decimal number with and use "maxDecimal" params', () => {
const input = 88.156789;
const output = currencyFormatter(1, 1, input, { params: { maxDecimal: 3 } } as Column, {}, {} as any);
expect(output).toBe(`88.157`);
});

it('should display a negative number with parentheses when "displayNegativeNumberWithParentheses" is enabled in the "params"', () => {
const input = -2.4;
const output = currencyFormatter(1, 1, input, { params: { displayNegativeNumberWithParentheses: true } } as Column, {}, {} as any);
expect(output).toBe(`(2.40)`);
});

it('should display a negative number with parentheses when "displayNegativeNumberWithParentheses" is enabled and thousand separator in the "params"', () => {
const input = -12345678.4;
const output = currencyFormatter(1, 1, input, { params: { displayNegativeNumberWithParentheses: true, thousandSeparator: ',' } } as Column, {}, {} as any);
expect(output).toBe(`(12,345,678.40)`);
});

it('should display a negative number with parentheses when "displayNegativeNumberWithParentheses" is enabled and thousand separator in the "params"', () => {
const input = -12345678.4;
const output = currencyFormatter(1, 1, input, { params: { currencyPrefix: '€', numberPrefix: 'Price ', currencySuffix: ' EUR', numberSuffix: ' /item', displayNegativeNumberWithParentheses: true, thousandSeparator: ',' } } as Column, {}, {} as any);
expect(output).toBe(`Price (€12,345,678.40 EUR) /item`);
});

it('should display a negative average with parentheses when input is negative and "displayNegativeNumberWithParentheses" is enabled in the Formatter Options', () => {
gridStub.getOptions = () => ({ formatterOptions: { displayNegativeNumberWithParentheses: true, minDecimal: 2 } } as GridOption);
const input = -2.4;
const output = currencyFormatter(1, 1, input, {} as Column, {}, gridStub);
expect(output).toBe(`(2.40)`);
});

it('should display a negative average with parentheses when input is negative and "displayNegativeNumberWithParentheses" is enabled and thousand separator in the Formatter Options', () => {
gridStub.getOptions = () => ({ formatterOptions: { displayNegativeNumberWithParentheses: true, decimalSeparator: ',', thousandSeparator: ' ' } } as GridOption);
const input = -12345678.4;
const output = currencyFormatter(1, 1, input, {} as Column, {}, gridStub);
expect(output).toBe(`(12 345 678,40)`);
});
});
32 changes: 32 additions & 0 deletions packages/common/src/formatters/currencyFormatter.ts
@@ -0,0 +1,32 @@
import { isNumber } from '@slickgrid-universal/utils';

import { Formatter } from '../interfaces/index';
import { formatNumber } from '../services/utilities';
import { retrieveFormatterOptions } from './formatterUtilities';

/**
* This Formatters allow the user to provide any currency symbol (as symbol prefix/suffix) and also provide extra text prefix/suffix.
* So with this, it allows the user to provide dual prefixes/suffixes via the following params
* You can pass "minDecimal", "maxDecimal", "decimalSeparator", "thousandSeparator", "numberPrefix", "currencyPrefix", "currencySuffix", and "numberSuffix" to the "params" property.
* For example:: `{ formatter: Formatters.decimal, params: { minDecimal: 2, maxDecimal: 4, prefix: 'Price ', currencyPrefix: '€', currencySuffix: ' EUR' }}`
* with value of 33.45 would result into: "Price €33.45 EUR"
*/
export const currencyFormatter: Formatter = (_row, _cell, value, columnDef, _dataContext, grid) => {
const {
currencyPrefix,
currencySuffix,
minDecimal,
maxDecimal,
numberPrefix,
numberSuffix,
decimalSeparator,
thousandSeparator,
wrapNegativeNumber,
} = retrieveFormatterOptions(columnDef, grid, 'decimal', 'cell');

if (isNumber(value)) {
const formattedNumber = formatNumber(value, minDecimal, maxDecimal, wrapNegativeNumber, currencyPrefix, currencySuffix, decimalSeparator, thousandSeparator);
return `${numberPrefix}${formattedNumber}${numberSuffix}`;
}
return value;
};
Expand Up @@ -12,7 +12,7 @@ export const dollarColoredBoldFormatter: Formatter = (_row, _cell, value, column
decimalSeparator,
thousandSeparator,
wrapNegativeNumber,
} = retrieveFormatterOptions(columnDef, grid, 'dollar', 'cell');
} = retrieveFormatterOptions(columnDef, grid, 'currency', 'cell');

if (isNumber(value)) {
const colorStyle = (value >= 0) ? 'green' : 'red';
Expand Down
2 changes: 1 addition & 1 deletion packages/common/src/formatters/dollarColoredFormatter.ts
Expand Up @@ -12,7 +12,7 @@ export const dollarColoredFormatter: Formatter = (_row, _cell, value, columnDef,
decimalSeparator,
thousandSeparator,
wrapNegativeNumber,
} = retrieveFormatterOptions(columnDef, grid, 'dollar', 'cell');
} = retrieveFormatterOptions(columnDef, grid, 'currency', 'cell');

if (isNumber(value)) {
const colorStyle = (value >= 0) ? 'green' : 'red';
Expand Down
2 changes: 1 addition & 1 deletion packages/common/src/formatters/dollarFormatter.ts
Expand Up @@ -12,7 +12,7 @@ export const dollarFormatter: Formatter = (_row, _cell, value, columnDef, _dataC
decimalSeparator,
thousandSeparator,
wrapNegativeNumber,
} = retrieveFormatterOptions(columnDef, grid, 'dollar', 'cell');
} = retrieveFormatterOptions(columnDef, grid, 'currency', 'cell');

if (isNumber(value)) {
return formatNumber(value, minDecimal, maxDecimal, wrapNegativeNumber, '$', '', decimalSeparator, thousandSeparator);
Expand Down
16 changes: 9 additions & 7 deletions packages/common/src/formatters/formatterUtilities.ts
Expand Up @@ -8,7 +8,7 @@ import { Constants } from '../constants';
const moment = (moment_ as any)['default'] || moment_; // patch to fix rollup "moment has no default export" issue, document here https://github.com/rollup/rollup/issues/670

export type FormatterType = 'group' | 'cell';
export type NumberType = 'decimal' | 'dollar' | 'percent' | 'regular';
export type NumberType = 'decimal' | 'currency' | 'percent' | 'regular';

/**
* Automatically add a Custom Formatter on all column definitions that have an Editor.
Expand Down Expand Up @@ -44,14 +44,14 @@ export function retrieveFormatterOptions(columnDef: Column, grid: SlickGrid, num
let numberSuffix = '';

switch (numberType) {
case 'currency':
defaultMinDecimal = Constants.DEFAULT_FORMATTER_CURRENCY_MIN_DECIMAL;
defaultMaxDecimal = Constants.DEFAULT_FORMATTER_CURRENCY_MAX_DECIMAL;
break;
case 'decimal':
defaultMinDecimal = Constants.DEFAULT_FORMATTER_NUMBER_MIN_DECIMAL;
defaultMaxDecimal = Constants.DEFAULT_FORMATTER_NUMBER_MAX_DECIMAL;
break;
case 'dollar':
defaultMinDecimal = Constants.DEFAULT_FORMATTER_DOLLAR_MIN_DECIMAL;
defaultMaxDecimal = Constants.DEFAULT_FORMATTER_DOLLAR_MAX_DECIMAL;
break;
case 'percent':
defaultMinDecimal = Constants.DEFAULT_FORMATTER_PERCENT_MIN_DECIMAL;
defaultMaxDecimal = Constants.DEFAULT_FORMATTER_PERCENT_MAX_DECIMAL;
Expand All @@ -64,13 +64,15 @@ export function retrieveFormatterOptions(columnDef: Column, grid: SlickGrid, num
const decimalSeparator = getValueFromParamsOrFormatterOptions('decimalSeparator', columnDef, grid, Constants.DEFAULT_NUMBER_DECIMAL_SEPARATOR);
const thousandSeparator = getValueFromParamsOrFormatterOptions('thousandSeparator', columnDef, grid, Constants.DEFAULT_NUMBER_THOUSAND_SEPARATOR);
const wrapNegativeNumber = getValueFromParamsOrFormatterOptions('displayNegativeNumberWithParentheses', columnDef, grid, Constants.DEFAULT_NEGATIVE_NUMBER_WRAPPED_IN_BRAQUET);
const currencyPrefix = getValueFromParamsOrFormatterOptions('currencyPrefix', columnDef, grid, '');
const currencySuffix = getValueFromParamsOrFormatterOptions('currencySuffix', columnDef, grid, '');

if (formatterType === 'cell') {
numberPrefix = getValueFromParamsOrFormatterOptions('numberPrefix', columnDef, grid, '');
numberSuffix = getValueFromParamsOrFormatterOptions('numberSuffix', columnDef, grid, '');
}

return { minDecimal, maxDecimal, decimalSeparator, thousandSeparator, wrapNegativeNumber, numberPrefix, numberSuffix };
return { minDecimal, maxDecimal, decimalSeparator, thousandSeparator, wrapNegativeNumber, currencyPrefix, currencySuffix, numberPrefix, numberSuffix };
}

/**
Expand All @@ -85,7 +87,7 @@ export function getValueFromParamsOrFormatterOptions(optionName: string, columnD

if (params && params.hasOwnProperty(optionName)) {
return params[optionName];
} else if (gridOptions.formatterOptions?.hasOwnProperty(optionName)) {
} else if (gridOptions?.formatterOptions?.hasOwnProperty(optionName)) {
return (gridOptions.formatterOptions as any)[optionName];
}
return defaultValue;
Expand Down
10 changes: 10 additions & 0 deletions packages/common/src/formatters/formatters.index.ts
Expand Up @@ -8,6 +8,7 @@ import { centerFormatter } from './centerFormatter';
import { checkboxFormatter } from './checkboxFormatter';
import { checkmarkFormatter } from './checkmarkFormatter';
import { checkmarkMaterialFormatter } from './checkmarkMaterialFormatter';
import { currencyFormatter } from './currencyFormatter';
import { collectionFormatter } from './collectionFormatter';
import { collectionEditorFormatter } from './collectionEditorFormatter';
import { complexObjectFormatter } from './complexObjectFormatter';
Expand Down Expand Up @@ -112,6 +113,15 @@ export const Formatters = {
*/
collectionEditor: collectionEditorFormatter,

/**
* Similar to "Formatters.decimal", but it allows you to provide prefixes and suffixes (currencyPrefix, currencySuffix, numberPrefix, numberSuffix)
* So with this, it allows the user to provide dual prefixes/suffixes via the following params
* You can pass "minDecimal", "maxDecimal", "decimalSeparator", "thousandSeparator", "numberPrefix", "currencyPrefix", "currencySuffix", and "numberSuffix" to the "params" property.
* For example:: `{ formatter: Formatters.decimal, params: { minDecimal: 2, maxDecimal: 4, prefix: 'Price ', currencyPrefix: '€', currencySuffix: ' EUR' }}`
* with value of 33.45 would result into: "Price €33.45 EUR"
*/
currency: currencyFormatter,

/** Takes a Date object and displays it as an ISO Date format (YYYY-MM-DD) */
dateIso: getAssociatedDateFormatter(FieldType.dateIso, '-'),

Expand Down

0 comments on commit ad373ab

Please sign in to comment.