diff --git a/packages/core/src/shared/__test__/common.spec.ts b/packages/core/src/shared/__test__/common.spec.ts index d33930e9eda..f12ed29d0a3 100644 --- a/packages/core/src/shared/__test__/common.spec.ts +++ b/packages/core/src/shared/__test__/common.spec.ts @@ -15,10 +15,34 @@ */ import { describe, expect, it } from 'vitest'; -import { cellToRange } from '../common'; +import { cellToRange, isFormulaId, isFormulaString } from '../common'; describe('Test common', () => { it('Test cellToRange', () => { expect(cellToRange(0, 1)).toStrictEqual({ startRow: 0, startColumn: 1, endRow: 0, endColumn: 1 }); }); + + it('Test isFormulaString', () => { + expect(isFormulaString('=SUM(1)')).toBe(true); + expect(isFormulaString('SUM(1)')).toBe(false); + expect(isFormulaString('=')).toBe(false); + expect(isFormulaString('')).toBe(false); + expect(isFormulaString(1)).toBe(false); + expect(isFormulaString(null)).toBe(false); + expect(isFormulaString(undefined)).toBe(false); + expect(isFormulaString(true)).toBe(false); + expect(isFormulaString({})).toBe(false); + expect(isFormulaString({ f: '' })).toBe(false); + }); + + it('Test isFormulaId', () => { + expect(isFormulaId('id1')).toBe(true); + expect(isFormulaId('')).toBe(false); + expect(isFormulaId(1)).toBe(false); + expect(isFormulaId(null)).toBe(false); + expect(isFormulaId(undefined)).toBe(false); + expect(isFormulaId(true)).toBe(false); + expect(isFormulaId({})).toBe(false); + expect(isFormulaId({ f: '' })).toBe(false); + }); }); diff --git a/packages/core/src/shared/common.ts b/packages/core/src/shared/common.ts index 0601d180bc8..27631985102 100644 --- a/packages/core/src/shared/common.ts +++ b/packages/core/src/shared/common.ts @@ -146,12 +146,22 @@ export function getColorStyle(color: Nullable): Nullable { return null; } +/** + * A string starting with an equal sign is a formula + * @param value + * @returns + */ export function isFormulaString(value: any): boolean { return Tools.isString(value) && value.substring(0, 1) === '=' && value.length > 1; } +/** + * any string + * @param value + * @returns + */ export function isFormulaId(value: any): boolean { - return Tools.isString(value) && value.indexOf('=') === -1 && value.length === 6; + return Tools.isString(value) && value.length > 0; } /** diff --git a/packages/core/src/types/interfaces/i-range.ts b/packages/core/src/types/interfaces/i-range.ts index bb5f3087556..ec7a505db00 100644 --- a/packages/core/src/types/interfaces/i-range.ts +++ b/packages/core/src/types/interfaces/i-range.ts @@ -213,9 +213,6 @@ export interface IOptionData { * */ contentsOnly?: boolean; - /** - * Whether to clear only the comments. - */ } /** diff --git a/packages/engine-formula/src/commands/mutations/set-array-formula-data.mutation.ts b/packages/engine-formula/src/commands/mutations/set-array-formula-data.mutation.ts index 44e7152ab35..a28ed892ddb 100644 --- a/packages/engine-formula/src/commands/mutations/set-array-formula-data.mutation.ts +++ b/packages/engine-formula/src/commands/mutations/set-array-formula-data.mutation.ts @@ -15,34 +15,23 @@ */ import type { IMutation } from '@univerjs/core'; -import { CommandType, Tools } from '@univerjs/core'; +import { CommandType } from '@univerjs/core'; import type { IAccessor } from '@wendellhu/redi'; import type { IArrayFormulaRangeType, IArrayFormulaUnitCellType } from '../../basics/common'; -import { FormulaDataModel } from '../../models/formula-data.model'; export interface ISetArrayFormulaDataMutationParams { arrayFormulaRange: IArrayFormulaRangeType; arrayFormulaCellData: IArrayFormulaUnitCellType; } -export const SetArrayFormulaDataUndoMutationFactory = (accessor: IAccessor): ISetArrayFormulaDataMutationParams => { - const formulaDataModel = accessor.get(FormulaDataModel); - const arrayFormulaRange = Tools.deepClone(formulaDataModel.getArrayFormulaRange()); - const arrayFormulaCellData = Tools.deepClone(formulaDataModel.getArrayFormulaCellData()); - return { - arrayFormulaRange, - arrayFormulaCellData, - }; -}; - +/** + * There is no need to process data here, it is used as the main thread to send data to the worker. The main thread has already updated the data in advance, and there is no need to update it again here. + */ export const SetArrayFormulaDataMutation: IMutation = { id: 'formula.mutation.set-array-formula-data', type: CommandType.MUTATION, handler: (accessor: IAccessor, params: ISetArrayFormulaDataMutationParams) => { - const formulaDataModel = accessor.get(FormulaDataModel); - formulaDataModel.setArrayFormulaRange(params.arrayFormulaRange); - formulaDataModel.setArrayFormulaCellData(params.arrayFormulaCellData); return true; }, }; diff --git a/packages/engine-formula/src/commands/mutations/set-formula-data.mutation.ts b/packages/engine-formula/src/commands/mutations/set-formula-data.mutation.ts index 30f8dbab60f..d83f30eb9fc 100644 --- a/packages/engine-formula/src/commands/mutations/set-formula-data.mutation.ts +++ b/packages/engine-formula/src/commands/mutations/set-formula-data.mutation.ts @@ -19,18 +19,18 @@ import { CommandType } from '@univerjs/core'; import type { IAccessor } from '@wendellhu/redi'; import type { IFormulaData } from '../../basics/common'; -import { FormulaDataModel } from '../../models/formula-data.model'; export interface ISetFormulaDataMutationParams { formulaData: IFormulaData; } +/** + * There is no need to process data here, it is used as the main thread to send data to the worker. The main thread has already updated the data in advance, and there is no need to update it again here. + */ export const SetFormulaDataMutation: IMutation = { id: 'formula.mutation.set-formula-data', type: CommandType.MUTATION, handler: (accessor: IAccessor, params: ISetFormulaDataMutationParams) => { - const formulaDataModel = accessor.get(FormulaDataModel); - formulaDataModel.setFormulaData(params.formulaData); return true; }, }; diff --git a/packages/engine-formula/src/controller/calculate.controller.ts b/packages/engine-formula/src/controller/calculate.controller.ts index b769eb15f0d..4278fb5dba1 100644 --- a/packages/engine-formula/src/controller/calculate.controller.ts +++ b/packages/engine-formula/src/controller/calculate.controller.ts @@ -64,7 +64,9 @@ export class CalculateController extends Disposable { this._calculateFormulaService.stopFormulaExecution(); } else if (command.id === SetFormulaDataMutation.id) { const formulaData = (command.params as ISetFormulaDataMutationParams).formulaData as IFormulaData; - this._formulaDataModel.setFormulaData(formulaData); + + // formulaData is the incremental data sent from the main thread and needs to be merged into formulaDataModel + this._formulaDataModel.mergeFormulaData(formulaData); } else if (command.id === SetFormulaCalculationStartMutation.id) { const params = command.params as ISetFormulaCalculationStartMutation; @@ -83,6 +85,7 @@ export class CalculateController extends Disposable { } const { arrayFormulaRange, arrayFormulaCellData } = params; + // TODO@Dushusir: Merge the array formula data into the formulaDataModel this._formulaDataModel.setArrayFormulaRange(arrayFormulaRange); this._formulaDataModel.setArrayFormulaCellData(arrayFormulaCellData); } @@ -114,8 +117,6 @@ export class CalculateController extends Disposable { const arrayFormulaCellData = this._formulaDataModel.getArrayFormulaCellData(); - // Synchronous to the main thread - // this._commandService.executeCommand(SetFormulaDataMutation.id, { formulaData }); this._calculateFormulaService.execute({ formulaData, arrayFormulaCellData, diff --git a/packages/engine-formula/src/index.ts b/packages/engine-formula/src/index.ts index 281ffa9d509..1962b119765 100644 --- a/packages/engine-formula/src/index.ts +++ b/packages/engine-formula/src/index.ts @@ -41,7 +41,6 @@ export { RegisterFunctionMutation } from './commands/mutations/register-function export { type ISetArrayFormulaDataMutationParams, SetArrayFormulaDataMutation, - SetArrayFormulaDataUndoMutationFactory, } from './commands/mutations/set-array-formula-data.mutation'; export { RemoveDefinedNameMutation, SetDefinedNameMutation, type ISetDefinedNameMutationSearchParam, type ISetDefinedNameMutationParam } from './commands/mutations/set-defined-name.mutation'; @@ -147,3 +146,5 @@ export { IFormulaRuntimeService, FormulaRuntimeService } from './services/runtim export { IFormulaCurrentConfigService, FormulaCurrentConfigService } from './services/current-data.service'; export { IActiveDirtyManagerService } from './services/active-dirty-manager.service'; + +export type { IRangeChange } from './models/formula-data.model'; diff --git a/packages/engine-formula/src/models/formula-data.model.ts b/packages/engine-formula/src/models/formula-data.model.ts index 42a34b30282..43df28e9341 100644 --- a/packages/engine-formula/src/models/formula-data.model.ts +++ b/packages/engine-formula/src/models/formula-data.model.ts @@ -30,11 +30,12 @@ import type { IUnitSheetNameMap, } from '../basics/common'; import { LexerTreeBuilder } from '../engine/analysis/lexer-tree-builder'; +import type { IFormulaIdMap } from './utils/formula-data-util'; +import { clearArrayFormulaCellDataByCell, updateFormulaDataByCellValue } from './utils/formula-data-util'; -export interface IFormulaIdMap { - f: string; - r: number; - c: number; +export interface IRangeChange { + oldCell: IRange; + newCell: IRange; } export class FormulaDataModel extends Disposable { @@ -116,19 +117,8 @@ export class FormulaDataModel extends Disposable { Object.keys(sheetData).forEach((sheetId) => { const cellMatrixData = sheetData[sheetId]; // The runtime data for array formula value calculated by the formula engine. - let arrayFormulaRangeMatrix = new ObjectMatrix(); // Original array formula range. - - let arrayFormulaCellMatrixData = new ObjectMatrix>(); // Original array formula cell data. - - if (this._arrayFormulaRange[unitId]?.[sheetId] != null) { - arrayFormulaRangeMatrix = new ObjectMatrix(this._arrayFormulaRange[unitId]?.[sheetId]); - } - - if (this._arrayFormulaCellData[unitId]?.[sheetId] != null) { - arrayFormulaCellMatrixData = new ObjectMatrix>( - this._arrayFormulaCellData[unitId]?.[sheetId] - ); - } + const arrayFormulaRangeMatrix = new ObjectMatrix(this._arrayFormulaRange[unitId]?.[sheetId]); // Original array formula range. + const arrayFormulaCellMatrixData = new ObjectMatrix>(this._arrayFormulaCellData[unitId]?.[sheetId]); // Original array formula cell data. /** * If the calculated value of the array formula is updated, clear the values within the original data formula range. @@ -238,12 +228,7 @@ export class FormulaDataModel extends Disposable { Object.keys(sheetData).forEach((sheetId) => { const arrayFormula = new ObjectMatrix(sheetData[sheetId]); - - let rangeMatrix = new ObjectMatrix(); - - if (this._arrayFormulaRange[unitId]?.[sheetId]) { - rangeMatrix = new ObjectMatrix(this._arrayFormulaRange[unitId]?.[sheetId]); - } + const rangeMatrix = new ObjectMatrix(this._arrayFormulaRange[unitId]?.[sheetId]); arrayFormula.forValue((r, c, v) => { rangeMatrix.setValue(r, c, v); @@ -256,6 +241,51 @@ export class FormulaDataModel extends Disposable { }); } + mergeFormulaData(formulaData: IFormulaData) { + Object.keys(formulaData).forEach((unitId) => { + const sheetData = formulaData[unitId]; + + if (sheetData === undefined) { + return; + } + + if (sheetData === null) { + delete this._formulaData[unitId]; + return; + } + + if (!this._formulaData[unitId]) { + this._formulaData[unitId] = {}; + } + + Object.keys(sheetData).forEach((sheetId) => { + const currentSheetData = sheetData[sheetId]; + + if (currentSheetData === undefined) { + return; + } + + if (currentSheetData === null) { + delete this._formulaData[unitId]?.[sheetId]; + return; + } + + const sheetFormula = new ObjectMatrix(currentSheetData); + const formulaMatrix = new ObjectMatrix(this._formulaData[unitId]?.[sheetId]); + + sheetFormula.forValue((r, c, v) => { + if (v == null) { + formulaMatrix.realDeleteValue(r, c); + } else { + formulaMatrix.setValue(r, c, v); + } + }); + + this._formulaData[unitId]![sheetId] = formulaMatrix.clone(); + }); + }); + } + deleteArrayFormulaRange(unitId: string, sheetId: string, row: number, column: number) { const cellMatrixData = this._arrayFormulaRange[unitId]?.[sheetId]; if (cellMatrixData == null) { @@ -349,42 +379,10 @@ export class FormulaDataModel extends Disposable { } const sheetFormulaDataMatrix = new ObjectMatrix(workbookFormulaData[sheetId]); + const newSheetFormulaDataMatrix = new ObjectMatrix(); cellMatrix.forValue((r, c, cell) => { - const formulaString = cell?.f || ''; - const formulaId = cell?.si || ''; - - const checkFormulaString = isFormulaString(formulaString); - const checkFormulaId = isFormulaId(formulaId); - - if (checkFormulaString && checkFormulaId) { - sheetFormulaDataMatrix.setValue(r, c, { - f: formulaString, - si: formulaId, - }); - - formulaIdMap.set(formulaId, { f: formulaString, r, c }); - } else if (checkFormulaString && !checkFormulaId) { - sheetFormulaDataMatrix.setValue(r, c, { - f: formulaString, - }); - } else if (!checkFormulaString && checkFormulaId) { - sheetFormulaDataMatrix.setValue(r, c, { - f: '', - si: formulaId, - }); - } else if (!checkFormulaString && !checkFormulaId && sheetFormulaDataMatrix.getValue(r, c)) { - const currentFormulaInfo = sheetFormulaDataMatrix.getValue(r, c); - const f = currentFormulaInfo?.f || ''; - const si = currentFormulaInfo?.si || ''; - - // The id that needs to be offset - if (isFormulaString(f) && isFormulaId(si)) { - deleteFormulaIdMap.set(si, f); - } - - sheetFormulaDataMatrix.realDeleteValue(r, c); - } + updateFormulaDataByCellValue(sheetFormulaDataMatrix, newSheetFormulaDataMatrix, formulaIdMap, deleteFormulaIdMap, r, c, cell); }); // Convert the formula ID to formula string @@ -400,11 +398,14 @@ export class FormulaDataModel extends Disposable { const f = formulaInfo.f; const x = c - formulaInfo.c; const y = r - formulaInfo.r; + sheetFormulaDataMatrix.setValue(r, c, { f, si: formulaId, x, y }); + newSheetFormulaDataMatrix.setValue(r, c, { f, si: formulaId, x, y }); } else if (typeof deleteFormula === 'string') { const x = cell.x || 0; const y = cell.y || 0; const offsetFormula = this._lexerTreeBuilder.moveFormulaRefOffset(deleteFormula, x, y); + deleteFormulaIdMap.set(formulaId, { r, c, @@ -412,18 +413,28 @@ export class FormulaDataModel extends Disposable { }); sheetFormulaDataMatrix.setValue(r, c, { f: offsetFormula, si: formulaId }); + newSheetFormulaDataMatrix.setValue(r, c, { f: offsetFormula, si: formulaId }); } else if (typeof deleteFormula === 'object') { const x = c - deleteFormula.c; const y = r - deleteFormula.r; + sheetFormulaDataMatrix.setValue(r, c, { f: deleteFormula.f, si: formulaId, x, y, }); + newSheetFormulaDataMatrix.setValue(r, c, { + f: deleteFormula.f, + si: formulaId, + x, + y, + }); } } }); + + return newSheetFormulaDataMatrix.clone(); } updateArrayFormulaRange( @@ -438,23 +449,10 @@ export class FormulaDataModel extends Disposable { if (!arrayFormulaRange) return; const arrayFormulaRangeMatrix = new ObjectMatrix(arrayFormulaRange); - const cellMatrix = new ObjectMatrix(cellValue); - cellMatrix.forValue((r, c, cell) => { - const arrayFormulaRangeValue = arrayFormulaRangeMatrix?.getValue(r, c); - if (arrayFormulaRangeValue == null) { - return true; - } - - const formulaString = cell?.f || ''; - const formulaId = cell?.si || ''; - - const checkFormulaString = isFormulaString(formulaString); - const checkFormulaId = isFormulaId(formulaId); - if (!checkFormulaString && !checkFormulaId) { - arrayFormulaRangeMatrix.realDeleteValue(r, c); - } + cellMatrix.forValue((r, c, cell) => { + arrayFormulaRangeMatrix.realDeleteValue(r, c); }); } @@ -478,26 +476,9 @@ export class FormulaDataModel extends Disposable { const arrayFormulaCellDataMatrix = new ObjectMatrix(arrayFormulaCellData); const cellMatrix = new ObjectMatrix(cellValue); - cellMatrix.forValue((r, c, cell) => { - const arrayFormulaRangeValue = arrayFormulaRangeMatrix?.getValue(r, c); - if (arrayFormulaRangeValue == null) { - return true; - } - - const formulaString = cell?.f || ''; - const formulaId = cell?.si || ''; - const checkFormulaString = isFormulaString(formulaString); - const checkFormulaId = isFormulaId(formulaId); - - if (!checkFormulaString && !checkFormulaId) { - const { startRow, startColumn, endRow, endColumn } = arrayFormulaRangeValue; - for (let r = startRow; r <= endRow; r++) { - for (let c = startColumn; c <= endColumn; c++) { - arrayFormulaCellDataMatrix.realDeleteValue(r, c); - } - } - } + cellMatrix.forValue((r, c, cell) => { + clearArrayFormulaCellDataByCell(arrayFormulaRangeMatrix, arrayFormulaCellDataMatrix, r, c); }); } @@ -651,7 +632,17 @@ export function initSheetFormulaData( } }); - if (formulaData[unitId]) { - formulaData[unitId]![sheetId] = sheetFormulaDataMatrix.getData(); + if (!formulaData[unitId]) { + formulaData[unitId] = {}; } + + const newSheetFormulaData = sheetFormulaDataMatrix.clone(); + + formulaData[unitId]![sheetId] = newSheetFormulaData; + + return { + [unitId]: { + [sheetId]: newSheetFormulaData, + }, + }; } diff --git a/packages/engine-formula/src/models/utils/formula-data-util.ts b/packages/engine-formula/src/models/utils/formula-data-util.ts new file mode 100644 index 00000000000..ed40080fce1 --- /dev/null +++ b/packages/engine-formula/src/models/utils/formula-data-util.ts @@ -0,0 +1,86 @@ +/** + * Copyright 2023-present DreamNum Inc. + * + * 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, Nullable, ObjectMatrix } from '@univerjs/core'; +import { isFormulaId, isFormulaString } from '@univerjs/core'; +import type { IFormulaDataItem } from '../../basics/common'; + +export interface IFormulaIdMap { + f: string; + r: number; + c: number; +} + +export function updateFormulaDataByCellValue(sheetFormulaDataMatrix: ObjectMatrix, newSheetFormulaDataMatrix: ObjectMatrix, formulaIdMap: Map, deleteFormulaIdMap: Map, r: number, c: number, cell: Nullable) { + const formulaString = cell?.f || ''; + const formulaId = cell?.si || ''; + + const checkFormulaString = isFormulaString(formulaString); + const checkFormulaId = isFormulaId(formulaId); + + if (checkFormulaString && checkFormulaId) { + sheetFormulaDataMatrix.setValue(r, c, { + f: formulaString, + si: formulaId, + }); + + formulaIdMap.set(formulaId, { f: formulaString, r, c }); + + newSheetFormulaDataMatrix.setValue(r, c, { + f: formulaString, + si: formulaId, + }); + } else if (checkFormulaString && !checkFormulaId) { + sheetFormulaDataMatrix.setValue(r, c, { + f: formulaString, + }); + newSheetFormulaDataMatrix.setValue(r, c, { + f: formulaString, + }); + } else if (!checkFormulaString && checkFormulaId) { + sheetFormulaDataMatrix.setValue(r, c, { + f: '', + si: formulaId, + }); + } else if (!checkFormulaString && !checkFormulaId && sheetFormulaDataMatrix.getValue(r, c)) { + const currentFormulaInfo = sheetFormulaDataMatrix.getValue(r, c); + const f = currentFormulaInfo?.f || ''; + const si = currentFormulaInfo?.si || ''; + + // The id that needs to be offset + // When the cell containing the formulas f and si is deleted, f and si lose their association, and f needs to be moved to the next cell containing the same si. + if (isFormulaString(f) && isFormulaId(si)) { + deleteFormulaIdMap.set(si, f); + } + + sheetFormulaDataMatrix.realDeleteValue(r, c); + newSheetFormulaDataMatrix.setValue(r, c, null); + } +} + +export function clearArrayFormulaCellDataByCell(arrayFormulaRangeMatrix: ObjectMatrix, arrayFormulaCellDataMatrix: ObjectMatrix>, r: number, c: number) { + const arrayFormulaRangeValue = arrayFormulaRangeMatrix?.getValue(r, c); + if (arrayFormulaRangeValue == null) { + return true; + } + + const { startRow, startColumn, endRow, endColumn } = arrayFormulaRangeValue; + for (let r = startRow; r <= endRow; r++) { + for (let c = startColumn; c <= endColumn; c++) { + arrayFormulaCellDataMatrix.realDeleteValue(r, c); + } + } +} diff --git a/packages/sheets-formula/README-zh.md b/packages/sheets-formula/README-zh.md index 3fb6cba3b00..60e62932bfd 100644 --- a/packages/sheets-formula/README-zh.md +++ b/packages/sheets-formula/README-zh.md @@ -181,7 +181,7 @@ univerAPI.unregisterFunction({ Uniscript 底层使用了 `@univerjs/facade`,你也可以直接在项目中使用类似 Uniscript 的 API,请参考 [注册公式](/guides/facade/register-function)。 -## 如何在初始化 Univer 时添加公式 +### 如何在初始化 Univer 时添加公式 按照以下步骤来实现一个自定义公式 `CUSTOMSUM`。 @@ -719,7 +719,7 @@ univer.registerPlugin(UniverSheetsCustomFunctionPlugin); - `description` 参数需要综合下内容进行提取,因为有的 Excel 描述很长,需要简化 - `abstract` 和 `links` 基本上不需要做改动 - `aliasFunctionName` 是可选参数,大部分公式不需要填写(也可以只设置某个国家的别名),暂时还未找到有公式别名文档来参考。目前找到一个函数翻译插件可能提供类似功能 [Excel 函数翻译工具](https://support.microsoft.com/zh-cn/office/excel-%E5%87%BD%E6%95%B0%E7%BF%BB%E8%AF%91%E5%B7%A5%E5%85%B7-f262d0c0-991c-485b-89b6-32cc8d326889) - - `functionParameter` 中需要为每个参数设定一个名称,我们推荐根据参数的含义进行变化,比如数值类型的 `key` 为 `number`(仅有一个数值参数的时候)或者 `number1`、`number2`(有多个数值参数的时候),范围为 `range`,条件为 `criteria`,求和范围为 `sum_range`(多个单词之间用 `_` 分割) + - `functionParameter` 中需要为每个参数设定一个名称,我们推荐根据参数的含义进行变化,比如数值类型的 `key` 为 `number`(仅有一个数值参数的时候)或者 `number1`、`number2`(有多个数值参数的时候),范围为 `range`,条件为 `criteria`,求和范围为 `sumRange`,采用驼峰式命名法。对于具体的参数内容, `name` 英文格式就使用带下划线的格式 `sum_range`,其他语言采用翻译的文本, `detail` 全部采用翻译。 - Office 函数文档中文翻译猜测用的机翻,部分翻译不容易理解,需要自己修改,一部分专用名词如下。 - 单元格参考 => 单元格引用 - 数字类型的参数统一翻译为:数值 @@ -745,11 +745,11 @@ univer.registerPlugin(UniverSheetsCustomFunctionPlugin); 位置在 [packages/engine-formula/src/functions/math/sumif/index.ts](https://github.com/dream-num/univer/blob/dev/packages/engine-formula/src/functions/math/sumif/index.ts)。 - 在当前公式的分类文件夹下新建公式文件夹,一个公式一个文件夹。然后新建 `index.ts` 文件来写公式算法,公式 `class` 的名称采用大驼峰的写法,认为公式是一个单词,带 `_` 或者 `.` 的公式认为是两个单词,比如 + 在当前公式的分类文件夹下新建公式文件夹,文件夹名称与公式相同,采用短横线命名,一个公式一个文件夹。然后新建 `index.ts` 文件来写公式算法,公式 `class` 的名称采用帕斯卡命名法(又叫大驼峰),认为公式是一个单词,带 `_` 或者 `.` 的公式认为是两个单词,比如 - - `SUMIF` => `Sumif` - - `NETWORKDAYS.INTL` => `Networkdays_Intl` - - `ARRAY_CONSTRAIN` => `Array_Constrain` + - `SUMIF` => 文件夹 `sumif`, 类 `Sumif` + - `NETWORKDAYS.INTL` => 文件夹 `networkdays-intl`, 类 `NetworkdaysIntl` + - `ARRAY_CONSTRAIN` => 文件夹 `array-constrain`, 类 `ArrayConstrain` 同级新建 `__tests__` 来写编写单元测试。写完之后,记得在分类目录下的 `function-map` 文件中添加公式算法和函数名映射用于注册这个函数算法。 @@ -776,7 +776,8 @@ univer.registerPlugin(UniverSheetsCustomFunctionPlugin); ### 公式实现注意事项 -- 任何公式的入参和出参都可以是 `A1`、`A1:B10`,调研 Excel 的时候需要把所有情况考虑到,比如 `=SIN(A1:B10)`,会展开一个正弦计算后的范围。 +- 大部分的公式规则请参考最新版本的 Excel,如果有不合理的地方,再参考 Google Sheets。 +- 任何公式的入参和出参都可以是 `A1`、`A1:B10`,单元格内容也可能是数字、字符串、布尔值、空单元格、错误值、数组等,虽然公式教程中说明了识别固定的数据类型,但是程序上实现是需要都兼容的,调研 Excel 的时候需要把所有情况考虑到,比如 `=SIN(A1:B10)`,会展开一个正弦计算后的范围。 - 例如 `XLOOKUP` 函数,要求两个入参的行或列至少又一个大小相等,这样才能进行矩阵计算。 - 例如 `SUMIF` 函数,大家以为是求和,但是它是可以根据第二个参数进行展开的 @@ -788,8 +789,13 @@ univer.registerPlugin(UniverSheetsCustomFunctionPlugin); - 公式的数值计算,需要使用内置的方法,尽量不要获取值自行计算。因为公式的参数可以是值、数组、引用。可以参考已有的 `sum`、`minus` 函数。 - 精度问题,公式引入了 `big.js`,使用内置方法会调用该库,但是相比原生计算会慢接近 100 倍,所以像 `sin` 等 `js` 方法,尽量用原生实现。 - 需要自定义计算,使用 `product` 函数,适合两个入参的计算,调用 `map` 对值自身进行迭代计算,适合对一个入参本身的值进行改变。 +- 公式算法支持两种配置 `needsExpandParams` 和 `needsReferenceObject` + - `needsExpandParams`: 函数是否需要扩展参数,主要处理类似 `LOOKUP` 函数需要处理不同大小向量的情况 + - `needsReferenceObject`:函数是否需要传入引用对象,设置之后 `BaseReferenceObject` 不会转化为 `ArrayValueObject` 而是直接传入公式算法,比如 `OFFSET` 函数 +- 公式计算错误会返回固定类型的错误,比如 `#NAME?`、`#VALUE!`,需要对齐 Excel,因为有判断报错类型的函数 `ISERR`、`ISNA`等,类型指定不对,结果就可能不一样。 +- 公式算法中,即使是必选参数,也需要拦截为 `null` 的情况并返回错误 `#N/A`,因为用户有可能不输入任何参数。这个行为在 Excel 中会被拦截,在 Google Sheets 中返回 `#N/A`,我们参照 Google Sheets。 -#### 公式基础工具 +### 公式基础工具 1. `ValueObjectFactory` 用来自动识别参数格式创建一个参数实例,范围类型的数据用 `RangeReferenceObject` 来创建参数实例 2. 数组 `toArrayValueObject` 可以与值直接运算,得到新的数组 diff --git a/packages/sheets-formula/README.md b/packages/sheets-formula/README.md index cf8bbdf81fe..179ec20291c 100644 --- a/packages/sheets-formula/README.md +++ b/packages/sheets-formula/README.md @@ -719,7 +719,7 @@ To implement a formula, you need to add formula description, internationalizatio - Extract the `description` from the content, as some Excel descriptions are lengthy and need simplification. - `abstract` and `links` generally do not need modification. - `aliasFunctionName` is optional; most formulas do not need to be filled (or can be set for aliases in specific countries). Currently, there is no documentation for formula aliases. Currently I have found a function translation plug-in that may provide similar functions [Excel Functions Translator](https://support.microsoft.com/en-us/office/excel-functions-translator-f262d0c0-991c-485b-89b6-32cc8d326889) - - `functionParameter` needs a name for each parameter. We recommend varying names based on the parameter's meaning, e.g., use `number` for a numeric parameter (if there is only one) or `number1`, `number2` for multiple numeric parameters. Use `range` for a range, `criteria` for conditions, and `sum_range` for the sum range (separated by `_` for multiple words). + - `functionParameter` needs a name for each parameter. We recommend varying names based on the parameter's meaning, e.g., use `number` for a numeric parameter (if there is only one) or `number1`, `number2` for multiple numeric parameters. Use `range` for a range, `criteria` for conditions, and `sumRange` for the sum range, use `camelCase`. For specific parameter content, the English format of `name` uses the underlined format `sum_range`, other languages use the translated text, and `detail` uses all translations. - Some Chinese translations in the Office function documentation are machine-translated and may be unclear. Modify as needed. For example, `单元格参考` (Cell Reference) should be translated as `单元格引用`. Numeric type parameters are uniformly translated as: `数值`. - Do not end `abstract` with a period (used in the search list when users input cells), but end `description` and `detail` with a period (used in descriptions). - Capitalize the first letter of English sentences. @@ -743,11 +743,11 @@ To implement a formula, you need to add formula description, internationalizatio Location: [packages/engine-formula/src/functions/math/sumif/index.ts](https://github.com/dream-num/univer/blob/dev/packages/engine-formula/src/functions/math/sumif/index.ts). - Create a new folder for the formula under the current formula category, with one folder per formula. Then create an `index.ts` file to write the formula algorithm. Use camel case for the formula `class` name, considering the formula as one word. If a formula contains `_` or `.`, treat it as two words, such as: + Create a new formula folder under the classification folder of the current formula. The folder name is the same as the formula, named with `kebab-case`, one folder for each formula. Then create a new `index.ts` file to write the formula algorithm. The name of the formula `class` adopts `PascalCase`. The formula is considered to be one word, and the formula with `_` or `.` is considered to be two words such as - - `SUMIF` => `Sumif` - - `NETWORKDAYS.INTL` => `Networkdays_Intl` - - `ARRAY_CONSTRAIN` => `Array_Constrain` + - `SUMIF` => folder `sumif`, class `Sumif` + - `NETWORKDAYS.INTL` => folder `networkdays-intl`, class `NetworkdaysIntl` + - `ARRAY_CONSTRAIN` => folder `array-constrain`, class `ArrayConstrain` Create a `__tests__` folder at the same level to write unit tests. After writing, remember to add the formula algorithm and function name mapping in the `function-map` file in the category directory to register the formula algorithm. @@ -772,20 +772,26 @@ To implement a formula, you need to add formula description, internationalizatio - After selecting `SUMIF` or entering `=sumif(`, trigger the formula details popup and carefully check the contents. - Select the data range, trigger the calculation, and check if the formula calculation result is correct. -#### Considerations for Formula Implementation +### Considerations for Formula Implementation -- Any formula's input and output can be `A1`, `A1:B10`, etc. When researching Excel, consider all cases, such as `=SIN(A1:B10)`, which expands to the calculated range. - - For example, the `XLOOKUP` function requires at least one of the rows or columns of its two inputs to be of equal size for matrix calculation. - - For example, the `SUMIF` function, although commonly used for summation, can expand based on the second parameter. +- For most formula rules, please refer to the latest version of Excel. If there are any unreasonable rules, please refer to Google Sheets. +- The input and output parameters of any formula can be `A1`, `A1:B10`, and the cell content may also be numbers, strings, Boolean values, empty cells, error values, arrays, etc., although the formula tutorial explains In order to identify fixed data types, the program implementation needs to be compatible. When researching Excel, consider all cases, such as `=SIN(A1:B10)`, which expands to the calculated range. + - For example, the `XLOOKUP` function requires at least one of the rows or columns of its two inputs to be of equal size for matrix calculation. + - For example, the `SUMIF` function, although commonly used for summation, can expand based on the second parameter. ![sumif array](./assets/sumif-array.png) ![sumif array result](./assets/sumif-array-result.png) - - Excel formula calculation is becoming more like numpy, for example: + - Excel formula calculation is becoming more like numpy, for example: ![numpy](./assets/numpy.png) -- For numerical calculations in formulas, use built-in methods and try to avoid obtaining values for manual calculation. Because formula parameters can be values, arrays, or references. You can refer to existing `sum` and `minus` functions. -- Precision issues: The formula introduces `big.js`, and using built-in methods will call this library. However, it is nearly 100 times slower than native calculations. Therefore, for methods like `sin`, it is advisable to use native implementations. -- For custom calculations, use the `product` function, suitable for calculating two input parameters. Call `map` to iterate over the values for changes to a parameter's own values. - -#### Formula Basic Tools +- For numerical calculations in formulas, use built-in methods and try to avoid obtaining values for manual calculation. Because formula parameters can be values, arrays, or references. You can refer to existing `sum` and `minus` functions. +- Precision issues: The formula introduces `big.js`, and using built-in methods will call this library. However, it is nearly 100 times slower than native calculations. Therefore, for methods like `sin`, it is advisable to use native implementations. +- For custom calculations, use the `product` function, suitable for calculating two input parameters. Call `map` to iterate over the values for changes to a parameter's own values. +- Formula algorithm supports two configurations `needsExpandParams` and `needsReferenceObject` + - `needsExpandParams`: Whether the function needs to expand parameters, mainly handles situations where the `LOOKUP` function needs to handle vectors of different sizes + - `needsReferenceObject`: Whether the function needs to pass in a reference object. After setting, `BaseReferenceObject` will not be converted into `ArrayValueObject` but will be passed directly into the formula algorithm, such as the `OFFSET` function +- Formula calculation errors will return fixed types of errors, such as `#NAME?`, `#VALUE!`, which need to be aligned with Excel, because there are functions `ISERR`, `ISNA`, etc. that determine the error type. If the type is not specified correctly, the result will be It may be different. +- In the formula algorithm, even if it is a required parameter, it is necessary to intercept the case of `null` and return the error `#N/A`, because the user may not enter any parameters. This behavior will be intercepted in Excel and `#N/A` will be returned in Google Sheets. We refer to Google Sheets. + +### Formula Basic Tools 1. `ValueObjectFactory` is used to automatically recognize parameter formats and create a parameter instance. Use `RangeReferenceObject` to create parameter instances for range-type data. 2. The array `toArrayValueObject` can be operated directly with values to get a new array. diff --git a/packages/sheets-formula/src/controllers/__tests__/update-formula.controller.spec.ts b/packages/sheets-formula/src/controllers/__tests__/update-formula.controller.spec.ts index d5d82ed91de..8355330a067 100644 --- a/packages/sheets-formula/src/controllers/__tests__/update-formula.controller.spec.ts +++ b/packages/sheets-formula/src/controllers/__tests__/update-formula.controller.spec.ts @@ -15,9 +15,9 @@ */ import type { ICellData, IWorkbookData, Nullable, Univer } from '@univerjs/core'; -import { cellToRange, Direction, ICommandService, IUniverInstanceService, LocaleType, RANGE_TYPE } from '@univerjs/core'; -import type { IInsertColCommandParams } from '@univerjs/sheets'; -import { InsertColCommand, MoveRowsCommand, NORMAL_SELECTION_PLUGIN_NAME, SelectionManagerService } from '@univerjs/sheets'; +import { CellValueType, Direction, ICommandService, IUniverInstanceService, LocaleType, RANGE_TYPE, RedoCommand, UndoCommand } from '@univerjs/core'; +import type { IDeleteRangeMoveLeftCommandParams, IDeleteRangeMoveUpCommandParams, IInsertColCommandParams, IInsertRowCommandParams, IMoveColsCommandParams, IMoveRangeCommandParams, IMoveRowsCommandParams, InsertRangeMoveDownCommandParams, InsertRangeMoveRightCommandParams, IRemoveRowColCommandParams, IRemoveSheetCommandParams, ISetWorksheetNameCommandParams } from '@univerjs/sheets'; +import { DeleteRangeMoveLeftCommand, DeleteRangeMoveUpCommand, InsertColCommand, InsertColMutation, InsertRangeMoveDownCommand, InsertRangeMoveRightCommand, InsertRowCommand, InsertRowMutation, MoveColsCommand, MoveColsMutation, MoveRangeCommand, MoveRangeMutation, MoveRowsCommand, MoveRowsMutation, NORMAL_SELECTION_PLUGIN_NAME, RemoveColCommand, RemoveColMutation, RemoveRowCommand, RemoveRowMutation, RemoveSheetCommand, RemoveSheetMutation, SelectionManagerService, SetRangeValuesMutation, SetSelectionsOperation, SetWorksheetNameCommand, SetWorksheetNameMutation } from '@univerjs/sheets'; import type { Injector } from '@wendellhu/redi'; import { afterEach, beforeEach, describe, expect, it } from 'vitest'; @@ -33,30 +33,68 @@ const TEST_WORKBOOK_DATA_DEMO = (): IWorkbookData => ({ id: 'sheet1', cellData: { 0: { + 6: { + f: '=A1:B2', + }, + }, + 1: { + 2: { + v: 1, + t: CellValueType.NUMBER, + }, + }, + 2: { + 1: { + v: 1, + t: CellValueType.NUMBER, + }, + 2: { + f: '=A1:B2', + }, + }, + 5: { + 2: { + f: '=SUM(A1:B2)', + }, 3: { - f: '=A1:C1', + v: 1, + t: CellValueType.NUMBER, + }, + }, + 6: { + 2: { + v: 1, + t: CellValueType.NUMBER, }, }, + 14: { + 0: { + f: '=A1:B2', + }, + 2: { + f: '=Sheet2!A1:B2', + }, + }, + }, + name: 'Sheet1', + }, + sheet2: { + id: 'sheet2', + cellData: { }, + name: 'Sheet2', }, }, locale: LocaleType.ZH_CN, name: '', - sheetOrder: [], + sheetOrder: ['sheet1', 'sheet2'], styles: {}, }); -// TODO@Dushusir: add move range,insert range,delete range test case describe('Test insert function operation', () => { let univer: Univer; let get: Injector['get']; let commandService: ICommandService; - let getValueByPosition: ( - startRow: number, - startColumn: number, - endRow: number, - endColumn: number - ) => Nullable; let getValues: ( startRow: number, startColumn: number, @@ -72,23 +110,43 @@ describe('Test insert function operation', () => { get = testBed.get; commandService = get(ICommandService); + commandService.registerCommand(MoveRangeCommand); + commandService.registerCommand(MoveRangeMutation); + commandService.registerCommand(MoveRowsCommand); + commandService.registerCommand(MoveRowsMutation); + + commandService.registerCommand(MoveColsCommand); + commandService.registerCommand(MoveColsMutation); + + commandService.registerCommand(InsertRowCommand); + commandService.registerCommand(InsertRowMutation); + commandService.registerCommand(InsertColCommand); + commandService.registerCommand(InsertColMutation); + + commandService.registerCommand(RemoveRowCommand); + commandService.registerCommand(RemoveRowMutation); + + commandService.registerCommand(RemoveColCommand); + commandService.registerCommand(RemoveColMutation); + + commandService.registerCommand(DeleteRangeMoveLeftCommand); + commandService.registerCommand(DeleteRangeMoveUpCommand); + commandService.registerCommand(InsertRangeMoveDownCommand); + commandService.registerCommand(InsertRangeMoveRightCommand); + + commandService.registerCommand(SetWorksheetNameCommand); + commandService.registerCommand(SetWorksheetNameMutation); + commandService.registerCommand(RemoveSheetCommand); + commandService.registerCommand(RemoveSheetMutation); + + commandService.registerCommand(SetSelectionsOperation); + commandService.registerCommand(SetRangeValuesMutation); commandService.registerCommand(SetFormulaDataMutation); commandService.registerCommand(SetArrayFormulaDataMutation); commandService.registerCommand(SetNumfmtFormulaDataMutation); - getValueByPosition = ( - startRow: number, - startColumn: number, - endRow: number, - endColumn: number - ): Nullable => - get(IUniverInstanceService) - .getUniverSheetInstance('test') - ?.getSheetBySheetId('sheet1') - ?.getRange(startRow, startColumn, endRow, endColumn) - .getValue(); getValues = ( startRow: number, startColumn: number, @@ -107,63 +165,798 @@ describe('Test insert function operation', () => { }); describe('update formula', () => { - describe('correct situations', () => { - it('move rows update formula', async () => { - const selectionManager = get(SelectionManagerService); - selectionManager.setCurrentSelection({ - pluginName: NORMAL_SELECTION_PLUGIN_NAME, - unitId: 'test', - sheetId: 'sheet1', - }); - - // A1 - selectionManager.add([ - { - range: { startRow: 0, startColumn: 0, endRow: 0, endColumn: 0, rangeType: RANGE_TYPE.NORMAL }, - primary: null, - style: null, - }, - ]); + it('Move range, update reference', async () => { + const params: IMoveRangeCommandParams = { + fromRange: { + startRow: 0, + startColumn: 0, + endRow: 1, + endColumn: 1, + rangeType: 0, + }, + toRange: { + startRow: 0, + startColumn: 3, + endRow: 1, + endColumn: 4, + rangeType: 0, + }, + }; + + expect(await commandService.executeCommand(MoveRangeCommand.id, params)).toBeTruthy(); + const values = getValues(5, 2, 5, 2); + expect(values).toStrictEqual([[{ f: '=SUM(D1:E2)' }]]); - const params = { - fromRange: cellToRange(0, 1), - toRange: cellToRange(1, 1), - }; + expect(await commandService.executeCommand(UndoCommand.id)).toBeTruthy(); + const valuesUndo = getValues(5, 2, 5, 2); + expect(valuesUndo).toStrictEqual([[{ f: '=SUM(A1:B2)' }]]); - await commandService.executeCommand(MoveRowsCommand.id, params); + expect(await commandService.executeCommand(RedoCommand.id)).toBeTruthy(); + const valuesRedo = getValues(5, 2, 5, 2); + expect(valuesRedo).toStrictEqual([[{ f: '=SUM(D1:E2)' }]]); + }); + + it('Move range, update position', async () => { + const params: IMoveRangeCommandParams = { + fromRange: { + startRow: 5, + startColumn: 2, + endRow: 5, + endColumn: 2, + rangeType: 0, + }, + toRange: { + startRow: 5, + startColumn: 3, + endRow: 5, + endColumn: 3, + rangeType: 0, + }, + }; + + expect(await commandService.executeCommand(MoveRangeCommand.id, params)).toBeTruthy(); + const values = getValues(5, 2, 5, 3); + expect(values).toStrictEqual([[{}, { f: '=SUM(A1:B2)' }]]); + + expect(await commandService.executeCommand(UndoCommand.id)).toBeTruthy(); + const valuesUndo = getValues(5, 2, 5, 3); + expect(valuesUndo).toStrictEqual([[{ f: '=SUM(A1:B2)' }, { v: 1, t: CellValueType.NUMBER }]]); + + expect(await commandService.executeCommand(RedoCommand.id)).toBeTruthy(); + const valuesRedo = getValues(5, 2, 5, 3); + expect(valuesRedo).toStrictEqual([[{}, { f: '=SUM(A1:B2)' }]]); + }); + + it('Move rows, update reference', async () => { + const selectionManager = get(SelectionManagerService); + selectionManager.setCurrentSelection({ + pluginName: NORMAL_SELECTION_PLUGIN_NAME, + unitId: 'test', + sheetId: 'sheet1', }); - it('Insert column', async () => { - const selectionManager = get(SelectionManagerService); - selectionManager.setCurrentSelection({ - pluginName: NORMAL_SELECTION_PLUGIN_NAME, - unitId: 'test', - sheetId: 'sheet1', - }); - - // A1 - selectionManager.add([ - { - range: { startRow: 0, startColumn: 1, endRow: 0, endColumn: 1, rangeType: RANGE_TYPE.NORMAL }, - primary: null, - style: null, - }, - ]); - - const params: IInsertColCommandParams = { - unitId: 'test', - subUnitId: 'sheet1', - range: cellToRange(0, 1), - direction: Direction.LEFT, - }; - - // FXIME why InsertColCommand sequenceExecute returns result false - await commandService.executeCommand(InsertColCommand.id, params); - const oldValue = getValueByPosition(0, 3, 0, 3); - expect(oldValue?.f).toBe('=A1:C1'); - - const newValue = getValueByPosition(0, 4, 0, 4); - // expect(newValue).toBe('=A1:D1'); + + // A1 + selectionManager.add([ + { + range: { startRow: 1, startColumn: 0, endRow: 1, endColumn: 0, rangeType: RANGE_TYPE.ROW }, + primary: null, + style: null, + }, + ]); + + const params: IMoveRowsCommandParams = { + fromRange: { + startRow: 1, + startColumn: 0, + endRow: 1, + endColumn: 19, + rangeType: 1, + }, + toRange: { + startRow: 4, + startColumn: 0, + endRow: 4, + endColumn: 19, + rangeType: 1, + }, + }; + + expect(await commandService.executeCommand(MoveRowsCommand.id, params)).toBeTruthy(); + const values = getValues(5, 2, 5, 2); + expect(values).toStrictEqual([[{ f: '=SUM(A1:B4)' }]]); + + expect(await commandService.executeCommand(UndoCommand.id)).toBeTruthy(); + const valuesUndo = getValues(5, 2, 5, 2); + expect(valuesUndo).toStrictEqual([[{ f: '=SUM(A1:B2)' }]]); + + expect(await commandService.executeCommand(RedoCommand.id)).toBeTruthy(); + const valuesRedo = getValues(5, 2, 5, 2); + expect(valuesRedo).toStrictEqual([[{ f: '=SUM(A1:B4)' }]]); + }); + + it('Move rows, update reference and position', async () => { + const selectionManager = get(SelectionManagerService); + selectionManager.setCurrentSelection({ + pluginName: NORMAL_SELECTION_PLUGIN_NAME, + unitId: 'test', + sheetId: 'sheet1', + }); + + // A1 + selectionManager.add([ + { + range: { startRow: 1, startColumn: 0, endRow: 1, endColumn: 0, rangeType: RANGE_TYPE.ROW }, + primary: null, + style: null, + }, + ]); + + const params: IMoveRowsCommandParams = { + fromRange: { + startRow: 1, + startColumn: 0, + endRow: 1, + endColumn: 19, + rangeType: 1, + }, + toRange: { + startRow: 9, + startColumn: 0, + endRow: 9, + endColumn: 19, + rangeType: 1, + }, + }; + + expect(await commandService.executeCommand(MoveRowsCommand.id, params)).toBeTruthy(); + const values = getValues(1, 2, 2, 2); + expect(values).toStrictEqual([[{ f: '=A1:B9' }], [{}]]); + const values2 = getValues(4, 2, 5, 2); + expect(values2).toStrictEqual([[{ f: '=SUM(A1:B9)' }], [{ v: 1, t: CellValueType.NUMBER }]]); + + expect(await commandService.executeCommand(UndoCommand.id)).toBeTruthy(); + const valuesUndo = getValues(1, 2, 2, 2); + expect(valuesUndo).toStrictEqual([[{ v: 1, t: CellValueType.NUMBER }], [{ f: '=A1:B2' }]]); + const valuesUndo2 = getValues(5, 2, 6, 2); + expect(valuesUndo2).toStrictEqual([[{ f: '=SUM(A1:B2)' }], [{ v: 1, t: CellValueType.NUMBER }]]); + + expect(await commandService.executeCommand(RedoCommand.id)).toBeTruthy(); + const valuesRedo = getValues(1, 2, 2, 2); + expect(valuesRedo).toStrictEqual([[{ f: '=A1:B9' }], [{}]]); + const valuesRedo2 = getValues(4, 2, 5, 2); + expect(valuesRedo2).toStrictEqual([[{ f: '=SUM(A1:B9)' }], [{ v: 1, t: CellValueType.NUMBER }]]); + }); + + it('Move columns, update reference', async () => { + const selectionManager = get(SelectionManagerService); + selectionManager.setCurrentSelection({ + pluginName: NORMAL_SELECTION_PLUGIN_NAME, + unitId: 'test', + sheetId: 'sheet1', + }); + + // A1 + selectionManager.add([ + { + range: { startRow: 0, startColumn: 0, endRow: 0, endColumn: 0, rangeType: RANGE_TYPE.COLUMN }, + primary: null, + style: null, + }, + ]); + + const params: IMoveColsCommandParams = { + fromRange: { + startRow: 0, + startColumn: 0, + endRow: 999, + endColumn: 0, + rangeType: 2, + }, + toRange: { + startRow: 0, + startColumn: 4, + endRow: 999, + endColumn: 4, + rangeType: 2, + }, + }; + + expect(await commandService.executeCommand(MoveColsCommand.id, params)).toBeTruthy(); + const values = getValues(0, 6, 0, 6); + expect(values).toStrictEqual([[{ f: '=A1:A2' }]]); + + expect(await commandService.executeCommand(UndoCommand.id)).toBeTruthy(); + const valuesUndo = getValues(0, 6, 0, 6); + expect(valuesUndo).toStrictEqual([[{ f: '=A1:B2' }]]); + + expect(await commandService.executeCommand(RedoCommand.id)).toBeTruthy(); + const valuesRedo = getValues(0, 6, 0, 6); + expect(valuesRedo).toStrictEqual([[{ f: '=A1:A2' }]]); + }); + + it('Move columns, update reference and position', async () => { + const selectionManager = get(SelectionManagerService); + selectionManager.setCurrentSelection({ + pluginName: NORMAL_SELECTION_PLUGIN_NAME, + unitId: 'test', + sheetId: 'sheet1', }); + + // A1 + selectionManager.add([ + { + range: { startRow: 0, startColumn: 1, endRow: 0, endColumn: 1, rangeType: RANGE_TYPE.COLUMN }, + primary: null, + style: null, + }, + ]); + + const params: IMoveColsCommandParams = { + fromRange: { + startRow: 0, + startColumn: 1, + endRow: 999, + endColumn: 1, + rangeType: 2, + }, + toRange: { + startRow: 0, + startColumn: 9, + endRow: 999, + endColumn: 9, + rangeType: 2, + }, + }; + + expect(await commandService.executeCommand(MoveColsCommand.id, params)).toBeTruthy(); + const values = getValues(2, 1, 2, 2); + expect(values).toStrictEqual([[{ f: '=A1:I2' }, {}]]); + const values2 = getValues(5, 1, 5, 2); + expect(values2).toStrictEqual([[{ f: '=SUM(A1:I2)' }, { v: 1, t: CellValueType.NUMBER }]]); + + expect(await commandService.executeCommand(UndoCommand.id)).toBeTruthy(); + const valuesUndo = getValues(2, 1, 2, 2); + expect(valuesUndo).toStrictEqual([[{ v: 1, t: CellValueType.NUMBER }, { f: '=A1:B2' }]]); + const valuesUndo2 = getValues(5, 2, 5, 3); + expect(valuesUndo2).toStrictEqual([[{ f: '=SUM(A1:B2)' }, { v: 1, t: CellValueType.NUMBER }]]); + + expect(await commandService.executeCommand(RedoCommand.id)).toBeTruthy(); + const valuesRedo = getValues(2, 1, 2, 2); + expect(valuesRedo).toStrictEqual([[{ f: '=A1:I2' }, {}]]); + const valuesRedo2 = getValues(5, 1, 5, 2); + expect(valuesRedo2).toStrictEqual([[{ f: '=SUM(A1:I2)' }, { v: 1, t: CellValueType.NUMBER }]]); + }); + + it('Insert row, update reference', async () => { + const params: IInsertRowCommandParams = { + unitId: 'test', + subUnitId: 'sheet1', + range: { + startRow: 1, + endRow: 1, + startColumn: 0, + endColumn: 19, + }, + direction: Direction.UP, + }; + + expect(await commandService.executeCommand(InsertRowCommand.id, params)).toBeTruthy(); + const values = getValues(0, 6, 0, 6); + expect(values).toStrictEqual([[{ f: '=A1:B3' }]]); + + expect(await commandService.executeCommand(UndoCommand.id)).toBeTruthy(); + const valuesUndo = getValues(0, 6, 0, 6); + expect(valuesUndo).toStrictEqual([[{ f: '=A1:B2' }]]); + + expect(await commandService.executeCommand(RedoCommand.id)).toBeTruthy(); + const valuesRedo = getValues(0, 6, 0, 6); + expect(valuesRedo).toStrictEqual([[{ f: '=A1:B3' }]]); + }); + + it('Insert row, update reference and position', async () => { + const params: IInsertRowCommandParams = { + unitId: 'test', + subUnitId: 'sheet1', + range: { + startRow: 1, + endRow: 1, + startColumn: 0, + endColumn: 19, + }, + direction: Direction.UP, + }; + + expect(await commandService.executeCommand(InsertRowCommand.id, params)).toBeTruthy(); + const values = getValues(2, 2, 3, 2); + expect(values).toStrictEqual([[{ v: 1, t: CellValueType.NUMBER }], [{ f: '=A1:B3' }]]); + const values2 = getValues(6, 2, 7, 2); + expect(values2).toStrictEqual([[{ f: '=SUM(A1:B3)' }], [{ v: 1, t: CellValueType.NUMBER }]]); + + expect(await commandService.executeCommand(UndoCommand.id)).toBeTruthy(); + const valuesUndo = getValues(1, 2, 2, 2); + expect(valuesUndo).toStrictEqual([[{ v: 1, t: CellValueType.NUMBER }], [{ f: '=A1:B2' }]]); + const valuesUndo2 = getValues(5, 2, 6, 2); + expect(valuesUndo2).toStrictEqual([[{ f: '=SUM(A1:B2)' }], [{ v: 1, t: CellValueType.NUMBER }]]); + + expect(await commandService.executeCommand(RedoCommand.id)).toBeTruthy(); + const valuesRedo = getValues(2, 2, 3, 2); + expect(valuesRedo).toStrictEqual([[{ v: 1, t: CellValueType.NUMBER }], [{ f: '=A1:B3' }]]); + const valuesRedo2 = getValues(6, 2, 7, 2); + expect(valuesRedo2).toStrictEqual([[{ f: '=SUM(A1:B3)' }], [{ v: 1, t: CellValueType.NUMBER }]]); + }); + + it('Insert column, update reference', async () => { + const params: IInsertColCommandParams = { + unitId: 'test', + subUnitId: 'sheet1', + range: { + startColumn: 1, + endColumn: 1, + startRow: 0, + endRow: 14, + }, + direction: Direction.LEFT, + cellValue: {}, + }; + + expect(await commandService.executeCommand(InsertColCommand.id, params)).toBeTruthy(); + const values = getValues(14, 0, 14, 0); + expect(values).toStrictEqual([[{ f: '=A1:C2' }]]); + + expect(await commandService.executeCommand(UndoCommand.id)).toBeTruthy(); + const valuesUndo = getValues(14, 0, 14, 0); + expect(valuesUndo).toStrictEqual([[{ f: '=A1:B2' }]]); + + expect(await commandService.executeCommand(RedoCommand.id)).toBeTruthy(); + const valuesRedo = getValues(14, 0, 14, 0); + expect(valuesRedo).toStrictEqual([[{ f: '=A1:C2' }]]); + }); + + it('Insert column, update reference and position', async () => { + const params: IInsertColCommandParams = { + unitId: 'test', + subUnitId: 'sheet1', + range: { + startColumn: 1, + endColumn: 1, + startRow: 0, + endRow: 14, + }, + direction: Direction.LEFT, + cellValue: {}, + }; + + expect(await commandService.executeCommand(InsertColCommand.id, params)).toBeTruthy(); + const values = getValues(2, 2, 2, 3); + expect(values).toStrictEqual([[{ v: 1, t: CellValueType.NUMBER }, { f: '=A1:C2' }]]); + const values2 = getValues(5, 3, 5, 4); + expect(values2).toStrictEqual([[{ f: '=SUM(A1:C2)' }, { v: 1, t: CellValueType.NUMBER }]]); + + expect(await commandService.executeCommand(UndoCommand.id)).toBeTruthy(); + const valuesUndo = getValues(2, 1, 2, 2); + expect(valuesUndo).toStrictEqual([[{ v: 1, t: CellValueType.NUMBER }, { f: '=A1:B2' }]]); + const valuesUndo2 = getValues(5, 2, 5, 3); + expect(valuesUndo2).toStrictEqual([[{ f: '=SUM(A1:B2)' }, { v: 1, t: CellValueType.NUMBER }]]); + + expect(await commandService.executeCommand(RedoCommand.id)).toBeTruthy(); + const valuesRedo = getValues(2, 2, 2, 3); + expect(valuesRedo).toStrictEqual([[{ v: 1, t: CellValueType.NUMBER }, { f: '=A1:C2' }]]); + const valuesRedo2 = getValues(5, 3, 5, 4); + expect(valuesRedo2).toStrictEqual([[{ f: '=SUM(A1:C2)' }, { v: 1, t: CellValueType.NUMBER }]]); + }); + + it('Remove row, update reference', async () => { + const params: IRemoveRowColCommandParams = { + range: { + startRow: 1, + endRow: 1, + startColumn: 0, + endColumn: 19, + }, + }; + + expect(await commandService.executeCommand(RemoveRowCommand.id, params)).toBeTruthy(); + const values = getValues(0, 6, 0, 6); + expect(values).toStrictEqual([[{ f: '=A1:B1' }]]); + + expect(await commandService.executeCommand(UndoCommand.id)).toBeTruthy(); + const valuesUndo = getValues(0, 6, 0, 6); + expect(valuesUndo).toStrictEqual([[{ f: '=A1:B2' }]]); + + expect(await commandService.executeCommand(RedoCommand.id)).toBeTruthy(); + const valuesRedo = getValues(0, 6, 0, 6); + expect(valuesRedo).toStrictEqual([[{ f: '=A1:B1' }]]); + }); + + it('Remove row, update reference and position', async () => { + const params: IRemoveRowColCommandParams = { + range: { + startRow: 1, + endRow: 1, + startColumn: 0, + endColumn: 19, + }, + }; + + expect(await commandService.executeCommand(RemoveRowCommand.id, params)).toBeTruthy(); + const values = getValues(1, 2, 2, 2); + expect(values).toStrictEqual([[{ f: '=A1:B1' }], [{}]]); + const values2 = getValues(4, 2, 5, 2); + expect(values2).toStrictEqual([[{ f: '=SUM(A1:B1)' }], [{ v: 1, t: CellValueType.NUMBER }]]); + + expect(await commandService.executeCommand(UndoCommand.id)).toBeTruthy(); + const valuesUndo = getValues(1, 2, 2, 2); + expect(valuesUndo).toStrictEqual([[{ v: 1, t: CellValueType.NUMBER }], [{ f: '=A1:B2' }]]); + const valuesUndo2 = getValues(5, 2, 6, 2); + expect(valuesUndo2).toStrictEqual([[{ f: '=SUM(A1:B2)' }], [{ v: 1, t: CellValueType.NUMBER }]]); + + expect(await commandService.executeCommand(RedoCommand.id)).toBeTruthy(); + const valuesRedo = getValues(1, 2, 2, 2); + expect(valuesRedo).toStrictEqual([[{ f: '=A1:B1' }], [{}]]); + const valuesRedo2 = getValues(4, 2, 5, 2); + expect(valuesRedo2).toStrictEqual([[{ f: '=SUM(A1:B1)' }], [{ v: 1, t: CellValueType.NUMBER }]]); + }); + + it('Remove column, update reference', async () => { + const params: IRemoveRowColCommandParams = { + range: { + startColumn: 1, + endColumn: 1, + startRow: 0, + endRow: 2, + }, + }; + + expect(await commandService.executeCommand(RemoveColCommand.id, params)).toBeTruthy(); + const values = getValues(14, 0, 14, 0); + expect(values).toStrictEqual([[{ f: '=A1:A2' }]]); + + expect(await commandService.executeCommand(UndoCommand.id)).toBeTruthy(); + const valuesUndo = getValues(14, 0, 14, 0); + expect(valuesUndo).toStrictEqual([[{ f: '=A1:B2' }]]); + + expect(await commandService.executeCommand(RedoCommand.id)).toBeTruthy(); + const valuesRedo = getValues(14, 0, 14, 0); + expect(valuesRedo).toStrictEqual([[{ f: '=A1:A2' }]]); + }); + + it('Remove column, update reference and position', async () => { + const params: IRemoveRowColCommandParams = { + range: { + startColumn: 1, + endColumn: 1, + startRow: 0, + endRow: 2, + }, + }; + + expect(await commandService.executeCommand(RemoveColCommand.id, params)).toBeTruthy(); + const values = getValues(2, 1, 2, 2); + expect(values).toStrictEqual([[{ f: '=A1:A2' }, {}]]); + const values2 = getValues(5, 1, 5, 2); + expect(values2).toStrictEqual([[{ f: '=SUM(A1:A2)' }, { v: 1, t: CellValueType.NUMBER }]]); + + expect(await commandService.executeCommand(UndoCommand.id)).toBeTruthy(); + const valuesUndo = getValues(2, 1, 2, 2); + expect(valuesUndo).toStrictEqual([[{ v: 1, t: CellValueType.NUMBER }, { f: '=A1:B2' }]]); + const valuesUndo2 = getValues(5, 2, 5, 3); + expect(valuesUndo2).toStrictEqual([[{ f: '=SUM(A1:B2)' }, { v: 1, t: CellValueType.NUMBER }]]); + + expect(await commandService.executeCommand(RedoCommand.id)).toBeTruthy(); + const valuesRedo = getValues(2, 1, 2, 2); + expect(valuesRedo).toStrictEqual([[{ f: '=A1:A2' }, {}]]); + const valuesRedo2 = getValues(5, 1, 5, 2); + expect(valuesRedo2).toStrictEqual([[{ f: '=SUM(A1:A2)' }, { v: 1, t: CellValueType.NUMBER }]]); + }); + + it('Delete move left, value on the left', async () => { + const params: IDeleteRangeMoveLeftCommandParams = { + range: { + startRow: 2, + startColumn: 1, + endRow: 2, + endColumn: 1, + rangeType: 0, + }, + }; + + expect(await commandService.executeCommand(DeleteRangeMoveLeftCommand.id, params)).toBeTruthy(); + const values = getValues(2, 1, 2, 2); + expect(values).toStrictEqual([[{ f: '=A1:B2' }, {}]]); + + expect(await commandService.executeCommand(UndoCommand.id)).toBeTruthy(); + const valuesUndo = getValues(2, 1, 2, 2); + expect(valuesUndo).toStrictEqual([[{ v: 1, t: CellValueType.NUMBER }, { f: '=A1:B2' }]]); + + expect(await commandService.executeCommand(RedoCommand.id)).toBeTruthy(); + const valuesRedo = getValues(2, 1, 2, 2); + expect(valuesRedo).toStrictEqual([[{ f: '=A1:B2' }, {}]]); + }); + + it('Delete move left, value on the right', async () => { + const params: IDeleteRangeMoveLeftCommandParams = { + range: { + startRow: 5, + startColumn: 1, + endRow: 5, + endColumn: 1, + rangeType: 0, + }, + }; + + expect(await commandService.executeCommand(DeleteRangeMoveLeftCommand.id, params)).toBeTruthy(); + const values = getValues(5, 1, 5, 2); + expect(values).toStrictEqual([[{ f: '=SUM(A1:B2)' }, { v: 1, t: CellValueType.NUMBER }]]); + + expect(await commandService.executeCommand(UndoCommand.id)).toBeTruthy(); + const valuesUndo = getValues(5, 2, 5, 3); + expect(valuesUndo).toStrictEqual([[{ f: '=SUM(A1:B2)' }, { v: 1, t: CellValueType.NUMBER }]]); + + expect(await commandService.executeCommand(RedoCommand.id)).toBeTruthy(); + const valuesRedo = getValues(5, 1, 5, 2); + expect(valuesRedo).toStrictEqual([[{ f: '=SUM(A1:B2)' }, { v: 1, t: CellValueType.NUMBER }]]); + }); + + it('Delete move left, update reference', async () => { + const params: IDeleteRangeMoveLeftCommandParams = { + range: { + startRow: 0, + startColumn: 1, + endRow: 1, + endColumn: 1, + rangeType: 0, + }, + }; + + expect(await commandService.executeCommand(DeleteRangeMoveLeftCommand.id, params)).toBeTruthy(); + const values = getValues(14, 0, 14, 0); + expect(values).toStrictEqual([[{ f: '=A1:A2' }]]); + + expect(await commandService.executeCommand(UndoCommand.id)).toBeTruthy(); + const valuesUndo = getValues(14, 0, 14, 0); + expect(valuesUndo).toStrictEqual([[{ f: '=A1:B2' }]]); + + expect(await commandService.executeCommand(RedoCommand.id)).toBeTruthy(); + const valuesRedo = getValues(14, 0, 14, 0); + expect(valuesRedo).toStrictEqual([[{ f: '=A1:A2' }]]); + }); + + it('Delete move up, value on the top', async () => { + const params: IDeleteRangeMoveUpCommandParams = { + range: { + startRow: 1, + startColumn: 2, + endRow: 1, + endColumn: 2, + rangeType: 0, + }, + }; + + expect(await commandService.executeCommand(DeleteRangeMoveUpCommand.id, params)).toBeTruthy(); + const values = getValues(1, 2, 2, 2); + expect(values).toStrictEqual([[{ f: '=A1:B2' }], [{}]]); + + expect(await commandService.executeCommand(UndoCommand.id)).toBeTruthy(); + const valuesUndo = getValues(1, 2, 2, 2); + expect(valuesUndo).toStrictEqual([[{ v: 1, t: CellValueType.NUMBER }], [{ f: '=A1:B2' }]]); + + expect(await commandService.executeCommand(RedoCommand.id)).toBeTruthy(); + const valuesRedo = getValues(1, 2, 2, 2); + expect(valuesRedo).toStrictEqual([[{ f: '=A1:B2' }], [{}]]); + }); + + it('Delete move up, value on the bottom', async () => { + const params: IDeleteRangeMoveUpCommandParams = { + range: { + startRow: 4, + startColumn: 2, + endRow: 4, + endColumn: 2, + rangeType: 0, + }, + }; + + expect(await commandService.executeCommand(DeleteRangeMoveUpCommand.id, params)).toBeTruthy(); + const values = getValues(4, 2, 5, 2); + expect(values).toStrictEqual([[{ f: '=SUM(A1:B2)' }], [{ v: 1, t: CellValueType.NUMBER }]]); + + expect(await commandService.executeCommand(UndoCommand.id)).toBeTruthy(); + const valuesUndo = getValues(5, 2, 6, 2); + expect(valuesUndo).toStrictEqual([[{ f: '=SUM(A1:B2)' }], [{ v: 1, t: CellValueType.NUMBER }]]); + + expect(await commandService.executeCommand(RedoCommand.id)).toBeTruthy(); + const valuesRedo = getValues(4, 2, 5, 2); + expect(valuesRedo).toStrictEqual([[{ f: '=SUM(A1:B2)' }], [{ v: 1, t: CellValueType.NUMBER }]]); + }); + + it('Delete move up, update reference', async () => { + const params: IDeleteRangeMoveUpCommandParams = { + range: { + startRow: 1, + startColumn: 0, + endRow: 1, + endColumn: 1, + rangeType: 0, + }, + }; + + expect(await commandService.executeCommand(DeleteRangeMoveUpCommand.id, params)).toBeTruthy(); + const values = getValues(0, 6, 0, 6); + expect(values).toStrictEqual([[{ f: '=A1:B1' }]]); + + expect(await commandService.executeCommand(UndoCommand.id)).toBeTruthy(); + const valuesUndo = getValues(0, 6, 0, 6); + expect(valuesUndo).toStrictEqual([[{ f: '=A1:B2' }]]); + + expect(await commandService.executeCommand(RedoCommand.id)).toBeTruthy(); + const valuesRedo = getValues(0, 6, 0, 6); + expect(valuesRedo).toStrictEqual([[{ f: '=A1:B1' }]]); + }); + + it('Insert move down, value on the top', async () => { + const params: InsertRangeMoveDownCommandParams = { + range: { + startRow: 1, + startColumn: 2, + endRow: 1, + endColumn: 2, + rangeType: 0, + }, + }; + + expect(await commandService.executeCommand(InsertRangeMoveDownCommand.id, params)).toBeTruthy(); + const values = getValues(2, 2, 3, 2); + expect(values).toStrictEqual([[{ v: 1, t: CellValueType.NUMBER }], [{ f: '=A1:B2' }]]); + + expect(await commandService.executeCommand(UndoCommand.id)).toBeTruthy(); + const valuesUndo = getValues(1, 2, 2, 2); + expect(valuesUndo).toStrictEqual([[{ v: 1, t: CellValueType.NUMBER }], [{ f: '=A1:B2' }]]); + + expect(await commandService.executeCommand(RedoCommand.id)).toBeTruthy(); + const valuesRedo = getValues(2, 2, 3, 2); + expect(valuesRedo).toStrictEqual([[{ v: 1, t: CellValueType.NUMBER }], [{ f: '=A1:B2' }]]); + }); + + it('Insert move down, value on the bottom', async () => { + const params: InsertRangeMoveDownCommandParams = { + range: { + startRow: 4, + startColumn: 2, + endRow: 4, + endColumn: 2, + rangeType: 0, + }, + }; + + expect(await commandService.executeCommand(InsertRangeMoveDownCommand.id, params)).toBeTruthy(); + const values = getValues(6, 2, 7, 2); + expect(values).toStrictEqual([[{ f: '=SUM(A1:B2)' }], [{ v: 1, t: CellValueType.NUMBER }]]); + + expect(await commandService.executeCommand(UndoCommand.id)).toBeTruthy(); + const valuesUndo = getValues(5, 2, 6, 2); + expect(valuesUndo).toStrictEqual([[{ f: '=SUM(A1:B2)' }], [{ v: 1, t: CellValueType.NUMBER }]]); + + expect(await commandService.executeCommand(RedoCommand.id)).toBeTruthy(); + const valuesRedo = getValues(6, 2, 7, 2); + expect(valuesRedo).toStrictEqual([[{ f: '=SUM(A1:B2)' }], [{ v: 1, t: CellValueType.NUMBER }]]); + }); + + it('Insert move down, update reference', async () => { + const params: InsertRangeMoveDownCommandParams = { + range: { + startRow: 1, + startColumn: 0, + endRow: 1, + endColumn: 1, + rangeType: 0, + }, + }; + + expect(await commandService.executeCommand(InsertRangeMoveDownCommand.id, params)).toBeTruthy(); + const values = getValues(0, 6, 0, 6); + expect(values).toStrictEqual([[{ f: '=A1:B3' }]]); + + expect(await commandService.executeCommand(UndoCommand.id)).toBeTruthy(); + const valuesUndo = getValues(0, 6, 0, 6); + expect(valuesUndo).toStrictEqual([[{ f: '=A1:B2' }]]); + + expect(await commandService.executeCommand(RedoCommand.id)).toBeTruthy(); + const valuesRedo = getValues(0, 6, 0, 6); + expect(valuesRedo).toStrictEqual([[{ f: '=A1:B3' }]]); + }); + + it('Insert move right, value on the left', async () => { + const params: InsertRangeMoveRightCommandParams = { + range: { + startRow: 2, + startColumn: 1, + endRow: 2, + endColumn: 1, + rangeType: 0, + }, + }; + + expect(await commandService.executeCommand(InsertRangeMoveRightCommand.id, params)).toBeTruthy(); + const values = getValues(2, 2, 2, 3); + expect(values).toStrictEqual([[{ v: 1, t: CellValueType.NUMBER }, { f: '=A1:B2' }]]); + + expect(await commandService.executeCommand(UndoCommand.id)).toBeTruthy(); + const valuesUndo = getValues(2, 1, 2, 2); + expect(valuesUndo).toStrictEqual([[{ v: 1, t: CellValueType.NUMBER }, { f: '=A1:B2' }]]); + + expect(await commandService.executeCommand(RedoCommand.id)).toBeTruthy(); + const valuesRedo = getValues(2, 2, 2, 3); + expect(valuesRedo).toStrictEqual([[{ v: 1, t: CellValueType.NUMBER }, { f: '=A1:B2' }]]); + }); + + it('Insert move right, value on the right', async () => { + const params: InsertRangeMoveRightCommandParams = { + range: { + startRow: 5, + startColumn: 1, + endRow: 5, + endColumn: 1, + rangeType: 0, + }, + }; + + expect(await commandService.executeCommand(InsertRangeMoveRightCommand.id, params)).toBeTruthy(); + const values = getValues(5, 3, 5, 4); + expect(values).toStrictEqual([[{ f: '=SUM(A1:B2)' }, { v: 1, t: CellValueType.NUMBER }]]); + + expect(await commandService.executeCommand(UndoCommand.id)).toBeTruthy(); + const valuesUndo = getValues(5, 2, 5, 3); + expect(valuesUndo).toStrictEqual([[{ f: '=SUM(A1:B2)' }, { v: 1, t: CellValueType.NUMBER }]]); + + expect(await commandService.executeCommand(RedoCommand.id)).toBeTruthy(); + const valuesRedo = getValues(5, 3, 5, 4); + expect(valuesRedo).toStrictEqual([[{ f: '=SUM(A1:B2)' }, { v: 1, t: CellValueType.NUMBER }]]); + }); + + it('Insert move right, update reference', async () => { + const params: InsertRangeMoveRightCommandParams = { + range: { + startRow: 0, + startColumn: 1, + endRow: 1, + endColumn: 1, + rangeType: 0, + }, + }; + + expect(await commandService.executeCommand(InsertRangeMoveRightCommand.id, params)).toBeTruthy(); + const values = getValues(14, 0, 14, 0); + expect(values).toStrictEqual([[{ f: '=A1:C2' }]]); + + expect(await commandService.executeCommand(UndoCommand.id)).toBeTruthy(); + const valuesUndo = getValues(14, 0, 14, 0); + expect(valuesUndo).toStrictEqual([[{ f: '=A1:B2' }]]); + + expect(await commandService.executeCommand(RedoCommand.id)).toBeTruthy(); + const valuesRedo = getValues(14, 0, 14, 0); + expect(valuesRedo).toStrictEqual([[{ f: '=A1:C2' }]]); + }); + + it('set name', async () => { + const params: ISetWorksheetNameCommandParams = { + subUnitId: 'sheet2', + name: 'Sheet2Rename', + }; + + expect(await commandService.executeCommand(SetWorksheetNameCommand.id, params)).toBeTruthy(); + const values = getValues(14, 2, 14, 2); + expect(values).toStrictEqual([[{ f: '=Sheet2Rename!A1:B2' }]]); + }); + + it('remove sheet', async () => { + const params: IRemoveSheetCommandParams = { + unitId: 'test', + subUnitId: 'sheet2', + }; + + expect(await commandService.executeCommand(RemoveSheetCommand.id, params)).toBeTruthy(); + const values = getValues(14, 2, 14, 2); + expect(values).toStrictEqual([[{ f: '=#REF!' }]]); }); }); }); diff --git a/packages/sheets-formula/src/controllers/update-formula.controller.ts b/packages/sheets-formula/src/controllers/update-formula.controller.ts index 7d9dea361a2..c6371ce4599 100644 --- a/packages/sheets-formula/src/controllers/update-formula.controller.ts +++ b/packages/sheets-formula/src/controllers/update-formula.controller.ts @@ -15,10 +15,8 @@ */ import type { - ICellData, ICommandInfo, IExecutionOptions, - IMutationCommonParams, IRange, IUnitRange, Nullable, @@ -27,16 +25,13 @@ import { Direction, Disposable, ICommandService, - isFormulaString, IUniverInstanceService, LifecycleStages, ObjectMatrix, OnLifecycle, RANGE_TYPE, Rectangle, - RedoCommand, Tools, - UndoCommand, } from '@univerjs/core'; import type { IFormulaData, IFormulaDataItem, ISequenceNode, IUnitSheetNameMap } from '@univerjs/engine-formula'; import { @@ -60,7 +55,6 @@ import type { IInsertSheetMutationParams, IMoveColsCommandParams, IMoveRangeCommandParams, - IMoveRangeMutationParams, IMoveRowsCommandParams, InsertRangeMoveDownCommandParams, InsertRangeMoveRightCommandParams, @@ -93,7 +87,6 @@ import { InsertSheetMutation, MoveColsCommand, MoveRangeCommand, - MoveRangeMutation, MoveRowsCommand, RemoveColCommand, RemoveRowCommand, @@ -103,7 +96,6 @@ import { SelectionManagerService, SetBorderCommand, SetRangeValuesMutation, - SetRangeValuesUndoMutationFactory, SetStyleCommand, SetWorksheetNameCommand, SheetInterceptorService, @@ -111,8 +103,8 @@ import { import { Inject, Injector } from '@wendellhu/redi'; import type { IRefRangeWithPosition } from './utils/offset-formula-data'; -import { offsetArrayFormula, offsetFormula, removeFormulaData } from './utils/offset-formula-data'; -import { handleRedoUndoMoveRange } from './utils/redo-undo-formula-data'; +import { removeFormulaData } from './utils/offset-formula-data'; +import { getFormulaReferenceMoveUndoRedo } from './utils/ref-range-formula'; interface IUnitRangeWithOffset extends IUnitRange { refOffsetX: number; @@ -161,7 +153,7 @@ enum OriginRangeEdgeType { * 2. Use refRange to offset the formula position and return undo/redo data to setRangeValues mutation - Redo data: Delete the old value at the old position on the match, and add the new value at the new position (the new value first checks whether the old position has offset content, if so, use the new offset content, if not, take the old value) - - Undo data: the old position on the match saves the old value, and the new position is left blank + - Undo data: the old position on the match saves the old value, and the new position delete value. Using undos when undoing will operate the data after the offset position. 3. onCommandExecuted, before formula calculation, use the setRangeValues information to delete the old formulaData, ArrayFormula and ArrayFormulaCellData, and send the worker (complementary setRangeValues after collaborative conflicts, normal operation triggers formula update, undo/redo are captured and processed here) */ @@ -198,6 +190,7 @@ export class UpdateFormulaController extends Disposable { if (command.id === SetRangeValuesMutation.id) { const params = command.params as ISetRangeValuesMutationParams; + if ( (options && options.onlyLocal === true) || params.trigger === SetStyleCommand.id || @@ -206,71 +199,38 @@ export class UpdateFormulaController extends Disposable { ) { return; } - this._handleSetRangeValuesMutation(params as ISetRangeValuesMutationParams, options); + this._handleSetRangeValuesMutation(params as ISetRangeValuesMutationParams); } else if (command.id === RemoveSheetMutation.id) { this._handleRemoveSheetMutation(command.params as IRemoveSheetMutationParams); } else if (command.id === InsertSheetMutation.id) { this._handleInsertSheetMutation(command.params as IInsertSheetMutationParams); - } else if ( - (command.params as IMutationCommonParams)?.trigger === UndoCommand.id || - (command.params as IMutationCommonParams)?.trigger === RedoCommand.id - ) { - this._handleRedoUndo(command); // TODO: handle in set range values } }) ); } - private _handleRedoUndo(command: ICommandInfo) { - const { id } = command; - const formulaData = this._formulaDataModel.getFormulaData(); - const arrayFormulaRange = this._formulaDataModel.getArrayFormulaRange(); - const arrayFormulaCellData = this._formulaDataModel.getArrayFormulaCellData(); - - switch (id) { - case MoveRangeMutation.id: - handleRedoUndoMoveRange( - command as ICommandInfo, - formulaData, - arrayFormulaRange, - arrayFormulaCellData - ); - break; - - // TODO:@Dushusir handle other mutations - } - - this._commandService.executeCommand(SetFormulaDataMutation.id, { - formulaData, - }); - this._commandService.executeCommand(SetArrayFormulaDataMutation.id, { - arrayFormulaRange, - arrayFormulaCellData, - }); - } - - private _handleSetRangeValuesMutation(params: ISetRangeValuesMutationParams, options?: IExecutionOptions) { + private _handleSetRangeValuesMutation(params: ISetRangeValuesMutationParams) { const { subUnitId: sheetId, unitId, cellValue } = params; - if ( - (options && options.onlyLocal === true) || - params.trigger === SetStyleCommand.id || - params.trigger === SetBorderCommand.id || - params.trigger === ClearSelectionFormatCommand.id || - cellValue == null - ) { + if (cellValue == null) { return; } - this._formulaDataModel.updateFormulaData(unitId, sheetId, cellValue); + const newSheetFormulaData = this._formulaDataModel.updateFormulaData(unitId, sheetId, cellValue); + const newFormulaData = { + [unitId]: { + [sheetId]: newSheetFormulaData, + }, + }; + this._formulaDataModel.updateArrayFormulaCellData(unitId, sheetId, cellValue); this._formulaDataModel.updateArrayFormulaRange(unitId, sheetId, cellValue); - this._formulaDataModel.updateNumfmtData(unitId, sheetId, cellValue); + this._formulaDataModel.updateNumfmtData(unitId, sheetId, cellValue); // TODO: move model to snapshot this._commandService.executeCommand( SetFormulaDataMutation.id, { - formulaData: this._formulaDataModel.getFormulaData(), + formulaData: newFormulaData, }, { onlyLocal: true, @@ -304,33 +264,38 @@ export class UpdateFormulaController extends Disposable { const { subUnitId: sheetId, unitId } = params; const formulaData = this._formulaDataModel.getFormulaData(); - removeFormulaData(formulaData, unitId, sheetId); + const newFormulaData = removeFormulaData(formulaData, unitId, sheetId); const arrayFormulaRange = this._formulaDataModel.getArrayFormulaRange(); - removeFormulaData(arrayFormulaRange, unitId, sheetId); + const newArrayFormulaRange = removeFormulaData(arrayFormulaRange, unitId, sheetId); const arrayFormulaCellData = this._formulaDataModel.getArrayFormulaCellData(); - removeFormulaData(arrayFormulaCellData, unitId, sheetId); + const newArrayFormulaCellData = removeFormulaData(arrayFormulaCellData, unitId, sheetId); - this._commandService.executeCommand( - SetFormulaDataMutation.id, - { - formulaData, - }, - { - onlyLocal: true, - } - ); - this._commandService.executeCommand( - SetArrayFormulaDataMutation.id, - { - arrayFormulaRange, - arrayFormulaCellData, - }, - { - onlyLocal: true, - } - ); + if (newFormulaData) { + this._commandService.executeCommand( + SetFormulaDataMutation.id, + { + formulaData: newFormulaData, + }, + { + onlyLocal: true, + } + ); + } + + if (newArrayFormulaRange && newArrayFormulaCellData) { + this._commandService.executeCommand( + SetArrayFormulaDataMutation.id, + { + arrayFormulaRange, + arrayFormulaCellData, + }, + { + onlyLocal: true, + } + ); + } } private _handleInsertSheetMutation(params: IInsertSheetMutationParams) { @@ -339,12 +304,12 @@ export class UpdateFormulaController extends Disposable { const formulaData = this._formulaDataModel.getFormulaData(); const { id: sheetId, cellData } = sheet; const cellMatrix = new ObjectMatrix(cellData); - initSheetFormulaData(formulaData, unitId, sheetId, cellMatrix); + const newFormulaData = initSheetFormulaData(formulaData, unitId, sheetId, cellMatrix); this._commandService.executeCommand( SetFormulaDataMutation.id, { - formulaData, + formulaData: newFormulaData, }, { onlyLocal: true, @@ -400,77 +365,21 @@ export class UpdateFormulaController extends Disposable { if (result) { const { unitSheetNameMap } = this._formulaDataModel.getCalculateData(); - let oldFormulaData = this._formulaDataModel.getFormulaData(); - const oldNumfmtItemMap = this._formulaDataModel.getNumfmtItemMap(); + const oldFormulaData = this._formulaDataModel.getFormulaData(); // change formula reference - const { newFormulaData: formulaData, refRanges } = this._getFormulaReferenceMoveInfo( + const { newFormulaData } = this._getFormulaReferenceMoveInfo( oldFormulaData, unitSheetNameMap, result ); - // TODO@Dushusir: handle offset formula data - // const {redos, undos} = refRangeFormula(oldFormulaData, newFormulaData, result); - - const workbook = this._currentUniverService.getCurrentUniverSheetInstance(); - const unitId = workbook.getUnitId(); - const sheetId = workbook.getActiveSheet().getSheetId(); - const selections = this._selectionManagerService.getSelections(); - - // offset arrayFormula - const arrayFormulaRange = this._formulaDataModel.getArrayFormulaRange(); - const arrayFormulaCellData = this._formulaDataModel.getArrayFormulaCellData(); - - // First use arrayFormulaCellData and the original arrayFormulaRange to calculate the offset of arrayFormulaCellData, otherwise the offset of arrayFormulaRange will be inaccurate. - const offsetArrayFormulaCellData = offsetFormula( - arrayFormulaCellData, - command, - unitId, - sheetId, - selections, - arrayFormulaRange, - refRanges - ); - let offsetArrayFormulaRange = offsetFormula( - arrayFormulaRange, - command, - unitId, - sheetId, - selections, - arrayFormulaRange - ); - offsetArrayFormulaRange = offsetArrayFormula(offsetArrayFormulaRange, command, unitId, sheetId); - - // Synchronous to the worker thread - this._commandService.executeCommand( - SetArrayFormulaDataMutation.id, - { - arrayFormulaRange: offsetArrayFormulaRange, - arrayFormulaCellData: offsetArrayFormulaCellData, - }, - { - onlyLocal: true, - } - ); + const { undos, redos } = getFormulaReferenceMoveUndoRedo(oldFormulaData, newFormulaData, result); - // offset formulaData - oldFormulaData = offsetFormula(oldFormulaData, command, unitId, sheetId, selections); - const offsetFormulaData = offsetFormula(formulaData, command, unitId, sheetId, selections); - - // TODO@Dushusir: Here we take the redos incremental data, - // Synchronously to the worker thread, and update the dependency cache. - this._commandService.executeCommand(SetFormulaDataMutation.id, { - formulaData: this._formulaDataModel.getFormulaData(), - }); - - // offset numfmtItemMap - const offsetNumfmtItemMap = offsetFormula(oldNumfmtItemMap, command, unitId, sheetId, selections); - this._commandService.executeCommand(SetNumfmtFormulaDataMutation.id, { - numfmtItemMap: offsetNumfmtItemMap, - }); - - return this._getUpdateFormulaMutations(oldFormulaData, offsetFormulaData); + return { + undos, + redos, + }; } return { @@ -720,76 +629,6 @@ export class UpdateFormulaController extends Disposable { }; } - private _getUpdateFormulaMutations(oldFormulaData: IFormulaData, formulaData: IFormulaData) { - const redos = []; - const undos = []; - const accessor = { - get: this._injector.get.bind(this._injector), - }; - - const formulaDataKeys = Object.keys(formulaData); - - for (const unitId of formulaDataKeys) { - const sheetData = formulaData[unitId]; - - if (sheetData == null) { - continue; - } - - const sheetDataKeys = Object.keys(sheetData); - - for (const subUnitId of sheetDataKeys) { - const value = oldFormulaData[unitId]?.[subUnitId]; - - const oldFormulaMatrix = new ObjectMatrix(value); - const formulaMatrix = new ObjectMatrix(sheetData[subUnitId]); - const cellMatrix = new ObjectMatrix(); - - formulaMatrix.forValue((r, c, formulaItem) => { - const formulaString = formulaItem?.f || ''; - const oldFormulaString = oldFormulaMatrix.getValue(r, c)?.f || ''; - - if (isFormulaString(formulaString)) { - // formula with formula id - if (isFormulaString(oldFormulaString) && formulaString !== oldFormulaString) { - cellMatrix.setValue(r, c, { f: formulaString }); - } else { - // formula with only id - cellMatrix.setValue(r, c, { f: formulaString, si: null }); - } - } - }); - - const cellValue = cellMatrix.getData(); - if (Tools.isEmptyObject(cellValue)) continue; - - const setRangeValuesMutationParams: ISetRangeValuesMutationParams = { - subUnitId, - unitId, - cellValue, - }; - - redos.push({ - id: SetRangeValuesMutation.id, - params: setRangeValuesMutationParams, - }); - - const undoSetRangeValuesMutationParams: ISetRangeValuesMutationParams = - SetRangeValuesUndoMutationFactory(accessor, setRangeValuesMutationParams); - - undos.push({ - id: SetRangeValuesMutation.id, - params: undoSetRangeValuesMutationParams, - }); - } - } - - return { - redos, - undos, - }; - } - private _getFormulaReferenceMoveInfo( formulaData: IFormulaData, unitSheetNameMap: IUnitSheetNameMap, diff --git a/packages/sheets-formula/src/controllers/utils/__tests__/ref-range-formula.spec.ts b/packages/sheets-formula/src/controllers/utils/__tests__/ref-range-formula.spec.ts new file mode 100644 index 00000000000..eb5a0fc890e --- /dev/null +++ b/packages/sheets-formula/src/controllers/utils/__tests__/ref-range-formula.spec.ts @@ -0,0 +1,104 @@ +/** + * Copyright 2023-present DreamNum Inc. + * + * 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 { describe, expect, it } from 'vitest'; + +import type { IFormulaDataItem } from '@univerjs/engine-formula'; +import { formulaDataItemToCellData, isFormulaDataItem } from '../ref-range-formula'; + +describe('Ref range formula test', () => { + describe('Util function', () => { + it('Function formulaDataItemToCellData', () => { + let formulaDataItem: IFormulaDataItem = { + f: '=SUM(1)', + }; + + let result = formulaDataItemToCellData(formulaDataItem); + + expect(result).toStrictEqual({ + f: '=SUM(1)', + }); + + formulaDataItem = { + f: '', + si: 'id1', + }; + + result = formulaDataItemToCellData(formulaDataItem); + + expect(result).toStrictEqual({ + si: 'id1', + }); + + formulaDataItem = { + f: '=SUM(1)', + si: 'id1', + }; + + result = formulaDataItemToCellData(formulaDataItem); + + expect(result).toStrictEqual({ + f: '=SUM(1)', + si: 'id1', + }); + + formulaDataItem = { + f: '=SUM(1)', + si: 'id1', + x: 0, + y: 0, + }; + + result = formulaDataItemToCellData(formulaDataItem); + + expect(result).toStrictEqual({ + f: '=SUM(1)', + si: 'id1', + }); + + formulaDataItem = { + f: '=SUM(1)', + si: 'id1', + x: 0, + y: 1, + }; + + result = formulaDataItemToCellData(formulaDataItem); + + expect(result).toStrictEqual({ + si: 'id1', + }); + + formulaDataItem = { + f: '', + si: '', + x: 0, + y: 1, + }; + + result = formulaDataItemToCellData(formulaDataItem); + + expect(result).toStrictEqual({ f: null, si: null }); + }); + + it('isFormulaDataItem', () => { + expect(isFormulaDataItem({ f: '=SUM(1)' })).toBeTruthy(); + expect(isFormulaDataItem({ f: '' })).toBeFalsy(); + expect(isFormulaDataItem({ f: '', si: 'id1' })).toBeTruthy(); + expect(isFormulaDataItem({ f: '', si: undefined })).toBeFalsy(); + }); + }); +}); diff --git a/packages/sheets-formula/src/controllers/utils/offset-formula-data.ts b/packages/sheets-formula/src/controllers/utils/offset-formula-data.ts index 0c68f403580..c691ddcb7fb 100644 --- a/packages/sheets-formula/src/controllers/utils/offset-formula-data.ts +++ b/packages/sheets-formula/src/controllers/utils/offset-formula-data.ts @@ -397,6 +397,11 @@ export function checkFormulaDataNull(formulaData: IFormulaDataGenerics, un export function removeFormulaData(formulaData: IFormulaDataGenerics, unitId: string, sheetId: string) { if (formulaData && formulaData[unitId] && formulaData[unitId]?.[sheetId]) { delete formulaData[unitId]![sheetId]; + return { + [unitId]: { + [sheetId]: null, + }, + }; } } export function removeValueFormulaArray(formulaRange: IRange, formulaMatrix: ObjectMatrix) { diff --git a/packages/sheets-formula/src/controllers/utils/ref-range-formula.ts b/packages/sheets-formula/src/controllers/utils/ref-range-formula.ts index 698f3f1a3f2..23990aba40a 100644 --- a/packages/sheets-formula/src/controllers/utils/ref-range-formula.ts +++ b/packages/sheets-formula/src/controllers/utils/ref-range-formula.ts @@ -14,9 +14,12 @@ * limitations under the License. */ -import type { IMutationInfo, IRange } from '@univerjs/core'; -import { ObjectMatrix, Tools } from '@univerjs/core'; -import type { IFormulaData } from '@univerjs/engine-formula'; +import type { ICellData, IMutationInfo, IObjectMatrixPrimitiveType, IRange, Nullable } from '@univerjs/core'; +import { cellToRange, Direction, isFormulaId, isFormulaString, ObjectMatrix, Tools } from '@univerjs/core'; +import type { IFormulaData, IFormulaDataItem, IRangeChange } from '@univerjs/engine-formula'; +import type { ISetRangeValuesMutationParams } from '@univerjs/sheets'; +import { EffectRefRangId, handleDeleteRangeMoveLeft, handleDeleteRangeMoveUp, handleInsertCol, handleInsertRangeMoveDown, handleInsertRangeMoveRight, handleInsertRow, handleIRemoveCol, handleIRemoveRow, handleMoveCols, handleMoveRange, handleMoveRows, runRefRangeMutations, SetRangeValuesMutation } from '@univerjs/sheets'; +import { checkFormulaDataNull } from './offset-formula-data'; export enum FormulaReferenceMoveType { MoveRange, // range @@ -44,92 +47,491 @@ export interface IFormulaReferenceMoveParam { sheetName?: string; } -/** - * For different Command operations, it may be necessary to perform traversal in reverse or in forward order, so first determine the type of Command and then perform traversal. - * @param oldFormulaData - * @param newFormulaData - * @param formulaReferenceMoveParam - * @returns - */ -export function refRangeFormula(oldFormulaData: IFormulaData, +export function getFormulaReferenceMoveUndoRedo(oldFormulaData: IFormulaData, newFormulaData: IFormulaData, formulaReferenceMoveParam: IFormulaReferenceMoveParam) { - const type = formulaReferenceMoveParam.type; - - if (type === FormulaReferenceMoveType.SetName) { - // TODO - } else if (type === FormulaReferenceMoveType.RemoveSheet) { - // TODO - } else if (type === FormulaReferenceMoveType.MoveRange) { - // TODO - } else if (type === FormulaReferenceMoveType.MoveRows) { - // TODO - } else if (type === FormulaReferenceMoveType.MoveCols) { - // TODO - } else if (type === FormulaReferenceMoveType.InsertRow) { - // TODO - } else if (type === FormulaReferenceMoveType.InsertColumn) { - return handleInsertCol(oldFormulaData, newFormulaData, formulaReferenceMoveParam); - } else if (type === FormulaReferenceMoveType.RemoveRow) { - // TODO - } else if (type === FormulaReferenceMoveType.RemoveColumn) { - // TODO - } else if (type === FormulaReferenceMoveType.DeleteMoveLeft) { - // TODO - } else if (type === FormulaReferenceMoveType.DeleteMoveUp) { - // TODO - } else if (type === FormulaReferenceMoveType.InsertMoveDown) { - // TODO - } else if (type === FormulaReferenceMoveType.InsertMoveRight) { - // TODO + const { type } = formulaReferenceMoveParam; + + if (type === FormulaReferenceMoveType.SetName || type === FormulaReferenceMoveType.RemoveSheet) { + return getFormulaReferenceSheet(oldFormulaData, newFormulaData); + } else { + return getFormulaReferenceRange(oldFormulaData, newFormulaData, formulaReferenceMoveParam); } } -function handleInsertCol(oldFormulaData: IFormulaData, +export function getFormulaReferenceSheet(oldFormulaData: IFormulaData, + newFormulaData: IFormulaData) { + const undos: IMutationInfo[] = []; + const redos: IMutationInfo[] = []; + + Object.keys(newFormulaData).forEach((unitId) => { + const newSheetData = newFormulaData[unitId]; + const oldSheetData = oldFormulaData[unitId]; + + if (newSheetData == null) { + return true; + } + + if (oldSheetData == null) { + return true; + } + + Object.keys(newSheetData).forEach((subUnitId) => { + const newSheetFormula = new ObjectMatrix(newSheetData[subUnitId]); + const oldSheetFormula = new ObjectMatrix(oldSheetData[subUnitId]); + const redoFormulaMatrix = new ObjectMatrix(); + const undoFormulaMatrix = new ObjectMatrix(); + + newSheetFormula.forValue((r, c, cell) => { + const newValue = formulaDataItemToCellData(cell); + + if (newValue === null) { + return; + } + + redoFormulaMatrix.setValue(r, c, newValue); + undoFormulaMatrix.setValue(r, c, oldSheetFormula.getValue(r, c)); + }); + + if (redoFormulaMatrix.getSizeOf() === 0) { + return; + } + + const redoSetRangeValuesMutationParams: ISetRangeValuesMutationParams = { + subUnitId, + unitId, + cellValue: redoFormulaMatrix.clone(), + }; + + const redoMutation = { + id: SetRangeValuesMutation.id, + params: redoSetRangeValuesMutationParams, + }; + + redos.push(redoMutation); + + const undoSetRangeValuesMutationParams: ISetRangeValuesMutationParams = { + subUnitId, + unitId, + cellValue: undoFormulaMatrix.clone(), + }; + + const undoMutation = { + id: SetRangeValuesMutation.id, + params: undoSetRangeValuesMutationParams, + }; + + undos.push(undoMutation); + }); + }); + + return { + undos, + redos, + }; +} + +export function getFormulaReferenceRange(oldFormulaData: IFormulaData, newFormulaData: IFormulaData, formulaReferenceMoveParam: IFormulaReferenceMoveParam) { + const { sheetId: subUnitId, unitId } = formulaReferenceMoveParam; + const { redoFormulaData, undoFormulaData } = refRangeFormula(oldFormulaData, newFormulaData, formulaReferenceMoveParam); + const redos: IMutationInfo[] = []; const undos: IMutationInfo[] = []; - const { type, unitId, sheetId, range, from, to } = formulaReferenceMoveParam; + if (Object.keys(redoFormulaData).length !== 0) { + const redoSetRangeValuesMutationParams: ISetRangeValuesMutationParams = { + subUnitId, + unitId, + cellValue: redoFormulaData, + }; - if (!Tools.isDefine(oldFormulaData)) { - return { - redos, - undos, + const redoMutation = { + id: SetRangeValuesMutation.id, + params: redoSetRangeValuesMutationParams, }; + + redos.push(redoMutation); } - const formulaDataKeys = Object.keys(oldFormulaData); + if (Object.keys(undoFormulaData).length !== 0) { + const undoSetRangeValuesMutationParams: ISetRangeValuesMutationParams = { + subUnitId, + unitId, + cellValue: undoFormulaData, + }; + + const undoMutation = { + id: SetRangeValuesMutation.id, + params: undoSetRangeValuesMutationParams, + }; + + undos.push(undoMutation); + } - if (formulaDataKeys.length === 0) { + return { + undos, + redos, + }; +} + +/** + * For different Command operations, it may be necessary to perform traversal in reverse or in forward order, so first determine the type of Command and then perform traversal. + * @param oldFormulaData + * @param newFormulaData + * @param formulaReferenceMoveParam + * @returns + */ +export function refRangeFormula(oldFormulaData: IFormulaData, + newFormulaData: IFormulaData, + formulaReferenceMoveParam: IFormulaReferenceMoveParam) { + let redoFormulaData: IObjectMatrixPrimitiveType> = {}; + let undoFormulaData: IObjectMatrixPrimitiveType> = {}; + + const { type, unitId, sheetId, range, from, to } = formulaReferenceMoveParam; + + if (checkFormulaDataNull(oldFormulaData, unitId, sheetId)) { return { - redos, - undos, + redoFormulaData, + undoFormulaData, }; } - for (const unitId of formulaDataKeys) { - const sheetData = oldFormulaData[unitId]; + const currentOldFormulaData = oldFormulaData[unitId]![sheetId]; + const currentNewFormulaData = newFormulaData[unitId]![sheetId]; + + const oldFormulaMatrix = new ObjectMatrix(currentOldFormulaData); + const newFormulaMatrix = new ObjectMatrix(currentNewFormulaData); + + // When undoing and redoing, the traversal order may be different. Record the range list of all single formula offsets, and then retrieve the traversal as needed. + const rangeList: IRangeChange[] = []; + let isReverse = false; + oldFormulaMatrix.forValue((row, column, cell) => { + // Offset is only needed when there is a formula + if (!isFormulaDataItem(cell)) { + return; + } + + const oldCell = cellToRange(row, column); + let newCell = null; - if (sheetData == null) { - continue; + switch (type) { + case FormulaReferenceMoveType.MoveRange: + if (from == null || to == null) { + return; + } + newCell = handleRefMoveRange(from, to, oldCell); + break; + case FormulaReferenceMoveType.MoveRows: + if (from == null || to == null) { + return; + } + newCell = handleRefMoveRows(from, to, oldCell); + break; + case FormulaReferenceMoveType.MoveCols: + if (from == null || to == null) { + return; + } + newCell = handleRefMoveCols(from, to, oldCell); + break; + default: + break; } - const sheetDataKeys = Object.keys(sheetData); - for (const sheetId of sheetDataKeys) { - const matrixData = new ObjectMatrix(sheetData[sheetId]); + if (Tools.isDefine(range)) { + switch (type) { + case FormulaReferenceMoveType.InsertRow: + newCell = handleRefInsertRow(range, oldCell); + isReverse = true; + break; + case FormulaReferenceMoveType.InsertColumn: + newCell = handleRefInsertCol(range, oldCell); + isReverse = true; + break; + case FormulaReferenceMoveType.RemoveRow: + newCell = handleRefRemoveRow(range, oldCell); + break; + case FormulaReferenceMoveType.RemoveColumn: + newCell = handleRefMoveCol(range, oldCell); + break; + case FormulaReferenceMoveType.DeleteMoveLeft: + newCell = handleRefDeleteMoveLeft(range, oldCell); + break; + case FormulaReferenceMoveType.DeleteMoveUp: + newCell = handleRefDeleteMoveUp(range, oldCell); + break; + case FormulaReferenceMoveType.InsertMoveDown: + newCell = handleRefInsertMoveDown(range, oldCell); + isReverse = true; + break; + case FormulaReferenceMoveType.InsertMoveRight: + newCell = handleRefInsertMoveRight(range, oldCell); + isReverse = true; + break; + default: + break; + } + } - matrixData.forValue((row, column, formulaDataItem) => { - if (!formulaDataItem) return true; + if (newCell == null) { + return; + } - const { f: formulaString, x, y, si } = formulaDataItem; + // Note: The formula may only update the reference and not offset the position. The situation where the position is not shifted cannot be intercepted here. + if (isReverse) { + rangeList.unshift({ + oldCell, + newCell, + }); + } else { + rangeList.push({ + oldCell, + newCell, }); } - } + }); + + redoFormulaData = getRedoFormulaData(rangeList, oldFormulaMatrix, newFormulaMatrix); + undoFormulaData = getUndoFormulaData(rangeList, oldFormulaMatrix); return { - redos, - undos, + redoFormulaData, + undoFormulaData, }; } + +function handleRefMoveRange(from: IRange, to: IRange, oldCell: IRange) { + const operators = handleMoveRange( + { + id: EffectRefRangId.MoveRangeCommandId, + params: { toRange: to, fromRange: from }, + }, + oldCell + ); + + return runRefRangeMutations(operators, oldCell); +} + +function handleRefMoveRows(from: IRange, to: IRange, oldCell: IRange) { + const operators = handleMoveRows( + { + id: EffectRefRangId.MoveRowsCommandId, + params: { toRange: to, fromRange: from }, + }, + oldCell + ); + + return runRefRangeMutations(operators, oldCell); +} + +function handleRefMoveCols(from: IRange, to: IRange, oldCell: IRange) { + const operators = handleMoveCols( + { + id: EffectRefRangId.MoveColsCommandId, + params: { toRange: to, fromRange: from }, + }, + oldCell + ); + + return runRefRangeMutations(operators, oldCell); +} + +function handleRefInsertRow(range: IRange, oldCell: IRange) { + const operators = handleInsertRow( + { + id: EffectRefRangId.InsertRowCommandId, + params: { range, unitId: '', subUnitId: '', direction: Direction.DOWN }, + }, + oldCell + ); + + return runRefRangeMutations(operators, oldCell); +} + +function handleRefInsertCol(range: IRange, oldCell: IRange) { + const operators = handleInsertCol( + { + id: EffectRefRangId.InsertColCommandId, + params: { range, unitId: '', subUnitId: '', direction: Direction.RIGHT }, + }, + oldCell + ); + + return runRefRangeMutations(operators, oldCell); +} + +function handleRefRemoveRow(range: IRange, oldCell: IRange) { + const operators = handleIRemoveRow( + { + id: EffectRefRangId.RemoveRowCommandId, + params: { range }, + }, + oldCell + ); + + return runRefRangeMutations(operators, oldCell); +} + +function handleRefMoveCol(range: IRange, oldCell: IRange) { + const operators = handleIRemoveCol( + { + id: EffectRefRangId.RemoveColCommandId, + params: { range }, + }, + oldCell + ); + + return runRefRangeMutations(operators, oldCell); +} + +function handleRefDeleteMoveLeft(range: IRange, oldCell: IRange) { + const operators = handleDeleteRangeMoveLeft( + { + id: EffectRefRangId.DeleteRangeMoveLeftCommandId, + params: { range }, + }, + oldCell + ); + + return runRefRangeMutations(operators, oldCell); +} + +function handleRefDeleteMoveUp(range: IRange, oldCell: IRange) { + const operators = handleDeleteRangeMoveUp( + { + id: EffectRefRangId.DeleteRangeMoveUpCommandId, + params: { range }, + }, + oldCell + ); + + return runRefRangeMutations(operators, oldCell); +} + +function handleRefInsertMoveDown(range: IRange, oldCell: IRange) { + const operators = handleInsertRangeMoveDown( + { + id: EffectRefRangId.InsertRangeMoveDownCommandId, + params: { range }, + }, + oldCell + ); + + return runRefRangeMutations(operators, oldCell); +} + +function handleRefInsertMoveRight(range: IRange, oldCell: IRange) { + const operators = handleInsertRangeMoveRight( + { + id: EffectRefRangId.InsertRangeMoveRightCommandId, + params: { range }, + }, + oldCell + ); + + return runRefRangeMutations(operators, oldCell); +} + +/** + * Delete the old value at the old position on the match, and add the new value at the new position (the new value first checks whether the old position has offset content, if so, use the new offset content, if not, take the old value) + * @param rangeList + * @param oldFormulaData + * @param newFormulaData + */ +function getRedoFormulaData(rangeList: IRangeChange[], oldFormulaMatrix: ObjectMatrix, newFormulaMatrix: ObjectMatrix) { + const redoFormulaData = new ObjectMatrix({}); + + rangeList.forEach((item) => { + const { oldCell, newCell } = item; + + const { startRow: oldStartRow, startColumn: oldStartColumn } = oldCell; + const { startRow: newStartRow, startColumn: newStartColumn } = newCell; + + const newFormula = newFormulaMatrix.getValue(oldStartRow, oldStartColumn) || oldFormulaMatrix.getValue(oldStartRow, oldStartColumn); + const newValue = formulaDataItemToCellData(newFormula); + + redoFormulaData.setValue(oldStartRow, oldStartColumn, { f: null, si: null }); + redoFormulaData.setValue(newStartRow, newStartColumn, newValue); + }); + + return redoFormulaData.clone(); +} + +/** + * The old position on the match saves the old value, and the new position delete value(for formulaData) + * @param rangeList + * @param oldFormulaData + * @param newFormulaData + */ +function getUndoFormulaData(rangeList: IRangeChange[], oldFormulaMatrix: ObjectMatrix) { + const undoFormulaData = new ObjectMatrix({}); + + rangeList.forEach((item) => { + const { oldCell, newCell } = item; + + const { startRow: oldStartRow, startColumn: oldStartColumn } = oldCell; + const { startRow: newStartRow, startColumn: newStartColumn } = newCell; + + const oldFormula = oldFormulaMatrix.getValue(oldStartRow, oldStartColumn); + const oldValue = formulaDataItemToCellData(oldFormula); + + undoFormulaData.setValue(newStartRow, newStartColumn, { f: null, si: null }); + undoFormulaData.setValue(oldStartRow, oldStartColumn, oldValue); + }); + + return undoFormulaData.clone(); +} + +/** + * Transfer the formulaDataItem to the cellData + * ┌────────────────────────────────┬─────────────────┐ + * │ IFormulaDataItem │ ICellData │ + * ├──────────────────┬─────┬───┬───┼───────────┬─────┤ + * │ f │ si │ x │ y │ f │ si │ + * ├──────────────────┼─────┼───┼───┼───────────┼─────┤ + * │ =SUM(1) │ │ │ │ =SUM(1) │ │ + * │ │ id1 │ │ │ │ id1 │ + * │ =SUM(1) │ id1 │ │ │ =SUM(1) │ id1 │ + * │ =SUM(1) │ id1 │ 0 │ 0 │ =SUM(1) │ id1 │ + * │ =SUM(1) │ id1 │ 0 │ 1 │ │ id1 │ + * └──────────────────┴─────┴───┴───┴───────────┴─────┘ + */ +export function formulaDataItemToCellData(formulaDataItem: IFormulaDataItem): ICellData | null { + const { f, si, x = 0, y = 0 } = formulaDataItem; + const checkFormulaString = isFormulaString(f); + const checkFormulaId = isFormulaId(si); + + if (!checkFormulaString && !checkFormulaId) { + return { + f: null, + si: null, + }; + } + + const cellData: ICellData = {}; + + if (checkFormulaId) { + cellData.si = si; + } + + if (checkFormulaString && x === 0 && y === 0) { + cellData.f = f; + } + + return cellData; +} + +export function isFormulaDataItem(cell: IFormulaDataItem) { + const formulaString = cell?.f || ''; + const formulaId = cell?.si || ''; + + const checkFormulaString = isFormulaString(formulaString); + const checkFormulaId = isFormulaId(formulaId); + + if (checkFormulaString || checkFormulaId) { + return true; + } + + return false; +} diff --git a/packages/sheets/src/commands/commands/delete-range-move-left.command.ts b/packages/sheets/src/commands/commands/delete-range-move-left.command.ts index 066f6b10ee8..7991363b9ef 100644 --- a/packages/sheets/src/commands/commands/delete-range-move-left.command.ts +++ b/packages/sheets/src/commands/commands/delete-range-move-left.command.ts @@ -82,11 +82,11 @@ export const DeleteRangeMoveLeftCommand: ICommand = { accessor, deleteRangeMutationParams ); - const redos: IMutationInfo[] = [...removeRangeRedo]; - const undos: IMutationInfo[] = [...removeRangeUndo]; + const redos: IMutationInfo[] = [...(sheetInterceptor.preRedos ?? []), ...removeRangeRedo]; + const undos: IMutationInfo[] = [...sheetInterceptor.undos, ...removeRangeUndo]; redos.push(...sheetInterceptor.redos); redos.push(followSelectionOperation(range, workbook, worksheet)); - undos.push(...sheetInterceptor.undos); + undos.push(...(sheetInterceptor.preUndos ?? [])); // execute do mutations and add undo mutations to undo stack if completed const result = sequenceExecute(redos, commandService).result; diff --git a/packages/sheets/src/commands/commands/delete-range-move-up.command.ts b/packages/sheets/src/commands/commands/delete-range-move-up.command.ts index 72e254f514f..f06052594b7 100644 --- a/packages/sheets/src/commands/commands/delete-range-move-up.command.ts +++ b/packages/sheets/src/commands/commands/delete-range-move-up.command.ts @@ -78,11 +78,11 @@ export const DeleteRangeMoveUpCommand: ICommand = { accessor, deleteRangeMutationParams ); - const redos: IMutationInfo[] = [...removeRangeRedo]; - const undos: IMutationInfo[] = [...removeRangeUndo]; + const redos: IMutationInfo[] = [...(sheetInterceptor.preRedos ?? []), ...removeRangeRedo]; + const undos: IMutationInfo[] = [...sheetInterceptor.undos, ...removeRangeUndo]; redos.push(...sheetInterceptor.redos); redos.push(followSelectionOperation(range, workbook, worksheet)); - undos.push(...sheetInterceptor.undos); + undos.push(...(sheetInterceptor.preUndos ?? [])); const result = await sequenceExecute(redos, commandService).result; if (result) { diff --git a/packages/sheets/src/commands/commands/insert-range-move-down.command.ts b/packages/sheets/src/commands/commands/insert-range-move-down.command.ts index b95700f669b..907ef90fb94 100644 --- a/packages/sheets/src/commands/commands/insert-range-move-down.command.ts +++ b/packages/sheets/src/commands/commands/insert-range-move-down.command.ts @@ -159,7 +159,10 @@ export const InsertRangeMoveDownCommand: ICommand = { }); redoMutations.push(...sheetInterceptor.redos); redoMutations.push(followSelectionOperation(range, workbook, worksheet)); - undoMutations.push(...sheetInterceptor.undos); + undoMutations.push(...(sheetInterceptor.preUndos ?? [])); + + redoMutations.unshift(...(sheetInterceptor.preRedos ?? [])); + undoMutations.unshift(...sheetInterceptor.undos); // execute do mutations and add undo mutations to undo stack if completed const result = sequenceExecute(redoMutations, commandService); diff --git a/packages/sheets/src/commands/commands/insert-range-move-right.command.ts b/packages/sheets/src/commands/commands/insert-range-move-right.command.ts index 076a936efb9..6e445bd50d3 100644 --- a/packages/sheets/src/commands/commands/insert-range-move-right.command.ts +++ b/packages/sheets/src/commands/commands/insert-range-move-right.command.ts @@ -156,7 +156,11 @@ export const InsertRangeMoveRightCommand: ICommand = { }); redoMutations.push(...sheetInterceptor.redos); redoMutations.push(followSelectionOperation(range, workbook, worksheet)); - undoMutations.push(...sheetInterceptor.undos); + undoMutations.push(...(sheetInterceptor.preUndos ?? [])); + + redoMutations.unshift(...(sheetInterceptor.preRedos ?? [])); + undoMutations.unshift(...sheetInterceptor.undos); + // execute do mutations and add undo mutations to undo stack if completed const result = sequenceExecute(redoMutations, commandService); if (result.result) { diff --git a/packages/sheets/src/commands/commands/insert-row-col.command.ts b/packages/sheets/src/commands/commands/insert-row-col.command.ts index a975cbaede0..966c6e20d3a 100644 --- a/packages/sheets/src/commands/commands/insert-row-col.command.ts +++ b/packages/sheets/src/commands/commands/insert-row-col.command.ts @@ -117,8 +117,8 @@ export const InsertRowCommand: ICommand = { if (result.result) { undoRedoService.pushUndoRedo({ unitID: params.unitId, - undoMutations: [{ id: RemoveRowMutation.id, params: undoRowInsertionParams }, ...intercepted.undos], - redoMutations: [{ id: InsertRowMutation.id, params: insertRowParams }, ...intercepted.redos], + undoMutations: [...(intercepted.preUndos ?? []), { id: RemoveRowMutation.id, params: undoRowInsertionParams }, ...intercepted.undos], + redoMutations: [...(intercepted.preRedos ?? []), { id: InsertRowMutation.id, params: insertRowParams }, ...intercepted.redos], }); return true; @@ -287,7 +287,7 @@ export const InsertColCommand: ICommand = { const result = sequenceExecute( [ - + ...(intercepted.preRedos ?? []), { id: InsertColMutation.id, params: insertColParams }, ...intercepted.redos, followSelectionOperation(range, workbook, worksheet), @@ -299,6 +299,7 @@ export const InsertColCommand: ICommand = { undoRedoService.pushUndoRedo({ unitID: params.unitId, undoMutations: [ + ...(intercepted.preUndos ?? []), { id: RemoveColMutation.id, params: undoColInsertionParams, @@ -306,6 +307,7 @@ export const InsertColCommand: ICommand = { ...intercepted.undos, ].filter(Boolean), redoMutations: [ + ...(intercepted.preRedos ?? []), { id: InsertColMutation.id, params: insertColParams }, ...intercepted.redos, ].filter(Boolean), diff --git a/packages/sheets/src/commands/commands/move-range.command.ts b/packages/sheets/src/commands/commands/move-range.command.ts index 53c9fba0984..6c53baea6c0 100644 --- a/packages/sheets/src/commands/commands/move-range.command.ts +++ b/packages/sheets/src/commands/commands/move-range.command.ts @@ -72,6 +72,7 @@ export const MoveRangeCommand: ICommand = { }); const redos = [ + ...(interceptorCommands.preRedos ?? []), ...moveRangeMutations.redos, ...interceptorCommands.redos, { @@ -85,6 +86,9 @@ export const MoveRangeCommand: ICommand = { }, ]; const undos = [ + ...(interceptorCommands.preUndos ?? []), + ...moveRangeMutations.undos, + ...interceptorCommands.undos, { id: SetSelectionsOperation.id, params: { @@ -94,8 +98,6 @@ export const MoveRangeCommand: ICommand = { selections: [{ range: params.fromRange, primary: getPrimaryForRange(params.fromRange, worksheet) }], } as ISetSelectionsOperationParams, }, - ...moveRangeMutations.undos, - ...interceptorCommands.undos, ]; const result = sequenceExecute(redos, commandService).result; diff --git a/packages/sheets/src/commands/commands/move-rows-cols.command.ts b/packages/sheets/src/commands/commands/move-rows-cols.command.ts index 69e29e89730..c96f325e5b6 100644 --- a/packages/sheets/src/commands/commands/move-rows-cols.command.ts +++ b/packages/sheets/src/commands/commands/move-rows-cols.command.ts @@ -144,12 +144,14 @@ export const MoveRowsCommand: ICommand = { const interceptorCommands = sheetInterceptorService.onCommandExecute({ id: MoveRowsCommand.id, params }); const redos = [ + ...(interceptorCommands.preRedos ?? []), { id: MoveRowsMutation.id, params: moveRowsParams }, { id: SetSelectionsOperation.id, params: setSelectionsParam }, ...interceptorCommands.redos, ]; const undos = [ + ...(interceptorCommands.preUndos ?? []), { id: MoveRowsMutation.id, params: undoMoveRowsParams }, { id: SetSelectionsOperation.id, params: undoSetSelectionsParam }, ...interceptorCommands.undos, @@ -271,12 +273,14 @@ export const MoveColsCommand: ICommand = { const interceptorCommands = sheetInterceptorService.onCommandExecute({ id: MoveColsCommand.id, params }); const redos = [ + ...(interceptorCommands.preRedos ?? []), { id: MoveColsMutation.id, params: moveColsParams }, { id: SetSelectionsOperation.id, params: setSelectionsParam }, ...interceptorCommands.redos, ]; const undos = [ + ...(interceptorCommands.preUndos ?? []), { id: MoveColsMutation.id, params: undoMoveColsParams }, { id: SetSelectionsOperation.id, params: undoSetSelectionsParam }, ...interceptorCommands.undos, diff --git a/packages/sheets/src/commands/commands/remove-sheet.command.ts b/packages/sheets/src/commands/commands/remove-sheet.command.ts index 98b4e7479ff..5c840df7c9e 100644 --- a/packages/sheets/src/commands/commands/remove-sheet.command.ts +++ b/packages/sheets/src/commands/commands/remove-sheet.command.ts @@ -89,8 +89,8 @@ export const RemoveSheetCommand: ICommand = { id: RemoveSheetCommand.id, params: { unitId, subUnitId }, }); - const redos = [{ id: RemoveSheetMutation.id, params: RemoveSheetMutationParams }, ...intercepted.redos]; - const undos = [...intercepted.undos, { id: InsertSheetMutation.id, params: InsertSheetMutationParams }]; + const redos = [...(intercepted.preRedos ?? []), { id: RemoveSheetMutation.id, params: RemoveSheetMutationParams }, ...intercepted.redos]; + const undos = [...(intercepted.preUndos ?? []), { id: InsertSheetMutation.id, params: InsertSheetMutationParams }, ...intercepted.undos]; const result = sequenceExecute(redos, commandService); if (result) { diff --git a/packages/sheets/src/commands/commands/set-worksheet-name.command.ts b/packages/sheets/src/commands/commands/set-worksheet-name.command.ts index db426e8e58b..237e55ab1c7 100644 --- a/packages/sheets/src/commands/commands/set-worksheet-name.command.ts +++ b/packages/sheets/src/commands/commands/set-worksheet-name.command.ts @@ -66,8 +66,8 @@ export const SetWorksheetNameCommand: ICommand = { params, }); - const redos = [{ id: SetWorksheetNameMutation.id, params: redoMutationParams }, ...interceptorCommands.redos]; - const undos = [...interceptorCommands.undos, { id: SetWorksheetNameMutation.id, params: undoMutationParams }]; + const redos = [...(interceptorCommands.preRedos ?? []), { id: SetWorksheetNameMutation.id, params: redoMutationParams }, ...interceptorCommands.redos]; + const undos = [...(interceptorCommands.preUndos ?? []), { id: SetWorksheetNameMutation.id, params: undoMutationParams }, ...interceptorCommands.undos]; const result = await sequenceExecute(redos, commandService).result; if (result) {