Improving performance of Trace Dependents script in Google Apps Script - javascript

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)];
}

Related

How to delete rows fast if they have empty values at specific columns in Google App Script

below is a code that checks if the cells at columns [G, H, J] are empty and delete the row where the the condition is true.
Nevertheless, the runtime of the code below is extremely slow, needs approx 15 minutes per 1500 table entries.
is there any way to optimise it ?
can I hash the rows that meet the condition below and then delete them all at once?
P.S: the original code can be found here https://gist.github.com/dDondero/285f8fd557c07e07af0e which I adapted it to my use case.
function deleteRows() {
var sheet = SpreadsheetApp.getActiveSheet();
var rows = sheet.getDataRange();
var lastRow =sheet.getRange(1,1).getDataRegion(SpreadsheetApp.Dimension.ROWS).getLastRow() + 1;
var values = rows.getValues();
var row;
var rowsDeleted = 0;
for (var i = 0; i < lastRow; i++) {
row = values[i];
if (row[9] == '' && row[7] == '' && row[6] == ''
) {
sheet.deleteRow((parseInt(i)+1) - rowsDeleted);
rowsDeleted++;
}
}
}
Try:
function DeleteEmptyRows() {
const sheet = SpreadsheetApp.getActiveSheet()
const values = sheet.getDataRange()
.getValues()
.filter(row => row[9] !== '' && row[7] !== '' && row[6] !== '')
sheet.getDataRange().clearContent()
return sheet.getRange(1, 1, values.length, values[0].length)
.setValues(values)
}
If you have any background colors attached to rows, let me know and I can make an adjustment for you.
This code will filter out all rows with the empty cells specified, clear the sheet, and then set the values.
Delete rows
function deleteRows() {
const ss = SpreadsheetApp.getActive();
const sh = ss.getActiveSheet();
const rg = sh.getDataRange();
const vs = rg.getValues().filter(r => !(!r[6] || !r[7] || !r[9]));
rg.clearContent();
sh.getRange(1, 1, vs.length, vs[0].length).setValues(vs);
}

SpreadsheetApp.getLastRows() provides empty records and not truncate to those with data

I'm new to JS and use of Google App Script, so pardon my lack of JS knowledge.
I got a simple .gs script to work. It converts content in one cell to a hyperlink (e.g. if text is "blah", it would be "https://example.com/blah").
The script retrieves the number of rows to operate on. For one sheet, this does it correctly and truncates it to the last entry (row=31) even though the sheet has 1000 rows. However, for another sheet, it does not and returns 999:
Here's the debug for when it wasn't right:
Apr 8, 2022, 11:20:53 AM Debug doiColumn: 10 numRows: 999
Here it is for another sheet that worked as intended:
Apr 8, 2022, 9:44:47 AM Debug doiColumn: 5 numRows: 31
The docs say: "Returns the position of the last row that has content."
For illustration, here is a snippet of the code:
let spreadsheet = SpreadsheetApp.getActive();
let sheet = SpreadsheetApp.getActiveSheet();
const column = getColumn(sheet, column_name);
let range = sheet.getRange(2, column, sheet.getLastRow() - 1, 1)
console.log(range.getValues());
const numRows = range.getNumRows();
console.log(`column: ${column} numRows: ${numRows}`)
I would like to make sure that .getLawRow() is truncated. Otherwise this script runs longer than it should.
Sorry I could not fit it into a comment
It requires col = column number, sh = sheet (not the name) ,ss = Spreadsheet (not the name)
function getColumnHeight(col, sh, ss) {
var ss = ss || SpreadsheetApp.getActive();
var sh = sh || ss.getActiveSheet();
var col = col || sh.getActiveCell().getColumn();
var rcA = [];
if (sh.getLastRow()){ rcA = sh.getRange(1, col, sh.getLastRow(), 1).getValues().flat().reverse(); }
let s = 0;
for (let i = 0; i < rcA.length; i++) {
if (rcA[i].toString().length == 0) {
s++;
} else {
break;
}
}
return rcA.length - s;
//const h = Utilities.formatString('col: %s len: %s', col, rcA.length - s);
//Logger.log(h);
//SpreadsheetApp.getUi().showModelessDialog(HtmlService.createHtmlOutput(h).setWidth(150).setHeight(100), 'Col Length')
}
There are other ways to do it but I prefer this one. It's been reliable for me
Using ranges like this sheet.getRange("A1:Z") will also provide you will a bunch of nulls from getLastRow() to getMaxRows() which then need to be filter out but if you do have a null within your data then that will also get removed and now you have bogus data because that row being removed now messes up your row order.
Try to replace .getLastRow() by
.getLastDataRow(column)
and add this prototype function
Object.prototype.getLastDataRow = function(col){
var lastRow = this.getLastRow();
if (col == null){col=1}
var range = this.getRange(lastRow,col);
if (range.getValue() !== "") {
return lastRow;
} else {
return range.getNextDataCell(SpreadsheetApp.Direction.UP).getRow();
}
};

