From 41c52ab8b32f48eb749ef17af6b6a55bdccbe688 Mon Sep 17 00:00:00 2001 From: Rafay Khan Date: Fri, 21 Apr 2023 15:30:36 -0400 Subject: [PATCH] Add support for minDecimalScale - In many currency-related use cases, a minimal decimal precision of 2 digits is common. This way, large numbers get displayed with two padded zeroes (i.e. $1,000,000.00) while small numbers support added precision of up-to n digits ($0.89765412 if n=8) - This functionality is not currently possible with this library due to fixedDecimalScale enforcing EXACTLY n digits of precision, while decimalScale provides a MAXIMUM number of digits - In a future release (breaking change) we can probably remove `fixedDecimalScale` and use only `minDecimalScale` and `decimalScale` to clamp the bounds of precision and satisfy every use case --- documentation/v5/docs/numeric_format.md | 25 +++++++++++ src/numeric_format.tsx | 52 ++++++++++++----------- src/types.ts | 1 + src/utils.tsx | 32 +++++++++++--- test/library/input_numeric_format.spec.js | 16 +++++++ 5 files changed, 96 insertions(+), 30 deletions(-) diff --git a/documentation/v5/docs/numeric_format.md b/documentation/v5/docs/numeric_format.md index abc573b9..999aa0ff 100644 --- a/documentation/v5/docs/numeric_format.md +++ b/documentation/v5/docs/numeric_format.md @@ -130,6 +130,31 @@ import { NumericFormat } from 'react-number-format'; > +### minDecimalScale `number` + +**default**: `undefined` + +If defined, it enforces a minimum number of digits after the decimal point. + +```js +import { NumericFormat } from 'react-number-format'; + +; +``` + +
+ + Demo + + + +
+ ### decimalSeparator `string` **default**: '.' diff --git a/src/numeric_format.tsx b/src/numeric_format.tsx index 07c5485e..8b08c9b3 100644 --- a/src/numeric_format.tsx +++ b/src/numeric_format.tsx @@ -1,30 +1,30 @@ 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( numStr: string, @@ -32,6 +32,7 @@ export function format( ) { const { decimalScale, + minDecimalScale, fixedDecimalScale, prefix = '', suffix = '', @@ -52,13 +53,15 @@ export function format( * 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) { @@ -333,6 +336,7 @@ export function useNumericFormat( onBlur = noop, thousandSeparator, decimalScale, + minDecimalScale, fixedDecimalScale, prefix = '', defaultValue, @@ -367,7 +371,7 @@ export function useNumericFormat( * 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; @@ -449,8 +453,8 @@ export function useNumericFormat( } // 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) { diff --git a/src/types.ts b/src/types.ts index 8abef042..4d8aee7f 100644 --- a/src/types.ts +++ b/src/types.ts @@ -96,6 +96,7 @@ export type NumericFormatProps = NumberFormatProps< allowedDecimalSeparators?: Array; thousandsGroupStyle?: 'thousand' | 'lakh' | 'wan' | 'none'; decimalScale?: number; + minDecimalScale?: number; fixedDecimalScale?: boolean; allowNegative?: boolean; allowLeadingZeros?: boolean; diff --git a/src/utils.tsx b/src/utils.tsx index e826a019..b01b18d8 100644 --- a/src/utils.tsx +++ b/src/utils.tsx @@ -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() {} @@ -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; } @@ -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 = @@ -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}`; diff --git a/test/library/input_numeric_format.spec.js b/test/library/input_numeric_format.spec.js index 7d565178..985755e1 100644 --- a/test/library/input_numeric_format.spec.js +++ b/test/library/input_numeric_format.spec.js @@ -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(); + 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(); expect(getInputValue(wrapper)).toEqual('24.45');