'Improving performance of Trace Dependents script in Google Apps Script
I have a Google Apps Script that will replicate Excel's 'Trace Dependents' function by finding all the dependents of a cell from the entire worksheet, taking into account named ranges. The script works perfectly for small worksheets, unfortunately when working with largish worksheets the script will time out before it manages to complete. I have worksheets with around 1m+ cells and the script sometimes manages to run fully but even then it takes around 5 minutes which is quite long.
Essentially the script works by looping through every formula in the worksheet and performing regex tests on them to see if the formulas include the audited cells reference or name.
I was wondering if there are any quick wins in my script that could help speed up the performance, or if anyone has any suggestions on how to go about improving somehow?
Apologies if this isn't the right place to ask this sort of question, if there is somewhere else I should ask this please let me know.
const getNamedRange = function (actSheet, cell) {
//loop through the sheets named ranges and if the nr's range is the cell, then that name is the name of the current cell
let matchedName;
actSheet.getNamedRanges().forEach((name) => {
if (name.getRange().getA1Notation() === cell) matchedName = name.getName();
});
return matchedName;
};
const isInRange = function (ss, currentCell, stringRange, j, k) {
//extract the sheet name from the range if it has one
const sheetName = stringRange[0].toString().match(/^(.*)!/)
? stringRange[0].toString().match(/^(.*)!/)[1]
: null;
//if there is a sheet name, get the range from that sheet, otherwise just get the range from the active sheet as it will be on the same sheet as audited cell
const range = sheetName
? ss.getSheetByName(sheetName).getRange(stringRange)
: ss.getActiveSheet().getRange(stringRange);
const startRow = range.getRow();
const endRow = startRow + range.getNumRows() - 1;
const startCol = range.getColumn();
const endCol = startCol + range.getNumColumns() - 1;
const cellRow = currentCell.getRow();
const cellCol = currentCell.getColumn();
const deps = [];
if (cellRow >= startRow && cellRow <= endRow && cellCol >= startCol && endCol <= endCol)
deps.push([j, k]);
return deps
};
function traceDependents() {
const ss = SpreadsheetApp.getActiveSpreadsheet();
const currentCell = ss.getCurrentCell();
const curCellRef = currentCell.getA1Notation();
const dependentRefs = [];
const sheets = ss.getSheets();
const actSheet = ss.getActiveSheet();
const actSheetName = actSheet.getName();
const actIndex = actSheet.getIndex();
//get the name of the cell
const namedRange = getNamedRange(actSheet, curCellRef);
//get the row and column text from the current cell
const rowText = currentCell.getRow().toString();
const columnText = curCellRef.substring(0, curCellRef.length - rowText.length);
//If the sheet name has a space, then need to add the quote marks and ! as per Google Sheets standard
const formattedActSheetName = actSheetName.includes(" ")
? `'${actSheetName}'!`
: `${actSheetName}!`;
for (let i = 0; i < sheets.length; i++) {
const range = sheets[i].getDataRange();
const formulas = range.getFormulas();
const dependents = [];
//If the sheet is the current sheet, then all references will not have the sheet ref, so it should be blank
const curSheetRef = i === actIndex - 1 ? "" : formattedActSheetName;
//create the tests to see if the formulas include the current cell
const crRegex = new RegExp(
`(?<!!|:)${curSheetRef}${curCellRef}(?![0-9])|` +
`(?<!!|:)${curSheetRef}\\$${curCellRef}(?![0-9])|` +
`(?<!!|:)${curSheetRef}[^$]${columnText}\\$${rowText}(?![0-9])|` +
`(?<!!|:)${curSheetRef}\\$${columnText}\\$${rowText}(?![0-9])`
);
const nrRegex = new RegExp(`(?<!_)${namedRange}(?!_)`);
//run through all of the cells in the sheet and test their formulas with the above to see if they are dependents
for (let j = 0; j < formulas.length; j++) {
const row = formulas[j];
for (let k = 0; k < row.length; k++) {
const cellFormula = row[k];
if (crRegex.test(cellFormula) || nrRegex.test(cellFormula))
dependents.push([j, k]);
//check if the current cell formula includes a range in it, e.g. A1:A20, if it does, create a unique array with all the large ranges
const largeRegex = new RegExp(
`(?<!!|:|\\$)${curSheetRef}\\$?[A-Z]{1,3}\\$?([0-9]{1,7})?:\\$?[A-Z]{1,3}\\$?([0-9]{1,7})?`,
"g"
);
const largeRange = [...new Set(cellFormula.match(largeRegex))];
//if there are any large ranges, check if the range includes the audited cell. If it does, add the cell to the dependents
if (largeRange) {
largeRange.forEach((range) => {
range.replaceAll("$", "");
isInRange(ss, currentCell, range, j, k).forEach((dep) =>
dependents.push(dep)
);
});
}
}
}
//Format the dependent's cell references with their sheet name to allow navigation to them
for (let l = 0; l < dependents.length; l++) {
const cell = range.getCell(dependents[l][0] + 1, dependents[l][1] + 1);
dependentRefs.push(`${sheets[i].getName()}!${cell.getA1Notation()}`);
}
}
//Add the current cell as the first element of the array
dependentRefs.unshift(`${actSheetName}!${curCellRef}`);
return [...new Set(dependentRefs)];
}
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow
| Solution | Source |
|---|
