Skip to content

Add support for minDecimalScale #752

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions documentation/v5/docs/numeric_format.md
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,31 @@ import { NumericFormat } from 'react-number-format';
></iframe>
</details>

### minDecimalScale `number`

**default**: `undefined`

If defined, it enforces a minimum number of digits after the decimal point.

```js
import { NumericFormat } from 'react-number-format';

<NumericFormat value={12323.3334} minDecimalScale={3} />;
```

<details>
<summary>
Demo
</summary>

<iframe src="https://codesandbox.io/embed/decimalscale-demo-uc92li?fontsize=14&hidenavigation=1&theme=dark&view=preview"
className='csb'
title="decimalScale-demo"
allow="accelerometer; ambient-light-sensor; camera; encrypted-media; geolocation; gyroscope; hid; microphone; midi; payment; usb; vr; xr-spatial-tracking"
sandbox="allow-forms allow-modals allow-popups allow-presentation allow-same-origin allow-scripts"
></iframe>
</details>

### decimalSeparator `string`

**default**: '.'
Expand Down
52 changes: 28 additions & 24 deletions src/numeric_format.tsx
Original file line number Diff line number Diff line change
@@ -1,37 +1,38 @@
import React from 'react';
import NumberFormatBase from './number_format_base';
import {
ChangeMeta,
FormatInputValueFunction,
InputAttributes,
NumberFormatBaseProps,
NumericFormatProps,
RemoveFormattingFunction,
SourceType,
} from './types';
import {
escapeRegExp,
splitDecimal,
limitToScale,
applyThousandSeparator,
getDefaultChangeMeta,
charIsNumber,
escapeRegExp,
fixLeadingZero,
noop,
useInternalValues,
getDefaultChangeMeta,
isNanValue,
isNil,
limitToScale,
noop,
roundToPrecision,
isNanValue,
setCaretPosition,
splitDecimal,
toNumericString,
charIsNumber,
useInternalValues,
} from './utils';
import {
NumericFormatProps,
ChangeMeta,
SourceType,
InputAttributes,
FormatInputValueFunction,
RemoveFormattingFunction,
NumberFormatBaseProps,
} from './types';
import NumberFormatBase from './number_format_base';

