snapshot of https://github.com/dream-num/univer.git (dev, 2026-09-16; blobs > 131072B omitted)
Some checks failed
📦 Build / build (push) Has been cancelled
🧪 CI Test / test shard (1/4) (push) Has been cancelled
🧪 CI Test / test shard (2/4) (push) Has been cancelled
🧪 CI Test / test shard (3/4) (push) Has been cancelled
🧪 CI Test / test shard (4/4) (push) Has been cancelled
🧪 CI Test / test (push) Has been cancelled
🧪 CI Test / eslint (push) Has been cancelled
🧪 CI Test / typecheck (push) Has been cancelled
🔫 Sync to Pro / dispatch-sync (push) Has been cancelled
🔮 Sync Mirror / sync (push) Has been cancelled
Some checks failed
📦 Build / build (push) Has been cancelled
🧪 CI Test / test shard (1/4) (push) Has been cancelled
🧪 CI Test / test shard (2/4) (push) Has been cancelled
🧪 CI Test / test shard (3/4) (push) Has been cancelled
🧪 CI Test / test shard (4/4) (push) Has been cancelled
🧪 CI Test / test (push) Has been cancelled
🧪 CI Test / eslint (push) Has been cancelled
🧪 CI Test / typecheck (push) Has been cancelled
🔫 Sync to Pro / dispatch-sync (push) Has been cancelled
🔮 Sync Mirror / sync (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,165 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { CellValueType, ObjectMatrix } from '@univerjs/core';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { CellReferenceObject } from '../cell-reference-object';
|
||||
import { RangeReferenceObject } from '../range-reference-object';
|
||||
|
||||
describe('CellReferenceObject', () => {
|
||||
it('should allow blank cells inside the Excel grid even when they are outside snapshot row data', () => {
|
||||
const reference = new CellReferenceObject('Sheet2!A151');
|
||||
|
||||
reference.setForcedSheetIdDirect('sheet-2');
|
||||
|
||||
expect(reference.isExceedRange()).toBe(false);
|
||||
});
|
||||
|
||||
it('should still reject cells outside the Excel grid', () => {
|
||||
const reference = new CellReferenceObject('Sheet2!A1048577');
|
||||
|
||||
reference.setForcedSheetIdDirect('sheet-2');
|
||||
|
||||
expect(reference.isExceedRange()).toBe(true);
|
||||
});
|
||||
|
||||
it('should still reject unresolved forced sheet names', () => {
|
||||
const reference = new CellReferenceObject('Sheet2!A151');
|
||||
|
||||
expect(reference.isExceedRange()).toBe(true);
|
||||
});
|
||||
|
||||
it('should preserve typed empty strings while iterating references', () => {
|
||||
const reference = new CellReferenceObject('A1:B1');
|
||||
reference.setDefaultUnitId('unit-1');
|
||||
reference.setDefaultSheetId('sheet-1');
|
||||
reference.setUnitData({
|
||||
'unit-1': {
|
||||
'sheet-1': {
|
||||
rowCount: 1,
|
||||
columnCount: 2,
|
||||
rowData: {},
|
||||
columnData: {},
|
||||
cellData: new ObjectMatrix({
|
||||
0: {
|
||||
0: { v: '', t: CellValueType.STRING },
|
||||
1: { v: '' },
|
||||
},
|
||||
}),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const values: unknown[] = [];
|
||||
reference.iterator((valueObject) => {
|
||||
values.push(valueObject?.getValue() ?? null);
|
||||
});
|
||||
|
||||
expect(values).toEqual(['', null]);
|
||||
});
|
||||
|
||||
it('should return null value objects for missing cells inside the grid', () => {
|
||||
const reference = new CellReferenceObject('A1');
|
||||
reference.setDefaultUnitId('unit-1');
|
||||
reference.setDefaultSheetId('sheet-1');
|
||||
reference.setUnitData({
|
||||
'unit-1': {
|
||||
'sheet-1': {
|
||||
rowCount: 1,
|
||||
columnCount: 1,
|
||||
rowData: {},
|
||||
columnData: {},
|
||||
cellData: new ObjectMatrix({}),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(reference.getCellByPosition().isNull()).toBe(true);
|
||||
expect(reference.getCellByPosition().getValue()).toBe(0);
|
||||
});
|
||||
|
||||
it('should apply shared-formula offsets when reading the referenced cell', () => {
|
||||
const reference = new CellReferenceObject('A1');
|
||||
reference.setDefaultUnitId('unit-1');
|
||||
reference.setDefaultSheetId('sheet-1');
|
||||
reference.setUnitData({
|
||||
'unit-1': {
|
||||
'sheet-1': {
|
||||
rowCount: 2,
|
||||
columnCount: 1,
|
||||
rowData: {},
|
||||
columnData: {},
|
||||
cellData: new ObjectMatrix({
|
||||
0: { 0: { v: 1, t: CellValueType.NUMBER } },
|
||||
1: { 0: { v: 2, t: CellValueType.NUMBER } },
|
||||
}),
|
||||
},
|
||||
},
|
||||
});
|
||||
reference.setRefOffset(0, 1);
|
||||
|
||||
expect(reference.getCellByPosition().getValue()).toBe(2);
|
||||
expect(reference.getCellByPosition(0, 0).getValue()).toBe(1);
|
||||
});
|
||||
|
||||
it('should not apply a shared-formula offset twice when unioning a materialized reference', () => {
|
||||
const sharedReference = new CellReferenceObject('$B$2');
|
||||
const materializedReference = new CellReferenceObject('B3');
|
||||
sharedReference.setRefOffset(0, 1);
|
||||
|
||||
const union = sharedReference.unionBy(materializedReference);
|
||||
|
||||
if (!(union instanceof RangeReferenceObject)) {
|
||||
throw new TypeError('Expected a range reference');
|
||||
}
|
||||
|
||||
expect(union.getRangePosition()).toEqual(expect.objectContaining({
|
||||
startRow: 1,
|
||||
endRow: 2,
|
||||
startColumn: 1,
|
||||
endColumn: 1,
|
||||
}));
|
||||
});
|
||||
|
||||
it('should apply matching shared-formula offsets once when unioning references', () => {
|
||||
const startReference = new CellReferenceObject('A1');
|
||||
const endReference = new CellReferenceObject('B1');
|
||||
startReference.setRefOffset(0, 1);
|
||||
endReference.setRefOffset(0, 1);
|
||||
|
||||
const union = startReference.unionBy(endReference);
|
||||
|
||||
if (!(union instanceof RangeReferenceObject)) {
|
||||
throw new TypeError('Expected a range reference');
|
||||
}
|
||||
|
||||
expect(union.getRangePosition()).toEqual(expect.objectContaining({
|
||||
startRow: 1,
|
||||
endRow: 1,
|
||||
startColumn: 0,
|
||||
endColumn: 1,
|
||||
}));
|
||||
});
|
||||
|
||||
it('should treat string enum number cell types as numeric cells', () => {
|
||||
const reference = new CellReferenceObject('A1');
|
||||
|
||||
const valueObject = reference.getCellValueObject({ v: '43497', t: String(CellValueType.NUMBER) as never });
|
||||
|
||||
expect(valueObject.isNumber()).toBe(true);
|
||||
expect(valueObject.getValue()).toBe(43497);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,294 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type { Nullable } from '@univerjs/core';
|
||||
import type { BaseValueObject } from '../../value-object/base-value-object';
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import { ErrorType } from '../../../basics/error-type';
|
||||
import { createNewArray } from '../../utils/array-object';
|
||||
import { ErrorValueObject } from '../../value-object/base-value-object';
|
||||
import { NumberValueObject } from '../../value-object/primitive-object';
|
||||
import { BaseReferenceObject } from '../base-reference-object';
|
||||
import { MultiAreaArrayMode, MultiAreaReferenceObject } from '../multi-area-reference-object';
|
||||
|
||||
interface ITestReferenceConfig {
|
||||
rowCount?: number;
|
||||
columnCount?: number;
|
||||
exceed?: boolean;
|
||||
unitId?: string;
|
||||
sheetId?: string;
|
||||
range?: { startRow: number; startColumn: number; endRow: number; endColumn: number };
|
||||
rangeData?: { startRow: number; startColumn: number; endRow: number; endColumn: number };
|
||||
iteratorValue?: number;
|
||||
}
|
||||
|
||||
class TestReferenceObject extends BaseReferenceObject {
|
||||
override readonly dispose = vi.fn(() => super.dispose());
|
||||
override readonly setRefOffset = vi.fn((x = 0, y = 0) => super.setRefOffset(x, y));
|
||||
|
||||
constructor(private readonly _config: ITestReferenceConfig = {}) {
|
||||
super('test-reference');
|
||||
}
|
||||
|
||||
override getRowCount(): number {
|
||||
return this._config.rowCount ?? 1;
|
||||
}
|
||||
|
||||
override getColumnCount(): number {
|
||||
return this._config.columnCount ?? 1;
|
||||
}
|
||||
|
||||
override isExceedRange(): boolean {
|
||||
return this._config.exceed ?? false;
|
||||
}
|
||||
|
||||
override getUnitId(): string {
|
||||
return this._config.unitId ?? 'unit-1';
|
||||
}
|
||||
|
||||
override getSheetId(): string {
|
||||
return this._config.sheetId ?? 'sheet-1';
|
||||
}
|
||||
|
||||
override getActiveSheetRowCount(): number {
|
||||
return 100;
|
||||
}
|
||||
|
||||
override getActiveSheetColumnCount(): number {
|
||||
return 26;
|
||||
}
|
||||
|
||||
override iterator(
|
||||
callback: (value: Nullable<BaseValueObject>, row: number, column: number) => Nullable<boolean>
|
||||
): void {
|
||||
callback(NumberValueObject.create(this._config.iteratorValue ?? 1), 0, 0);
|
||||
}
|
||||
|
||||
override getFirstCell(): BaseValueObject {
|
||||
return NumberValueObject.create(this._config.iteratorValue ?? 1);
|
||||
}
|
||||
|
||||
override getRangePosition() {
|
||||
return this._config.range ?? { startRow: 0, startColumn: 0, endRow: 0, endColumn: 0 };
|
||||
}
|
||||
|
||||
override getRangeData() {
|
||||
return this._config.rangeData ?? this.getRangePosition();
|
||||
}
|
||||
|
||||
override toArrayValueObject() {
|
||||
const rowCount = this.getRowCount();
|
||||
const columnCount = this.getColumnCount();
|
||||
return createNewArray(
|
||||
Array.from(
|
||||
{ length: rowCount },
|
||||
() => Array.from(
|
||||
{ length: columnCount },
|
||||
() => NumberValueObject.create(this._config.iteratorValue ?? 1)
|
||||
)
|
||||
),
|
||||
rowCount,
|
||||
columnCount
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function createAreaStub(config?: ITestReferenceConfig): TestReferenceObject {
|
||||
return new TestReferenceObject(config);
|
||||
}
|
||||
|
||||
function createErrorAreaStub(): ErrorValueObject {
|
||||
return ErrorValueObject.create(ErrorType.REF);
|
||||
}
|
||||
|
||||
describe('MultiAreaReferenceObject', () => {
|
||||
it('should manage areas and multi-area flags', () => {
|
||||
const areaA = createAreaStub({ rowCount: 2, columnCount: 3 });
|
||||
const areaB = createAreaStub({ rowCount: 4, columnCount: 5 });
|
||||
const multi = new MultiAreaReferenceObject('token', [[areaA]]);
|
||||
|
||||
multi.addArea(areaB);
|
||||
multi.addArea([areaA, areaB]);
|
||||
|
||||
expect(multi.isMultiArea()).toBe(true);
|
||||
expect(multi.isRange()).toBe(false);
|
||||
expect(multi.isCell()).toBe(false);
|
||||
expect(multi.isRow()).toBe(false);
|
||||
expect(multi.isColumn()).toBe(false);
|
||||
expect(multi.getAreas().length).toBe(3);
|
||||
expect(multi.getRowCount()).toBe(3);
|
||||
expect(multi.getColumnCount()).toBe(2);
|
||||
});
|
||||
|
||||
it('should ignore error areas in count, range and sheet inference', () => {
|
||||
const area = createAreaStub({
|
||||
unitId: 'unit-A',
|
||||
sheetId: 'sheet-A',
|
||||
range: { startRow: 2, startColumn: 3, endRow: 4, endColumn: 5 },
|
||||
});
|
||||
const errorArea = createErrorAreaStub();
|
||||
const multi = new MultiAreaReferenceObject('token', [[errorArea, area]]);
|
||||
|
||||
expect(multi.getUnitId()).toBe('unit-A');
|
||||
expect(multi.getSheetId()).toBe('sheet-A');
|
||||
expect(multi.getActiveSheetRowCount()).toBe(100);
|
||||
expect(multi.getActiveSheetColumnCount()).toBe(26);
|
||||
expect(multi.isExceedRange()).toBe(false);
|
||||
expect(multi.getRangePosition()).toEqual({ startRow: 2, startColumn: 3, endRow: 4, endColumn: 5 });
|
||||
});
|
||||
|
||||
it('should propagate offset and iterate in row-major order with stop signal', () => {
|
||||
const area1 = createAreaStub({ iteratorValue: 1 });
|
||||
const area2 = createAreaStub({ iteratorValue: 2 });
|
||||
const multi = new MultiAreaReferenceObject('token', [[area1, area2]]);
|
||||
|
||||
multi.setRefOffset(2, 3);
|
||||
expect(area1.setRefOffset).toHaveBeenCalledWith(2, 3);
|
||||
expect(area2.setRefOffset).toHaveBeenCalledWith(2, 3);
|
||||
|
||||
const values: number[] = [];
|
||||
multi.iterator((v) => {
|
||||
const value = v?.getValue();
|
||||
if (typeof value === 'number') {
|
||||
values.push(value);
|
||||
}
|
||||
return values.length < 1;
|
||||
});
|
||||
expect(values).toEqual([1]);
|
||||
});
|
||||
|
||||
it('should convert multi-area to array object and unit range', () => {
|
||||
const area1 = createAreaStub({
|
||||
range: { startRow: 0, startColumn: 0, endRow: 1, endColumn: 1 },
|
||||
rangeData: { startRow: 0, startColumn: 0, endRow: 1, endColumn: 1 },
|
||||
unitId: 'u1',
|
||||
sheetId: 's1',
|
||||
iteratorValue: 9,
|
||||
});
|
||||
const area2 = createAreaStub({
|
||||
range: { startRow: 5, startColumn: 6, endRow: 7, endColumn: 8 },
|
||||
rangeData: { startRow: 5, startColumn: 6, endRow: 7, endColumn: 8 },
|
||||
unitId: 'u1',
|
||||
sheetId: 's1',
|
||||
iteratorValue: 5,
|
||||
});
|
||||
const multi = new MultiAreaReferenceObject('token', [[area1, area2]]);
|
||||
|
||||
const array = multi.toArrayValueObject();
|
||||
expect(array.getRowCount()).toBe(1);
|
||||
expect(array.getColumnCount()).toBe(2);
|
||||
expect(array.get(0, 0)?.getValue()).toBe(9);
|
||||
expect(array.get(0, 1)?.getValue()).toBe(5);
|
||||
|
||||
expect(multi.getRangePosition()).toEqual({
|
||||
startRow: 0,
|
||||
startColumn: 0,
|
||||
endRow: 7,
|
||||
endColumn: 8,
|
||||
});
|
||||
expect(multi.getRangeData()).toEqual({
|
||||
startRow: 0,
|
||||
startColumn: 0,
|
||||
endRow: 7,
|
||||
endColumn: 8,
|
||||
});
|
||||
expect(multi.toUnitRange()).toEqual({
|
||||
unitId: 'u1',
|
||||
sheetId: 's1',
|
||||
range: {
|
||||
startRow: 0,
|
||||
startColumn: 0,
|
||||
endRow: 7,
|
||||
endColumn: 8,
|
||||
},
|
||||
});
|
||||
expect(multi.getFirstCell().getValue()).toBe(9);
|
||||
});
|
||||
|
||||
it('should stack every value and preserve every sheet range in stack mode', () => {
|
||||
const area1 = createAreaStub({
|
||||
rowCount: 2,
|
||||
columnCount: 2,
|
||||
unitId: 'u1',
|
||||
sheetId: 'jan',
|
||||
iteratorValue: 1,
|
||||
});
|
||||
const area2 = createAreaStub({
|
||||
rowCount: 1,
|
||||
columnCount: 2,
|
||||
unitId: 'u1',
|
||||
sheetId: 'feb',
|
||||
iteratorValue: 2,
|
||||
});
|
||||
const multi = new MultiAreaReferenceObject(
|
||||
'Jan:Feb!A1:B2',
|
||||
[[area1], [area2]],
|
||||
MultiAreaArrayMode.STACK_AREAS
|
||||
);
|
||||
|
||||
const array = multi.toArrayValueObject();
|
||||
expect(multi.getRowCount()).toBe(3);
|
||||
expect(multi.getColumnCount()).toBe(2);
|
||||
expect(array.getArrayValue().map((row) => row.map((value) => value?.getValue()))).toEqual([
|
||||
[1, 1],
|
||||
[1, 1],
|
||||
[2, 2],
|
||||
]);
|
||||
expect(multi.toUnitRanges().map(({ sheetId }) => sheetId)).toEqual(['jan', 'feb']);
|
||||
});
|
||||
|
||||
it('should fallback to parent behavior when all areas are invalid', () => {
|
||||
const invalidArea = createAreaStub({
|
||||
range: {
|
||||
startRow: Number.POSITIVE_INFINITY,
|
||||
startColumn: Number.POSITIVE_INFINITY,
|
||||
endRow: Number.NEGATIVE_INFINITY,
|
||||
endColumn: Number.NEGATIVE_INFINITY,
|
||||
},
|
||||
rangeData: {
|
||||
startRow: Number.POSITIVE_INFINITY,
|
||||
startColumn: Number.POSITIVE_INFINITY,
|
||||
endRow: Number.NEGATIVE_INFINITY,
|
||||
endColumn: Number.NEGATIVE_INFINITY,
|
||||
},
|
||||
});
|
||||
const multi = new MultiAreaReferenceObject('token', [[invalidArea]]);
|
||||
|
||||
expect(multi.getRangePosition()).toEqual({
|
||||
startRow: -1,
|
||||
startColumn: -1,
|
||||
endRow: -1,
|
||||
endColumn: -1,
|
||||
});
|
||||
expect(multi.getRangeData()).toEqual({
|
||||
startRow: -1,
|
||||
startColumn: -1,
|
||||
endRow: -1,
|
||||
endColumn: -1,
|
||||
});
|
||||
});
|
||||
|
||||
it('should dispose all areas', () => {
|
||||
const area1 = createAreaStub();
|
||||
const area2 = createAreaStub();
|
||||
const multi = new MultiAreaReferenceObject('token', [[area1, area2]]);
|
||||
|
||||
multi.dispose();
|
||||
expect(area1.dispose).toHaveBeenCalledTimes(1);
|
||||
expect(area2.dispose).toHaveBeenCalledTimes(1);
|
||||
expect(multi.getAreas()).toEqual([]);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,164 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { ObjectMatrix } from '@univerjs/core';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { TableOptionType } from '../../../basics/common';
|
||||
import { ErrorType } from '../../../basics/error-type';
|
||||
import { TableReferenceObject } from '../table-reference-object';
|
||||
|
||||
const options = new Map([
|
||||
['#All', TableOptionType.ALL],
|
||||
['#Data', TableOptionType.DATA],
|
||||
['#Totals', TableOptionType.TOTALS],
|
||||
['#This Row', TableOptionType.THIS_ROW],
|
||||
]);
|
||||
|
||||
describe('TableReferenceObject current row', () => {
|
||||
it('includes row zero for a headerless Base virtual table', () => {
|
||||
const reference = new TableReferenceObject('Orders[[#Data],[Amount]]', {
|
||||
sheetId: 'orders',
|
||||
titleMap: new Map([['Amount', 0]]),
|
||||
range: { startRow: 0, endRow: 1, startColumn: 0, endColumn: 0 },
|
||||
showHeader: false,
|
||||
}, '[[#Data],[Amount]]', options);
|
||||
reference.setDefaultUnitId('base');
|
||||
reference.setUnitData({
|
||||
base: {
|
||||
orders: {
|
||||
cellData: new ObjectMatrix({ 0: { 0: { v: 10 } }, 1: { 0: { v: 20 } } }),
|
||||
rowCount: 2,
|
||||
columnCount: 1,
|
||||
rowData: {},
|
||||
columnData: {},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(reference.toArrayValueObject(false).getFirstCell().getValue()).toBe(10);
|
||||
});
|
||||
|
||||
it('returns N/A when the host row has no corresponding target table row', () => {
|
||||
const reference = new TableReferenceObject('Table[[#This Row],[Amount]]', {
|
||||
sheetId: 'sheet',
|
||||
titleMap: new Map([['Amount', 0]]),
|
||||
range: { startRow: 0, endRow: 2, startColumn: 0, endColumn: 0 },
|
||||
}, '[[#This Row],[Amount]]', options);
|
||||
reference.setDefaultUnitId('unit');
|
||||
reference.setCurrentRowAndColumn(4, 0);
|
||||
reference.setUnitData({
|
||||
unit: {
|
||||
sheet: {
|
||||
cellData: new ObjectMatrix({ 1: { 0: { v: 10 } }, 2: { 0: { v: 20 } } }),
|
||||
rowCount: 3,
|
||||
columnCount: 1,
|
||||
rowData: {},
|
||||
columnData: {},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(reference.toArrayValueObject(false).getFirstCell().getValue()).toBe(ErrorType.NA);
|
||||
});
|
||||
|
||||
it('excludes a declared totals row from data references', () => {
|
||||
const table = {
|
||||
sheetId: 'sheet',
|
||||
titleMap: new Map([['Amount', 0]]),
|
||||
range: { startRow: 0, endRow: 3, startColumn: 0, endColumn: 0 },
|
||||
showFooter: true,
|
||||
};
|
||||
|
||||
expect(new TableReferenceObject('Table[Amount]', table, '[Amount]', options).getRangeData()).toEqual({
|
||||
startRow: 1,
|
||||
endRow: 2,
|
||||
startColumn: 0,
|
||||
endColumn: 0,
|
||||
});
|
||||
expect(new TableReferenceObject(
|
||||
'Table[[#Totals],[Amount]]',
|
||||
table,
|
||||
'[[#Totals],[Amount]]',
|
||||
options
|
||||
).getRangeData()).toEqual({
|
||||
startRow: 3,
|
||||
endRow: 3,
|
||||
startColumn: 0,
|
||||
endColumn: 0,
|
||||
});
|
||||
});
|
||||
|
||||
it('matches CRLF structured references against LF table column names', () => {
|
||||
const reference = new TableReferenceObject('Table_FMEA[[#This Row],[F\r\n(1-5)]]', {
|
||||
sheetId: 'sheet',
|
||||
titleMap: new Map([['F\n(1-5)', 3]]),
|
||||
range: { startRow: 7, endRow: 33, startColumn: 0, endColumn: 14 },
|
||||
}, '[[#This Row],[F\r\n(1-5)]]', options);
|
||||
reference.setCurrentRowAndColumn(8, 6);
|
||||
|
||||
expect(reference.getRangeData()).toEqual({
|
||||
startRow: 8,
|
||||
endRow: 8,
|
||||
startColumn: 3,
|
||||
endColumn: 3,
|
||||
});
|
||||
});
|
||||
|
||||
it('treats commas inside single-bracket column titles as title text', () => {
|
||||
const reference = new TableReferenceObject('Table1[Deposit,\r\nCredit (+)]', {
|
||||
sheetId: 'sheet',
|
||||
titleMap: new Map([['Deposit,\nCredit (+)', 6]]),
|
||||
range: { startRow: 0, endRow: 10, startColumn: 0, endColumn: 7 },
|
||||
}, '[Deposit,\r\nCredit (+)]', options);
|
||||
|
||||
expect(reference.getRangeData()).toEqual({
|
||||
startRow: 1,
|
||||
endRow: 10,
|
||||
startColumn: 6,
|
||||
endColumn: 6,
|
||||
});
|
||||
});
|
||||
|
||||
it('treats a trailing colon inside a single-bracket column title as title text', () => {
|
||||
const reference = new TableReferenceObject('Table1[Business Name:]', {
|
||||
sheetId: 'sheet',
|
||||
titleMap: new Map([['Business Name:', 3]]),
|
||||
range: { startRow: 0, endRow: 10, startColumn: 0, endColumn: 6 },
|
||||
}, '[Business Name:]', options);
|
||||
|
||||
expect(reference.getRangeData()).toEqual({
|
||||
startRow: 1,
|
||||
endRow: 10,
|
||||
startColumn: 3,
|
||||
endColumn: 3,
|
||||
});
|
||||
});
|
||||
});
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
@@ -0,0 +1,783 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type { ICellData, IRange, IUnitRange, Nullable } from '@univerjs/core';
|
||||
import type { IArrayFormulaRangeType, IRuntimeUnitDataType, IUnitData, IUnitSheetNameMap, IUnitStylesData } from '../../basics/common';
|
||||
import type { BaseValueObject, IArrayValueObject } from '../value-object/base-value-object';
|
||||
import { CellValueType, DateSystem, isNullCell, isTextFormat, moveRangeByOffset, ObjectMatrix } from '@univerjs/core';
|
||||
import { FormulaAstLRU } from '../../basics/cache-lru';
|
||||
import { ERROR_TYPE_SET, ErrorType } from '../../basics/error-type';
|
||||
import { ObjectClassType } from '../../basics/object-class-type';
|
||||
import { getCellValue } from '../utils/cell';
|
||||
import { getRuntimeFeatureCell } from '../utils/get-runtime-feature-cell';
|
||||
import { ArrayValueObject, ValueObjectFactory } from '../value-object/array-value-object';
|
||||
import { ErrorValueObject } from '../value-object/base-value-object';
|
||||
import {
|
||||
createBooleanValueObjectByRawValue,
|
||||
createNumberValueObjectByRawValue,
|
||||
NullValueObject,
|
||||
NumberValueObject,
|
||||
StringValueObject,
|
||||
} from '../value-object/primitive-object';
|
||||
|
||||
export type NodeValueType = BaseValueObject | BaseReferenceObject | AsyncObject | AsyncArrayObject;
|
||||
|
||||
export type FunctionVariantType = BaseValueObject | BaseReferenceObject;
|
||||
|
||||
const FORMULA_CACHE_LRU_COUNT = 10000;
|
||||
|
||||
export const FORMULA_REF_TO_ARRAY_CACHE = new FormulaAstLRU<ArrayValueObject>(FORMULA_CACHE_LRU_COUNT);
|
||||
export class BaseReferenceObject extends ObjectClassType {
|
||||
private _dateSystem = DateSystem.Date1900;
|
||||
|
||||
private _forcedSheetId: Nullable<string> = '';
|
||||
|
||||
private _forcedSheetName: string = '';
|
||||
|
||||
private _defaultSheetId: string = '';
|
||||
|
||||
private _rangeData: IRange = {
|
||||
startColumn: -1,
|
||||
startRow: -1,
|
||||
endRow: -1,
|
||||
endColumn: -1,
|
||||
};
|
||||
|
||||
private _unitData: IUnitData = {};
|
||||
|
||||
private _unitStylesData: IUnitStylesData = {};
|
||||
|
||||
private _filteredOutRows: number[] = [];
|
||||
|
||||
private _defaultUnitId: string = '';
|
||||
|
||||
private _forcedUnitId: string = '';
|
||||
|
||||
private _unitQualifier: string = '';
|
||||
|
||||
private _runtimeData: IRuntimeUnitDataType = {};
|
||||
|
||||
private _arrayFormulaCellData: IRuntimeUnitDataType = {};
|
||||
|
||||
private _arrayFormulaRange: IArrayFormulaRangeType = {};
|
||||
|
||||
private _runtimeArrayFormulaCellData: IRuntimeUnitDataType = {};
|
||||
|
||||
private _runtimeArrayFormulaRange: IArrayFormulaRangeType = {};
|
||||
|
||||
private _runtimeFeatureCellData: { [featureId: string]: IRuntimeUnitDataType } = {};
|
||||
|
||||
private _refOffsetX = 0;
|
||||
|
||||
private _refOffsetY = 0;
|
||||
|
||||
private _currentRow?: number;
|
||||
|
||||
private _currentColumn?: number;
|
||||
|
||||
constructor(private _token: string) {
|
||||
super();
|
||||
}
|
||||
|
||||
override dispose(): void {
|
||||
this._unitData = {};
|
||||
|
||||
this._unitStylesData = {};
|
||||
|
||||
this._runtimeData = {};
|
||||
}
|
||||
|
||||
getToken() {
|
||||
return this._token;
|
||||
}
|
||||
|
||||
setToken(token: string) {
|
||||
this._token = token;
|
||||
}
|
||||
|
||||
isExceedRange() {
|
||||
const { startRow, endRow, startColumn, endColumn } = this.getRangePosition();
|
||||
|
||||
if (startRow < 0 || startColumn < 0 || endRow >= this.getActiveSheetRowCount() || endColumn >= this.getActiveSheetColumnCount()) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
setRefOffset(x: number = 0, y: number = 0) {
|
||||
this._refOffsetX = x;
|
||||
this._refOffsetY = y;
|
||||
}
|
||||
|
||||
getRefOffset() {
|
||||
return {
|
||||
x: this._refOffsetX,
|
||||
y: this._refOffsetY,
|
||||
};
|
||||
}
|
||||
|
||||
getRangePosition() {
|
||||
const { x, y } = this.getRefOffset();
|
||||
const newRange = moveRangeByOffset(this.getRangeData(), x, y);
|
||||
|
||||
if (Number.isNaN(newRange.startRow)) {
|
||||
newRange.startRow = 0;
|
||||
}
|
||||
|
||||
if (Number.isNaN(newRange.startColumn)) {
|
||||
newRange.startColumn = 0;
|
||||
}
|
||||
|
||||
if (Number.isNaN(newRange.endRow)) {
|
||||
newRange.endRow = this.getActiveSheetRowCount() - 1;
|
||||
}
|
||||
|
||||
if (Number.isNaN(newRange.endColumn)) {
|
||||
newRange.endColumn = this.getActiveSheetColumnCount() - 1;
|
||||
}
|
||||
|
||||
return newRange;
|
||||
}
|
||||
|
||||
override isReferenceObject() {
|
||||
return true;
|
||||
}
|
||||
|
||||
getDateSystem() {
|
||||
return this._dateSystem;
|
||||
}
|
||||
|
||||
withDateSystem(dateSystem: DateSystem): this {
|
||||
this._dateSystem = dateSystem;
|
||||
return this;
|
||||
}
|
||||
|
||||
iterator(
|
||||
callback: (valueObject: Nullable<BaseValueObject>, rowIndex: number, columnIndex: number) => Nullable<boolean>
|
||||
) {
|
||||
const { startRow, endRow, startColumn, endColumn } = this.getRangePosition();
|
||||
|
||||
if (this._checkIfWorksheetMiss()) {
|
||||
return callback(ErrorValueObject.create(ErrorType.REF), startRow, startColumn);
|
||||
}
|
||||
|
||||
const unitId = this._forcedUnitId || this._defaultUnitId;
|
||||
const sheetId = this._forcedSheetId || this._defaultSheetId;
|
||||
|
||||
for (let r = startRow; r <= endRow; r++) {
|
||||
for (let c = startColumn; c <= endColumn; c++) {
|
||||
if (r < 0 || c < 0) {
|
||||
return callback(ErrorValueObject.create(ErrorType.REF), r, c);
|
||||
}
|
||||
|
||||
const cell = this.getCellData(r, c)!;
|
||||
let result: Nullable<boolean> = false;
|
||||
if (isNullCell(cell) && !isTypedEmptyStringCell(cell)) {
|
||||
result = callback(null, r, c);
|
||||
continue;
|
||||
}
|
||||
|
||||
let resultObjectValue = this.getCellValueObject(cell);
|
||||
|
||||
// Set numfmt pattern for first cell
|
||||
// TODO@Dushusir it does not have to be a number, the string can also be set a number format
|
||||
if (r === startRow && c === startColumn) {
|
||||
const pattern = this.getCellPattern(unitId, sheetId, r, c);
|
||||
if (pattern && resultObjectValue.isNumber()) {
|
||||
const value = Number(resultObjectValue.getValue());
|
||||
resultObjectValue = NumberValueObject.create(value, pattern).withDateSystem(this._dateSystem);
|
||||
}
|
||||
}
|
||||
|
||||
result = callback(resultObjectValue, r, c);
|
||||
|
||||
if (result === false) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
getFirstCell() {
|
||||
if (this._checkIfWorksheetMiss()) {
|
||||
return ErrorValueObject.create(ErrorType.REF);
|
||||
}
|
||||
|
||||
const { startRow, startColumn } = this.getRangePosition();
|
||||
const cell = this.getCellData(startRow, startColumn);
|
||||
|
||||
if (!cell) {
|
||||
return NumberValueObject.create(0);
|
||||
}
|
||||
|
||||
let cellValueObject = this.getCellValueObject(cell);
|
||||
|
||||
// Set numfmt pattern,
|
||||
// TODO@Dushusir it does not have to be a number, the string can also be set a number format
|
||||
const unitId = this._forcedUnitId || this._defaultUnitId;
|
||||
const sheetId = this._forcedSheetId || this._defaultSheetId;
|
||||
const pattern = this.getCellPattern(unitId, sheetId, startRow, startColumn);
|
||||
if (pattern && cellValueObject.isNumber()) {
|
||||
const value = Number(cellValueObject.getValue());
|
||||
cellValueObject = NumberValueObject.create(value, pattern).withDateSystem(this._dateSystem);
|
||||
}
|
||||
|
||||
return cellValueObject;
|
||||
}
|
||||
|
||||
getRangeData() {
|
||||
return this._rangeData;
|
||||
}
|
||||
|
||||
setRangeData(range: IRange) {
|
||||
this._rangeData = range;
|
||||
}
|
||||
|
||||
getUnitId() {
|
||||
if (this._forcedUnitId && this._forcedUnitId.length > 0) {
|
||||
return this._forcedUnitId;
|
||||
}
|
||||
return this._defaultUnitId;
|
||||
}
|
||||
|
||||
getSheetId() {
|
||||
if (this._forcedSheetId && this._forcedSheetId.length > 0) {
|
||||
return this._forcedSheetId;
|
||||
}
|
||||
return this._defaultSheetId;
|
||||
}
|
||||
|
||||
setForcedUnitIdDirect(unitId: string) {
|
||||
if (unitId.length > 0) {
|
||||
this._forcedUnitId = unitId;
|
||||
}
|
||||
}
|
||||
|
||||
setUnitQualifier(unitQualifier: string) {
|
||||
this._unitQualifier = unitQualifier;
|
||||
}
|
||||
|
||||
getUnitQualifier() {
|
||||
return this._unitQualifier;
|
||||
}
|
||||
|
||||
getForcedUnitId() {
|
||||
return this._forcedUnitId;
|
||||
}
|
||||
|
||||
setForcedSheetId(sheetNameMap: IUnitSheetNameMap) {
|
||||
if (!this._forcedSheetName) {
|
||||
return;
|
||||
}
|
||||
this._forcedSheetId = sheetNameMap[this.getUnitId()]?.[this._forcedSheetName];
|
||||
}
|
||||
|
||||
setForcedSheetIdDirect(sheetId: string) {
|
||||
this._forcedSheetId = sheetId;
|
||||
}
|
||||
|
||||
getForcedSheetId() {
|
||||
return this._forcedSheetId;
|
||||
}
|
||||
|
||||
setForcedSheetName(sheetName: string) {
|
||||
if (sheetName.length > 0) {
|
||||
this._forcedSheetName = sheetName;
|
||||
}
|
||||
}
|
||||
|
||||
getForcedSheetName() {
|
||||
return this._forcedSheetName;
|
||||
}
|
||||
|
||||
setDefaultSheetId(sheetId: string) {
|
||||
this._defaultSheetId = sheetId;
|
||||
}
|
||||
|
||||
getDefaultSheetId() {
|
||||
return this._defaultSheetId;
|
||||
}
|
||||
|
||||
setDefaultUnitId(unitId: string) {
|
||||
this._defaultUnitId = unitId;
|
||||
}
|
||||
|
||||
getDefaultUnitId() {
|
||||
return this._defaultUnitId;
|
||||
}
|
||||
|
||||
getUnitData() {
|
||||
return this._unitData;
|
||||
}
|
||||
|
||||
setUnitData(unitData: IUnitData) {
|
||||
this._unitData = unitData;
|
||||
}
|
||||
|
||||
getUnitStylesData() {
|
||||
return this._unitStylesData;
|
||||
}
|
||||
|
||||
setUnitStylesData(unitStylesData: IUnitStylesData) {
|
||||
this._unitStylesData = unitStylesData;
|
||||
}
|
||||
|
||||
getFilteredOutRows() {
|
||||
return this._filteredOutRows;
|
||||
}
|
||||
|
||||
setFilteredOutRows(filteredOutRows: number[]) {
|
||||
this._filteredOutRows = filteredOutRows;
|
||||
}
|
||||
|
||||
getRuntimeData() {
|
||||
return this._runtimeData;
|
||||
}
|
||||
|
||||
setRuntimeData(runtimeData: IRuntimeUnitDataType) {
|
||||
this._runtimeData = runtimeData;
|
||||
}
|
||||
|
||||
getArrayFormulaCellData() {
|
||||
return this._arrayFormulaCellData;
|
||||
}
|
||||
|
||||
setArrayFormulaCellData(unitData: IRuntimeUnitDataType) {
|
||||
this._arrayFormulaCellData = unitData;
|
||||
}
|
||||
|
||||
getArrayFormulaRange() {
|
||||
return this._arrayFormulaRange;
|
||||
}
|
||||
|
||||
setArrayFormulaRange(rangeData: IArrayFormulaRangeType) {
|
||||
this._arrayFormulaRange = rangeData;
|
||||
}
|
||||
|
||||
getRuntimeArrayFormulaCellData() {
|
||||
return this._runtimeArrayFormulaCellData;
|
||||
}
|
||||
|
||||
setRuntimeArrayFormulaCellData(unitData: IRuntimeUnitDataType) {
|
||||
this._runtimeArrayFormulaCellData = unitData;
|
||||
}
|
||||
|
||||
getRuntimeArrayFormulaRange() {
|
||||
return this._runtimeArrayFormulaRange;
|
||||
}
|
||||
|
||||
setRuntimeArrayFormulaRange(rangeData: IArrayFormulaRangeType) {
|
||||
this._runtimeArrayFormulaRange = rangeData;
|
||||
}
|
||||
|
||||
getRuntimeFeatureCellData() {
|
||||
return this._runtimeFeatureCellData;
|
||||
}
|
||||
|
||||
setRuntimeFeatureCellData(unitData: { [featureId: string]: IRuntimeUnitDataType }) {
|
||||
this._runtimeFeatureCellData = unitData;
|
||||
}
|
||||
|
||||
getActiveSheetRowCount() {
|
||||
return this.getCurrentActiveSheetData()?.rowCount || 0;
|
||||
}
|
||||
|
||||
getActiveSheetColumnCount() {
|
||||
return this.getCurrentActiveSheetData()?.columnCount || 0;
|
||||
}
|
||||
|
||||
getRowCount() {
|
||||
const rangeData = this.getRangeData();
|
||||
return rangeData.endRow - rangeData.startRow + 1;
|
||||
}
|
||||
|
||||
getColumnCount() {
|
||||
const rangeData = this.getRangeData();
|
||||
return rangeData.endColumn - rangeData.startColumn + 1;
|
||||
}
|
||||
|
||||
getRowData() {
|
||||
return this.getCurrentActiveSheetData()?.rowData || {};
|
||||
}
|
||||
|
||||
getColumnData() {
|
||||
return this.getCurrentActiveSheetData()?.columnData || {};
|
||||
}
|
||||
|
||||
isCell() {
|
||||
return false;
|
||||
}
|
||||
|
||||
isColumn() {
|
||||
return false;
|
||||
}
|
||||
|
||||
isRow() {
|
||||
return false;
|
||||
}
|
||||
|
||||
isRange() {
|
||||
return false;
|
||||
}
|
||||
|
||||
isTable() {
|
||||
return false;
|
||||
}
|
||||
|
||||
isCurrentRowForRange(): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
isCurrentColumnForRange(): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
isMultiArea(): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
unionBy(referenceObject: BaseReferenceObject): NodeValueType {
|
||||
/** abstract */
|
||||
return ErrorValueObject.create(ErrorType.REF);
|
||||
}
|
||||
|
||||
unionRange(rangeData1: IRange, rangeData2: IRange): IRange {
|
||||
/** abstract */
|
||||
return {
|
||||
startRow: -1,
|
||||
startColumn: -1,
|
||||
endRow: -1,
|
||||
endColumn: -1,
|
||||
};
|
||||
}
|
||||
|
||||
getCellValueObject(cell: ICellData) {
|
||||
const value = getCellValue(cell);
|
||||
const cellValueType = Number(cell.t);
|
||||
if (ERROR_TYPE_SET.has(value as ErrorType)) {
|
||||
return ErrorValueObject.create(value as ErrorType);
|
||||
}
|
||||
|
||||
if (cellValueType === CellValueType.NUMBER) {
|
||||
const pattern = this._getPatternByCell(cell);
|
||||
|
||||
if (isTextFormat(pattern)) {
|
||||
return StringValueObject.create(value.toString()).withDateSystem(this._dateSystem);
|
||||
}
|
||||
|
||||
return createNumberValueObjectByRawValue(value, pattern).withDateSystem(this._dateSystem);
|
||||
}
|
||||
if (cellValueType === CellValueType.STRING || cellValueType === CellValueType.FORCE_STRING) {
|
||||
// A1 is `"test"`, =A1 also needs to get `"test"`
|
||||
return StringValueObject.create(value.toString()).withDateSystem(this._dateSystem);
|
||||
}
|
||||
if (cellValueType === CellValueType.BOOLEAN) {
|
||||
return createBooleanValueObjectByRawValue(value);
|
||||
}
|
||||
|
||||
return ValueObjectFactory.create(value, false, this._dateSystem);
|
||||
}
|
||||
|
||||
private _getPatternByCell(cell: ICellData) {
|
||||
const styles = this._unitStylesData[this.getUnitId()];
|
||||
|
||||
if (!styles) return '';
|
||||
|
||||
const style = styles.getStyleByCell(cell);
|
||||
return style?.n?.pattern || '';
|
||||
}
|
||||
|
||||
getCellByRow(row: number) {
|
||||
return this.getCellByPosition(row);
|
||||
}
|
||||
|
||||
getCellByColumn(column: number) {
|
||||
return this.getCellByPosition(undefined, column);
|
||||
}
|
||||
|
||||
getCurrentActiveSheetData() {
|
||||
return this._unitData[this.getUnitId()]?.[this.getSheetId()];
|
||||
}
|
||||
|
||||
getCurrentStylesData() {
|
||||
return this._unitStylesData[this.getUnitId()];
|
||||
}
|
||||
|
||||
getCurrentRuntimeSheetData() {
|
||||
return this._runtimeData?.[this.getUnitId()]?.[this.getSheetId()];
|
||||
}
|
||||
|
||||
getCurrentActiveArrayFormulaCellData() {
|
||||
return this._arrayFormulaCellData?.[this.getUnitId()]?.[this.getSheetId()];
|
||||
}
|
||||
|
||||
getCurrentRuntimeActiveArrayFormulaCellData() {
|
||||
return this._runtimeArrayFormulaCellData?.[this.getUnitId()]?.[this.getSheetId()];
|
||||
}
|
||||
|
||||
getCurrentActiveArrayFormulaRange() {
|
||||
return this._arrayFormulaRange?.[this.getUnitId()]?.[this.getSheetId()];
|
||||
}
|
||||
|
||||
getCurrentRuntimeActiveArrayFormulaRange() {
|
||||
return this._runtimeArrayFormulaRange?.[this.getUnitId()]?.[this.getSheetId()];
|
||||
}
|
||||
|
||||
getCellData(row: number, column: number) {
|
||||
const activeSheetData = this.getCurrentActiveSheetData();
|
||||
|
||||
const activeRuntimeData = this.getCurrentRuntimeSheetData();
|
||||
|
||||
const activeArrayFormulaCellData = this.getCurrentActiveArrayFormulaCellData();
|
||||
|
||||
const activeRuntimeArrayFormulaCellData = this.getCurrentRuntimeActiveArrayFormulaCellData();
|
||||
|
||||
const activeArrayFormulaRangeMatrix = new ObjectMatrix<IRange>(this.getCurrentActiveArrayFormulaRange());
|
||||
|
||||
const activeRuntimeArrayFormulaRangeMatrix = new ObjectMatrix<IRange>(this.getCurrentRuntimeActiveArrayFormulaRange());
|
||||
|
||||
let cellData: Nullable<ICellData> = activeRuntimeData?.getValue(row, column) ||
|
||||
activeRuntimeArrayFormulaCellData?.getValue(row, column) ||
|
||||
this.getRuntimeFeatureCellValue(row, column);
|
||||
|
||||
if (cellData) {
|
||||
return cellData;
|
||||
}
|
||||
|
||||
cellData = activeArrayFormulaCellData?.getValue(row, column);
|
||||
|
||||
if (cellData) {
|
||||
// Check if the cell is in the runtime array formula range. if so, do not return the original array formula cell data.
|
||||
let isInRuntimeArrayFormulaRange = false;
|
||||
|
||||
activeArrayFormulaRangeMatrix.forValue((_row, _column, range) => {
|
||||
if (row >= range.startRow && row <= range.endRow && column >= range.startColumn && column <= range.endColumn && activeRuntimeArrayFormulaRangeMatrix.getValue(_row, _column)) {
|
||||
isInRuntimeArrayFormulaRange = true;
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
if (!isInRuntimeArrayFormulaRange) {
|
||||
return cellData;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
return activeSheetData?.cellData.getValue(row, column);
|
||||
}
|
||||
|
||||
getRuntimeFeatureCellValue(row: number, column: number) {
|
||||
return getRuntimeFeatureCell(row, column, this.getSheetId(), this.getUnitId(), this._runtimeFeatureCellData);
|
||||
}
|
||||
|
||||
getCellByPosition(rowRaw?: number, columnRaw?: number) {
|
||||
const range = this.getRangePosition();
|
||||
const row = rowRaw ?? range.startRow;
|
||||
const column = columnRaw ?? range.startColumn;
|
||||
|
||||
const cell = this.getCellData(row, column);
|
||||
|
||||
if (!cell) {
|
||||
return NullValueObject.create();
|
||||
}
|
||||
|
||||
return this.getCellValueObject(cell);
|
||||
}
|
||||
|
||||
setCurrentRowAndColumn(row: number, column: number) {
|
||||
this._currentRow = row;
|
||||
this._currentColumn = column;
|
||||
}
|
||||
|
||||
getCurrentRow() {
|
||||
return this._currentRow;
|
||||
}
|
||||
|
||||
getCurrentColumn() {
|
||||
return this._currentColumn;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the pattern of the cell
|
||||
* @param unitId
|
||||
* @param sheetId
|
||||
* @param row
|
||||
* @param column
|
||||
* @returns
|
||||
*/
|
||||
getCellPattern(unitId: string, sheetId: string, row: number, column: number) {
|
||||
const currentStyles = this._unitStylesData[unitId];
|
||||
|
||||
if (!currentStyles) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const currentCell = this._unitData[unitId]?.[sheetId]?.cellData?.getValue(row, column);
|
||||
|
||||
if (!currentCell) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const style = currentStyles.getStyleByCell(currentCell);
|
||||
|
||||
return style?.n?.pattern || '';
|
||||
}
|
||||
|
||||
toArrayValueObject(useCache: boolean = true): ArrayValueObject {
|
||||
const { startRow, endRow, startColumn, endColumn } = this.getRangePosition();
|
||||
|
||||
// A cached range can be reused only when its string and formatted-number interpretation matches.
|
||||
const key = `${this.getUnitId()}_${this.getSheetId()}_${startRow}_${endRow}_${startColumn}_${endColumn}_${this._dateSystem}`;
|
||||
|
||||
const array = FORMULA_REF_TO_ARRAY_CACHE.get(key);
|
||||
|
||||
if (array && useCache) {
|
||||
return array;
|
||||
}
|
||||
|
||||
const rowSize = endRow - startRow + 1;
|
||||
const columnSize = endColumn - startColumn + 1;
|
||||
|
||||
if (rowSize < 0 || columnSize < 0) {
|
||||
return this._getBlankArrayValueObject();
|
||||
}
|
||||
|
||||
const arrayValueList: BaseValueObject[][] = new Array(rowSize);
|
||||
this.iterator((valueObject: Nullable<BaseValueObject>, rowIndex: number, columnIndex: number) => {
|
||||
const row = rowIndex - startRow;
|
||||
const column = columnIndex - startColumn;
|
||||
if (!arrayValueList[row]) {
|
||||
arrayValueList[row] = new Array(columnSize);
|
||||
}
|
||||
|
||||
if (valueObject == null) {
|
||||
valueObject = NullValueObject.create();
|
||||
}
|
||||
|
||||
arrayValueList[row][column] = valueObject;
|
||||
});
|
||||
|
||||
const arrayValueObjectData: IArrayValueObject = {
|
||||
calculateValueList: arrayValueList,
|
||||
rowCount: arrayValueList.length,
|
||||
columnCount: arrayValueList[0]?.length || 0,
|
||||
unitId: this.getUnitId(),
|
||||
sheetId: this.getSheetId(),
|
||||
row: startRow,
|
||||
column: startColumn,
|
||||
useInvertedIndexCache: true,
|
||||
};
|
||||
|
||||
const arrayValueObject = ArrayValueObject.create(arrayValueObjectData, this._dateSystem);
|
||||
|
||||
useCache && FORMULA_REF_TO_ARRAY_CACHE.set(key, arrayValueObject);
|
||||
|
||||
return arrayValueObject;
|
||||
}
|
||||
|
||||
toUnitRange() {
|
||||
return {
|
||||
range: this.getRangePosition(),
|
||||
sheetId: this.getSheetId(),
|
||||
unitId: this.getUnitId(),
|
||||
};
|
||||
}
|
||||
|
||||
toUnitRanges(): IUnitRange[] {
|
||||
return [this.toUnitRange()];
|
||||
}
|
||||
|
||||
private _checkIfWorksheetMiss() {
|
||||
if ((this._forcedSheetId == null || this._forcedSheetId.length === 0) && this._forcedSheetName.length > 0) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private _getBlankArrayValueObject() {
|
||||
const arrayValueObjectData: IArrayValueObject = {
|
||||
calculateValueList: [],
|
||||
rowCount: 0,
|
||||
columnCount: 0,
|
||||
unitId: this.getUnitId(),
|
||||
sheetId: this.getSheetId(),
|
||||
row: 0,
|
||||
column: 0,
|
||||
};
|
||||
|
||||
return ArrayValueObject.create(arrayValueObjectData, this._dateSystem);
|
||||
}
|
||||
}
|
||||
|
||||
function isTypedEmptyStringCell(cell: Nullable<ICellData>): boolean {
|
||||
return cell?.v === '' && (cell.t === CellValueType.STRING || cell.t === CellValueType.FORCE_STRING);
|
||||
}
|
||||
|
||||
export class AsyncObject extends ObjectClassType {
|
||||
constructor(private _promise: Promise<BaseValueObject>) {
|
||||
super();
|
||||
}
|
||||
|
||||
override isAsyncObject() {
|
||||
return true;
|
||||
}
|
||||
|
||||
async getValue() {
|
||||
return this._promise;
|
||||
}
|
||||
}
|
||||
|
||||
export class AsyncArrayObject extends ObjectClassType {
|
||||
constructor(private _promiseList: Array<Array<BaseValueObject | AsyncObject>>) {
|
||||
super();
|
||||
}
|
||||
|
||||
override isAsyncArrayObject() {
|
||||
return true;
|
||||
}
|
||||
|
||||
async getValue() {
|
||||
const variants: BaseValueObject[][] = [];
|
||||
|
||||
for (let r = 0; r < this._promiseList.length; r++) {
|
||||
const promiseCells = this._promiseList[r];
|
||||
if (variants[r] == null) {
|
||||
variants[r] = [];
|
||||
}
|
||||
for (let c = 0; c < promiseCells.length; c++) {
|
||||
const promiseCell = promiseCells[c];
|
||||
if ((promiseCell as AsyncObject).isAsyncObject()) {
|
||||
variants[r][c] = await (promiseCell as AsyncObject).getValue();
|
||||
} else {
|
||||
variants[r][c] = promiseCell as BaseValueObject;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const arrayValueObjectData: IArrayValueObject = {
|
||||
calculateValueList: variants,
|
||||
rowCount: variants.length,
|
||||
columnCount: variants[0]?.length || 0,
|
||||
unitId: '',
|
||||
sheetId: '',
|
||||
row: 0,
|
||||
column: 0,
|
||||
};
|
||||
|
||||
return ArrayValueObject.create(arrayValueObjectData);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,149 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type { IRange } from '@univerjs/core';
|
||||
|
||||
import { ErrorType } from '../../basics/error-type';
|
||||
|
||||
import { deserializeRangeWithSheetWithCache } from '../utils/reference-cache';
|
||||
import { ErrorValueObject } from '../value-object/base-value-object';
|
||||
import { BaseReferenceObject } from './base-reference-object';
|
||||
import { RangeReferenceObject } from './range-reference-object';
|
||||
|
||||
const EXCEL_MAX_ROWS = 1_048_576;
|
||||
const EXCEL_MAX_COLUMNS = 16_384;
|
||||
|
||||
export class CellReferenceObject extends BaseReferenceObject {
|
||||
constructor(token: string) {
|
||||
super(token);
|
||||
const grid = deserializeRangeWithSheetWithCache(token);
|
||||
this.setUnitQualifier(grid.unitId);
|
||||
this.setForcedUnitIdDirect(grid.unitId);
|
||||
this.setForcedSheetName(grid.sheetName);
|
||||
this.setRangeData(grid.range);
|
||||
}
|
||||
|
||||
override isCell() {
|
||||
return true;
|
||||
}
|
||||
|
||||
override isExceedRange() {
|
||||
if ((this.getForcedSheetId() == null || this.getForcedSheetId()?.length === 0) && this.getForcedSheetName().length > 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const { startRow, endRow, startColumn, endColumn } = this.getRangePosition();
|
||||
return startRow < 0 || startColumn < 0 || endRow >= EXCEL_MAX_ROWS || endColumn >= EXCEL_MAX_COLUMNS;
|
||||
}
|
||||
|
||||
override unionBy(referenceObject: BaseReferenceObject) {
|
||||
if (!referenceObject.isCell()) {
|
||||
return ErrorValueObject.create(ErrorType.REF);
|
||||
}
|
||||
|
||||
const cellReferenceObject = referenceObject as CellReferenceObject;
|
||||
// if (cellReferenceObject.getForcedSheetName() !== undefined) {
|
||||
// return ErrorValueObject.create(ErrorType.REF);
|
||||
// }
|
||||
|
||||
const newRangeData = this.unionRange(this.getRangePosition(), cellReferenceObject.getRangePosition());
|
||||
|
||||
return this._createRange(newRangeData);
|
||||
}
|
||||
|
||||
override unionRange(rangeData1: IRange, rangeData2: IRange) {
|
||||
const startRow1 = rangeData1.startRow;
|
||||
const startColumn1 = rangeData1.startColumn;
|
||||
|
||||
const startRow2 = rangeData2.startRow;
|
||||
const startColumn2 = rangeData2.startColumn;
|
||||
const range: IRange = {
|
||||
startRow: -1,
|
||||
startColumn: -1,
|
||||
endRow: -1,
|
||||
endColumn: -1,
|
||||
};
|
||||
if (startRow1 > startRow2) {
|
||||
range.startRow = startRow2;
|
||||
range.endRow = startRow1;
|
||||
} else {
|
||||
range.startRow = startRow1;
|
||||
range.endRow = startRow2;
|
||||
}
|
||||
|
||||
if (startColumn1 > startColumn2) {
|
||||
range.startColumn = startColumn2;
|
||||
range.endColumn = startColumn1;
|
||||
} else {
|
||||
range.startColumn = startColumn1;
|
||||
range.endColumn = startColumn2;
|
||||
}
|
||||
|
||||
if (rangeData1.startAbsoluteRefType) {
|
||||
range.startAbsoluteRefType = rangeData1.startAbsoluteRefType;
|
||||
}
|
||||
|
||||
if (rangeData2.startAbsoluteRefType) {
|
||||
range.endAbsoluteRefType = rangeData2.startAbsoluteRefType;
|
||||
}
|
||||
|
||||
return range;
|
||||
}
|
||||
|
||||
private _createRange(newRangeData: IRange) {
|
||||
const rangeReferenceObject = new RangeReferenceObject(
|
||||
newRangeData,
|
||||
this.getForcedSheetId(),
|
||||
this.getForcedUnitId()
|
||||
);
|
||||
|
||||
rangeReferenceObject.setUnitData(this.getUnitData());
|
||||
|
||||
rangeReferenceObject.setDefaultSheetId(this.getDefaultSheetId());
|
||||
|
||||
rangeReferenceObject.setDefaultUnitId(this.getDefaultUnitId());
|
||||
|
||||
rangeReferenceObject.setRuntimeData(this.getRuntimeData());
|
||||
|
||||
rangeReferenceObject.setUnitStylesData(this.getUnitStylesData());
|
||||
|
||||
rangeReferenceObject.setArrayFormulaCellData(this.getArrayFormulaCellData());
|
||||
|
||||
rangeReferenceObject.setRuntimeArrayFormulaCellData(this.getRuntimeArrayFormulaCellData());
|
||||
|
||||
rangeReferenceObject.setRuntimeFeatureCellData(this.getRuntimeFeatureCellData());
|
||||
|
||||
const forceSheetId = this.getForcedSheetId();
|
||||
|
||||
rangeReferenceObject.setForcedSheetName(this.getForcedSheetName());
|
||||
|
||||
if (forceSheetId != null) {
|
||||
rangeReferenceObject.setForcedSheetIdDirect(forceSheetId);
|
||||
}
|
||||
|
||||
// const forcedSheetID = this.getForcedSheetId();
|
||||
// if (forcedSheetID) {
|
||||
// rangeReferenceObject.setForcedSheetIdDirect(forcedSheetID);
|
||||
// }
|
||||
|
||||
const forcedUnitId = this.getForcedUnitId();
|
||||
if (forcedUnitId) {
|
||||
rangeReferenceObject.setForcedUnitIdDirect(forcedUnitId);
|
||||
}
|
||||
|
||||
return rangeReferenceObject;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type { IRange } from '@univerjs/core';
|
||||
import { RANGE_TYPE } from '@univerjs/core';
|
||||
import { ErrorType } from '../../basics/error-type';
|
||||
import { matchToken } from '../../basics/token';
|
||||
import { deserializeRangeWithSheetWithCache } from '../utils/reference-cache';
|
||||
import { ErrorValueObject } from '../value-object/base-value-object';
|
||||
import { BaseReferenceObject } from './base-reference-object';
|
||||
|
||||
export class ColumnReferenceObject extends BaseReferenceObject {
|
||||
constructor(token: string) {
|
||||
super(token);
|
||||
const grid = deserializeRangeWithSheetWithCache(token);
|
||||
this.setUnitQualifier(grid.unitId);
|
||||
this.setForcedUnitIdDirect(grid.unitId);
|
||||
this.setForcedSheetName(grid.sheetName);
|
||||
const range: IRange = {
|
||||
...grid.range,
|
||||
startColumn: grid.range.startColumn,
|
||||
startRow: Number.NaN,
|
||||
endColumn: grid.range.endColumn,
|
||||
endRow: Number.NaN,
|
||||
rangeType: RANGE_TYPE.COLUMN,
|
||||
};
|
||||
this.setRangeData(range);
|
||||
}
|
||||
|
||||
override isColumn() {
|
||||
return true;
|
||||
}
|
||||
|
||||
override unionBy(referenceObject: BaseReferenceObject) {
|
||||
if (!referenceObject.isColumn()) {
|
||||
return ErrorValueObject.create(ErrorType.REF);
|
||||
}
|
||||
|
||||
const columnReferenceObject = referenceObject as ColumnReferenceObject;
|
||||
if (
|
||||
columnReferenceObject.getForcedSheetName() !== undefined &&
|
||||
columnReferenceObject.getForcedSheetName() !== ''
|
||||
) {
|
||||
return ErrorValueObject.create(ErrorType.REF);
|
||||
}
|
||||
|
||||
const currentRangeData = this.getRangeData();
|
||||
|
||||
// if (currentRangeData.endColumn !== -1) {
|
||||
// return ErrorValueObject.create(ErrorType.REF);
|
||||
// }
|
||||
|
||||
const newColumnRange = columnReferenceObject.getRangeData();
|
||||
|
||||
const newColumn = newColumnRange.startColumn;
|
||||
|
||||
// if the column is already in the range, return the same object
|
||||
if (newColumn >= currentRangeData.startColumn && newColumn <= currentRangeData.endColumn) {
|
||||
return this;
|
||||
}
|
||||
|
||||
const column = currentRangeData.startColumn;
|
||||
|
||||
if (newColumn > column) {
|
||||
currentRangeData.endColumn = newColumn;
|
||||
} else {
|
||||
currentRangeData.startColumn = newColumn;
|
||||
currentRangeData.endColumn = column;
|
||||
}
|
||||
|
||||
if (newColumnRange.startAbsoluteRefType) {
|
||||
currentRangeData.endAbsoluteRefType = newColumnRange.startAbsoluteRefType;
|
||||
}
|
||||
|
||||
currentRangeData.rangeType = RANGE_TYPE.COLUMN;
|
||||
|
||||
this.setToken(`${this.getToken()}${matchToken.COLON}${columnReferenceObject.getToken()}`);
|
||||
|
||||
return this;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,451 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type { IUnitRange, Nullable } from '@univerjs/core';
|
||||
import type { ArrayValueObject } from '../value-object/array-value-object';
|
||||
import type { BaseValueObject } from '../value-object/base-value-object';
|
||||
import { createNewArray } from '../utils/array-object';
|
||||
import { ErrorValueObject } from '../value-object/base-value-object';
|
||||
import { NullValueObject } from '../value-object/primitive-object';
|
||||
import { BaseReferenceObject } from './base-reference-object';
|
||||
|
||||
export type MultiAreaValue = BaseReferenceObject | ErrorValueObject;
|
||||
|
||||
export enum MultiAreaArrayMode {
|
||||
FIRST_CELL_GRID = 'first-cell-grid',
|
||||
STACK_AREAS = 'stack-areas',
|
||||
}
|
||||
|
||||
export class MultiAreaReferenceObject extends BaseReferenceObject {
|
||||
/**
|
||||
* 2D structure:
|
||||
* - First dimension: stored by rows
|
||||
* - Second dimension: each AreaValue within that row
|
||||
*/
|
||||
private _areas: MultiAreaValue[][] = [];
|
||||
|
||||
constructor(
|
||||
token: string,
|
||||
areas: MultiAreaValue[][] = [],
|
||||
private readonly _arrayMode = MultiAreaArrayMode.FIRST_CELL_GRID
|
||||
) {
|
||||
// The parent class's rangeData becomes meaningless for multi-area.
|
||||
// We only reuse the generic infrastructure from BaseReferenceObject.
|
||||
super(token);
|
||||
this._areas = areas;
|
||||
}
|
||||
|
||||
override dispose(): void {
|
||||
this._areas.forEach((row) => {
|
||||
row.forEach((a) => a.dispose());
|
||||
});
|
||||
this._areas = [];
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------
|
||||
// Area Management
|
||||
// ------------------------------------------------------------
|
||||
|
||||
getAreas(): MultiAreaValue[][] {
|
||||
return this._areas;
|
||||
}
|
||||
|
||||
setAreas(areas: MultiAreaValue[][]): void {
|
||||
this._areas = areas;
|
||||
}
|
||||
|
||||
/**
|
||||
* Append an area:
|
||||
* - If a single AreaValue is passed, it will be wrapped as one row.
|
||||
* - If an AreaValue[] is passed, it will be inserted as an entire row.
|
||||
*/
|
||||
addArea(area: MultiAreaValue | MultiAreaValue[]): void {
|
||||
if (Array.isArray(area)) {
|
||||
this._areas.push(area);
|
||||
} else {
|
||||
this._areas.push([area]);
|
||||
}
|
||||
}
|
||||
|
||||
/** Flatten the 2D areas to reuse 1D logic */
|
||||
private _flatAreas(): MultiAreaValue[] {
|
||||
// When TS 4.x isn't smart about flat's types, you can handwrite reduce:
|
||||
// return this._areas.reduce<AreaValue[]>((acc, row) => acc.concat(row), []);
|
||||
return this._areas.flat();
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------
|
||||
// Multi-area identification
|
||||
// ------------------------------------------------------------
|
||||
|
||||
override isMultiArea(): boolean {
|
||||
return true;
|
||||
}
|
||||
|
||||
override isRange(): boolean {
|
||||
// A multi-area reference is not a single rectangle.
|
||||
return false;
|
||||
}
|
||||
|
||||
override isCell(): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
override isRow(): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
override isColumn(): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------
|
||||
// Range-related queries
|
||||
// ------------------------------------------------------------
|
||||
|
||||
override getRowCount(): number {
|
||||
if (this._arrayMode === MultiAreaArrayMode.FIRST_CELL_GRID) {
|
||||
return this._areas.length;
|
||||
}
|
||||
|
||||
let total = 0;
|
||||
for (const a of this._flatAreas()) {
|
||||
if (a instanceof BaseReferenceObject) {
|
||||
total += a.getRowCount();
|
||||
}
|
||||
}
|
||||
return total;
|
||||
}
|
||||
|
||||
override getColumnCount(): number {
|
||||
if (this._arrayMode === MultiAreaArrayMode.FIRST_CELL_GRID) {
|
||||
return this._areas.reduce((max, row) => Math.max(max, row.length), 0);
|
||||
}
|
||||
|
||||
return this._flatAreas().reduce(
|
||||
(max, area) => area instanceof BaseReferenceObject ? Math.max(max, area.getColumnCount()) : max,
|
||||
0
|
||||
);
|
||||
}
|
||||
|
||||
override isExceedRange(): boolean {
|
||||
return this._flatAreas().some((a) => {
|
||||
return a instanceof BaseReferenceObject && a.isExceedRange();
|
||||
});
|
||||
}
|
||||
|
||||
override setRefOffset(x = 0, y = 0): void {
|
||||
super.setRefOffset(x, y);
|
||||
// Propagate offset to sub-areas
|
||||
this._flatAreas().forEach((a) => {
|
||||
if (a instanceof BaseReferenceObject) {
|
||||
a.setRefOffset(x, y);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private _getReferenceArea(): BaseReferenceObject | undefined {
|
||||
return this._flatAreas().find((area): area is BaseReferenceObject => area instanceof BaseReferenceObject);
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------
|
||||
// Unit/Sheet resolution
|
||||
// ------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Multi-area reference may span multiple sheets, but Excel requires
|
||||
* them to be in the same sheet for most functions. We follow Excel semantics
|
||||
* by returning the first area’s identifiers.
|
||||
*/
|
||||
override getUnitId(): string {
|
||||
return this._getReferenceArea()?.getUnitId() ?? super.getUnitId();
|
||||
}
|
||||
|
||||
override getSheetId(): string {
|
||||
return this._getReferenceArea()?.getSheetId() ?? super.getSheetId();
|
||||
}
|
||||
|
||||
override getActiveSheetRowCount(): number {
|
||||
return this._getReferenceArea()?.getActiveSheetRowCount() ?? 0;
|
||||
}
|
||||
|
||||
override getActiveSheetColumnCount(): number {
|
||||
return this._getReferenceArea()?.getActiveSheetColumnCount() ?? 0;
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------
|
||||
// Cell traversal
|
||||
// ------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Iterate through all areas in order, flattening the multi-area into
|
||||
* a sequence of cells.
|
||||
*
|
||||
* Note: The order here is "row-major":
|
||||
* iterate by the row order of _areas, then within each row by the area order.
|
||||
*/
|
||||
override iterator(
|
||||
callback: (v: Nullable<BaseValueObject>, row: number, col: number) => Nullable<boolean>
|
||||
) {
|
||||
for (const row of this._areas) {
|
||||
let stopRow = false;
|
||||
|
||||
for (const area of row) {
|
||||
if (!(area instanceof BaseReferenceObject)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let stopArea = false;
|
||||
area.iterator((v, r, c) => {
|
||||
const res = callback(v, r, c);
|
||||
if (res === false) {
|
||||
stopArea = true;
|
||||
stopRow = true;
|
||||
return false;
|
||||
}
|
||||
return res;
|
||||
});
|
||||
|
||||
if (stopArea) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (stopRow) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Excel defines the "first cell" of a multi-area reference
|
||||
* as the first cell of the first area.
|
||||
*/
|
||||
override getFirstCell() {
|
||||
const first = this._getReferenceArea();
|
||||
if (!first) {
|
||||
return super.getFirstCell(); // fallback
|
||||
}
|
||||
return first.getFirstCell();
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------
|
||||
// Conversion to Array or Cube
|
||||
// ------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* For multi-area, we only take the *first cell* of each area and
|
||||
* arrange them into a 2D ArrayValueObject:
|
||||
*
|
||||
* - outer `_areas` dimension => rows
|
||||
* - inner `_areas[row]` dimension => columns
|
||||
*/
|
||||
override toArrayValueObject(): ArrayValueObject {
|
||||
if (this._arrayMode === MultiAreaArrayMode.STACK_AREAS) {
|
||||
return this._stackAreas();
|
||||
}
|
||||
|
||||
const rows = this._areas.length;
|
||||
|
||||
if (rows === 0) {
|
||||
// If you have a dedicated empty-array factory, replace with your implementation
|
||||
return createNewArray([], 0, 0);
|
||||
}
|
||||
|
||||
const cols = this.getColumnCount();
|
||||
|
||||
const result: BaseValueObject[][] = [];
|
||||
|
||||
for (let r = 0; r < rows; r++) {
|
||||
const rowAreas = this._areas[r] ?? [];
|
||||
result[r] = [];
|
||||
|
||||
for (let c = 0; c < cols; c++) {
|
||||
const area = rowAreas[c];
|
||||
if (!area) {
|
||||
result[r][c] = NullValueObject.create();
|
||||
continue;
|
||||
}
|
||||
|
||||
// If it's already an error value, put it directly
|
||||
if (area instanceof ErrorValueObject) {
|
||||
result[r][c] = area;
|
||||
continue;
|
||||
}
|
||||
|
||||
result[r][c] = area.toArrayValueObject(false).getRealValue(0, 0) ?? NullValueObject.create();
|
||||
}
|
||||
}
|
||||
|
||||
return createNewArray(result, rows, cols);
|
||||
}
|
||||
|
||||
private _stackAreas(): ArrayValueObject {
|
||||
const result: BaseValueObject[][] = [];
|
||||
const cols = this.getColumnCount();
|
||||
|
||||
for (const area of this._flatAreas()) {
|
||||
if (area instanceof ErrorValueObject) {
|
||||
const row = new Array<BaseValueObject>(cols).fill(NullValueObject.create());
|
||||
row[0] = area;
|
||||
result.push(row);
|
||||
continue;
|
||||
}
|
||||
|
||||
const array = area.toArrayValueObject(false);
|
||||
for (let r = 0; r < array.getRowCount(); r++) {
|
||||
const row: BaseValueObject[] = [];
|
||||
for (let c = 0; c < cols; c++) {
|
||||
row[c] = array.getRealValue(r, c) ?? NullValueObject.create();
|
||||
}
|
||||
result.push(row);
|
||||
}
|
||||
}
|
||||
|
||||
return createNewArray(result, result.length, cols);
|
||||
}
|
||||
|
||||
override getRangePosition() {
|
||||
const flat = this._flatAreas();
|
||||
|
||||
// If there is no area, fall back to BaseReferenceObject behavior.
|
||||
if (!flat.length) {
|
||||
return super.getRangePosition();
|
||||
}
|
||||
|
||||
let minStartRow = Number.POSITIVE_INFINITY;
|
||||
let minStartColumn = Number.POSITIVE_INFINITY;
|
||||
let maxEndRow = Number.NEGATIVE_INFINITY;
|
||||
let maxEndColumn = Number.NEGATIVE_INFINITY;
|
||||
|
||||
for (const area of flat) {
|
||||
if (!(area instanceof BaseReferenceObject)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const { startRow, startColumn, endRow, endColumn } = area.getRangePosition();
|
||||
|
||||
// Skip any invalid range silently.
|
||||
if (
|
||||
!Number.isFinite(startRow) ||
|
||||
!Number.isFinite(startColumn) ||
|
||||
!Number.isFinite(endRow) ||
|
||||
!Number.isFinite(endColumn)
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (startRow < minStartRow) minStartRow = startRow;
|
||||
if (startColumn < minStartColumn) minStartColumn = startColumn;
|
||||
if (endRow > maxEndRow) maxEndRow = endRow;
|
||||
if (endColumn > maxEndColumn) maxEndColumn = maxEndColumn < endColumn ? endColumn : maxEndColumn;
|
||||
}
|
||||
|
||||
// If all areas were invalid, defer to the parent implementation.
|
||||
if (
|
||||
!Number.isFinite(minStartRow) ||
|
||||
!Number.isFinite(minStartColumn) ||
|
||||
!Number.isFinite(maxEndRow) ||
|
||||
!Number.isFinite(maxEndColumn)
|
||||
) {
|
||||
return super.getRangePosition();
|
||||
}
|
||||
|
||||
return {
|
||||
startRow: minStartRow,
|
||||
startColumn: minStartColumn,
|
||||
endRow: maxEndRow,
|
||||
endColumn: maxEndColumn,
|
||||
};
|
||||
}
|
||||
|
||||
override toUnitRange() {
|
||||
// For a multi-area reference, this returns the bounding rectangle
|
||||
// that covers all areas, along with the representative unitId/sheetId.
|
||||
//
|
||||
// This is useful for features that only need an approximate outer range,
|
||||
// such as viewport calculations or rough dependency tracking.
|
||||
const range = this.getRangePosition();
|
||||
|
||||
return {
|
||||
range,
|
||||
sheetId: this.getSheetId(),
|
||||
unitId: this.getUnitId(),
|
||||
};
|
||||
}
|
||||
|
||||
override toUnitRanges(): IUnitRange[] {
|
||||
return this._flatAreas()
|
||||
.filter((area): area is BaseReferenceObject => area instanceof BaseReferenceObject)
|
||||
.flatMap((area) => area.toUnitRanges());
|
||||
}
|
||||
|
||||
override getRangeData() {
|
||||
const flat = this._flatAreas();
|
||||
|
||||
// No areas: fall back to the base implementation (which usually returns the
|
||||
// internal _rangeData or some default).
|
||||
if (!flat.length) {
|
||||
return super.getRangeData();
|
||||
}
|
||||
|
||||
let minStartRow = Number.POSITIVE_INFINITY;
|
||||
let minStartColumn = Number.POSITIVE_INFINITY;
|
||||
let maxEndRow = Number.NEGATIVE_INFINITY;
|
||||
let maxEndColumn = Number.NEGATIVE_INFINITY;
|
||||
|
||||
for (const area of flat) {
|
||||
if (!(area instanceof BaseReferenceObject)) {
|
||||
continue;
|
||||
}
|
||||
// Use the raw rangeData (without offset) of each area
|
||||
const { startRow, startColumn, endRow, endColumn } = area.getRangeData();
|
||||
|
||||
// Skip invalid/NaN ranges silently
|
||||
if (
|
||||
!Number.isFinite(startRow) ||
|
||||
!Number.isFinite(startColumn) ||
|
||||
!Number.isFinite(endRow) ||
|
||||
!Number.isFinite(endColumn)
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (startRow < minStartRow) minStartRow = startRow;
|
||||
if (startColumn < minStartColumn) minStartColumn = startColumn;
|
||||
if (endRow > maxEndRow) maxEndRow = endRow;
|
||||
if (endColumn > maxEndColumn) maxEndColumn = endColumn;
|
||||
}
|
||||
|
||||
// If all areas were invalid, reuse the parent behavior.
|
||||
if (
|
||||
!Number.isFinite(minStartRow) ||
|
||||
!Number.isFinite(minStartColumn) ||
|
||||
!Number.isFinite(maxEndRow) ||
|
||||
!Number.isFinite(maxEndColumn)
|
||||
) {
|
||||
return super.getRangeData();
|
||||
}
|
||||
|
||||
return {
|
||||
startRow: minStartRow,
|
||||
startColumn: minStartColumn,
|
||||
endRow: maxEndRow,
|
||||
endColumn: maxEndColumn,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type { IRange, Nullable } from '@univerjs/core';
|
||||
|
||||
import { BaseReferenceObject } from './base-reference-object';
|
||||
|
||||
export class RangeReferenceObject extends BaseReferenceObject {
|
||||
constructor(range: IRange, forcedSheetId?: Nullable<string>, forcedUnitId?: string) {
|
||||
super('');
|
||||
this.setRangeData(range);
|
||||
if (forcedSheetId) {
|
||||
this.setForcedSheetIdDirect(forcedSheetId);
|
||||
}
|
||||
|
||||
if (forcedUnitId) {
|
||||
this.setForcedUnitIdDirect(forcedUnitId);
|
||||
}
|
||||
}
|
||||
|
||||
override isRange() {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type { IRange } from '@univerjs/core';
|
||||
import { RANGE_TYPE } from '@univerjs/core';
|
||||
|
||||
import { ErrorType } from '../../basics/error-type';
|
||||
import { matchToken } from '../../basics/token';
|
||||
|
||||
import { deserializeRangeWithSheetWithCache } from '../utils/reference-cache';
|
||||
import { ErrorValueObject } from '../value-object/base-value-object';
|
||||
import { BaseReferenceObject } from './base-reference-object';
|
||||
|
||||
export class RowReferenceObject extends BaseReferenceObject {
|
||||
constructor(token: string) {
|
||||
super(token);
|
||||
const grid = deserializeRangeWithSheetWithCache(token);
|
||||
this.setUnitQualifier(grid.unitId);
|
||||
this.setForcedUnitIdDirect(grid.unitId);
|
||||
this.setForcedSheetName(grid.sheetName);
|
||||
const range: IRange = {
|
||||
...grid.range,
|
||||
startColumn: Number.NaN,
|
||||
startRow: grid.range.startRow,
|
||||
endColumn: Number.NaN,
|
||||
endRow: grid.range.endRow,
|
||||
rangeType: RANGE_TYPE.ROW,
|
||||
};
|
||||
this.setRangeData(range);
|
||||
}
|
||||
|
||||
override isRow() {
|
||||
return true;
|
||||
}
|
||||
|
||||
override unionBy(referenceObject: BaseReferenceObject) {
|
||||
if (!referenceObject.isRow()) {
|
||||
return ErrorValueObject.create(ErrorType.REF);
|
||||
}
|
||||
|
||||
const rowReferenceObject = referenceObject as RowReferenceObject;
|
||||
if (rowReferenceObject.getForcedSheetName() !== undefined && rowReferenceObject.getForcedSheetName() !== '') {
|
||||
return ErrorValueObject.create(ErrorType.REF);
|
||||
}
|
||||
|
||||
const currentRangeData = this.getRangeData();
|
||||
|
||||
// if (currentRangeData.endRow !== -1) {
|
||||
// return ErrorValueObject.create(ErrorType.REF);
|
||||
// }
|
||||
const newRowRange = rowReferenceObject.getRangeData();
|
||||
|
||||
const newRow = newRowRange.startRow;
|
||||
|
||||
// if the row is already in the range, return the same object
|
||||
if (newRow >= currentRangeData.startRow && newRow <= currentRangeData.endRow) {
|
||||
return this;
|
||||
}
|
||||
|
||||
const row = currentRangeData.startRow;
|
||||
|
||||
if (newRow > row) {
|
||||
currentRangeData.endRow = newRow;
|
||||
} else {
|
||||
currentRangeData.startRow = newRow;
|
||||
currentRangeData.endRow = row;
|
||||
}
|
||||
|
||||
if (newRowRange.startAbsoluteRefType) {
|
||||
currentRangeData.endAbsoluteRefType = newRowRange.startAbsoluteRefType;
|
||||
}
|
||||
|
||||
currentRangeData.rangeType = RANGE_TYPE.ROW;
|
||||
|
||||
this.setToken(`${this.getToken()}${matchToken.COLON}${rowReferenceObject.getToken()}`);
|
||||
|
||||
return this;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,375 @@
|
||||
/**
|
||||
* Copyright 2023-present DreamNum Co., Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type { ICellData, Nullable } from '@univerjs/core';
|
||||
import type { ISuperTable, IUnitSheetNameMap } from '../../basics/common';
|
||||
import { TableOptionType } from '../../basics/common';
|
||||
import { ErrorType } from '../../basics/error-type';
|
||||
import { matchToken } from '../../basics/token';
|
||||
import { BaseReferenceObject } from './base-reference-object';
|
||||
|
||||
/**
|
||||
* Table reference object compatible with Excel Structured References
|
||||
*
|
||||
* Supported (examples):
|
||||
* - =Table[Column] // Single column (default DATA)
|
||||
* - =Table[[ColumnA]:[ColumnB]] // Column range (default DATA)
|
||||
* - =Table[#All] / [#Data] / [#Headers] / [#Totals] / [#This Row]
|
||||
* - =Table[[#All],[Column]] // Section + column/column range
|
||||
* - =Table[[#Data],[ColA]:[ColB]]
|
||||
* - =Table[[#This Row],[Column]]
|
||||
*
|
||||
* Not supported (invalid):
|
||||
* - Multiple sections in parallel (e.g., [[#Headers],[#Data]])
|
||||
* - Non-existent sections (e.g., #Title)
|
||||
* - Column indices instead of column names (e.g., [[1]:[3]])
|
||||
*/
|
||||
export class TableReferenceObject extends BaseReferenceObject {
|
||||
private _isCurrentRowForRange: boolean = false;
|
||||
|
||||
constructor(
|
||||
token: string,
|
||||
private _tableData: ISuperTable,
|
||||
/**
|
||||
* Structured reference body (with or without outer []):
|
||||
* - "#Data" / "#All"
|
||||
* - "[Col]" / "[[ColA]:[ColB]]"
|
||||
* - "[[#Data],[Col]]" / "[[#All],[ColA]:[ColB]]"
|
||||
*/
|
||||
private _columnDataString: string | undefined,
|
||||
/** Section text to enum mapping ('#All', '#Data', '#Headers', '#Totals', '#This Row') */
|
||||
private _tableOptionMap: Map<string, TableOptionType>
|
||||
) {
|
||||
super(token);
|
||||
|
||||
const { sheetId, range, titleMap } = this._tableData;
|
||||
this.setForcedSheetIdDirect(sheetId);
|
||||
|
||||
const { startColumn, endColumn, type } = this._parseStructuredRef(this._columnDataString, titleMap);
|
||||
|
||||
const tableStartRow = range.startRow;
|
||||
const tableEndRow = range.endRow;
|
||||
const dataStartRow = tableStartRow + (this._tableData.showHeader === false ? 0 : 1);
|
||||
const dataEndRow = tableEndRow - (this._tableData.showFooter === true ? 1 : 0);
|
||||
|
||||
let startRow = -1;
|
||||
let endRow = -1;
|
||||
|
||||
switch (type) {
|
||||
case TableOptionType.ALL:
|
||||
startRow = tableStartRow;
|
||||
endRow = tableEndRow;
|
||||
break;
|
||||
case TableOptionType.DATA:
|
||||
startRow = dataStartRow;
|
||||
endRow = dataEndRow;
|
||||
break;
|
||||
case TableOptionType.HEADERS:
|
||||
if (this._tableData.showHeader === false) {
|
||||
startRow = -1;
|
||||
endRow = -1;
|
||||
break;
|
||||
}
|
||||
startRow = tableStartRow;
|
||||
endRow = tableStartRow;
|
||||
break;
|
||||
case TableOptionType.TOTALS:
|
||||
startRow = tableEndRow;
|
||||
endRow = tableEndRow;
|
||||
break;
|
||||
case TableOptionType.THIS_ROW: {
|
||||
const r = this._resolveThisRow(dataStartRow, dataEndRow);
|
||||
startRow = r;
|
||||
endRow = r;
|
||||
break;
|
||||
}
|
||||
default:
|
||||
startRow = dataStartRow;
|
||||
endRow = dataEndRow;
|
||||
break;
|
||||
}
|
||||
|
||||
this.setRangeData({
|
||||
startColumn,
|
||||
endColumn,
|
||||
startRow,
|
||||
endRow,
|
||||
});
|
||||
}
|
||||
|
||||
override getRangeData() {
|
||||
const rangeData = super.getRangeData();
|
||||
if (this._isCurrentRowForRange) {
|
||||
const { startRow, startColumn, endRow, endColumn } = rangeData;
|
||||
const currentRow = this.getCurrentRow();
|
||||
return {
|
||||
startRow: currentRow == null ? startRow : currentRow,
|
||||
endRow: currentRow == null ? endRow : currentRow,
|
||||
startColumn,
|
||||
endColumn,
|
||||
};
|
||||
}
|
||||
return rangeData;
|
||||
}
|
||||
|
||||
override getCellData(row: number, column: number): Nullable<ICellData> {
|
||||
if (this._isCurrentRowForRange) {
|
||||
const { startRow, endRow } = this._tableData.range;
|
||||
const lastCurrentRow = endRow - (this._tableData.showFooter === true ? 1 : 0);
|
||||
if (row < startRow || row > lastCurrentRow) {
|
||||
return { v: ErrorType.NA };
|
||||
}
|
||||
}
|
||||
return super.getCellData(row, column);
|
||||
}
|
||||
|
||||
override getRefOffset() {
|
||||
return {
|
||||
x: 0,
|
||||
y: 0,
|
||||
};
|
||||
}
|
||||
|
||||
override isTable() {
|
||||
return true;
|
||||
}
|
||||
|
||||
override isCurrentRowForRange(): boolean {
|
||||
return this._isCurrentRowForRange;
|
||||
}
|
||||
|
||||
override setForcedSheetId(sheetNameMap: IUnitSheetNameMap) {
|
||||
// No-op: already set in constructor
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse structured reference body, returning column range and Section type.
|
||||
* Determination rules:
|
||||
* - Whether it's a Section depends on "whether it starts with # after stripping", not whether it still has brackets.
|
||||
* - No comma: Either Section, or column/column range
|
||||
* - Has comma: Left is Section, right is column/column range
|
||||
*/
|
||||
private _parseStructuredRef(
|
||||
raw: string | undefined,
|
||||
titleMap: Map<string, number>
|
||||
): { startColumn: number; endColumn: number; type: TableOptionType } {
|
||||
const { range } = this._tableData;
|
||||
const fullStartCol = range.startColumn ?? 0;
|
||||
const fullEndCol = range.endColumn ?? 0;
|
||||
|
||||
if (!raw || raw.trim().length === 0) {
|
||||
// Empty reference: By conventional semantics → all columns in data area
|
||||
return { startColumn: fullStartCol, endColumn: fullEndCol, type: TableOptionType.DATA };
|
||||
}
|
||||
|
||||
const s = raw.trim();
|
||||
|
||||
// Section-only form (without brackets): e.g., "#Data"
|
||||
if (s[0] !== '[') {
|
||||
const typeMaybe = this._mapSection(s); // Only hits if starts with #
|
||||
if (typeMaybe !== undefined) {
|
||||
return { startColumn: fullStartCol, endColumn: fullEndCol, type: typeMaybe };
|
||||
}
|
||||
// Not a Section: Treat as column name (rare, but for error tolerance)
|
||||
const { startColumn, endColumn } = this._parseColumnOrRange(s, titleMap, fullStartCol);
|
||||
return { startColumn, endColumn, type: TableOptionType.DATA };
|
||||
}
|
||||
|
||||
// Strip one layer of outer brackets
|
||||
const body = this._stripOuterBracketOnce(s);
|
||||
const commaAt = this._findCommaAtTopLevel(body);
|
||||
|
||||
if (body.length === 0) {
|
||||
// Empty brackets: By conventional semantics → all columns in data area
|
||||
return { startColumn: fullStartCol, endColumn: fullEndCol, type: TableOptionType.DATA };
|
||||
}
|
||||
|
||||
if (commaAt === -1) {
|
||||
// No comma: Either Section, or column/column range
|
||||
if (body.startsWith('#')) {
|
||||
const typeMaybe = this._mapSection(body);
|
||||
if (typeMaybe !== undefined) {
|
||||
return { startColumn: fullStartCol, endColumn: fullEndCol, type: typeMaybe };
|
||||
}
|
||||
// '#...' but not recognized → fallback to DATA (could also throw error)
|
||||
return { startColumn: fullStartCol, endColumn: fullEndCol, type: TableOptionType.DATA };
|
||||
}
|
||||
// Not a Section: column/column range. Note: passing original s (with brackets) is more robust.
|
||||
const { startColumn, endColumn } = this._parseColumnOrRange(body, titleMap, fullStartCol);
|
||||
return { startColumn, endColumn, type: TableOptionType.DATA };
|
||||
}
|
||||
|
||||
// Has comma: Left Section + Right column/column range
|
||||
const sectionRaw = body.slice(0, commaAt).trim(); // May be "[#Data]" or "#Data"
|
||||
const columnsRaw = body.slice(commaAt + 1).trim(); // May be "[Col]" or "[[ColA]:[ColB]]" or "Col"
|
||||
|
||||
const section = this._parseSectionMaybeBracketed(sectionRaw);
|
||||
if (section === undefined) {
|
||||
const { startColumn, endColumn } = this._parseColumnOrRange(body, titleMap, fullStartCol);
|
||||
return { startColumn, endColumn, type: TableOptionType.DATA };
|
||||
}
|
||||
const { startColumn, endColumn } = this._parseColumnOrRange(columnsRaw, titleMap, fullStartCol);
|
||||
|
||||
return { startColumn, endColumn, type: section };
|
||||
}
|
||||
|
||||
/** Strip one layer of outer brackets from "[...]" (return as-is if none) */
|
||||
private _stripOuterBracketOnce(s: string): string {
|
||||
if (s.length >= 2 && s[0] === '[' && s[s.length - 1] === ']') {
|
||||
return s.slice(1, -1);
|
||||
}
|
||||
return s;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find first comma at depth=0 (used to split Section and column parts)
|
||||
* Compatible with nesting like "[[#Data],[ColA]:[ColB]]".
|
||||
*/
|
||||
private _findCommaAtTopLevel(s: string): number {
|
||||
let depth = 0;
|
||||
for (let i = 0; i < s.length; i++) {
|
||||
const ch = s[i];
|
||||
if (ch === '[') depth++;
|
||||
else if (ch === ']') depth = Math.max(0, depth - 1);
|
||||
else if (ch === matchToken.COMMA && depth === 0) return i;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse Section, compatible with both "[#Data]" and "#Data" inputs
|
||||
* Returns TableOptionType if matched; returns undefined when the comma belongs to a column title.
|
||||
*/
|
||||
private _parseSectionMaybeBracketed(raw: string): TableOptionType | undefined {
|
||||
const x = raw.trim();
|
||||
const inner = (x.startsWith('[') && x.endsWith(']')) ? this._stripOuterBracketOnce(x) : x;
|
||||
return this._mapSection(inner);
|
||||
}
|
||||
|
||||
/**
|
||||
* Section mapping: Only accepts keywords starting with #.
|
||||
* Returns undefined if not a valid Section (caller treats as column or fallback).
|
||||
*/
|
||||
private _mapSection(x: string): TableOptionType | undefined {
|
||||
const key = x.trim();
|
||||
if (!key.startsWith('#')) return undefined;
|
||||
|
||||
// Exact match
|
||||
const hit = this._tableOptionMap.get(key);
|
||||
if (hit !== undefined) return hit;
|
||||
|
||||
// Lenient: Remove extra spaces / ignore case
|
||||
const norm = key.replace(/\s+/g, ' ').toLowerCase();
|
||||
for (const [k, v] of this._tableOptionMap.entries()) {
|
||||
const kk = k.replace(/\s+/g, ' ').toLowerCase();
|
||||
if (kk === norm) return v;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse column selection:
|
||||
* - "[Col]" / "Col" => Single column
|
||||
* - "[[ColA]:[ColB]]" / "[ColA]:[ColB]" / "ColA:ColB" => Column range
|
||||
*
|
||||
* Rules:
|
||||
* - First find colon at top level, strip one layer of brackets from left and right sides (if present)
|
||||
* - Returns -1 if column name not found (caller should handle as parse error)
|
||||
*/
|
||||
private _parseColumnOrRange(
|
||||
raw: string,
|
||||
titleMap: Map<string, number>,
|
||||
fullStartCol: number
|
||||
): { startColumn: number; endColumn: number } {
|
||||
const s = raw.trim();
|
||||
|
||||
const singleColumnIndex = this._titleToIndex(this._stripOuterBracketIfAny(s), titleMap);
|
||||
if (singleColumnIndex !== -1) {
|
||||
const column = fullStartCol + singleColumnIndex;
|
||||
return { startColumn: column, endColumn: column };
|
||||
}
|
||||
|
||||
// Check if there's a range colon at top level
|
||||
const colonAt = this._findColonAtTopLevel(s);
|
||||
if (colonAt === -1) {
|
||||
// Single column (may be "[Col]" or "Col")
|
||||
const name = this._stripOuterBracketIfAny(s);
|
||||
const idx = this._titleToIndex(name, titleMap);
|
||||
return { startColumn: fullStartCol + idx, endColumn: +fullStartCol + idx };
|
||||
}
|
||||
|
||||
// Range: Left and right may be "[ColA]" / "ColA"
|
||||
const left = s.slice(0, colonAt).trim();
|
||||
const right = s.slice(colonAt + 1).trim();
|
||||
|
||||
const leftName = this._stripOuterBracketIfAny(left);
|
||||
const rightName = this._stripOuterBracketIfAny(right);
|
||||
|
||||
const l = this._titleToIndex(leftName, titleMap);
|
||||
const r = this._titleToIndex(rightName, titleMap);
|
||||
|
||||
// Excel requires l <= r; swap for error tolerance here (could throw error for strict conformance)
|
||||
if (l !== -1 && r !== -1 && l > r) {
|
||||
return { startColumn: fullStartCol + r, endColumn: fullStartCol + l };
|
||||
}
|
||||
return { startColumn: fullStartCol + l, endColumn: fullStartCol + r };
|
||||
}
|
||||
|
||||
/** Strip one layer of outer brackets; return as-is if none (compatible with "[Col]" and "Col") */
|
||||
private _stripOuterBracketIfAny(t: string): string {
|
||||
return (t.length >= 2 && t[0] === '[' && t[t.length - 1] === ']')
|
||||
? t.slice(1, -1)
|
||||
: t;
|
||||
}
|
||||
|
||||
/** Find range colon at depth=0 */
|
||||
private _findColonAtTopLevel(s: string): number {
|
||||
let depth = 0;
|
||||
for (let i = 0; i < s.length; i++) {
|
||||
const ch = s[i];
|
||||
if (ch === '[') depth++;
|
||||
else if (ch === ']') depth = Math.max(0, depth - 1);
|
||||
else if (ch === matchToken.COLON && depth === 0) return i;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
/** Column title → column index; returns -1 if not found (caller should handle as parse error) */
|
||||
private _titleToIndex(name: string, titleMap: Map<string, number>): number {
|
||||
const key = name.trim();
|
||||
const hit = titleMap.get(key);
|
||||
if (hit !== undefined) return hit;
|
||||
|
||||
// Defensive: Full-width space → half-width, then trim
|
||||
const keyNorm = key.replace(/\u3000/g, ' ').trim();
|
||||
const normalizedHit = titleMap.get(keyNorm);
|
||||
if (normalizedHit !== undefined) return normalizedHit;
|
||||
|
||||
const lineBreakNorm = keyNorm.replace(/\r\n?/g, '\n');
|
||||
for (const [title, index] of titleMap.entries()) {
|
||||
if (title.replace(/\u3000/g, ' ').trim().replace(/\r\n?/g, '\n') === lineBreakNorm) {
|
||||
return index;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
/** Resolve #This Row's row number; takes first data row (tableStartRow+1) when no context available */
|
||||
private _resolveThisRow(dataStartRow: number, dataEndRow: number): number {
|
||||
this._isCurrentRowForRange = true;
|
||||
return Math.min(dataStartRow, dataEndRow);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user