* [se] Fix dependents and precedents trace
- Fix dependents and precedents trace for cells with circular reference.

* [se] Fix bug with overflow call stack

* [se] Fix bug with recursion dep. trace
- Fix bug with iterative drawing traces for dependents

* [se] For trace
- Fix incorrect drawing traces for pinned cells in formula.

---------

Co-authored-by: Dmitry Gvozdev <dmitriy.gvozdev@onlyoffice.com>
This commit is contained in:
Igor Zotov
2024-06-17 17:49:05 +03:00
committed by GitHub
parent 847cee6498
commit f595d7dad3
2 changed files with 147 additions and 4 deletions

View File

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

View File

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