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

This commit is contained in:
upstream-mirror
2026-09-16 05:09:45 +05:00
commit bcf6ef63ad
7118 changed files with 1236854 additions and 0 deletions

View File

@@ -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);
});
});

View File

@@ -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([]);
});
});

View File

@@ -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.
*/

View File

@@ -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);
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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,
};
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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);
}
}