I'm keep getting REF error in all the cells that I paste the data to after writing the formula

So I wrote this code that will paste the first 7 columns from sheet2(sourcing) to sheet1(sheet1) and I also wrote a formula to every cell so if the code in the sourcing sheet changes then the one in sheet1 would change, but when I did that I got #REF! in every cell so I went to file --> settings and changed the calculations --> Iterative calculation from Off to On and the error was gone but i got 0 in every cell which isnt the right value of the cell.
Here is the code.
function moveCode() {
try {
const ss1 = SpreadsheetApp.getActive();
const ss2 = SpreadsheetApp.openById("1A3tJiIEkDP_5R6mEY25IeAsO2XV_oveJwnUovxRd-1U");//or url whatever
const ssh = ss1.getSheetByName('Sheet1');
const dsh = ss2.getSheetByName('Sourcing');
const lastRow = dsh.getLastRow();
if (lastRow < 2) return;
var srange = dsh.getDataRange();
var formulas = [];
var i=0;
var j= 0;
var row = null;
// srange.getNumRows()-1 because skip the first row
for( i=0; i<srange.getNumRows()-1; i++ ) {
row = [];
for( j=1; j<8; j++ ) {
row.push("=Sheet1!"+String.fromCharCode(64+j)+(i+2).toString()); // A = 65
}
formulas.push(row);
}
ssh.getRange(2,1,formulas.length,formulas[0].length).setValues(formulas);
}
catch(err) {
Logger.log(err);
}
}
I think there is something wrong with the formula but I'm not sure whats wrong.
If you need more explanation please let me know.
Thank you.
In your situation, how about using IMPORTRANGE? When your script is modified, it becomes as follows.
Modified script:
function moveCode() {
try {
const ss1 = SpreadsheetApp.getActive();
const ss2 = SpreadsheetApp.openById("1A3tJiIEkDP_5R6mEY25IeAsO2XV_oveJwnUovxRd-1U");//or url whatever
const ssh = ss1.getSheetByName('Sheet1');
const dsh = ss2.getSheetByName('Sourcing');
const lastRow = dsh.getLastRow();
if (lastRow < 2) return;
ssh.getRange(2, 1).setFormula(`=IMPORTRANGE("${ss2.getUrl()}","'Sourcing'!A2:G${lastRow}")`);
}
catch (err) {
Logger.log(err);
}
}
After the formula was put in the cell, please accept for loading the values from the Spreadsheet.
References:
IMPORTRANGE
setFormula(formula)

Get Google sheets column by name in Google Scripts

I have a script that allows me to get the contents of a column from my Google Sheet and display it in my HTML form while removing any duplicates of the same name.
Example: red, red, yellow, yellow, blue, green would show in the dropdown menu as red, yellow, blue, green.
The thing is, I would like to get the column contents by name and not by number i.e 1.
Here is my script:
function getColors() {
var sheet = SpreadsheetApp.openById("1czFXXQAIbW9IlAPwHQ0D5S_a-Ew82p-obBEalJFNJTI").getSheetByName("Vinyl Costs");
var getLastRow = sheet.getLastRow();
var return_array = [];
for(var i = 2; i <= getLastRow; i++)
{
if(return_array.indexOf(sheet.getRange(i, 1).getValue()) === -1) {
return_array.push(sheet.getRange(i, 1).getValue());
}
}
return return_array;
}
I've found a similar question and the accepted answer was this:
function getByName(colName, row) {
var sheet = SpreadsheetApp.getActiveSheet();
var data = sheet.getDataRange().getValues();
var col = data[0].indexOf(colName);
if (col != -1) {
return data[row-1][col];
}
}
But I can't seem to make that work with mine? This is my first ever Google Script so I don't really understand it 100% yet.
I changed the functions a bit.
For one thing, getByName now gets not all values of the sheet, but only the first row.
function getColors() {
const sheet = SpreadsheetApp.openById("1czFXXQAIbW9IlAPwHQ0D5S_a-Ew82p-obBEalJFNJTI").getSheetByName("Vinyl Costs");
const colName = 'your_column_name';
const colNum = getColNumberByName(colName);
if (colNum === null) {
Logger.log('Column ' + colName + ' was not found!');
return [];
}
const firstRow = 2;
const lastRow = sheet.getLastRow();
// get all values from column
const columnData = sheet.getRange(firstRow, colNum, lastRow).getValues().flat();
// filter values on duplicates
return columnData.filter((el, i) => i === columnData.indexOf(el) && el !== '');
}
function getColNumByName(colName, row = 1) {
const sheet = SpreadsheetApp.openById("1czFXXQAIbW9IlAPwHQ0D5S_a-Ew82p-obBEalJFNJTI").getSheetByName("Vinyl Costs");
const [data] = sheet.getRange(row, 1, row, sheet.getLastColumn()).getValues();
const col = data.indexOf(colName);
// adding 1 because column nums starting from 1
return col === -1 ? null : col + 1;
}

