Skip to content
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

fix(sheet): sheet name quote #4198

Merged
merged 15 commits into from
Jan 7, 2025
Merged
134 changes: 134 additions & 0 deletions e2e/visual-comparison/sheets/sheets-visual-comparison.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -156,3 +156,137 @@ test('diff facade sheet hooks', async () => {
const screenshot = await page.locator(SHEET_MAIN_CANVAS_ID).screenshot();
await expect(screenshot).toMatchSnapshot(filename, { maxDiffPixels: 5 });
});

test('diff set force string cell', async () => {
const browser = await chromium.launch({
headless: !!isCI, // Set to false to see the browser window
});
const context = await browser.newContext({
viewport: { width: 1280, height: 1280 },
deviceScaleFactor: 2, // Set your desired DPR
});
const page = await context.newPage();
await page.goto('http://localhost:3000/sheets/');
await page.waitForTimeout(2000);

await page.evaluate(() => window.E2EControllerAPI.loadDefaultStyleSheet());
await page.waitForTimeout(2000);

await page.evaluate(async () => {
const activeWorkbook = window.univerAPI.getActiveWorkbook();
const activeSheet = activeWorkbook.getActiveSheet();

const sheetId = activeSheet.getSheetId();
const unitId = activeWorkbook.getId();

await window.univerAPI.executeCommand('sheet.operation.set-selections', {
selections: [
{
range: {
startRow: 0,
startColumn: 7,
endRow: 0,
endColumn: 7,
rangeType: 0,
unitId,
sheetId,
},
primary: {
actualRow: 0,
actualColumn: 7,
isMerged: false,
isMergedMainCell: false,
startRow: 0,
startColumn: 7,
endRow: 0,
endColumn: 7,
},
style: {
strokeWidth: 1,
stroke: '#274fee',
fill: 'rgba(39,79,238,0.07)',
widgets: {},
widgetSize: 6,
widgetStrokeWidth: 1,
widgetStroke: '#ffffff',
autofillSize: 6,
autofillStrokeWidth: 1,
autofillStroke: '#ffffff',
rowHeaderFill: 'rgba(39,79,238,0.07)',
rowHeaderStroke: '#274fee',
rowHeaderStrokeWidth: 1,
columnHeaderFill: 'rgba(39,79,238,0.07)',
columnHeaderStroke: '#274fee',
columnHeaderStrokeWidth: 1,
expandCornerSize: 40,
},
},
],
unitId,
subUnitId: sheetId,
type: 2,
});

activeWorkbook.startEditing();
await window.univerAPI.getActiveDocument().appendText("'1");
activeWorkbook.endEditing(true);
});

const filename = generateSnapshotName('set-force-string-cell');
const screenshot = await page.locator(SHEET_MAIN_CANVAS_ID).screenshot();
await expect(screenshot).toMatchSnapshot(filename, { maxDiffPixels: 5 });

await page.waitForTimeout(2000);
await browser.close();
});