export function format<BaseType = InputAttributes>(
numStr: string,
props: NumericFormatProps<BaseType>,
) {
const {
decimalScale,
minDecimalScale,
fixedDecimalScale,
prefix = '',
suffix = '',
Expand All @@ -52,13 +53,15 @@ export function format<BaseType = InputAttributes>(
* Or if decimalScale is > 0 and fixeDecimalScale is true (even if numStr has no decimal)
*/
const hasDecimalSeparator =
(decimalScale !== 0 && numStr.indexOf('.') !== -1) || (decimalScale && fixedDecimalScale);
(decimalScale !== 0 && numStr.indexOf('.') !== -1) ||
(decimalScale && fixedDecimalScale) ||
(minDecimalScale && minDecimalScale !== 0);

let { beforeDecimal, afterDecimal, addNegation } = splitDecimal(numStr, allowNegative); // eslint-disable-line prefer-const

//apply decimal precision if its defined
if (decimalScale !== undefined) {
afterDecimal = limitToScale(afterDecimal, decimalScale, !!fixedDecimalScale);
if (decimalScale !== undefined || minDecimalScale !== undefined) {
afterDecimal = limitToScale(afterDecimal, decimalScale, minDecimalScale, !!fixedDecimalScale);
}

if (thousandSeparator) {
Expand Down Expand Up @@ -333,6 +336,7 @@ export function useNumericFormat<BaseType = InputAttributes>(
onBlur = noop,
thousandSeparator,
decimalScale,
minDecimalScale,
fixedDecimalScale,
prefix = '',
defaultValue,
Expand Down Expand Up @@ -367,7 +371,7 @@ export function useNumericFormat<BaseType = InputAttributes>(
* we don't need to do it for onChange events, as we want to prevent typing there
*/
if (_valueIsNumericString && typeof decimalScale === 'number') {
return roundToPrecision(value, decimalScale, Boolean(fixedDecimalScale));
return roundToPrecision(value, decimalScale, minDecimalScale, Boolean(fixedDecimalScale));
}

return value;
Expand Down Expand Up @@ -449,8 +453,8 @@ export function useNumericFormat<BaseType = InputAttributes>(
}

// apply fixedDecimalScale on blur event
if (fixedDecimalScale && decimalScale) {
_value = roundToPrecision(_value, decimalScale, fixedDecimalScale);
if ((fixedDecimalScale && decimalScale) || minDecimalScale) {
_value = roundToPrecision(_value, decimalScale, minDecimalScale, fixedDecimalScale);
}

if (_value !== numAsString) {
Expand Down
1 change: 1 addition & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@ export type NumericFormatProps<BaseType = InputAttributes> = NumberFormatProps<
allowedDecimalSeparators?: Array<string>;
thousandsGroupStyle?: 'thousand' | 'lakh' | 'wan' | 'none';
decimalScale?: number;
minDecimalScale?: number;
fixedDecimalScale?: boolean;
allowNegative?: boolean;
allowLeadingZeros?: boolean;
Expand Down
32 changes: 26 additions & 6 deletions src/utils.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { useMemo, useRef, useState } from 'react';
import { NumberFormatBaseProps, FormatInputValueFunction, OnValueChange } from './types';
import { FormatInputValueFunction, NumberFormatBaseProps, OnValueChange } from './types';

// basic noop function
export function noop() {}
Expand Down Expand Up @@ -98,11 +98,20 @@ export function fixLeadingZero(numStr?: string) {
* limit decimal numbers to given scale
* Not used .fixedTo because that will break with big numbers
*/
export function limitToScale(numStr: string, scale: number, fixedDecimalScale: boolean) {
export function limitToScale(
numStr: string,
scale: number,
minDecimalScale: number,
fixedDecimalScale: boolean,
) {
let str = '';
const filler = fixedDecimalScale ? '0' : '';
for (let i = 0; i <= scale - 1; i++) {
str += numStr[i] || filler;
if (i < minDecimalScale) {
str += numStr[i] || '0';
} else {
str += numStr[i] || filler;
}
}
return str;
}
Expand Down Expand Up @@ -157,11 +166,17 @@ export function toNumericString(num: string | number) {
* This method is required to round prop value to given scale.
* Not used .round or .fixedTo because that will break with big numbers
*/
export function roundToPrecision(numStr: string, scale: number, fixedDecimalScale: boolean) {
export function roundToPrecision(
numStr: string,
scale: number,
minDecimalScale: number,
fixedDecimalScale: boolean,
) {
//if number is empty don't do anything return empty string
if (['', '-'].indexOf(numStr) !== -1) return numStr;

const shouldHaveDecimalSeparator = (numStr.indexOf('.') !== -1 || fixedDecimalScale) && scale;
const shouldHaveDecimalSeparator =
(numStr.indexOf('.') !== -1 || fixedDecimalScale || minDecimalScale) && scale;
const { beforeDecimal, afterDecimal, hasNegation } = splitDecimal(numStr);
const floatValue = parseFloat(`0.${afterDecimal || '0'}`);
const floatValueStr =
Expand All @@ -180,7 +195,12 @@ export function roundToPrecision(numStr: string, scale: number, fixedDecimalScal
return current + roundedStr;
}, roundedDecimalParts[0]);

const decimalPart = limitToScale(roundedDecimalParts[1] || '', scale, fixedDecimalScale);
const decimalPart = limitToScale(
roundedDecimalParts[1] || '',
scale,
minDecimalScale,
fixedDecimalScale,
);
const negation = hasNegation ? '-' : '';
const decimalSeparator = shouldHaveDecimalSeparator ? '.' : '';
return `${negation}${intPart}${decimalSeparator}${decimalPart}`;
Expand Down
16 changes: 16 additions & 0 deletions test/library/input_numeric_format.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,22 @@ describe('Test NumberFormat as input with numeric format options', () => {
expect(getInputValue(wrapper)).toEqual('4111.11');
});

it('should enforce a minimum decimal scale if specified', () => {
const wrapper = mount(<NumericFormat decimalScale={8} minDecimalScale={2} value={24} />);
expect(getInputValue(wrapper)).toEqual('24.00');

const input = wrapper.find('input');
input.simulate('change', getCustomEvent('24.1234'));
expect(getInputValue(wrapper)).toEqual('24.1234');

input.simulate('change', getCustomEvent('24.1234567890'));
expect(getInputValue(wrapper)).toEqual('24.12345678');

// Rounding logic
wrapper.setProps({ value: 24.123456789 });
expect(getInputValue(wrapper)).toEqual('24.12345679');
});

it('should not add zeros to fixedDecimalScale is not set', () => {
const wrapper = mount(<NumericFormat decimalScale={4} value={24.45} />);
expect(getInputValue(wrapper)).toEqual('24.45');
Expand Down