From d3de1472f23b4978d74a221e4b11db5408900ef9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=8E=8B=E6=B5=A9?= Date: Mon, 6 Dec 2021 11:00:27 +0800 Subject: [PATCH] fix: should format max/min with precision (#385) Co-authored-by: wanghao --- src/InputNumber.tsx | 41 ++++++++++++++++++++++++++++++++++------ src/utils/MiniDecimal.ts | 25 ++++++++++++++++++++++++ tests/input.test.tsx | 21 ++++++++++++++++++++ tests/util.test.tsx | 20 +++++++++++++++++++- 4 files changed, 100 insertions(+), 7 deletions(-) diff --git a/src/InputNumber.tsx b/src/InputNumber.tsx index e9914912..541be08f 100644 --- a/src/InputNumber.tsx +++ b/src/InputNumber.tsx @@ -2,15 +2,29 @@ import * as React from 'react'; import classNames from 'classnames'; import KeyCode from 'rc-util/lib/KeyCode'; import { composeRef } from 'rc-util/lib/ref'; -import getMiniDecimal, { DecimalClass, toFixed, ValueType } from './utils/MiniDecimal'; +import getMiniDecimal, { + DecimalClass, + roundDownUnsignedDecimal, + roundUpUnsignedDecimal, + toFixed, + ValueType +} from './utils/MiniDecimal'; import StepHandler from './StepHandler'; -import { getNumberPrecision, num2str, validateNumber } from './utils/numberUtil'; +import { getNumberPrecision, num2str, trimNumber, validateNumber } from './utils/numberUtil'; import useCursor from './hooks/useCursor'; import useUpdateEffect from './hooks/useUpdateEffect'; import useFrame from './hooks/useFrame'; /** * We support `stringMode` which need handle correct type when user call in onChange + * format max or min value + * 1. if isInvalid return null + * 2. if precision is undefined, return decimal + * 3. format with precision + * I. if max > 0, round down with precision. Example: max= 3.5, precision=0 afterFormat: 3 + * II. if max < 0, round up with precision. Example: max= -3.5, precision=0 afterFormat: -4 + * III. if min > 0, round up with precision. Example: min= 3.5, precision=0 afterFormat: 4 + * IV. if min < 0, round down with precision. Example: max= -3.5, precision=0 afterFormat: -3 */ const getDecimalValue = (stringMode: boolean, decimalValue: DecimalClass) => { if (stringMode || decimalValue.isEmpty()) { @@ -20,9 +34,24 @@ const getDecimalValue = (stringMode: boolean, decimalValue: DecimalClass) => { return decimalValue.toNumber(); }; -const getDecimalIfValidate = (value: ValueType) => { +const getDecimalIfValidate = (value: ValueType, precision: number | undefined, isMax?: boolean) => { const decimal = getMiniDecimal(value); - return decimal.isInvalidate() ? null : decimal; + if (decimal.isInvalidate()) { + return null; + } + + if (precision === undefined) { + return decimal; + } + + const {negative, integerStr, decimalStr, negativeStr} = trimNumber(decimal.toString()); + const unSignedNumberStr = integerStr +'.' + decimalStr; + + if ((isMax && !negative) || (!isMax && negative)) { + return getMiniDecimal(negativeStr + roundDownUnsignedDecimal(unSignedNumberStr, precision)); + } else { + return getMiniDecimal(negativeStr + roundUpUnsignedDecimal(unSignedNumberStr, precision)); + } }; export interface InputNumberProps @@ -232,8 +261,8 @@ const InputNumber = React.forwardRef( } // >>> Max & Min limit - const maxDecimal = React.useMemo(() => getDecimalIfValidate(max), [max]); - const minDecimal = React.useMemo(() => getDecimalIfValidate(min), [min]); + const maxDecimal = React.useMemo(() => getDecimalIfValidate(max, precision, true), [max, precision]); + const minDecimal = React.useMemo(() => getDecimalIfValidate(min, precision, false), [min, precision]); const upDisabled = React.useMemo(() => { if (!maxDecimal || !decimalValue || decimalValue.isInvalidate()) { diff --git a/src/utils/MiniDecimal.ts b/src/utils/MiniDecimal.ts index 78c1c43f..6088b861 100644 --- a/src/utils/MiniDecimal.ts +++ b/src/utils/MiniDecimal.ts @@ -264,6 +264,31 @@ export default function getMiniDecimal(value: ValueType): DecimalClass { return new NumberDecimal(value); } +/** + * round up an unsigned number str, like: 1.4 -> 2, 1.5 -> 2 + */ +export function roundUpUnsignedDecimal(numStr: string, precision: number) { + const {integerStr, decimalStr} = trimNumber(numStr); + const advancedDecimal = getMiniDecimal(integerStr + '.' + decimalStr).add( + `0.${'0'.repeat(precision)}${5}`, + ); + return toFixed(advancedDecimal.toString(), '.', precision); +} + +/** + * round up an unsigned number str, like: 1.4 -> 1, 1.5 -> 1 + */ +export function roundDownUnsignedDecimal(numStr: string, precision: number) { + const {negativeStr, integerStr, decimalStr} = trimNumber(numStr); + const numberWithoutDecimal = `${negativeStr}${integerStr}`; + if (precision === 0) { + return integerStr; + } + return `${numberWithoutDecimal}.${decimalStr + .padEnd(precision, '0') + .slice(0, precision)}`; +} + /** * Align the logic of toFixed to around like 1.5 => 2 */ diff --git a/tests/input.test.tsx b/tests/input.test.tsx index a52e735e..be4b98b2 100644 --- a/tests/input.test.tsx +++ b/tests/input.test.tsx @@ -59,6 +59,27 @@ describe('InputNumber.Input', () => { expect(wrapper.getInputValue()).toEqual('-98'); }); + it('negative min with higher precision', () => { + const wrapper = prepareWrapper('-4', {min: -3.5, precision: 0}); + expect(wrapper.getInputValue()).toEqual('-3'); + }); + + it('positive min with higher precision', () => { + const wrapper = prepareWrapper('4', {min: 3.5, precision: 0}); + expect(wrapper.getInputValue()).toEqual('4'); + }); + + it('negative max with higher precision', () => { + const wrapper = prepareWrapper('-4', {max: -3.5, precision: 0}); + expect(wrapper.getInputValue()).toEqual('-4'); + }); + + it('positive max with higher precision', () => { + const wrapper = prepareWrapper('4', {max: 3.5, precision: 0}); + expect(wrapper.getInputValue()).toEqual('3'); + }); + + // https://github.com/ant-design/ant-design/issues/9439 it('input negative zero', () => { const wrapper = prepareWrapper('-0', {}, true); diff --git a/tests/util.test.tsx b/tests/util.test.tsx index b1de5059..a6702608 100644 --- a/tests/util.test.tsx +++ b/tests/util.test.tsx @@ -2,8 +2,10 @@ import getMiniDecimal, { BigIntDecimal, DecimalClass, NumberDecimal, - ValueType, + roundDownUnsignedDecimal, + roundUpUnsignedDecimal, toFixed, + ValueType, } from '../src/utils/MiniDecimal'; jest.mock('../src/utils/supportUtil'); @@ -150,5 +152,21 @@ describe('InputNumber.Util', () => { // expect(toFixed('77.88', '.', 1)).toEqual('77.9'); expect(toFixed('-77.88', '.', 1)).toEqual('-77.9'); }); + + it('round down', () => { + expect(roundDownUnsignedDecimal('77.89', 1)).toEqual('77.8'); + expect(roundDownUnsignedDecimal('77.1', 2)).toEqual('77.10'); + expect(roundDownUnsignedDecimal('77.81', 1)).toEqual('77.8'); + expect(roundDownUnsignedDecimal('77.50', 1)).toEqual('77.5'); + expect(roundDownUnsignedDecimal('77.5999', 0)).toEqual('77'); + expect(roundDownUnsignedDecimal('77.0001', 0)).toEqual('77'); + }) + it('round up', () => { + expect(roundUpUnsignedDecimal('77.89', 1)).toEqual('77.9'); + expect(roundUpUnsignedDecimal('77.81', 1)).toEqual('77.9'); + expect(roundUpUnsignedDecimal('77.89', 0)).toEqual('78'); + expect(roundUpUnsignedDecimal('77.599', 0)).toEqual('78'); + expect(roundUpUnsignedDecimal('77.01', 0)).toEqual('78'); + }) }); });