From 9040e5f997d5a0d680d0634a56ee25facdcf7284 Mon Sep 17 00:00:00 2001 From: Dushusir <1414556676@qq.com> Date: Mon, 11 Nov 2024 16:49:12 +0800 Subject: [PATCH] fix(sheet): covert number when edit number with point (#3948) --- .../__tests__/end-edit.controller.spec.ts | 147 +++++++++++++++++- .../src/controllers/utils/char-tools.ts | 50 ++++-- 2 files changed, 184 insertions(+), 13 deletions(-) diff --git a/packages/sheets-ui/src/controllers/editor/__tests__/end-edit.controller.spec.ts b/packages/sheets-ui/src/controllers/editor/__tests__/end-edit.controller.spec.ts index 1880e2d6ec..f2b9a280f2 100644 --- a/packages/sheets-ui/src/controllers/editor/__tests__/end-edit.controller.spec.ts +++ b/packages/sheets-ui/src/controllers/editor/__tests__/end-edit.controller.spec.ts @@ -16,7 +16,7 @@ import type { ICellData, IDocumentData, Injector, Univer, Workbook } from '@univerjs/core'; import type { IFunctionService } from '@univerjs/engine-formula'; -import { CellValueType, IConfigService, IContextService, IResourceLoaderService, LocaleService, LocaleType, Tools } from '@univerjs/core'; +import { CellValueType, IConfigService, IContextService, LocaleService, LocaleType, Tools } from '@univerjs/core'; import { LexerTreeBuilder } from '@univerjs/engine-formula'; import { SpreadsheetSkeleton } from '@univerjs/engine-render'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; @@ -91,7 +91,6 @@ describe('Test EndEditController', () => { let contextService: IContextService; let lexerTreeBuilder: LexerTreeBuilder; let spreadsheetSkeleton: SpreadsheetSkeleton; - let resourceLoaderService: IResourceLoaderService; let configService: IConfigService; let getCellDataByInputCell: (cell: ICellData, inputCell: ICellData) => ICellData | null; let normalizeStringByLexer: (str: string) => string; @@ -108,7 +107,6 @@ describe('Test EndEditController', () => { localeService = get(LocaleService); contextService = get(IContextService); lexerTreeBuilder = new LexerTreeBuilder(); - resourceLoaderService = get(IResourceLoaderService); configService = get(IConfigService); const worksheet = workbook.getActiveSheet()!; @@ -460,5 +458,148 @@ describe('Test EndEditController', () => { const isRichTextWithCellBody2 = isRichText(cellBody2); expect(isRichTextWithCellBody2).toBe(false); }); + + describe('normalizeStringByLexer', () => { + it('should convert leading . to 0.', () => { + expect(normalizeStringByLexer('=.07/0.1')).toBe('=0.07/0.1'); + }); + + it('should remove unnecessary trailing zeros', () => { + expect(normalizeStringByLexer('=1.0+2.00')).toBe('=1+2'); + }); + + it('should not modify normal cell references', () => { + expect(normalizeStringByLexer('=A1+B2')).toBe('=A1+B2'); + expect(normalizeStringByLexer('=Sheet1!A1')).toBe('=Sheet1!A1'); + }); + + it('should not modify function names', () => { + expect(normalizeStringByLexer('=SUM(A1:A10)')).toBe('=SUM(A1:A10)'); + expect(normalizeStringByLexer('=IF(A1>0, "Yes", "No")')).toBe('=IF(A1>0, "Yes", "No")'); + }); + + it('should handle negative numbers', () => { + expect(normalizeStringByLexer('=-.5*-2.00')).toBe('=-0.5*-2'); + }); + + it('should handle numbers with leading zeros', () => { + expect(normalizeStringByLexer('=001.00+000.20')).toBe('=1+0.2'); + }); + + it('should handle percentages', () => { + expect(normalizeStringByLexer('=50% + .5')).toBe('=50% + 0.5'); + }); + + it('should handle scientific notation', () => { + expect(normalizeStringByLexer('=1.00E+03')).toBe('=1000'); + expect(normalizeStringByLexer('=1e3 + 2E-2')).toBe('=1000 + 0.02'); + + expect(normalizeStringByLexer('=1e20 + 2E-2')).toBe('=100000000000000000000 + 0.02'); + // Excel =1.1E+21 + 0.02, numfmt =1.1e+21 + 0.02 + expect(normalizeStringByLexer('=11e20 + 2E-2')).toBe('=1.1e+21 + 0.02'); + // Excel =1E+21 + 0.02, numfmt =1e+21 + 0.02 + expect(normalizeStringByLexer('=1e21 + 2E-2')).toBe('=1e+21 + 0.02'); + + expect(normalizeStringByLexer('=2e-6')).toBe('=0.000002'); + // Excel =0.0000002, numfmt =2e-7 + expect(normalizeStringByLexer('=2e-7')).toBe('=2e-7'); + // Excel =1E+21 + 0.0000000000000000002, numfmt =1e+21 + 2e-19 + expect(normalizeStringByLexer('=1E+21 + 2E-19')).toBe('=1e+21 + 2e-19'); + // Excel =1E+21 + 2E-20, numfmt =1e+21 + 2e-20 + expect(normalizeStringByLexer('=1E+21 + 0.2E-19')).toBe('=1e+21 + 2e-20'); + // Excel =1E+21 + 2E-20, numfmt =1e+21 + 2e-20 + expect(normalizeStringByLexer('=1E+21 + 2E-20')).toBe('=1e+21 + 2e-20'); + }); + + it('should handle dates', () => { + expect(normalizeStringByLexer('="2023-01-01"')).toBe('="2023-01-01"'); + }); + + it('should handle complex expressions', () => { + expect(normalizeStringByLexer('=SUM(A1:A10)/.5 - 2.00')).toBe('=SUM(A1:A10)/0.5 - 2'); + }); + + it('should not modify logical operators', () => { + expect(normalizeStringByLexer('=A1>=B2')).toBe('=A1>=B2'); + }); + + it('should handle names with numbers', () => { + expect(normalizeStringByLexer('=Rate1 + Rate2')).toBe('=Rate1 + Rate2'); + }); + + it('should handle underscores in names', () => { + expect(normalizeStringByLexer('=_myVar + another_var')).toBe('=_myVar + another_var'); + }); + + it('should handle numbers adjacent to letters', () => { + expect(normalizeStringByLexer('=A1*B2')).toBe('=A1*B2'); + expect(normalizeStringByLexer('=C3/2.0')).toBe('=C3/2'); + }); + + it('should handle decimal numbers without leading zero', () => { + expect(normalizeStringByLexer('=.123')).toBe('=0.123'); + }); + + it('should handle multiple numbers in expression', () => { + expect(normalizeStringByLexer('=.1 + .2 + .3')).toBe('=0.1 + 0.2 + 0.3'); + }); + + it('should not modify cell ranges', () => { + expect(normalizeStringByLexer('=SUM(A1:B2)')).toBe('=SUM(A1:B2)'); + }); + + it('should handle complex numbers', () => { + expect(normalizeStringByLexer('=COMPLEX(0, .5)')).toBe('=COMPLEX(0, 0.5)'); + }); + + it('should handle percentage calculations', () => { + expect(normalizeStringByLexer('=.5*50%')).toBe('=0.5*50%'); + }); + + it('should handle brackets', () => { + expect(normalizeStringByLexer('=(.5 + .25)/(.1)')).toBe('=(0.5 + 0.25)/(0.1)'); + }); + + it('should not modify text outside of formulas', () => { + expect(normalizeStringByLexer('Just some text.')).toBe('Just some text.'); + }); + + it('should handle formulas starting with + or -', () => { + expect(normalizeStringByLexer('=+1.0+-2.00')).toBe('=+1+-2'); // Follows Google Sheets' behavior + }); + + it('should handle numbers in logical expressions', () => { + expect(normalizeStringByLexer('=IF(A1>.5, TRUE, FALSE)')).toBe('=IF(A1>0.5, TRUE, FALSE)'); + expect(normalizeStringByLexer('=IF("A1>.5", TRUE, FALSE)')).toBe('=IF("A1>.5", TRUE, FALSE)'); + }); + + it('should handle exponents', () => { + expect(normalizeStringByLexer('=2^0.0')).toBe('=2^0'); + }); + + it('should not modify quoted numbers', () => { + expect(normalizeStringByLexer('="The value is .5"')).toBe('="The value is .5"'); + }); + + it('should handle numbers with only zeros', () => { + expect(normalizeStringByLexer('=0.0 + 0.00')).toBe('=0 + 0'); + }); + + it('should handle zero with decimal point', () => { + expect(normalizeStringByLexer('=0. + .0')).toBe('=0 + 0'); + }); + + it('should handle negative numbers with leading zeros', () => { + expect(normalizeStringByLexer('=-0002.00')).toBe('=-2'); + }); + + it('should handle positive numbers with leading zeros', () => { + expect(normalizeStringByLexer('=+0002.50')).toBe('=+2.5'); // Follows Google Sheets' behavior + }); + + it('should handle escaped quotes in strings', () => { + expect(normalizeStringByLexer('="He said, ""Hello, .5!"""')).toBe('="He said, ""Hello, .5!"""'); + }); + }); }); }); diff --git a/packages/sheets-ui/src/controllers/utils/char-tools.ts b/packages/sheets-ui/src/controllers/utils/char-tools.ts index dfcbcf5c2d..d932f80abe 100644 --- a/packages/sheets-ui/src/controllers/utils/char-tools.ts +++ b/packages/sheets-ui/src/controllers/utils/char-tools.ts @@ -104,6 +104,7 @@ function normalizeFormulaString(str: string, normalStr: string, lexerTreeBuilder if (!nodes) return str; let _normalStr = normalStr; + let totalOffset = 0; // Cumulative offset nodes.forEach((node, index) => { if (typeof node === 'object') { @@ -111,34 +112,49 @@ function normalizeFormulaString(str: string, normalStr: string, lexerTreeBuilder // boolean or function name if (booleanMap[token.toLowerCase()]) { - const startIndex = node.startIndex + 1; - const endIndex = node.endIndex + 2; + const startIndex = node.startIndex + totalOffset + 1; + const endIndex = node.endIndex + totalOffset + 2; + _normalStr = replaceString(token.toLocaleUpperCase(), _normalStr, startIndex, endIndex); } else if ((node.nodeType === sequenceNodeType.FUNCTION && hasFunctionName(token, functionService, nodes, index)) || node.nodeType === sequenceNodeType.REFERENCE) { const sheetNameIndex = token.indexOf('!'); if (sheetNameIndex > -1) { const refBody = token.substring(sheetNameIndex + 1); - const startIndex = node.startIndex + (sheetNameIndex + 1) + 1; - const endIndex = node.endIndex + 2; + const startIndex = node.startIndex + totalOffset + (sheetNameIndex + 1) + 1; + const endIndex = node.endIndex + totalOffset + 2; + _normalStr = replaceString(refBody.toLocaleUpperCase(), _normalStr, startIndex, endIndex); } else { - const startIndex = node.startIndex + 1; - const endIndex = node.endIndex + 2; + const startIndex = node.startIndex + totalOffset + 1; + const endIndex = node.endIndex + totalOffset + 2; + _normalStr = replaceString(token.toLocaleUpperCase(), _normalStr, startIndex, endIndex); } } else if ((token.startsWith('"') && token.endsWith('"')) || (token.startsWith("'") && token.endsWith("'"))) { - const startIndex = node.startIndex + 2; - const endIndex = node.endIndex + 1; + const startIndex = node.startIndex + totalOffset + 2; + const endIndex = node.endIndex + totalOffset + 1; _normalStr = replaceString(str.slice(startIndex, endIndex), _normalStr, startIndex, endIndex); } else if (node.nodeType !== sequenceNodeType.ARRAY) { const parsedValue = numfmt.parseValue(token); if (parsedValue == null) { - const startIndex = node.startIndex + 1; - const endIndex = node.endIndex + 2; + const startIndex = node.startIndex + totalOffset + 1; + const endIndex = node.endIndex + totalOffset + 2; _normalStr = replaceString(str.slice(startIndex, endIndex), _normalStr, startIndex, endIndex); } + // Entering =.07/0.1 in a cell will automatically convert to =0.07/0.1 + // Entering =1.0+2.00 in a cell will automatically convert to =1+2 + else if (typeof parsedValue.v === 'number') { + const v = `${parsedValue.v}`; + const startIndex = node.startIndex + totalOffset + 1; + const endIndex = node.endIndex + totalOffset + 2; + const { processedString, offset } = processNumberStringWithSpaces(token, v); + _normalStr = replaceString(processedString, _normalStr, startIndex, endIndex); + + // Update cumulative offset + totalOffset += offset; + } } } }); @@ -169,3 +185,17 @@ function toHalfWidth(char: string): string { function replaceString(replacedString: string, normalStr: string, startIndex: number, endIndex: number): string { return normalStr.substring(0, startIndex) + replacedString + normalStr.substring(endIndex); } + +function processNumberStringWithSpaces(token: string, numStr: string): { processedString: string; offset: number } { + // Preserve original leading and trailing spaces + const leadingSpaces = token.match(/^\s*/)?.[0] || ''; + const trailingSpaces = token.match(/\s*$/)?.[0] || ''; + + // Reassemble, keeping the original spaces + const processedString = leadingSpaces + numStr + trailingSpaces; + + // Calculating the offset + const offset = processedString.length - token.length; + + return { processedString, offset }; +}