Speed up changes from script to scattered cells in google sheets - google-apps-script

I have a sheet that shows the results of calculations based on other backing sheets. Changes are made manually, but the "summary" sheet is only formulae. These changes appear in scattered cells, non-contiguous for the most part.
I want to highlight which cells have changed in the summary sheet after a manual change in the backing sheets. For that, I'm using a second summary sheet which starts as a copy of the main one.
The final ingredient is a script that runs after edits. It traverses the summary range and compares values to the second copy. Any differences get highlighted in the main summary and copied back to the second summary.
This process does work but is quite slow, I think due to the updates. Pseudo-code:
var src = summary.getRange(...)
var dst = copy.getRange(...)
var src_cell;
var dst_cell;
src.setBackground('white'); // Bulk reset of changes
for (row = 1; row < src.getNumRows(); row++) {
for (col = 1; col < src.getNumColumns(); col++) {
src_cell = src.getCell(row, col);
dst_cell = src.getCell(row, col);
if (src_cell.getDisplayValue() != dst_cell.getDisplayValue()) {
dst_cell.setValue(src_cell.getDisplayValue());
src_cell.setBackground('gray');
}
}
}
I think there is no way to bulk-update scattered ranges, which seems a straightforward solution.
I'm looking for ways to speed up this process, either in the script or by using some other strategy.