test('diff set text format number cell', async () => {
const browser = await chromium.launch({
headless: !!isCI, // Set to false to see the browser window
});
const context = await browser.newContext({
viewport: { width: 1280, height: 1280 },
deviceScaleFactor: 2, // Set your desired DPR
});
const page = await context.newPage();
await page.goto('http://localhost:3000/sheets/');
await page.waitForTimeout(2000);

await page.evaluate(() => window.E2EControllerAPI.loadDefaultStyleSheet());
await page.waitForTimeout(2000);

await page.evaluate(async () => {
await window.univerAPI.executeCommand('sheet.command.numfmt.set.numfmt', {
values: [
{
row: 0,
col: 7,
pattern: '@@@',
type: 'text',
},
],
});

await window.univerAPI.getActiveWorkbook().getActiveSheet().getRange('H1').setValue(2);

await window.univerAPI.getActiveWorkbook().getActiveSheet().getRange('I1').setValue(3);

await window.univerAPI.executeCommand('sheet.command.numfmt.set.numfmt', {
values: [
{
row: 0,
col: 8,
pattern: '@@@',
type: 'text',
},
],
});
});

const filename = generateSnapshotName('set-text-format-number-cell');
const screenshot = await page.locator(SHEET_MAIN_CANVAS_ID).screenshot();
await expect(screenshot).toMatchSnapshot(filename, { maxDiffPixels: 5 });

await page.waitForTimeout(2000);
await browser.close();
});
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
Expand Up @@ -1456,7 +1456,18 @@ export class LexerTreeBuilder extends Disposable {
} else {
const nextCurrentString = formulaStringArray[cur + 1];
if (nextCurrentString && nextCurrentString === matchToken.SINGLE_QUOTATION) {
// handle 'Sheet'1'!A1

// Add the first single quotation
this._pushSegment(currentString);
this._addSequenceArray(sequenceArray, currentString, cur);
cur++;

// Add the second single quotation
this._pushSegment(nextCurrentString);
this._addSequenceArray(sequenceArray, nextCurrentString, cur);
cur++;
continue;
} else {
this._closeSingleQuotation();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,19 @@ describe('Test Reference', () => {
});

it('serializeRangeToRefString', () => {
expect(
serializeRangeToRefString({
range: {
startColumn: 0,
endColumn: 10,
startRow: 5,
endRow: 10,
rangeType: RANGE_TYPE.COLUMN,
},
sheetName: '',
unitId: '',
})
).toEqual('A:K');
expect(
serializeRangeToRefString({
range: {
Expand Down Expand Up @@ -140,6 +153,45 @@ describe('Test Reference', () => {
unitId: 'workbook-1',
})
).toEqual("'[workbook-1]sheet1'!16:16");
expect(
serializeRangeToRefString({
range: {
startColumn: Number.NaN,
endColumn: Number.NaN,
startRow: 15,
endRow: 15,
rangeType: RANGE_TYPE.ROW,
},
sheetName: "sheet'1",
unitId: '',
})
).toEqual("'sheet''1'!16:16");
expect(
serializeRangeToRefString({
range: {
startColumn: Number.NaN,
endColumn: Number.NaN,
startRow: 15,
endRow: 15,
rangeType: RANGE_TYPE.ROW,
},
sheetName: "sheet''1",
unitId: '',
})
).toEqual("'sheet''''1'!16:16");
expect(
serializeRangeToRefString({
range: {
startColumn: Number.NaN,
endColumn: Number.NaN,
startRow: 15,
endRow: 15,
rangeType: RANGE_TYPE.ROW,
},
sheetName: "sheet'1",
unitId: 'workbook-1',
})
).toEqual("'[workbook-1]sheet''1'!16:16");
});

it('deserializeRangeWithSheet', () => {
Expand Down Expand Up @@ -259,7 +311,7 @@ describe('Test Reference', () => {
'12a',
'💩a',
'❤️b',
"Sheet'",
"Sheet'1",
'!Sheet',
'!Sheet',
'Sheet1(副本)',
Expand Down Expand Up @@ -305,11 +357,40 @@ describe('Test Reference', () => {
unitId: '',
});

// with single quote
expect(handleRefStringInfo("'sheet''1'!A1")).toStrictEqual({
refBody: 'A1',
sheetName: "sheet'1",
unitId: '',
});

// with double quote
expect(handleRefStringInfo("'sheet''''1'!A1")).toStrictEqual({
refBody: 'A1',
sheetName: "sheet''1",
unitId: '',
});

expect(handleRefStringInfo("'[Book-1.xlsx]Sheet1'!$A$4")).toStrictEqual({
refBody: '$A$4',
sheetName: 'Sheet1',
unitId: 'Book-1.xlsx',
});

// with single quote
expect(handleRefStringInfo("'[Book''1.xlsx]Sheet1'!$A$4")).toStrictEqual({
refBody: '$A$4',
sheetName: 'Sheet1',
unitId: "Book'1.xlsx",
});

// with double quote
expect(handleRefStringInfo("'[Book''''1.xlsx]Sheet1'!$A$4")).toStrictEqual({
refBody: '$A$4',
sheetName: 'Sheet1',
unitId: "Book''1.xlsx",
});

expect(handleRefStringInfo("'[Book-1.xlsx]sheet-1'!$A$4")).toStrictEqual({
refBody: '$A$4',
sheetName: 'sheet-1',
Expand Down
36 changes: 30 additions & 6 deletions packages/engine-formula/src/engine/utils/reference.ts
Original file line number Diff line number Diff line change
Expand Up @@ -153,10 +153,7 @@ export function serializeRange(range: IRange): string {
* @param range
*/
export function serializeRangeWithSheet(sheetName: string, range: IRange): string {
if (needsQuoting(sheetName)) {
return `'${sheetName}'!${serializeRange(range)}`;
}
return `${sheetName}!${serializeRange(range)}`;
return `${addQuotesBothSides(sheetName)}!${serializeRange(range)}`;
}

/**
Expand All @@ -167,7 +164,7 @@ export function serializeRangeWithSheet(sheetName: string, range: IRange): strin
*/
export function serializeRangeWithSpreadsheet(unit: string, sheetName: string, range: IRange): string {
if (needsQuoting(unit) || needsQuoting(sheetName)) {
return `'[${unit}]${sheetName}'!${serializeRange(range)}`;
return `'[${quoteSheetName(unit)}]${quoteSheetName(sheetName)}'!${serializeRange(range)}`;
}

return `[${unit}]${sheetName}!${serializeRange(range)}`;
Expand Down Expand Up @@ -206,7 +203,7 @@ export function handleRefStringInfo(refString: string) {

if (unitIdMatch != null) {
unitId = unitIdMatch[0].trim();
unitId = unitId.slice(1, unitId.length - 1);
unitId = unquoteSheetName(unitId.slice(1, unitId.length - 1));
refString = refString.replace(UNIT_NAME_REGEX_PRECOMPILING, '');
}

Expand All @@ -218,6 +215,8 @@ export function handleRefStringInfo(refString: string) {
if (sheetName[0] === "'" && sheetName[sheetName.length - 1] === "'") {
sheetName = sheetName.substring(1, sheetName.length - 1);
}

sheetName = unquoteSheetName(sheetName);
refBody = refString.substring(sheetNameIndex + 1);
} else {
refBody = refString;
Expand Down Expand Up @@ -426,6 +425,31 @@ export function needsQuoting(name: string) {
return false;
}

/**
* Add quotes to the sheet name
*/
export function addQuotesBothSides(name: string) {
return needsQuoting(name) ? `'${quoteSheetName(name)}'` : name;
}

/**
* Add a single quote before the single quote
* @param name
* @returns Quoted name
*/
function quoteSheetName(name: string) {
return name.replace(/'/g, "''");
}

/**
* Replace double single quotes with single quotes
* @param name
* @returns Unquoted name
*/
function unquoteSheetName(name: string) {
return name.replace(/''/g, "'");
}

function isA1Notation(name: string) {
const match = name.match(/[1-9][0-9]{0,6}/);
// Excel has a limit on the number of rows and columns: targetRow > 1048576 || targetColumn > 16384, Univer has no limit
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,16 +17,16 @@
import { describe, expect, it } from 'vitest';

import { ErrorType } from '../../../../basics/error-type';
import { ArrayValueObject, transformToValue, transformToValueObject } from '../../../../engine/value-object/array-value-object';
import { ErrorValueObject } from '../../../../engine/value-object/base-value-object';
import {
BooleanValueObject,
NumberValueObject,
StringValueObject,
} from '../../../../engine/value-object/primitive-object';
import { getObjectValue } from '../../../__tests__/create-function-test-bed';
import { FUNCTION_NAMES_LOOKUP } from '../../function-names';
import { Address } from '../index';
import { ArrayValueObject, transformToValue, transformToValueObject } from '../../../../engine/value-object/array-value-object';
import { getObjectValue } from '../../../__tests__/create-function-test-bed';

describe('Test address', () => {
const testFunction = new Address(FUNCTION_NAMES_LOOKUP.ADDRESS);
Expand Down Expand Up @@ -65,6 +65,10 @@ describe('Test address', () => {
expect(calculate(2, 3, 1, false, '[Book1]Sheet1')).toBe("'[Book1]Sheet1'!R2C3");
});

it('Absolute reference to sheet name with single quote', async () => {
expect(calculate(1, 1, 1, true, "Sheet'1")).toBe("'Sheet''1'!$A$1");
});

it('Absolute reference to another worksheet', async () => {
expect(calculate(2, 3, 1, false, 'EXCEL SHEET')).toBe("'EXCEL SHEET'!R2C3");
});
Expand Down
Loading
Loading