import * as React from 'react'; import { IStoreState, ID, ICell, IRow, ICellNN } from '../../index.data'; import { getRowByCell, getCell, getCellij, getRow, isTreeRow, getRowWithCells } from '../selectors'; import { pickPatch, getTypedId } from '../../utils'; import { idMap, dagSort, dissoc } from 'valor-app-utils'; import * as R from 'rambda'; import { evalCellValue } from '../formula_support'; import { removeCellFromDeps, getParser, mergeParsedResults, extractChangedCells, } from '../formula_support/postProcess'; import { ERROR_REF } from '../../formula-parser/error'; import { tryRealizeAbstractFormulaForCell } from '../../cellTypes/formula/FormulaEditor/NamedFormulaEditor/AbstractCellMapper/helper'; import { IRowWithCell } from '../../SpreadSheetProvider/index.data'; import { FormulaManagerConfig } from '../../formula-manager/index.data'; import { SpreadSheetRuntime } from '../../RuntimeContext'; /** * @deprecated * 填充row-no型cell的value * 每当tree-context或collapsed发生变化时调用 */ export function fillRowNoCellValue(state: IStoreState) { console.warn('暂未使用'); const newCells = Object.keys(state.cells).reduce((acc, _cellId) => { const cellId = getTypedId(_cellId); const cell = state.cells[cellId]!; const row = getRowByCell(state, cellId); const isRowNoCell = row.type === 'body' && (cell!.type || state.columns[getCellij(state, cellId).j].type) === 'row-no'; const newCell = isRowNoCell ? { ...cell, value: { isLeaf: state.treeContext[cell!.rowId!].childrenIds.length === 0, expanded: true, path: [...state.treeContext[cell!.rowId!].path], }, } : cell; return { ...acc, [cellId]: newCell } as any; }, state.cells); return pickPatch(state, { cells: newCells }); } /** * @param replace 如果replace=true, 则直接覆盖整个style ( undo场合 ) */ export function setCellStyle( state: IStoreState, ids: ID[], style: React.CSSProperties, replace = false, ) { const newCells = Object.assign({}, state.cells); for (let id of ids) { const newStyle = replace ? { ...style } : { ...state.cells[id]!.style, ...style }; const newCell = newCells[id]!.locked ? newCells[id] : Object.assign({}, newCells[id], { style: newStyle }); newCells[id] = newCell; } // const cells = { // ...state.cells, // [id]: { // id, // ...state.cells[id], // style: replace ? { ...style } : { ...state.cells[id]!.style, ...style }, // }, // }; return { cells: newCells }; } /** * @deprecated * 设置某个单元格的值 */ export function setCellValue(state: IStoreState, id: ID, value: any) { console.error('此方法已过期, 请改用更统一的 setCellData方法, 以便正确推算公式'); if (value === state.cells[id]!.value) return; const cells = { ...state.cells, [id]: { ...state.cells[id], value } }; return { cells }; } /** * patch某个单元格 * @param replace 在undo时, 应直接replace为整个老cell, 而在do时, 一般应merge新cell到老cell */ export function setCellData( state: IStoreState, id: ID, cellData: Partial, replace = false, runtime: SpreadSheetRuntime, ) { const oldCell = state.cells[id]; const newCell_ = replace ? { ...cellData } : { ...state.cells[id], ...cellData }; // 尝试实现单元格的抽象公式 ( 仅当该单元格中, 所有引用到的单元格能完美匹配时才实现 ) const newCell = tryRealizeAbstractFormulaForCell(state, newCell_ as any)!; // id 可能由 number 变成 string, 不需比较 if (R.equals(dissoc(oldCell, 'id'), dissoc(newCell, 'id') as any)) return {}; let patch: Partial = {}; const newState = { ...state, cells: { ...state.cells, [id]: newCell as ICellNN } }; if (runtime && runtime.batchMode) return newState; // 0. 不存在以下场景: cell有公式, 却可以直接输入value, 从而覆盖公式 (也就是说: 如果有公式, 则必须先清除公式, 再输入值, 两步) // code should ignore // 1. 比较新cell与老cell, 如果公式有效且不相同, 应重算value, 并重置 deps if ( oldCell!.formula !== newCell!.formula || oldCell!.formulaDisabled !== newCell.formulaDisabled ) { // 公式有变化 // 1.1 如果公式有效, 那么重算 if (newCell.formula && !newCell.formulaDisabled) { const parser = getParser( newState as IStoreState, id, runtime && runtime.formulaManager.config, ); const parsedResult = evalCellValue(newState as IStoreState, newCell as ICell, parser); // 注意这里的deps需要反过来 patch = { ...mergeParsedResults(newState as IStoreState, { [id]: parsedResult }) }; } else { // 如果公式已经失效了, 那么清理老依赖 let deps: { varDeps: Record; cellDeps: Record; } = R.pick(['varDeps', 'cellDeps'], state) as any; patch = { ...removeCellFromDeps(id, deps), cells: { ...state.cells, [id]: newCell as ICellNN }, } as any; } } else if (oldCell!.value !== newCell.value) { // value有变化 patch = { cells: { ...state.cells, [id]: newCell as ICellNN }, cellDeps: state.cellDeps, varDeps: state.varDeps, } as any; } if (R.isEmpty(patch)) { patch = { cells: { ...state.cells, [id]: newCell as ICellNN } } as any; } // 2. 比较新cell.value与老cell.value, 如果不相同, 应重新传播到其它引用单元格 if (oldCell!.value !== patch.cells![id]!.value) { const newCells = valuePropagation( { ...state, ...patch }, id, runtime && runtime.formulaManager.config, runtime && runtime.disableAutoCalculate, ); patch = { ...patch, ...newCells }; } return patch; } /** * 单元格A0 -> [A1,A2], 则A0 的变化, 将引起 A1与A2的变化, 这个变化将一直传递 * 这个方法只负责传播变化 ( 假定变化真的发生了 ) * 访问这个方法前, 请自行确保 value 已发生变化 */ export function valuePropagation( state: IStoreState, cellId: ID, config?: FormulaManagerConfig, disableAutoRecalculate?: boolean, ): { cells: Record } { if (disableAutoRecalculate) { return { cells: state.cells }; } let cycleCount = 0; const usedCellIds: ID[] = []; function recursive(cellDeps: Record, cellId: ID): Record { if (usedCellIds.indexOf(cellId) >= 0) { return {}; } usedCellIds.push(cellId); if (cycleCount++ > 1000) { throw new Error(ERROR_REF); } const depCells = cellDeps[cellId]; if (!depCells || R.isEmpty(depCells)) return { [cellId]: [] }; // const acc = depCells.reduce((acc, depCellId) => { // return { ...acc, ...recursive(cellDeps, depCellId) }; // }, {}); const acc: Record = {}; depCells.forEach(depCellId => { Object.assign(acc, recursive(cellDeps, depCellId)); }); return { ...acc, [cellId]: cellDeps[cellId] }; } // 获得 [[11, [14]], [12, [11, 14]], [13,[11,12]], [14,[]]] 这样的数组 const changedCellIds = R.toPairs(recursive(state.cellDeps, cellId)).map(([a, b]) => [ getTypedId(a), b, ]) as any; // 获得 [[13],[12], [11], [14]] const dagSortedCellIds = dagSort(changedCellIds); // 获得 [12, 11, 14] , 注意排除 第一个数 ( 这是已经计算过的 ) const sortedCellIds = (R.flatten(dagSortedCellIds) as ID[]).filter(it => it !== cellId); // reduce => foreach let newState = Object.assign({}, state, { cells: state.cells }); sortedCellIds.forEach(id => { const parser = getParser(newState, id, config); const cell = getCell(newState, id)!; const parsedResult = evalCellValue(newState, cell, parser); newState.cells[id] = Object.assign({}, newState.cells[id]!, { value: parsedResult.result, error: parsedResult.error, }); }); /* reduce性能低, 优化成foreach const newState_old = sortedCellIds.reduce((acc, id) => { const parser = getParser(acc, id, config); const cell = getCell(acc, id)!; const parsedResult = evalCellValue(acc, cell, parser); var newCells = Object.assign({}, acc.cells); newCells[id] = Object.assign({}, acc.cells[id]!, { value: parsedResult.result, error: parsedResult.error, }); return Object.assign({}, acc, { cells: newCells }); // return { // ...acc, // cells: { // ...acc.cells, // [id]: { ...acc.cells[id]!, value: parsedResult.result, error: parsedResult.error }, // }, // }; }, state); */ // console.timeEnd('valuePropagation'); return { cells: newState.cells }; } export function tryEnableFormula( state: IStoreState, affectedRowIds: ID[], helpers?: { normalizeRow: (rows: IRow[], row: IRowWithCell) => IRowWithCell; }, ): { rows: IRow[]; cells: Record } { if (!helpers) return { rows: [], cells: {} }; const changed = affectedRowIds.reduce( (acc, rowId) => { const row: IRowWithCell = getRowWithCells(state, rowId); const newRowWithCells = helpers.normalizeRow(state.rows, row); // 仅找到变动的cell const newCells = extractChangedCells(state, newRowWithCells.cells); const newRow = dissoc(newRowWithCells, 'cells'); const rowChanged = !R.equals(newRow, dissoc(row, 'cells')); return { cells: acc.cells.concat(newCells), rows: rowChanged ? acc.rows.concat(newRow) : acc.rows, }; }, { cells: [] as ICellNN[], rows: [] as IRow[] }, ); return { rows: changed.rows, cells: idMap(changed.cells) }; } /** * 对于affectedRowIds, 逐个检查并设置单元格的 disabledFormula属性 * update- 1: 对于非叶子行, 若有dynamicFormula.nonleaf的单元格, 则将之复制到 formula * old 1.对于有children的行内单元格, 并且存在 sumChildren, 那么开启公式 * * update- 2: 对于叶子行, 若有dynamicFormula.leaf的单元格, 则将之复制到 formula * old 2. 对于没有children的行内单元格, 并且存在 sumChildren, 那么关闭公式 * * update- 3: 不检查具体公式, 而是检查 dynamicFormula * old 3. 具体来说, 目前仅 sumChildren 需要检查 * @param state * @param affectedRowIds : 受影响的行, 比如 新插入行1, 则[行1, 行1.parent受影响] * @returns : {id:cell}, 注意这是patch部分, 也就是: 只有变动的 cell会进入 */ /* export function tryEnableFormula1(state: IStoreState, affectedRowIds: ID[]) { const { treeContext, cells } = state; // 只有 tree area 才需要tryEnable const affectedRows = affectedRowIds.map(rowId => getRow(state, rowId)!).filter(isTreeRow); const affectedCells = affectedRows.reduce( (acc, row) => { const { childrenIds } = treeContext[row.id]; const hasChildren = !!childrenIds && childrenIds.length > 0; const rowCellIds = (row.cellIds || []).filter(Boolean); const newAffectedCells: ICell[] = []; for (let k = 0; k < rowCellIds.length; k++) { const cell = cells[rowCellIds[k]!]; if (hasChildren && cell!.dynamicFormula) { // 由叶 => 非叶, 则 nonleaf 公式总是生效 ( 想想如果 child1.costType=1 && child2.costType=3 同时存在 ) // 即不受applyWhenLevelChange的影响 if (cell!.dynamicFormula.nonleaf) { newAffectedCells.push({ ...cell!, formula: cell!.dynamicFormula.nonleaf, formulaDisabled: false, // 动态公式生效后, 设置locked locked: true, }); } else if (cell!.dynamicFormula.leaf === cell!.formula) { // 应付 testing中, 第23单元格的情形: 动态公式失效 newAffectedCells.push({ ...cell!, formula: '', formulaDisabled: true, // 动态公式失效后, 设置locked locked: false, }); } } else if (!hasChildren && cell!.dynamicFormula) { // 由 非叶 => 叶, 则 只有applyWhenLevelChange=true时, 才需要设置leaf公式 if (cell!.dynamicFormula.leaf && cell!.dynamicFormula.applyWhenLevelChange) { newAffectedCells.push({ ...cell!, formula: cell!.dynamicFormula.leaf, formulaDisabled: false, // 动态公式生效后, 设置locked locked: true, }); } else if (cell!.dynamicFormula.nonleaf === cell!.formula) { // 表示这个公式, 是由nonleaf设置, 既然现在成了叶结点 , 当然需要取消 // 应付 testing中, 第33单元格的情形 newAffectedCells.push({ ...cell!, formula: '', formulaDisabled: true, // 动态公式失效后, 设置locked locked: false, }); } } // if ( // hasChildren && // cell!.formula && // cell!.formula!.includes('sumChildren') && // cell!.formulaDisabled // ) { // newAffectedCells.push({ ...cell!, formulaDisabled: false }); // } else if ( // !hasChildren && // cell!.formula && // cell!.formula!.includes('sumChildren') && // !cell!.formulaDisabled // ) { // newAffectedCells.push({ ...cell!, formulaDisabled: true }); // } } return [...acc, ...newAffectedCells]; }, [] as ICell[], ); return idMap(affectedCells); } */