Trying to paste Values from formula Google App Script

This is just a snippet of my code from Google App Script which iterates through each row in columns 1, 2, 3. If an edit is made in column 3, an incremental ID will be generated and a concatenation of the same row and different columns will also be generated - in this case Column D, E, and F. I am struggling with figuring out a way to change the formulas into values. What am I missing here?
// Location format = [sheet, ID Column, ID Column Row Start, Edit Column]
var locations = [
["Consolidated Media Plan",1,9,3]
];
function onEdit(e){
// Set a comment on the edited cell to indicate when it was changed.
//Entry data
var range = e.range;
var col = range.getColumn();
var row = range.getRow();
// Location Data
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheet = ss.getActiveSheet();
function getNewID(){
function IDrange(){
var dataRange = sheet.getDataRange();
var lastRow = dataRange.getLastRow();
return sheet.getRange(IDrowStart,IDcol,lastRow-IDrowStart).getValues();
};
//Get largest Value in range
function getLastID(range){
var sorted = range.sort();
var lastIDval = sorted[sorted.length-1][0];
return lastIDval;
};
//Stores leading letters and zeroes and trailing letters
function getLettersNzeroes(id){
//Get any letters or zeroes.
var re = new RegExp("^([a-zA-Z0])$");
var letterZero = [];
for(char = 0; char < id.length; char++){
if(re.test(id[char])){
letterZero.push([char,id[char]]);// [[position, letter or zero]]
};
};
// Categorize letters and zeroes into start and end blocks
var startLetterZero = "",
endLetter = "",
len = letterZero.length - 1;
for(j = 0; j < letterZero.length; j++){
if(letterZero[j][0] === j){
startLetterZero += letterZero[j][1];
}else if(letterZero[j][1] !== "0" && letterZero[len][0] - (len - j) == letterZero[j][0]){
endLetter += letterZero[j][1];
};
};
var startNend = {"start":startLetterZero,"end":endLetter};
return startNend;
};
//Gets last id number. Adds 1 an checks to set if its new length is greater than the lastNumber.
function getNewNumber(id){
var removeZero = false;
var lastNum = parseInt(id.replace(/\D/g,''),10);//Remove letters
var newNum = (lastNum+1).toString();
if(lastNum.toString().length !== newNum.length){
var removeZero = true;
};
var newNumSet = {"num":newNum, "removeZero": removeZero};
return newNumSet
};
var lastID = getLastID(IDrange());
var lettersNzeroes = getLettersNzeroes(lastID);
var newNumber = getNewNumber(lastID);
//If the number is 9,99,999,9999 etc we need to remove a zero if it exists.
if(newNumber.removeZero === true && lettersNzeroes.start.indexOf("0") !== -1.0){
lettersNzeroes.start = lettersNzeroes.start.slice(0,-1);
};
//Rejoin everything together
var newID = lettersNzeroes.start +
newNumber.num +
lettersNzeroes.end;
return newID;
};
for(i = 0; i < locations.length; i++){
var sheetID = locations[i][0],
IDcol = locations[i][1],
IDrowStart = locations[i][2],
EditCol = locations[i][3];
var offset = IDcol - EditCol;
var cell = sheet.getActiveCell();
if(sheetID === sheet.getName()){
if(EditCol === col){
//ID Already Exists the editing cell isn't blank.
if(cell.offset(0,offset).isBlank() && cell.isBlank() === false){
var newID = getNewID();
cell.offset(0,offset).setValue(newID);
cell.offset(0,-1).setFormulaR1C1('=concatenate(R[0]C[-1],"_",INDEX(Glossary!K:K,MATCH(R[0]C[2],Glossary!J:J,0)))');
};
};
};
};
};
EDIT:
This is my full code, I have been unsuccessful with trying to retrieve just the values of the formula within the same (i.e, If C9 gets edited, a formula with the values specific to the 9th row should be populated)
Also, I've tried to add an index/match formula to the concatenation formula at the bottom of the code - it works as expected on the google sheets, but when I run it with the script it pastes the correct formula but it returns a #NAME? error message. However, when I copy and paste the exact same formula in the cell, it works perfectly, any idea what could be causing this error?
This works for me. I know it's not exactly the same thing but I didn't have access to getNewId()
function onEdit(e) {
var sh=e.range.getSheet();
if(sh.getName()!='Sheet1')return;
//e.source.toast('flag1');
if(e.range.columnStart==3 && e.range.offset(0,1).isBlank() && e.value) {
//e.source.toast('flag2');
e.range.offset(0,1).setValue(e.value);
e.range.offset(0,2).setFormulaR1C1('=concatenate(R[0]C[-1],"_",R[0]C[-2],"_",R[0]C[-3],"_",R[0]C[-4])');
}
}

Categories

Resources