Per official "best practices," you should batch-read associated cell data rather than repeatedly read and possibly write values. This statement does assume that setting values in dst does not influence values for future reads.
Thus, the simplest change is to use Range#getDisplayValues on src and dst:
...
src.setBackground("white");
var srcValues = src.getDisplayValues();
var dstValues = dst.getDisplayValues();
srcValues.forEach(function (srcRow, r) {
var dstRow = dstValues[r];
srcRow.forEach(function (value, c) {
if (value !== dstRow[c]) {
dst.getCell(r + 1, c + 1).setValue(value);
src.getCell(r + 1, c + 1).setBackground("gray");
}
});
});
An additional optimization is to use the RangeList class to batch the changes. To create a RangeList, you need an array of cell / range notations, which can use R1C1- or A1-style addressing. R1C1 is simplest to compute.
...
var dstChanges = [];
var srcChanges = [];
...
if (value !== dstRow[c]) {
dstChanges.push({row: r + 1, col: c + 1, newValue: value});
srcChanges.push({row: r + 1, col: c + 1});
}
...
if (srcChanges.length > 0) {
var srcRow = src.getRow();
var dstRow = dst.getRow();
var srcCol = src.getColumn();
var dstCol = dst.getColumn();
copy.getRangeList(dstChanges.map(function (obj) {
return "R" + (obj.row + dstRow) + "C" + (obj.col + dstCol);
}).getRanges().forEach(function (rg, i) {
rg.setValue(dstChanges[i].newValue);
});
summary.getRangeList(srcChanges.map(function (obj) {
return "R" + (obj.row + srcRow) + "C" + (obj.col + srcCol);
}).setBackground("gray");
}
...
Other Refs
Array#forEach
Array#map
Array#push

Related

Improve loading efficiency of App Script code in Google Sheets

ISSUE
I have a spreadsheet whereby I generate the end column based on the other columns present. I do this using the app script code below. There are now 1147 rows in this spreadsheet and I often notice a long period of loading to retrieve all of the rows.
Are there any suggestions on how I can improve the efficiency and responsiveness?
EXAMPLE
ARRAY FORMULA ON END COLUMN
=ARRAYFORMULA(
IF(A2:A="Spec", "# SPEC "&B2:B,
IF(A2:A="Scenario", "## "&B2:B,
IF(A2:A="Step", "* "&TAGS(C2:C,D2:D),
IF(A2:A="Tag", "Tags: "&REGEXREPLACE(B2:B,"\s",""),
IF(A2A="", ""))))))
APP SCRIPT CODE
Utilities.sleep(3000)
/** #OnlyCurrentDoc */
function TAGS(input,textreplacement) {
if (input.length > 0) {
var lst = input.split(",")
var rep = textreplacement.match(/<[^>]*>/g)
for (i in rep){
textreplacement = textreplacement.replace(rep[i],'"'+lst[i]+'"')
}
return textreplacement
}
else{
return textreplacement
}
}
EDIT
From the image below I would like to replace everything with triangle brackets < > in column D, with the values in column C, separated by comma.
I use the Array Formula in column E to do an initial conversion and then use the TAGS function to add in the values.
Ideally I would use the Array Formula in one cell at the top of column E to do all the replacements.
Custom functions in Google Apps Script tend to take long time to process and I wouldn't recommend to use it in several cells. I would like to understand better what you trying to do with this data in order to answer properly, but anyway, I would try one of these two solutions:
1 - Inline formula:
Using only native functions has a better performance. Not sure how you could achieve this, since you are iterating inside that TAGS function.
2- Calculate values interely with Script and replace values in column E:
You could create a function that may run from onEdit event or get activated by a custom menu. Generally it would be like this:
function calculateColumnE() {
var sheet = SpreadsheetApp.openById('some-id').getSheetByName('some-name');
var row_count = sheet.getLastRow();
var input_data = sheet.getRange(1, 1, row_count, 4).getValues();
var data = [];
for (var i = 0; i < row_count; i++) {
var row_data; // this variable will receive value for column E in this row
/*
...
manage input_data here
...
*/
data.push([row_data]); // data array MUST be a 2 dimensional array
}
sheet.getRange(1, 5, data.length, 1).setValues(data);
}
EDIT
Here is the full code for solution 2:
function TAGS(input,textreplacement) { //keeping your original function
if (input.length > 0) {
var lst = input.split(",")
var rep = textreplacement.match(/<[^>]*>/g)
for (i in rep){
textreplacement = textreplacement.replace(rep[i],'"'+lst[i]+'"')
}
return textreplacement
}
else{
return textreplacement
}
}
function calculateColumnE() {
var sheet = SpreadsheetApp.openById('some-id').getSheetByName('some-name');
var row_count = sheet.getLastRow();
var input_data = sheet.getRange(1, 1, row_count, 4).getValues();
var data = [];
for (var i = 0; i < row_count; i++) {
var row_data; // this variable will receive value for column E in this row
if (input_data[i][0] == "Spec") {
row_data = "# SPEC " + input_data[i][1];
} else if (input_data[i][0] == "Scenario") {
row_data = "## " + input_data[i][1];
} else if (input_data[i][0] == "Step") {
row_data = "* " + TAGS(input_data[i][2], input_data[i][3]);
} else if (input_data[i][0] == "Tag") {
row_data = "Tags: " + input_data[i][1].replace(/\s/, ''); // not sure what this is doing
} else if (input_data[i][0] == "") {
row_data = "";
}
data.push([row_data]); // data array MUST be a 2 dimensional array
}
sheet.getRange(1, 5, data.length, 1).setValues(data);
}
I also created a working example, which you can check here: https://docs.google.com/spreadsheets/d/1q2SYD7nYubSuvkMOKQAFuGsrGzrMElzZNIFb8PjM7Yk/edit#gid=0 (send me request if you need it).
It works like a charm using onEdit event to trigger calculateColumnE() with few lines, I'm curious to know about the result in your 1000+ rows sheet. If it get slow, you may need to run this function manually.
Not sure if this will be faster:
function TAGS(input,tr) {
if (input.length > 0) {
var lst = input.split(",");
var i=0;
tr=tr.replace(/<[^>]*>/g,function(){return '"' + lst[i++] + '"';});
}
return tr;
}

Using Google App Scripts to append data to a sheet using MetaData to make sure it goes in the right column

I want to pass an object to a Google Sheet Web App that I have written and have that data appended to the Google Sheet. I want to make sure the data ends up in the correct columns.
I can append the data to the file, but this could cause issues if columns are added/maniputated etc.
I have created column metadata for each column that corresponds to the object key.
I can read through the column metadata and find what column number each one represents. ie. if I get the metadata for "orderNumber" i can see it is in row 1.
Code for web app.
function doGet(e) {
var p = e.parameter;
var sheetName = "Orders";
var sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName(sheetName);
var appendList = JSON.parse(p.appendList)[0];
var returnValue = {};
returnValue["status"] ="error";
//var appendRow = sheet.getLastRow()+1;
var values = []
for (var key in appendList) {
values.push(appendList[key]);
Logger.log(searchAndReturnCol(key+"Column")); // just to show I can get the column number from meta data
}
sheet.appendRow(values);
return ContentService.createTextOutput(JSON.stringify(returnValue));
}
function testDoGet() { // emmulates what will past over by the app
var e = [];
var test = [{
'orderNumber' : "vsdv",
'name' : "Bob",
'porkDumpling' : 0,
'prawnDumpling' : 0,
'vegetarianDumpling' : 0,
'sauce' : "Spicey",
'paymentMethod' : "Cash",
'dollarReceivable' : 5,
'dollarReceived' :5,
'collectionDate' : 44234244,
'packed' : "No",
'collected' : "No",
'comments' : "This is a comment"
}]
var mod = JSON.stringify (test)
e.parameter = {
'appendList':mod,
}
doGet(e)
//displayLog (doGet(e));
}
Code to find metadata
function searchAndReturnCol (key){
var colLevel = cSAM.SAM.searchByKey (SSID , key);
return colLevel.matchedDeveloperMetadata[0].developerMetadata.location.dimensionRange.endIndex
}
What I am unsure about is how to bring the two ideas together. I want to check the key in the object and then make sure that this data is inserted into the correct column based on the column metadata.
In your Spreadsheet (the sheet of Orders), each column has the developer metadata.
Each key of developer metadata is the same with the keys of object you want to put to Spreadsheet.
You want to put the values to the column, when the keys both the developer metadata and the data you give are the same.
You want to achieve this using Google Apps Script.
If my understanding is correct, how about this modification? Please think of this as just one of several answers.
Modification points:
When doGet is run, the developer metadata is retrieved in order of the column. At that time, using the key of retrieved developer metadata, the data for putting to Spreadsheet is created from the giving data.
Modified script:
When your script is modified, please modify as follows.
From:
var values = []
for (var key in appendList) {
values.push(appendList[key]);
Logger.log(searchAndReturnCol(key+"Column")); // just to show I can get the column number from meta data
}
To:
var columnToLetter = function(column) { // From https://stackoverflow.com/a/21231012/7108653
var temp, letter = '';
while (column > 0) {
temp = (column - 1) % 26;
letter = String.fromCharCode(temp + 65) + letter;
column = (column - temp - 1) / 26;
}
return letter;
};
var col = sheet.getLastColumn();
var values = [];
for (var i = 0; i < col; i++) {
var d = sheet.getRange(columnToLetter(i + 1) + ":" + columnToLetter(i + 1)).getDeveloperMetadata();
for (var j = 0; j < d.length; j++) {
values.push(appendList[d[j].getKey()]);
}
}
Note:
If above modified script didn't retrieve the developer metadata from your Spreadsheet, please add the developer metadata to each column using the following script. If you want to rearrange the keys for columns, please modify keys.
function createDeveloperMetadata() {
var columnToLetter = function(column) { // From https://stackoverflow.com/a/21231012/7108653
var temp, letter = '';
while (column > 0) {
temp = (column - 1) % 26;
letter = String.fromCharCode(temp + 65) + letter;
column = (column - temp - 1) / 26;
}
return letter;
};
var keys = {orderNumber:"",name:"",porkDumpling:"",prawnDumpling:"",vegetarianDumpling:"",sauce:"",paymentMethod:"",dollarReceivable:"",dollarReceived:"",collectionDate:"",packed:"",collected:"",comments:""};
var sheet = SpreadsheetApp.getActiveSheet();
Object.keys(keys).forEach(function(e, i) {
sheet.getRange(columnToLetter(i + 1) + ":" + columnToLetter(i + 1)).addDeveloperMetadata(e, keys[e]);
});
}
When you add the developer metadata, please check the current metadata. Because the same keys can be added to the metadata.
If you want to update all metadata, I recommend to remove them and add new metadata.
When you modified your script of Web Apps, please redeploy Web Apps as new version. By this, the latest script is reflected to Web Apps. Please be careful this. In your script, when you test the script with testDoGet, it is not required to redeploy it.
Reference:
Class DeveloperMetadata
If I misunderstood your question and this was not the direction you want, I apologize.

Detect formula errors in Google Sheets using Script

My ultimate goal is here, but because I've gotten no replies, I'm starting to learn things from scratch (probably for the best anyway). Basically, I want a script that will identify errors and fix them
Well, the first part of that is being able to ID the errors. Is there a way using Google Script to identify if a cell has an error in it, and return a particular message as a result? Or do I just have to do an if/else that says "if the cell value is '#N/A', do this", plus "if the cell value is '#ERROR', do this", continuing for various errors?. Basically I want ISERROR(), but in the script
Use a helper function to abstract away the nastiness:
function isError_(cell) {
// Cell is a value, e.g. came from `range.getValue()` or is an element of an array from `range.getValues()`
const errorValues = ["#N/A", "#REF", .... ];
for (var i = 0; i < errorValues.length; ++i)
if (cell == errorValues[i])
return true;
return false;
}
function foo() {
const vals = SpreadsheetApp.getActive().getSheets()[0].getDataRange().getValues();
for (var row = 0; row < vals.length; ++row) {
for (var col = 0; col < vals[0].length; ++col) {
if (isError_(vals[row][col])) {
Logger.log("Array element (" + row + ", " + col + ") is an error value.");
}
}
}
}
Using Array#indexOf in the helper function:
function isError_(cell) {
// Cell is a value, e.g. came from `range.getValue()` or is an element of an array from `range.getValues()`
// Note: indexOf uses strict equality when comparing cell to elements of errorValues, so make sure everything's a primitive...
const errorValues = ["#N/A", "#REF", .... ];
return (errorValues.indexOf(cell) !== -1);
}
If/when Google Apps Script is upgraded with Array#includes, that would be a better option than Array#indexOf:
function isError_(cell) {
// cell is a value, e.g. came from `range.getValue()` or is an element of an array from `range.getValues()`
const errorValues = ["#N/A", "#REF", .... ];
return errorValues.includes(cell);
}
Now that the v8 runtime is available, there are a number of other changes one could make to the above code snippets (arrow functions, etc) but note that changing things in this manner is not required.
Update: 25 March 2020
#tehhowch remarked "If/when Google Apps Script is upgraded with Array#includes, that would be a better option than Array#indexOf".
Array.includes does now run in Apps Script and, as anticipated provides a far more simple approach when compared to indexOf.
This example varies from the previous answers by using a specific range to show that looping through each cell is not required. In fact, this answer will apply to any range length.
The two key aspects of the answer are:
map: to create an array for each column
includes: used in an IF statement to test for a true or false value.
function foo() {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sourcename = "source_sheet";
var source = ss.getSheetByName(sourcename);
var sourcerange = source.getRange("A2:E500");
var sourcevalues = sourcerange.getValues();
// define the error values
var errorValues = ["#NULL!", "#DIV/0!", "#VALUE!", "#REF!", "#NAME?", "#NUM!", "#N/A","#ERROR!"];
// loop though the columns
for (var c = 0;c<5;c++){
// create an array for the column
var columnoutput = sourcevalues.map(function(e){return e[c];});
// loop through errors
for (var errorNum=0; errorNum<errorValues.length;errorNum++){
// get the error
var errorvalue = errorValues[errorNum]
// Logger.log("DEBUG: column#:"+c+", error#:"+e+", error value = "+errorvalue+", does col include error = "+columnoutput.includes(errorvalue));
// if the error exists in this column then resposnse = true, if the error doesn't exist then response = false
if (columnoutput.includes(errorvalue) != true){
Logger.log("DEBUG: Column#:"+c+", error#:"+errorNum+"-"+errorvalue+" - No ERROR");
} else {
Logger.log("DEBUG: column#:"+c+", error#:"+errorNum+"-"+errorvalue+"- ERROR EXISTS");
}
}
}
return;
}
Shorter yet, use a nested forEach() on the [vals] array, then check to see if the cell value matches a value of the [errorValues] array with indexOf. This was faster than a for loop...
function foo() {
const errorValues = ["#NULL!", "#DIV/0!", "#VALUE!", "#REF!", "#NAME?", "#NUM!", "#N/A","#ERROR!"];
const vals = SpreadsheetApp.getActive().getSheets()[0].getDataRange().getValues();
vals.forEach((val,row) => { val.forEach((item, col) => {
(errorValues.indexOf(item) !== -1) ? Logger.log("Array element (" + row + ", " + col + ") is an error value.") : false ;
});
});
}
I had a similar question and resolved using getDisplayValue() instead of getValue()
Try something like:
function checkCells(inputRange) {
var inputRangeCells = SpreadsheetApp.getActiveSheet().getRange(inputRange);
var cellValue;
for(var i=0; i < inputRangeCells.length; i++) {
cellValue = inputRangeCells[i].getDisplayValue();
if (cellValue=error1.....) { ... }
}
}
Display value should give you what's displayed to the user rather than #ERROR!

Return Row Data from Spreadsheet having specified string without looping through all Rows

I have a task at hand wherein I need to return the entire Row Data containing a user-defined string.
One way of achieving it is looping through all the rows, but this works only when you know in which column to search, as shown using the code given below.
var sheetData = SpreadsheetApp.getActiveSpreadsheet().getDataRange().getValues();
var searchString = "testSearch";
for(var i=0;i<sheetData.getLastRow();i++)
{
//assuming we know that the search string is going to be in Column 2
if(sheetData[i][1].search(searchString)!=-1)
{
var rowData = sheetData[i];
return rowData;
}
}
So my question is, is there any way in which we can achieve this, without having to loop through all rows one by one?
To make the problem statement more clear, I wish to achieve something like the 'Find' feature as demonstrated in the image below:
This would make it very easy to skim through huge data spread across multiple sheets/spreadsheets.
Note: I am searching for a solution to this using Google Apps Script.
Here is code that gets the data from the row with the match. It can be a partial match. You do not need to know the number of columns or rows. You do not need to know which column to look in.
To make this code work, replace Put Sheet Tab Name Here with the name of the sheet tab to use. The searchString can be passed into the function. If nothing is passed in, then the code uses a hard coded value for the search.
function findRowOfSearchString(searchString) {
var arrIndexOfAllMatches,dataAsStringWithBrackets,dataRange,i,
isThereA_Match,j,ll,L,L2,matchOfAllInnerBrackets,numberOfColumns,numberOfRows,
rowCutOff,rowsOfMatchedData,sh,sheetData,ss,thisIndex,thisMatchIndex,thisRow,thisRowData;
ll = function(a,b) {
Logger.log(a + ": " + b)
}
if (!searchString) {
searchString = "testSe";
}
ss = SpreadsheetApp.getActiveSpreadsheet();
sh = ss.getSheetByName('Put Sheet Tab Name Here');//
dataRange = sh.getDataRange();
numberOfColumns = dataRange.getNumColumns();
numberOfRows = dataRange.getNumRows(); //changed 'getNumColumns' to 'getNumRows'
sheetData = dataRange.getValues();//Get a 2D array of all sheet data
dataAsStringWithBrackets = JSON.stringify(sheetData);
//ll('dataAsStringWithBrackets: ',dataAsStringWithBrackets)
isThereA_Match = dataAsStringWithBrackets.indexOf(searchString);
//ll('isThereA_Match: ',isThereA_Match)
if (isThereA_Match === -1) {return;}//There is no match - stop
arrIndexOfAllMatches = [];
L = dataAsStringWithBrackets.length;
//ll('L',L)
thisMatchIndex = 0;
for (i=0;i<L;i++) {
//ll('i',i)
thisMatchIndex = dataAsStringWithBrackets.indexOf(searchString,thisMatchIndex + 1);
//ll('thisMatchIndex',thisMatchIndex)
if (thisMatchIndex === -1) {//No more matches were found
//ll('No more matches found',thisMatchIndex)
break;
}
arrIndexOfAllMatches.push(thisMatchIndex);
}
//ll('arrIndexOfAllMatches',arrIndexOfAllMatches)
matchOfAllInnerBrackets = [];
thisMatchIndex = 0;
for (i=0;i<L;i++){
thisMatchIndex = dataAsStringWithBrackets.indexOf("],[",thisMatchIndex + 1);
//ll('thisMatchIndex',thisMatchIndex)
if (thisMatchIndex === -1) {//No more matches were found
//ll('No more matches found',thisMatchIndex)
break;
}
matchOfAllInnerBrackets.push(thisMatchIndex);
}
ll('matchOfAllInnerBrackets',matchOfAllInnerBrackets)
rowsOfMatchedData = [];
L = arrIndexOfAllMatches.length;
L2 = matchOfAllInnerBrackets.length;
for (i=0;i<L;i++){
thisIndex = arrIndexOfAllMatches[i];
ll('thisIndex: ' ,thisIndex)
for (j=0;j<L2;j++){
rowCutOff = matchOfAllInnerBrackets[j];
ll('rowCutOff: ',rowCutOff)
if (rowCutOff > thisIndex) {
ll('greater than: ' ,thisIndex > rowCutOff)
thisRow = j+1;
ll('thisRow: ', (thisRow))
rowsOfMatchedData.push(thisRow)
break;
}
}
}
ll('rowsOfMatchedData: ',rowsOfMatchedData)
L = rowsOfMatchedData.length;
for (i=0;i<L;i++){
thisRowData = sh.getRange(rowsOfMatchedData[i], 1, 1, numberOfColumns).getValues();
ll('thisRowData: ',thisRowData)
}
}

Removing invalid named ranges in GAS

I have defined some named ranges in a sheet that I later delete. Afterwards, the ranges remain in the sidebar "Data->Named ranges...", with the range "#REF". I would like to delete them because I don't want them to accumulate.
They are not listed in SpreadsheetApp.GetActiveSpreadsheet.getNamedRanges().
How can I delete them programatically?
An alternative solution would be how to define a named range that is removed when a sheet is deleted. This happens if you have a named range in a sheet that is duplicated - the named range has a name like "'Sheet1Copy'!RangeName", but it's not possible to define a name like this.
Use removeNamedRange(name) to remove a Named Range. It will work even with Named Ranges that has #REF! as range and are not returned by SpreadsheetApp.getActiveSpreadsheet().getNamedRanges().
In order to make easier to maintain your spreadsheets free of Named Ranges with #REF! as range, keep a list of your Named Ranges. You could use an auxiliary spreadsheet for that.
UPDATED BELOW
The accepted answer works fine if you are removing a unique Named Range, but not if you have more than one Range sharing the same Name (i.e. Sheet-scoped Named Ranges vs Spreadsheet-scoped Named Ranges).
The first instance of the Named Range will probably be scoped to the entire Spreadsheet, whereas any copies of the Sheet (that contain the original Named Range) will feature Named Ranges that are scoped to the copied Sheet (e.g. 'SheetCopy'!NamedRange instead of NamedRange).
If you want to remove Sheet-scoped Named Ranges whose references are no longer valid, try running the following script from the Script Editor:
function removeDeadReferences()
{
var activeSS = SpreadsheetApp.getActiveSpreadsheet();
var sheets = activeSS.getSheets();
var sheetNamedRanges, loopRangeA1Notation;
var x, i;
// minimum sheet count is 1, no need to check for empty array, but why not
if (sheets.length)
{
for (x in sheets)
{
sheetNamedRanges = sheets[x].getNamedRanges();
// check for empty array
if (sheetNamedRanges.length)
{
for (i = 0; i < sheetNamedRanges.length; i++)
{ // get A1 notation of referenced cells for testing purposes
loopRangeA1Notation = sheetNamedRanges[i].getRange().getA1Notation();
// check for length to prevent throwing errors during tests
if (loopRangeA1Notation.length)
{ // check for bad reference
// note: not sure why the trailing "!" mark is currently omitted
// ....: so there are added tests to ensure future compatibility
if (
loopRangeA1Notation.slice(0,1) === "#"
|| loopRangeA1Notation.slice(-1) === "!"
|| loopRangeA1Notation.indexOf("REF") > -1
)
{
sheetNamedRanges[i].remove();
}
}
}
}
}
}
}
Edit / Update
I recently needed to run this script again, and Google must've made changes to the way the .getRange() function works. The previous script will not work if the Named Range contains an invalid reference.
Also, after re-reading the initial question, I feel like I was too focused on the Sheet-scoped aspect of the answer.
The OP seemed to be after more of an automated experience, which the Accepted Answer does not provide. In addition to not requiring an externally maintained list of Named Ranges, the following script will remove both Sheet-scoped and Spreadsheet-scoped Named Ranges that contain invalid references.
Here's the updated script, tested to work with the V8 engine:
function removeDeadReferences()
{
var activeSS = SpreadsheetApp.getActiveSpreadsheet();
var sheets = activeSS.getSheets();
var sheet;
var sheetName;
var sheetNamedRanges, sheetNamedRange, sheetNamedRangeName;
var loopRange, loopRangeA1Notation;
var x, i;
// minimum sheet count is 1, no need to check for empty array
for (x in sheets)
{
sheet = sheets[x];
// for logging
sheetName = sheet.getName();
sheetNamedRanges = sheet.getNamedRanges();
// check for empty array
if (sheetNamedRanges.length)
{
for (i = 0; i < sheetNamedRanges.length; i++)
{
sheetNamedRange = sheetNamedRanges[i];
// for logging
sheetNamedRangeName = sheetNamedRange.getName();
// v8 engine won't allow you to get range if it is invalid
try {
loopRange = sheetNamedRange.getRange();
}
catch (error)
{
Logger.log(error);
loopRange = null;
}
// get A1 notation of referenced cells for testing purposes
loopRangeA1Notation = (
loopRange != null
? loopRange.getA1Notation()
: false
);
// check for bad reference
// added tests to ensure future compatibility
// but any of these should suffice
// comment out ones you don't want to test for
if (
loopRangeA1Notation == false
|| loopRangeA1Notation.slice(0,1) === "#"
|| loopRangeA1Notation.slice(-1) === "!"
|| loopRangeA1Notation.indexOf("REF") > -1
)
{
Logger.log("The named range, '" + sheetNamedRangeName + "', within the Sheet named, '" + sheetName + "', was removed.");
sheetNamedRange.remove();
}
}
}
}
}
The function below loops sheets and deletes "bad" named ranges.
You may see it live here. Make your own copy and go to menu:
⚡ Test Automation > 🍏 Sheets > 🦶 Delete Bad Named Ranges
please try
function test_deleteBadNamedRanges() {
var sets = {
delimiter: '|',
file: '', // leave blank to use active spreadsheet
sheet_names: 'nonono|Sheet1|Sheet2', // note put empty string or null to use all sheets
remove_copies: true, // set true to delete 'copy of sheet'!named_ranges
success_message: '🦶Removed "bad" named ranges!'
}
var result = deleteBadNamedRanges_(sets);
console.log(result);
console.log(sets);
}
function deleteBadNamedRanges_(sets) {
var book, msg = '';
try {
if (!sets.file || set.file === '') {
book = SpreadsheetApp.getActive();
} else {
book = SpreadsheetApp.openById(sets.file);
}
} catch (err) {
msg = '🤪Could not get the book for some reason.'
msg += ' The error is: ' + err;
return msg;
}
if (!book) {
msg = '🤪The book object could not be retrieved.'
return msg;
}
/** function to get sheets by names */
var sheets = [];
var errors_log = [];
var getSheetsByNames_ = function(book, names) {
var sheets = [], sht, msg;
for (var i = 0; i < names.length; i++) {
mag = 'ok';
try {
sht = book.getSheetByName(names[i]);
} catch (err) {
msg = '❌no sheet ' + names[i] + '. ';
msg += 'Err: ' + err;
sht = false;
}
if (!sht) {
msg = '❌not found sheet ' + names[i]
}
sheets.push(sht);
errors_log.push(msg);
}
return sheets;
}
/** get sheets */
if (!sets.sheet_names || sets.sheet_names === '') {
sheets = book.getSheets();
} else {
sheets = getSheetsByNames_(
book,
sets.sheet_names.split(sets.delimiter));
}
/** delete bad named ranges for each sheet */
var res = [];
for (var i = 0; i < sheets.length; i++) {
sets.sheet = sheets[i];
if (sets.sheet) {
res.push(deleteBadNamedRangesSheet_(sets))
} else {
res.push(errors_log[i]);
}
}
sets.sheet = '_end_of_loop_';
var result = sets.success_message + '\\n\\n' +
res.join('\\n');
return result;
}
/**
*
* delete bad ranges for 1 sheet
*/
function deleteBadNamedRangesSheet_(sets) {
var sheet = sets.sheet;
var sheet_name = sets.sheet.getName();
// for logs
if (!sets.removed_ramed_ranges) {
sets.removed_ramed_ranges = [];
}
/** function to remove named range
* and log it to memory
*/
var count_deleted = 0;
var add2removed_ = function(namedrange, a1, name) {
namedrange.remove();
sets.removed_ramed_ranges.push({
sheet_name: sheet_name,
name: name,
a1: a1
});
count_deleted++;
return 0;
}
/** loop and check named ranges */
var ranges = sheet.getNamedRanges();
var a1 = '', nr, name = '', check_copy;
for (var i = 0; i < ranges.length; i++) {
nr = ranges[i];
a1 = nr.getRange().getA1Notation();
name = nr.getName();
// remove copy of named range
if (sets.remove_copies) {
check_copy = name
.replace(sheet_name, '')
.substring(0,3);
if (check_copy === "''!") {
add2removed_(nr, a1, name);
}
}
// remove named range
if (a1 == '#REF!') {
add2removed_(nr, a1, name);
}
}
return '✔️Sheet ' + sheet_name + ', deleted: ' + count_deleted;
}