import { IStoreState, ID, ICell, IRow, ICellNN } from '../../index.data'; import { getCellIdByStr, sumLeft, sumRight, sumChildren, sumSiblings, sumSamePosition, getRangeValue, } from '../formula_support'; import Parser from '../../formula-parser/parser'; import * as R from 'rambda'; import { ERROR_REF, ERROR_VALUE } from '../../formula-parser/error'; import { ParsedResult } from '../../formula-parser/data'; import { getCell, getRow, getRowi, getCellij } from '../selectors'; import { mergeParsedResults, restoreCellRefIfNeeded } from '../formula_support/postProcess'; import { getTypedId } from '../../utils'; import { idMap } from 'valor-app-utils'; import { FormulaManagerConfig } from '../../formula-manager/index.data'; import { getExternalCellById, isExternalCell } from '../formula_support/externalCell'; /** * 如何正确使用这个方法? 请查看单元测试 * 逐个单元格, 对 [整个state进行重算] * 使用场景: 首次重建整个依赖图, 并初始化全部计算值 ( 或增删改行时, 局部计算 ) * 不适合场景: 在依赖图已ok的前提下, 进行二次计算 ( 很可能这个场景最终不会出现 ) * 重点:1. 递归计算单元格, 比如要算cel1l1,可能需要先算出cell2, 2.已经算过的单元就不要再算了 * 需要得到两个结果: * 1. 重新构建整个依赖图 * 2. 重算全部公式 * 不返回整个全新state, 仅返回差异, 本质上可以当成一个action * @param options 使用了这个参数, 将仅计算 部分行, 而不会整体计算 * @param options.targetRowIds 进行行的增删升降级, 只需局部更新某些row, 不进行多余计算 * @param options.force 如果为true, 则强制计算所有delay单元格 */ export function recalculate( state: IStoreState, options?: { targetRowIds?: ID[]; force?: boolean; config?: FormulaManagerConfig }, ) { console.info( '受影响的行数', ((options && options.targetRowIds) || []).length || state.rows.length, ); console.info( '受影响的行序号:', ((options && options.targetRowIds) || []).map(id => getRowi(state, id) + ', '), ); const targetRows = options && options.targetRowIds ? options.targetRowIds.map(it => getRow(state, it)) : state.rows; const targetRowsI = targetRows.map(it => getRowi(state, it.id)); console.time('recalculate1'); // 清除多余的varDeps & cellDeps const { cellDeps, varDeps } = cleanDeps(state, targetRows); console.timeEnd('recalculate1'); const isValuedCell = (cell: ICell) => cell!.formulaDisabled || !cell!.formula || cell!.formula.trim().length === 0; const isDelayCell = (cell: ICell) => options && !options.force && cell!.delay; const known_array: ParsedResult[] = Object.values(state.cells) .filter(cell => { // 假设delay的cell为已知, 前提是必须打开sheet时立即计算一次, 否则无初始值 const { i } = getCellij(state, cell!.id); return isDelayCell(cell) || isValuedCell(cell) || targetRowsI.indexOf(i) < 0; }) .map(cell => ({ result: cell!.value, error: cell!.error! })); const known: Record = idMap(known_array, 'id'); // .reduce( // (acc, cell) => // Object.assign({}, acc, { [cell!.id]: { result: cell!.value, error: cell!.error } }), // {}, // ); const diffs_cells = Object.values(state.cells).filter( cell => // 暂时只支持number公式 // cell.dataType === 'number' && targetRowsI.indexOf(getCellij(state, cell!.id).i) >= 0 && !isValuedCell(cell) && !isDelayCell(cell), ); const diffs_ids = diffs_cells.map(it => it!.id); console.time('recalculate2'); // 用foreach, 性能从2秒降到1ms let diffs = { cells: Object.assign({}, state.cells), cellDeps, varDeps }; let stateForCycle = Object.assign({}, state, { cells: diffs.cells }); diffs_ids.forEach(cellId => { stateForCycle.cells = diffs.cells; const results: Record = evalCellValueRecusively( // assign性能低, 1000元素需2ms, 循环1000次就是2秒 // Object.assign({}, state, { // cells: Object.assign({}, state.cells, diffs.cells), // }), stateForCycle, cellId, known, options && options.config, ); Object.keys(results).forEach(cellId => { known[cellId] = results[cellId]; }); diffs = mergeParsedResults(diffs, results); }); // const diffs = diffs_ids.reduce( // (acc: any, cellId) => { // const results: Record = evalCellValueRecusively( // Object.assign({}, state, { // cells: Object.assign({}, state.cells, acc.cells), // }), // cellId, // known, // ); // Object.keys(results).forEach(cellId => { // known[cellId] = results[cellId]; // }); // console.log('acc', acc); // return mergeParsedResults(acc, results); // }, // { // cells: state.cells, // cellDeps, // varDeps, // }, // ); console.timeEnd('recalculate2'); console.time('recalculate3'); // 尝试恢复cell的引用 const newDiffs = restoreCellRefIfNeeded(state, diffs); console.timeEnd('recalculate3'); return newDiffs; // return diffs; } /* // 'sheet1!15' => ExternalCell function getExtCellById(cellId: string, config?: FormulaManagerConfig): ICellNN | null { if (isExternalCell(cellId)) { if (!config) { throw new Error('查找表间引用出错: 必须在初始化SpreadSheet时, 配置 formulaConfig 属性!'); } const [extSheetId, extCellId] = (cellId + '').split('!'); return config.getExternalCell(extSheetId, extCellId); } return null; } */ /** * 递归地计算某个单元格的值 * 适用场景: 全部重算 * 基本假定: 依赖都未完成计算, 需要逐个计算, 并且排重 * 不适用: 单个单元格的即时计算 * @param state * @param cellId 要计算的单元格的值 * @return cells: 递归累积的cell.value,error * 特别指出: 返回值 并不仅是当前cellId对应的返回值, 而包括中间计算的所有返回值, 目的:全部重算时,算过的不再算 * 所以, 如果想自动重算, 则使用本方法, 如果仅想即时计算单个单元格, 则直接使用evalCellValue */ export function evalCellValueRecusively( state: IStoreState, _cellId: ID, known: Record = {}, config?: FormulaManagerConfig, ): Record { const results = {} as ReturnType; // 用于递归 function inner(cellId: ID) { // 在这一级抛出错误, 会中断程序 // if (1 == 1) throw Error(ERROR_REF); if (known[cellId] !== undefined) { // if (known[cellId].error) throw new Error(ERROR_VALUE); return known[cellId].result; } const cell = getCell(state, cellId, config); // 第一次总是从外部进入, 一定不会出现cell===null, 所以下面这句应该是多余的, 不会引起中断 if (!cell) throw new Error(ERROR_REF); if (!cell.formula || cell.formulaDisabled) { return cell.value; } const parser: Parser = new Parser({ getCellValue: sCell => { const cellId = getCellIdByStr(state, sCell, config)!; if (cellId + '' === _cellId + '') { console.error('循环引用!'); throw new Error(ERROR_REF); } if (R.isNil(cellId)) throw new Error(ERROR_REF); let x; if (isExternalCell(cellId)) { var externalCell = getExternalCellById(cellId + '', config); if (!externalCell) { console.log(`引用外部单元格不存在: ${cellId}`); throw new Error(ERROR_REF); } x = externalCell.value; } else { x = inner(cellId); } return { result: x, error: null, deps: { cells: [cellId] } }; }, getRangeValue: ([sStartCellId, sEndCellId]: [string, string]) => { const startCellId = getCellIdByStr(state, sStartCellId, config); const endCellId = getCellIdByStr(state, sEndCellId, config); return getRangeValue(state, startCellId, endCellId, inner); }, sumLeft: (args: any[] | undefined) => { return args ? sumLeft(state, cellId, args[0], args[1], inner) : sumLeft(state, cellId, undefined, undefined, inner); }, sumRight: (args: any[] | undefined) => { return args ? sumRight(state, cellId, args[0], args[1], inner) : sumRight(state, cellId, undefined, undefined, inner); }, sumSamePosition: (args: any[] | undefined) => sumSamePosition(state, cellId, args ? args[0] : undefined, inner), sumChildren: () => sumChildren(state, cellId, inner), sumSiblings: () => sumSiblings(state, cellId, inner), variables: state.variables, }); const result = parser.parseWithDeps(cell.formula); results[cellId!] = result; return result.result; } inner(_cellId); return results; } /** * 清除多余的deps * 两种情况: * 1. state中已删除某些行, 但一直到calculate时才能重置 deps, 因此在这里清除 * 2. 对于exceptRows, 因为需要重新计算, 为了避免 之前的deps错误地保留 , 因此先清除 */ export function cleanDeps( state: IStoreState, exceptRows: IRow[], ): { cellDeps: Record; varDeps: Record } { const exceptCellIds = (R.flatten( exceptRows.map(it => it.cellIds!.filter(Boolean)), ) as any) as ID[]; const allCellIds = Object.values(state.cells).map(it => it!.id); // 性能比R.difference好8-80倍 const validCellIds = R.filter(it1 => exceptCellIds.indexOf(it1) < 0, allCellIds); // 这处是性能瓶颈, 但当280行时, 插入1行, 仅需284ms const cellDeps = Object.keys(state.cellDeps || {}).reduce( (acc, cellId) => { if (validCellIds.indexOf(getTypedId(cellId)) >= 0) { // slow 几十倍 // const hostCellIds = R.intersection(state.cellDeps[cellId], validCellIds); // fast const hostCellIds = R.filter(it => validCellIds.indexOf(it) >= 0, state.cellDeps[cellId]); if (hostCellIds.length > 0) { acc[cellId] = hostCellIds; } } return acc; }, {} as Record, ); const varDeps = Object.keys(state.varDeps || {}).reduce( (acc, varName) => { acc[varName] = R.intersection(state.varDeps[varName], validCellIds); return acc; }, {} as Record, ); return { varDeps, cellDeps }; }