diff --git a/cell/model/FormulaObjects/traceDependents.js b/cell/model/FormulaObjects/traceDependents.js index 994df6c80c..86fe808ae9 100644 --- a/cell/model/FormulaObjects/traceDependents.js +++ b/cell/model/FormulaObjects/traceDependents.js @@ -72,6 +72,8 @@ function (window, undefined) { // isCalculated: null // } }; + this.aPassedPrecedents = null; + this.aPassedDependents = null; this._lockChangeDocument = null; } @@ -513,6 +515,7 @@ function (window, undefined) { if (!this.dependents[cellIndex]) { // if dependents by cellIndex didn't exist, create it this.dependents[cellIndex] = {}; + let parentCellIndex = null; for (let i in cellListeners) { if (cellListeners.hasOwnProperty(i)) { let parent = cellListeners[i].parent; @@ -564,7 +567,7 @@ function (window, undefined) { } } - let parentCellIndex = getParentIndex(parent); + parentCellIndex = getParentIndex(parent); if (parentCellIndex === null) { //if (parentCellIndex === null || (typeof(parentCellIndex) === "number" && isNaN(parentCellIndex))) { continue; @@ -573,7 +576,7 @@ function (window, undefined) { this._setPrecedents(parentCellIndex, cellIndex, true); } } - if (Object.keys(this.dependents[cellIndex]).length === 0) { + if (Object.keys(this.dependents[cellIndex]).length === 0 && cellIndex !== parentCellIndex) { delete this.dependents[cellIndex]; this.ws.workbook.handlers.trigger("asc_onError", c_oAscError.ID.TraceDependentsNoFormulas, c_oAscError.Level.NoCritical); } @@ -581,9 +584,16 @@ function (window, undefined) { if (this.checkCircularReference(cellIndex, true)) { return; } + if (this.checkPassedDependents(cellIndex)) { + return; + } // if dependents by cellIndex aldready exist, check current tree let currentIndex = Object.keys(this.dependents[cellIndex])[0]; let isUpdated = false; + let bCellHasNotTrace = false; + if (Object.keys(this.dependents[cellIndex]).length === 0) { + bCellHasNotTrace = true; + } for (let i in cellListeners) { if (cellListeners.hasOwnProperty(i)) { let parent = cellListeners[i].parent; @@ -611,7 +621,7 @@ function (window, undefined) { } // if the child cell does not yet have a dependency with listeners, create it - if (!this._getDependents(cellIndex, elemCellIndex)) { + if (!this._getDependents(cellIndex, elemCellIndex) && cellIndex !== elemCellIndex) { this._setDependents(cellIndex, elemCellIndex); this._setPrecedents(elemCellIndex, cellIndex, true); isUpdated = true; @@ -620,11 +630,18 @@ function (window, undefined) { } if (!isUpdated) { + this.setPassedDependents(cellIndex); for (let i in this.dependents[cellIndex]) { if (this.dependents[cellIndex].hasOwnProperty(i)) { - this._calculateDependents(i, curListener, true); + this._calculateDependents(+i, curListener, true); } } + if (!isSecondCall) { + this.clearPassedDependents(); + } + } + if (Object.keys(this.dependents[cellIndex]).length === 0 && bCellHasNotTrace) { + this.ws.workbook.handlers.trigger("asc_onError", c_oAscError.ID.TraceDependentsNoFormulas, c_oAscError.Level.NoCritical); } } } else if (!isSecondCall) { @@ -641,6 +658,9 @@ function (window, undefined) { if (!this.dependents[from]) { this.dependents[from] = {}; } + if (from === to) { + return; + } this.dependents[from][to] = 1; }; TraceDependentsManager.prototype._setDefaultData = function () { @@ -907,7 +927,11 @@ function (window, undefined) { let currentCellIndex = AscCommonExcel.getCellIndex(row, col); let formulaInfoObject = this.checkUnrecordedAndFormNewStack(currentCellIndex, formulaParsed), isHaveUnrecorded, newOutStack; + let bCellHasNotTrace = false; + if (this.precedents[currentCellIndex] && Object.keys(this.precedents[currentCellIndex]).length === 0) { + bCellHasNotTrace = true; + } if (formulaInfoObject) { isHaveUnrecorded = formulaInfoObject.isHaveUnrecorded; newOutStack = formulaInfoObject.newOutStack; @@ -935,6 +959,8 @@ function (window, undefined) { isArea = elemType === cElementType.cellsRange || elemType === cElementType.name, isDefName = elemType === cElementType.name || elemType === cElementType.name3D, isTable = elemType === cElementType.table, areaName; + let bPinCell = false; + let sValue = null; if (elemType === cElementType.cell || is3D || isArea || isDefName || isTable) { let cellRange = new asc_Range(col, row, col, row), elemRange, elemCellIndex; @@ -954,6 +980,8 @@ function (window, undefined) { } else if (elemRange.isOneCell()) { isArea = false; } + sValue = elemValue.value; + bPinCell = sValue.includes("$"); } else if (isTable) { let currentWsId = elem.ws.Id, elemWsId = elem.area.ws ? elem.area.ws.Id : elem.area.wsFrom.Id; @@ -962,6 +990,8 @@ function (window, undefined) { elemRange = elem.area.bbox ? elem.area.bbox : (elem.area.range ? elem.area.range.bbox : null); isArea = ref ? true : !elemRange.isOneCell(); } else { + sValue = elem.value; + bPinCell = sValue.includes("$"); elemRange = elem.range.bbox ? elem.range.bbox : elem.bbox; } @@ -989,6 +1019,20 @@ function (window, undefined) { } } } + } else if (bPinCell) { + const FIRST_INDEX_VALUE = 0; + let nIndex = sValue.indexOf("$"); + if (nIndex === FIRST_INDEX_VALUE) { + sValue = sValue.slice(nIndex + 1); + let bStaticCell = sValue.includes("$"); + if (bStaticCell) { + elemCellIndex = AscCommonExcel.getCellIndex(elemRange.r1, elemRange.c1); + } else { + elemCellIndex = AscCommonExcel.getCellIndex(elemRange.r1 + (row - base.nRow), elemRange.c1); + } + } else { + elemCellIndex = AscCommonExcel.getCellIndex(elemRange.r1, elemRange.c1 + (col - base.nCol)); + } } else { elemCellIndex = AscCommonExcel.getCellIndex(elemRange.r1 + (row - base.nRow), elemRange.c1 + (col - base.nCol)); } @@ -1060,7 +1104,11 @@ function (window, undefined) { if (this.checkCircularReference(currentCellIndex, false)) { return; } + if (this.checkPassedPrecedents(currentCellIndex)) { + return; + } this.setPrecedentsLoop(true); + this.setPassedPrecedents(currentCellIndex); let isHavePrecedents = false; // check first level, then if function return false, check second, third and so on for (let i in this.precedents[currentCellIndex]) { @@ -1074,6 +1122,12 @@ function (window, undefined) { } this.setPrecedentsLoop(false); + if (!isSecondCall) { + this.clearPassedPrecedents(); + } + } + if (Object.keys(this.precedents[currentCellIndex]).length === 0 && bCellHasNotTrace) { + this.ws.workbook.handlers.trigger("asc_onError", c_oAscError.ID.TracePrecedentsNoValidReference, c_oAscError.Level.NoCritical); } }; TraceDependentsManager.prototype.checkUnrecordedAndFormNewStack = function (cellIndex, formulaParsed) { @@ -1252,6 +1306,9 @@ function (window, undefined) { if (!this.precedents[from]) { this.precedents[from] = {}; } + if (from === to) { + return; + } // TODO calculated: 1, not_calculated: 2 // TODO isAreaHeader: "A3:B4" // this.precedents[from][to] = isDependent ? 2 : 1; @@ -1399,6 +1456,59 @@ function (window, undefined) { } } }; + /** + * Sets passed precedents cells index for recursive calls + * @memberof TraceDependentsManager + * @param {number} currentCellIndex + */ + TraceDependentsManager.prototype.setPassedPrecedents = function (currentCellIndex) { + if (!this.aPassedPrecedents) { + this.aPassedPrecedents = []; + } + this.aPassedPrecedents.push(currentCellIndex); + }; + /** + * Checks current cell index is already passed in recursive calls + * @memberof TraceDependentsManager + * @param {number} currentCellIndex + * @returns {boolean} + */ + TraceDependentsManager.prototype.checkPassedPrecedents = function (currentCellIndex) { + return !!(this.aPassedPrecedents && this.aPassedPrecedents.includes(currentCellIndex)); + }; + /** + * Clears attribute of passed precedents + * @memberof TraceDependentsManager + */ + TraceDependentsManager.prototype.clearPassedPrecedents = function () { + this.aPassedPrecedents = null; + }; + /** + * Sets passed dependents cells index for recursive calls + * @memberof TraceDependentsManager + * @param {number} currentCellIndex + */ + TraceDependentsManager.prototype.setPassedDependents = function (currentCellIndex) { + if (!this.aPassedDependents) { + this.aPassedDependents = []; + } + this.aPassedDependents.push(currentCellIndex); + }; + /** + * Checks current cell index is already passed in recursive calls + * @memberof TraceDependentsManager + * @param {number} currentCellIndex + * @returns {boolean} + */ + TraceDependentsManager.prototype.checkPassedDependents = function (currentCellIndex) { + return !!(this.aPassedDependents && this.aPassedDependents.includes(currentCellIndex)); + }; + /** + * Clears attribute of passed dependents + */ + TraceDependentsManager.prototype.clearPassedDependents = function () { + this.aPassedDependents = null; + }; //------------------------------------------------------------export--------------------------------------------------- diff --git a/tests/cell/spreadsheet-calculation/FormulaTrace.js b/tests/cell/spreadsheet-calculation/FormulaTrace.js index 7d9889c1b9..9730738f81 100644 --- a/tests/cell/spreadsheet-calculation/FormulaTrace.js +++ b/tests/cell/spreadsheet-calculation/FormulaTrace.js @@ -2323,6 +2323,39 @@ $(function() { // // asc_Paste -> false // // asc_PasteData -> true* // }); + QUnit.test("Test: \"Recursive formulas\"", function (assert) { + // Case #1: Precedent trace for formula - =A100+1. A dot or line shouldn't be traced + ws.getRange2("A100").setValue("=A100+1"); + let A100Index = AscCommonExcel.getCellIndex(ws.getRange2("A100").bbox.r1, ws.getRange2("A100").bbox.c1); + ws.selectionRange.ranges = [ws.getRange2("A100").getBBox0()]; + ws.selectionRange.setActiveCell(ws.getRange2("A100").getBBox0().r1, ws.getRange2("C1").getBBox0().c1); + + api.asc_TracePrecedents(); + assert.strictEqual(traceManager._getPrecedents(A100Index, A100Index), undefined, "Precedent trace. Formula A100+1. A100. Dot shouldn't be show"); + + // clear traces + api.asc_RemoveTraceArrows(Asc.c_oAscRemoveArrowsType.all); + // Case #2: Dependent trace for formula - =A100+1. A dot or line shouldn't be traced + api.asc_TraceDependents(); + assert.strictEqual(traceManager._getDependents(A100Index, A100Index), undefined, "Dependent trace. Formula A100+1. A100. Dot shouldn't be show"); + // Case #3: Precedent trace for formula - A100: A100+B100, B100: B100+C100. A100<-B100. Arrow without dot in A100 cell. + ws.getRange2("A100").setValue("=A100+B100"); + ws.getRange2("B100").setValue("=B100+C100"); + A100Index = AscCommonExcel.getCellIndex(ws.getRange2("A100").bbox.r1, ws.getRange2("A100").bbox.c1); + let B100Index = AscCommonExcel.getCellIndex(ws.getRange2("B100").bbox.r1, ws.getRange2("B100").bbox.c1); + ws.selectionRange.ranges = [ws.getRange2("A100").getBBox0()]; + ws.selectionRange.setActiveCell(ws.getRange2("A100").getBBox0().r1, ws.getRange2("C1").getBBox0().c1); + + api.asc_TracePrecedents(); + assert.strictEqual(traceManager._getPrecedents(A100Index, B100Index), 1, "Precedent trace. Formula: A100: A100+B100, B100: B100+C100. A100<-B100. Arrow to A100 without dot in A100 cell."); + assert.strictEqual(traceManager._getPrecedents(A100Index, A100Index), undefined, "Precedent trace. Formula: A100: A100+B100, B100: B100+C100. A100. Dot shouldn't be show"); + + // clear traces + api.asc_RemoveTraceArrows(Asc.c_oAscRemoveArrowsType.all); + // Case #4: Dependent trace for formula - A100: A100+B100, B100: B100+C100. A100->B100. Arrow without dot in B100 cell. + api.asc_TraceDependents(); + assert.strictEqual(traceManager._getDependents(A100Index, A100Index), undefined, "Dependent trace. Formula: A100: A100+B100, B100: B100+C100. A100. Dot shouldn't be show"); + }); } QUnit.module("FormulaTrace");