diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index fd20354..1426eec 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -1,117 +1,8 @@ -name: CI +name: ✅ test -on: - push: - branches: [master] - pull_request: - branches: [master] +on: [push, pull_request] jobs: - setup: - runs-on: ubuntu-latest - steps: - - name: checkout - uses: actions/checkout@master - - - uses: actions/setup-node@v1 - with: - node-version: '12' - - - name: cache package-lock.json - uses: actions/cache@v2 - with: - path: package-temp-dir - key: lock-${{ github.sha }} - - - name: create package-lock.json - run: npm i --package-lock-only - - - name: hack for singe file - run: | - if [ ! -d "package-temp-dir" ]; then - mkdir package-temp-dir - fi - cp package-lock.json package-temp-dir - - - name: cache node_modules - id: node_modules_cache_id - uses: actions/cache@v2 - with: - path: node_modules - key: node_modules-${{ hashFiles('**/package-temp-dir/package-lock.json') }} - - - name: install - if: steps.node_modules_cache_id.outputs.cache-hit != 'true' - run: npm ci - - lint: - runs-on: ubuntu-latest - steps: - - name: checkout - uses: actions/checkout@master - - - name: restore cache from package-lock.json - uses: actions/cache@v2 - with: - path: package-temp-dir - key: lock-${{ github.sha }} - - - name: restore cache from node_modules - uses: actions/cache@v2 - with: - path: node_modules - key: node_modules-${{ hashFiles('**/package-temp-dir/package-lock.json') }} - - - name: lint - run: npm run lint - - - name: ts check - run: npm run lint:tsc - - needs: setup - - compile: - runs-on: ubuntu-latest - steps: - - name: checkout - uses: actions/checkout@master - - - name: restore cache from package-lock.json - uses: actions/cache@v2 - with: - path: package-temp-dir - key: lock-${{ github.sha }} - - - name: restore cache from node_modules - uses: actions/cache@v2 - with: - path: node_modules - key: node_modules-${{ hashFiles('**/package-temp-dir/package-lock.json') }} - - - name: compile - run: npm run compile - - needs: setup - - coverage: - runs-on: ubuntu-latest - steps: - - name: checkout - uses: actions/checkout@master - - - name: restore cache from package-lock.json - uses: actions/cache@v2 - with: - path: package-temp-dir - key: lock-${{ github.sha }} - - - name: restore cache from node_modules - uses: actions/cache@v2 - with: - path: node_modules - key: node_modules-${{ hashFiles('**/package-temp-dir/package-lock.json') }} - - - name: coverage - run: npm test -- --coverage && bash <(curl -s https://codecov.io/bash) - - needs: setup + test: + uses: react-component/rc-test/.github/workflows/test.yml@main + secrets: inherit diff --git a/.gitignore b/.gitignore index 8a331ed..9525e09 100644 --- a/.gitignore +++ b/.gitignore @@ -28,9 +28,11 @@ yarn.lock /es/ package-lock.json .doc +.dumi +dist # umi .umi .umi-production .umi-test -.env.local \ No newline at end of file +.env.local diff --git a/.umirc.ts b/.umirc.ts index e979399..d57fedc 100644 --- a/.umirc.ts +++ b/.umirc.ts @@ -2,11 +2,9 @@ import { defineConfig } from 'dumi'; export default defineConfig({ - title: 'rc-mini-decimal', - favicon: 'https://avatars0.githubusercontent.com/u/9441414?s=200&v=4', - logo: 'https://avatars0.githubusercontent.com/u/9441414?s=200&v=4', - outputPath: '.doc', - exportStatic: {}, + themeConfig: { + name: 'mini-decimal', + }, styles: [ ` .markdown table { diff --git a/package.json b/package.json index f40a602..0d96416 100644 --- a/package.json +++ b/package.json @@ -27,38 +27,39 @@ "compile": "father build", "deploy": "npm run docs:build && npm run docs:deploy", "docs:build": "dumi build", - "docs:deploy": "gh-pages -d docs-dist", + "docs:deploy": "gh-pages -d dist", + "coverage": "rc-test --coverage", "lint": "eslint src/ --ext .tsx,.ts", "lint:tsc": "tsc -p tsconfig.json --noEmit", "now-build": "npm run docs:build", - "prepublishOnly": "npm run compile && np --no-cleanup --yolo --no-publish", + "prepublishOnly": "npm run compile && rc-np", "prettier": "prettier --write \"**/*.{js,jsx,tsx,ts,less,md,json}\"", "start": "dumi dev", - "test": "umi-test", - "test:coverage": "npm run test --coverage", + "test": "rc-test", "watch": "father dev" }, "dependencies": { "@babel/runtime": "^7.18.0" }, "devDependencies": { - "@rc-component/father-plugin": "^1.0.0", - "@testing-library/jest-dom": "^5.16.4", - "@testing-library/react": "^13.0.0", - "@types/jest": "^26.0.20", - "@types/react": "^18.0.0", - "@types/react-dom": "^18.0.0", - "@umijs/fabric": "^2.5.2", - "dumi": "^1.1.0", - "eslint": "^7.18.0", - "father": "^4.0.0-rc.8", - "gh-pages": "^3.1.0", - "np": "^5.0.3", - "prettier": "^2.1.2", + "@rc-component/father-plugin": "^2.0.4", + "@rc-component/np": "^1.0.4", + "@testing-library/jest-dom": "^6.1.5", + "@testing-library/react": "^16.1.0", + "@types/jest": "^29.5.12", + "@types/node": "^22.1.0", + "@types/react": "^18.3.12", + "@types/react-dom": "^18.3.1", + "@umijs/fabric": "^4.0.1", + "dumi": "^2.3.8", + "eslint": "^8.57.0", + "father": "^4.4.4", + "gh-pages": "^6.1.1", + "prettier": "^3.3.3", "react": "^18.0.0", "react-dom": "^18.0.0", - "typescript": "^4.6.3", - "umi-test": "^1.9.7" + "rc-test": "^7.0.15", + "typescript": "^5.4.5" }, "engines": { "node": ">=8.x" diff --git a/src/NumberDecimal.ts b/src/NumberDecimal.ts index 03e844e..85d122b 100644 --- a/src/NumberDecimal.ts +++ b/src/NumberDecimal.ts @@ -1,5 +1,5 @@ import type { DecimalClass, ValueType } from './interface'; -import { getNumberPrecision, isEmpty, num2str } from './numberUtil'; +import { getNumberPrecision, isE, isEmpty, num2str } from './numberUtil'; /** * We can remove this when IE not support anymore @@ -110,6 +110,10 @@ export default class NumberDecimal implements DecimalClass { return ''; } + if (isE(this.number) && getNumberPrecision(this.number) > 100) { + return String(this.number); + } + return num2str(this.number); } } diff --git a/src/numberUtil.ts b/src/numberUtil.ts index 72e60d0..64ab1b4 100644 --- a/src/numberUtil.ts +++ b/src/numberUtil.ts @@ -59,6 +59,77 @@ export function isE(number: string | number) { return !Number.isNaN(Number(str)) && str.includes('e'); } +type ParsedScientificNotation = { + decimal: string; + digits: string; + exponent: number; + integer: string; + negative: boolean; +}; + +/** + * Parse a scientific-notation string into reusable parts. + * + * The idea is to split the value into mantissa and exponent first, then + * normalize the mantissa into sign, integer/decimal segments, and a compact + * digit sequence so later logic can move the decimal point without re-parsing. + */ +function parseScientificNotation(numStr: string): ParsedScientificNotation { + const [mantissa, exponent = '0'] = numStr.toLowerCase().split('e'); + const negative = mantissa.startsWith('-'); + const unsignedMantissa = negative ? mantissa.slice(1) : mantissa; + const [integer = '0', decimal = ''] = unsignedMantissa.split('.'); + const digits = `${integer}${decimal}`.replace(/^0+/, '') || '0'; + + return { + decimal, + digits, + exponent: Number(exponent), + integer, + negative, + }; +} + +/** + * Expand parsed scientific notation into a plain decimal string. + * + * The core idea is to calculate where the decimal point lands after applying + * the exponent, then rebuild the string by either padding zeros or inserting + * the decimal point inside the normalized digit sequence. + */ +function expandScientificNotation(parsed: ParsedScientificNotation) { + const { decimal, digits, exponent, integer, negative } = parsed; + + if (digits === '0') { + return '0'; + } + + const integerDigits = integer.replace(/^0+/, '').length; + const leadingDecimalZeros = (decimal.match(/^0*/) || [''])[0].length; + const initialDecimalIndex = integerDigits || -leadingDecimalZeros; + const decimalIndex = initialDecimalIndex + exponent; + + let expanded = ''; + + if (decimalIndex <= 0) { + expanded = `0.${'0'.repeat(-decimalIndex)}${digits}`; + } else if (decimalIndex >= digits.length) { + expanded = `${digits}${'0'.repeat(decimalIndex - digits.length)}`; + } else { + expanded = `${digits.slice(0, decimalIndex)}.${digits.slice(decimalIndex)}`; + } + + return `${negative ? '-' : ''}${expanded}`; +} + +function getScientificPrecision(parsed: ParsedScientificNotation) { + if (parsed.exponent >= 0) { + return Math.max(0, parsed.decimal.length - parsed.exponent); + } + + return Math.abs(parsed.exponent) + parsed.decimal.length; +} + /** * [Legacy] Convert 1e-9 to 0.000000001. * This may lose some precision if user really want 1e-9. @@ -67,13 +138,7 @@ export function getNumberPrecision(number: string | number) { const numStr: string = String(number); if (isE(number)) { - let precision = Number(numStr.slice(numStr.indexOf('e-') + 2)); - - const decimalMatch = numStr.match(/\.(\d+)/); - if (decimalMatch?.[1]) { - precision += decimalMatch[1].length; - } - return precision; + return getScientificPrecision(parseScientificNotation(numStr)); } return numStr.includes('.') && validateNumber(numStr) @@ -99,7 +164,11 @@ export function num2str(number: number): string { ); } - numStr = number.toFixed(getNumberPrecision(numStr)); + const parsed = parseScientificNotation(numStr); + const precision = getScientificPrecision(parsed); + + numStr = + precision > 100 ? expandScientificNotation(parsed) : number.toFixed(precision); } return trimNumber(numStr).fullStr; diff --git a/tests/util.test.tsx b/tests/util.test.tsx index e5a6ec3..e07ef9c 100644 --- a/tests/util.test.tsx +++ b/tests/util.test.tsx @@ -4,6 +4,7 @@ import getMiniDecimal, { toFixed, } from '../src/MiniDecimal'; import type { DecimalClass, ValueType } from '../src/MiniDecimal'; +import { num2str } from '../src/numberUtil'; jest.mock('../src/supportUtil'); const { supportBigInt } = require('../src/supportUtil'); @@ -38,6 +39,8 @@ describe('InputNumber.Util', () => { classList.forEach(({ name, getDecimal, mockSupportBigInt }) => { describe(name, () => { + const tinyScientificValue = `0.${'0'.repeat(306)}1`; + beforeEach(() => { supportBigInt.mockImplementation(() => { return mockSupportBigInt !== false; @@ -61,6 +64,12 @@ describe('InputNumber.Util', () => { expect(getDecimal('1.').toString()).toEqual('1'); }); + it('parses very small scientific notation', () => { + expect(getDecimal('1e-307').toString()).toEqual( + mockSupportBigInt === false ? '1e-307' : tinyScientificValue, + ); + }); + it('invalidate', () => { expect(getDecimal('abc').toString()).toEqual(''); }); @@ -135,6 +144,17 @@ describe('InputNumber.Util', () => { }); }); + describe('scientific notation', () => { + it('keeps num2str behavior correct', () => { + expect(num2str(0.123e-1)).toEqual('0.0123'); + expect(num2str(-0.123e-1)).toEqual('-0.0123'); + expect(num2str(0.00123e2)).toEqual('0.123'); + expect(num2str(0e5)).toEqual('0'); + expect(num2str(1.23e-19)).toEqual(`0.${'0'.repeat(18)}123`); + expect(num2str(-1.23e-20)).toEqual(`-0.${'0'.repeat(19)}123`); + }); + }); + describe('toFixed', () => { it('less than precision', () => { expect(toFixed('1.1', ',', 2)).toEqual('1,10'); diff --git a/tsconfig.json b/tsconfig.json index c28fb52..5fbc200 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -7,10 +7,17 @@ "declaration": true, "skipLibCheck": true, "esModuleInterop": true, + "types": ["@testing-library/jest-dom", "node", "jest"], "paths": { "@/*": ["src/*"], "@@/*": ["src/.umi/*"], "@rc-component/portal": ["src/Portal.tsx"] } - } + }, + "include": [ + "./src/**/*.ts", + "./src/**/*.tsx", + "./docs/**/*.tsx", + "./tests/**/*.tsx" + ] }