From b21b0ed7c962f93de97383ef1ff8385e1455c1fb Mon Sep 17 00:00:00 2001 From: Wing-Kam Wong Date: Fri, 20 May 2022 11:29:53 +0800 Subject: [PATCH] refactor: circular reference detection on formula --- .../components/editColumn/FormulaOptions.vue | 35 +++++++++++++------ 1 file changed, 24 insertions(+), 11 deletions(-) diff --git a/packages/nc-gui/components/project/spreadsheet/components/editColumn/FormulaOptions.vue b/packages/nc-gui/components/project/spreadsheet/components/editColumn/FormulaOptions.vue index 2f5827856d..a40a658afb 100644 --- a/packages/nc-gui/components/project/spreadsheet/components/editColumn/FormulaOptions.vue +++ b/packages/nc-gui/components/project/spreadsheet/components/editColumn/FormulaOptions.vue @@ -257,26 +257,28 @@ export default { } // check circular reference - // e.g. formula1 -> formula2 -> formula1 - const regex = /cl_\w{14}/g - const formulaPaths = this.meta.columns.filter(c => c.uidt === UITypes.Formula).reduce((res, c) => { - const neighbours = (c.colOptions.formula.match(regex) || []).filter(colId => (this.meta.columns.filter(col => (col.id === colId && col.uidt === UITypes.Formula)).length)) + // e.g. formula1 -> formula2 -> formula1 should return circular reference error + const formulaPaths = [ + // all formula fields excluding itself + ...this.meta.columns.filter(c => c.id !== this.column.id && c.uidt === UITypes.Formula), + // include target formula field before saving + ...this.meta.columns.filter(c => c.title === pt.name && c.uidt === UITypes.Formula) + ].reduce((res, c) => { + // in `formula`, get all the target neighbours + // i.e. all column id (e.g. cl_xxxxxxxxxxxxxx) with formula type + const neighbours = (c.colOptions.formula.match(/cl_\w{14}/g) || []).filter(colId => (this.meta.columns.filter(col => (col.id === colId && col.uidt === UITypes.Formula)).length)) if (neighbours.length > 0) { - // e.g. formula1 -> [formula2, formula3] + // e.g. formula column 1 -> [formula column 2, formula column3] res.push({ [c.id]: neighbours }) } return res }, []) - const targetFormula = this.meta.columns.filter(c => c.title === pt.name && c.uidt === UITypes.Formula)[0] - if (targetFormula) { - formulaPaths.push({ - [this.column.id]: [targetFormula.id] - }) - } const vertices = formulaPaths.length if (vertices > 0) { + // perform kahn's algo for cycle detection const adj = new Map() const inDegrees = new Map() + // init adjacency list & indegree for (const [_, v] of Object.entries(formulaPaths)) { const src = Object.keys(v)[0] const neighbours = v[src] @@ -287,25 +289,36 @@ export default { } } const queue = [] + // put all vertices with in-degree = 0 (i.e. no incoming edges) to queue inDegrees.forEach((inDegree, col) => { if (inDegree === 0) { + // in-degree = 0 means we start traversing from this node queue.push(col) } }) + // init count of visited vertices let visited = 0 + // BFS while (queue.length !== 0) { + // remove a vertex from the queue const src = queue.shift() + // if this node has neighbours, increase visited by 1 const neighbours = adj.get(src) || new Set() if (neighbours.size > 0) { visited += 1 } + // iterate each neighbouring nodes neighbours.forEach((neighbour) => { + // decrease in-degree of its neighbours by 1 inDegrees.set(neighbour, inDegrees.get(neighbour) - 1) + // if in-degree becomes 0 if (inDegrees.get(neighbour) === 0) { + // then put the neighboring node to the queue queue.push(neighbour) } }) } + // vertices not same as visited = cycle found if (vertices !== visited) { arr.push('Can’t save field because it causes a circular reference') }