How to delete Named Ranges with Tab names via Apps Script - google-apps-script

I'm trying to clean a spreadsheet of a slew of Named Ranges I no longer need, and leave behind the few I'm still using. All of these Named Ranges include the Tab name, because they originate on a template Tab (named tmp), from which other Tabs are duplicated. Even after I delete all the spinoff Tabs from the sheet and leave behind only tmp, the 'tmp'! appears in the names of the Ranges, both as displayed in the Named Ranges sidebar and as they come in on getNamedRanges().
When I try to selectively delete obviated Named Ranges, no matter how I spec the name of the Ranges, I get errors saying no such Named Ranges exist. Basically, I'm feeding back the same information getNamedRanges() and getRange().getSheet().getSheetName() give me, only to have it garbled along the way.
The problem is isolated in the following test snippet, and involves rendering the single quotes around the Tab name. I have tried several approaches, including escaping the single quotes with slashes, and have added to the code the Comments of the errors I got on the line targetDoc.removeNamedRange(namedRange).
const analyzerDoc = '1pYgcX2dxzHd4cCofy0RFZTzEl36QesiakMGIqCC2QlY'
const openAnalyzerDoc = SpreadsheetApp.openById(analyzerDoc)
function testDeleteNamedRange (){
var docUrl = openAnalyzerDoc.getRangeByName('docUrl').getValue();
var targetDoc = SpreadsheetApp.openByUrl(docUrl);
// var namedRange = 'dyCl_MoodEntries' // The named range "dyCl_MoodEntries" does not exist.
// var namedRange = 'tmp!dyCl_MoodEntries' // The named range "tmp!dyCl_MoodEntries" does not exist.
// var namedRange = "'tmp'!dyCl_MoodEntries" // The named range "'tmp'!dyCl_MoodEntries" does not exist.
// var namedRange = "\'tmp\'!dyCl_MoodEntries" // The named range "'tmp'!dyCl_MoodEntries" does not exist.
targetDoc.removeNamedRange(namedRange);
}
This bug is in the way of a longer function, which is working fine but for the part isolated in this test function.
The longer function gets the names and Tabs of Ranges to delete from this sheet:
What is the right way to do this? Thank you!

This function will remove all of the named range that have their sheet name within the range name.
function deleteAllNamedRange() {
const ss = SpreadsheetApp.getActive();
ss.getNamedRanges().filter(r => ~r.getName().indexOf(r.getRange().getSheet().getName())).forEach(r => r.remove());
}

I have an answer to my own question. There is probably more than one solution, but I have chosen to sidestep the challenge I am facing, and instead of specifying the Named Ranges by name, I am going to spec them by their position in the document's Named Ranges, and simply use remove() instead of removeNamedRange(namedRange). I had gotten so caught up in the recommended method involving forEach, that I had forgotten that the outcome of getNamedRanges() is not an object, but an array.
The solution then lies in amending my process of collecting the Names and other information from the result of getNamedRanges(). Instead of using forEach, I loop over the results of getNamedRanges(), and while I get the information I desire concerning each Named Range, I also log the loop iteration and thereby get the Index Number of each Named Range.
I proceed as before, pasting this information in a Tab where I can select which Ranges to delete.
My delete function then loops over the Named Ranges directly, in reverse order, and checks the loop iteration against the Ranges I have ticked off in that analysis Tab.
I have tested this in a sample document; you may view it here.
In this demo, all functions are within the same document, so I'm using getActive() instead of openByUrl.
This document has 3 Tabs named Sheet1, Sheet2 and Sheet3. Each Tab has 3 Named Ranges named Moe, Larry and Curly. There is also a Tab NamedRanges which the following function collects Named Range into:
function getnamedRanges() {
var namedRanges = SpreadsheetApp.getActive().getNamedRanges();
var namedRangeData = [];
for (i=0; i<namedRanges.length; i++) {
var namedRange = namedRanges[i];
var nrName = namedRange.getName();
var nrRange = namedRange.getRange().getA1Notation();
namedRangeData.push([nrName,nrRange,i])
}
SpreadsheetApp.getActive().getSheetByName('NamedRanges').getRange(2,1,namedRangeData.length,3).setValues(namedRangeData)
}
Here's the Named Range Tab after running that function, and choosing 3 Named Ranges to delete:
Next, here is the function that removes the selected Named Ranges:
function deleteSelectedNamedRanges () {
var namedRangeData = SpreadsheetApp.getActive().getSheetByName('NamedRanges').getDataRange().getValues();
namedRangeData.shift(); // Remove header row data.
var rangesToDelete = namedRangeData.filter(function(nrDatum) {if (nrDatum[3]==true) return nrDatum});
// [3] equivalates to Column D, the checkboxes where I select which Named Ranges to delete.
console.log (rangesToDelete.map(value => value[0])); // [ 'Sheet3\'!Moe', 'Sheet2\'!Curly', 'Sheet1\'!Moe' ]
console.log (rangesToDelete.map(value => value[2])); // [ 0, 1, 5 ] // [2] is the index number of the Named Ranges.
/* The order here derives from how values in Tab Named Ranges happen to be sorted.
In this instance, I have not changed that order, so the Named Ranges To Delete are in ascending order.
For one thing, this is the opposite of what we want;
for another, I want to be able to sort the Named Range Tab freely before making my selections.
So, we must sort this data in DESCENDING order. */
rangesToDelete.sort(function(value1,value2){if (value1[2]<value2[2]) return 1; if (value1[2]>value2[2]) return -1; return 0});
console.log (rangesToDelete.map(value => value[0])); // [ 'Sheet1\'!Moe', 'Sheet2\'!Curly', 'Sheet3\'!Moe' ]
var rangesToDeleteIndexNumbers = rangesToDelete.map(value => value[2])
console.log (rangesToDeleteIndexNumbers); // [ 5, 1, 0 ]
var namedRanges = SpreadsheetApp.getActive().getNamedRanges();
for (i=namedRanges.length-1; i>=0; i--) {
/* We must loop in descending order because deleting Named Ranges will change the index numbers
of all Named Ranges that come after each we delete. */
if (rangesToDeleteIndexNumbers.indexOf(i) !== -1) {namedRanges[i].remove(); console.log ('Removed NR # '+i)}
// Delete Named Range if this iteration number can be found in rangesToDeleteIndexNumbers.
}
}
After running this function, you can see that the 3 Names Ranges have been removed, leaving 6 behind:

Related

I want to make a script in Google sheet to find and match strings of texts between different sheets

I am working on a Google sheet script to manage stocks of items in a game, which is supposed to work as such:
People can make request to deposite or withdraw items using a Google form, which send all the infos, including what resource and in what amount, to a first "log" sheet. I then want a script to read these logs, and use them to update a different sheet, which show the actual stocks.
I should mention, there's about 800 different items to stock, and we like to move them around (up or down the list) because we're dumb.
So my idea what the have the script first retrieve the name of the item we made a request for, then try to match it in the stock sheet.
If it can, it should then add or substract the amount to the stock.
If it can't, it should just colour the log line in red so we can see it and redo the request.
My first problem is that I have no idea if a script in Gsheet can stay active for a long time, and the second is that I have even less of an idea how to properly retrieve a string of text and store it, then compare it with others, and that +800 times each time.
Thank you !
From the question
My first problem is that I have no idea if a script in Gsheet can stay active for a long time,
Google Apps Script have quotas. In this case, the corresponding quota is the execution time limit. For free accounts the limit is 6 minutes, for Workspace accounts the limit is 30 minutes.
and the second is that I have even less of an idea how to properly retrieve a string of text and store it, then compare it with others, and that +800 times each time.
Start by reading https://developers.google.com/apps-script/guides/sheets
Tl;Dr.
You need to learn the pretty basics of JavaScript.
You might use the Spreadsheet Service (Class SpreadsheetApp) or the Advanced Sheets Service, i.e.
/**
* Returns the values from the data range of the active sheet
*
*/
function readData(){
const sheet = spreadsheet.getActiveSheet();
const values = sheet.getDataRange().getValues();
return values;
}
You should decide where do you will store the values, then use JavaScript comparison expressions. You might use loops (for, while, do..while, or use Array methods like Array.prototype.forEach()
Here is an example how it could be done for simplest case, for manual firing of the functions.
Let's say you have the log sheet that look like this:
And your data sheet looks like this:
Here is the function that takes all items from the log sheet, sums them and put on the data sheet:
function add_all_items_from_log() {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var log = ss.getSheetByName('log').getDataRange().getValues();
// put all data into the object {item1:q, item2:q, item3:q, ...etc}
var obj = {};
for (let [date, item, q] of log) {
if (item in obj) obj[item] += q; else obj[item] = q;
}
console.log(obj);
// convert the object into a 2d array [[item1,q], [item2,q], [item3,q], ...]
var array = Object.keys(obj).map(key => [key, obj[key]]);
console.log(array);
// put the array on the data sheet (starting from second row)
var sheet = ss.getSheetByName('data');
sheet.getRange(2,1,sheet.getLastRow()).clearContent();
sheet.getRange(2,1,array.length, array[0].length).setValues(array);
}
The result:
Here is the function that takes item from the last line of the log sheet and add the item to the data sheet:
function add_last_item_from_log() {
var ss = SpreadsheetApp.getActiveSpreadsheet();
// get item from the last row of the log sheet
var [date, item, q] = ss.getSheetByName('log').getDataRange().getValues().pop();
console.log(date, item, q);
// get data from the data sheet
var sheet = ss.getSheetByName('data');
var [header, ...data] = sheet.getDataRange().getValues();
// put the data into the object {item1:q, item2:q, item3:q, ...etc}
var obj = {};
data.forEach(row => obj[row[0]] = row[1]);
console.log(obj);
// add the item to the object
if (item in obj) obj[item] += q; else obj[item] = q;
// convert the object into a 2d array [[item1,q], [item2,q], [item3,q], ...]
var array = Object.keys(obj).map(key => [key, obj[key]]);
console.log(array);
// put the array on the sheet (starting from second row)
var sheet = ss.getSheetByName('data');
sheet.getRange(2,1,sheet.getLastRow()).clearContent();
sheet.getRange(2,1,array.length, array[0].length).setValues(array);
}
Here is my sheet.
You can run these function manually from Text Editor. Just to see how it works. But actually, as far as I can tell, you better to run the last function (or its variant) automatically every time the log sheet is updated from the Form submit. It can be done with the trigger onFormSubmit().
And this is a simplest case. If you have 800+ items and many columns the code may require some optimizations.

Trying to limit an existing script to a specific sheet

My first issue is this, I have an items log sheet where I want to add and manage individual unique items in our inventory. I created a data validation dependent dropdown list for a main category and found out how to build a script to dynamically create a secondary category dropdown list based on the selected main category.
For Example:
If cell B2 (Main Category) is set to Carabiner (based on data validation range on another sheet) THEN cell C2 (secondary Category) will dynamically create a dropdown list relative to the Carabiner main category (i.e. locking, non-locking)
That is simple enough if you only have one row to create the dropdown lists, but I wanted to be able to pick from a secondary category list in each row dependent on which was picked in the main category cell.
I found a video of a script that did just that and got it working just fine.
Now the problem is that the script runs data validation on every other sheet. How can I limit the script to only run on a specific sheet?
Here is the script:
function onEdit() {
// this line just refers to the current file var start = SpreadsheetApp.getActiveSpreadsheet().getActiveSheet(); var current = start.getActiveCell()
// var to refer to the worksheets -lists is where the data validation range will come from, and main is where we want to use that data validation range var list = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("Indirect_Categ_Ranges") var main = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("Items_Log");
// has the user selected a category? Refers to the column number in the Items_Log sheet where the user has picked the main category
if (current.getColumn()==2)
{
//to copy the selected sub-category -the 2,1 is the row and column on the Indirect_Categ_Ranges sheet where this script will dynamically update the main category picked to define what the indirect function will display in the next column
var choice = current.getValue()
list.getRange(2,1).setValue(choice)
//clear any validation -the 2,3,1000 looks to start clearing validation at row 2, column 3 and down for up to 1000 entries
main.getRange(2,3,5000).clearDataValidations()
// create the rule - var_point defines the offset number of rows and columns to shift to initiate the dynamic dependent dropdown list, the var_items defines where the to look for the range to build the dropdown list
var point = current.offset(0,1)
var items = list.getRange(2,2,50)
var rule = SpreadsheetApp.newDataValidation().requireValueInRange(items,true).build();
point.setDataValidation(rule)
}
}
Also, is there a way to have the clear validations to run no matter how many rows there are?
The function will run anytime there's an edit and there's nothing you can do to stop that. You can, instead, terminate execution preemptively if it's not the sheet you care about.
The event object tells you which range was edited. You can get that range's sheet to know which sheet was edited. If the name matches, then execute the other stuff.
function onEdit(e) {
if (e.range.getSheet().getName() === 'Items_Log') {
// Data validation
}
}
It's not great practice to use .getActiveRange() or .getActiveSheet() when you want what was actually edited because there is a chance, however small, that the edited range may differ from the active range at the time of function execution.
Explanation:
You need to take advantage of the event object.
That object contains relevant information to the edits you make.
For example:
e.source is equivalent to SpreadsheetApp.getActive()
e.range is equivalent to .getActiveCell().
To run the code only for a particular sheet, in this case Items_Log, add a condition to check if the name of the active sheet matches that name:
if (current.getColumn()==2 && start.getName()=="Items_Log")
where start is the active sheet:
var start = e.source.getActiveSheet();
Solution:
function onEdit(e) {
var start = e.source.getActiveSheet();
var current = e.range;
var list = e.source.getSheetByName("Indirect_Categ_Ranges")
var main = e.source.getSheetByName("Items_Log");
if (current.getColumn()==2 && start.getName()=="Items_Log")
{
var choice = current.getValue()
list.getRange(2,1).setValue(choice)
main.getRange(2,3,5000).clearDataValidations()
var point = current.offset(0,1)
var items = list.getRange(2,2,50)
var rule = SpreadsheetApp.newDataValidation().requireValueInRange(items,true).build();
point.setDataValidation(rule)
}
}
Thanks for the help. after looking at your suggestions and trying a couple things, I found that simply adding the code:
&& start.getName()=="Items_Log")
To the end of the line:
if (current....
Worked and solved the issue.

Changing info on a different sheet in the same spreadsheet

I have two ranges of equal size on different sheets in the same spreadsheet. I am trying to find a row (based off of user input) in the first sheet and then use that index to modify a table in the second sheet that counts how many times that certain index has been used before (to make a nice looking pie chart).
This code runs but will not produce results on the second sheet. I've gone through the debugging process and my best guess is that for some reason, my for in loop is not running through. Attached is my code that takes in the beforementioned index and attempts to perform the second half of my goal.
function acceptToEncounterChart(ghostrow) {
var ss = SpreadsheetApp.getActiveSpreadsheet();
SpreadsheetApp.setActiveSheet(ss.getSheets()[1]);
ss = SpreadsheetApp.getActiveSpreadsheet();
var sheet = ss.getSheetByName("Average Encounter Chart");
var range = sheet.getRange("B3:B14")
for(var i in range) {
if(ghostrow == i) {
var before = range[i][0].getValue()
range[i][0].setValue(before + 1);
}
}
SpreadsheetApp.setActiveSheet(ss.getSheets()[0]);
};
Explanation:
I am not entirely sure what is your goal.
However, here is some fixes / improvements starting from the beginning:
You define 2 times the same variable ss with exactly the same value.
You don't need to set the active sheet, if your goal is to just get the sheet, therefore this line is redundant:
SpreadsheetApp.setActiveSheet(ss.getSheets()[1]);
Variable range is not an array but a range object. You can't index it and therefore you can't also use a for loop to iterate over a single object. For the same exact reason, the code inside the if statement is wrong, you can't index range. But you don't see any errors because the if statement evaluates to false.
In JavaScript and in many other programming languages, array indexes start from 0. Since your range starts from cell B3 or row 3, you need to use i+3 to match the data with the range.
For the same reason as the previous point, ghostrow is an index, not a row. The if statement compares an array index i with ghostrow, so ghostrow should not be confused with the actual sheet row. For example, if you choose ghostrow=5 then the current script will increment the value of the cell B8 (remember i+3) by 1.
Solution:
Here is a workable code snippet:
function acceptToEncounterChart(ghostrow) {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheet = ss.getSheetByName("Average Encounter Chart");
var data = sheet.getRange("B3:B14").getValues().flat();
data.forEach((v,i)=>{
if(ghostrow == i){
sheet.getRange(i+3,2).setValue(v+1)
}
});
ss.setActiveSheet(ss.getSheets()[0]);
}
Related:
Please explore the official google apps script documentation.

Iterate over sheet using GS while there is any data in columns

I need to do a small script, its basic idea is to have a sheet with names in one columns and some random info in another (to the right). The sheet has some count (unknown in advance) of such records, so they are like
John 39483984
George 3498349
Layla 23948
So that a user can enter any number of such simple records, my script must create a file for each name (file is of the same name) and write the number into that file. I managed to find how to create files (though still couldn't find out how to create file in the current folder, same as where the sheet is located - this is just a side question, but if you know how to do it, please tell me). The only real problem is iterating through the records. My idea was to go through them one by one and stop when there is an empty record - basic strategy, but I wasn't able to find how to implement it (yep!). There are range functions, but there I should know in advance the range; also there is a function to get selected cells but that will require a user to select the records, which is strange.
So please suggest me a solution if it exists in this frustration Google Script.
function createFilesForEachNameInSheet() {
// First, you connect to the spreadsheet, and store the connection into a variable
var ss = SpreadsheetApp.openById("SPREADSHEET_KEY_GOES_HERE"); // you do know how to get the spreadsheet key, right?
// Then, you take the sheet from that spreadsheet
var sheet = ss.getSheetByName("Sheet1");
// Then, the "problematic range". You get the ENTIRE range, from end to end, as such:
var wholeRange = sheet.getRange(1,1,sheet.getLastRow(),sheet.getLastColumn());
// Then, you fetch its values:
var rangeValues = wholeRange.getValues();
// At this point you have a bi-dimensional array, representing the rows and columns.
// Assuming you have 2 columns, in the first column you have the names, and in the second you have the unknown value
// You need to use the already known for loop, iterate over all the data, and store it first in an object, so that you create the file only ONCE.
var objectData = {};
for (var i=0;i<rangeValues.length;i++) {
var thisName = rangeValues[i][0];
var thisValue = rangeValues[i][1];
if (objectData.thisName == undefined) objectData.thisName = [];
objectData.thisName.push(thisValue);
}
// Now we have our values grouped by name.
// Let's create a file for each name.
for (var name in objectData) {
DriveApp.createFile(name, objectData.name.join("\n"));
}
// NOTE: if you want to create into a specific folder, you first target it, using the DriveApp:
var folders = DriveApp.getFoldersByName("the folder name goes here");
// folders variable is now an iterator, containing each folder with that name.
// we will iterate over it as follows, and select the one we want.
// the logic for it, you'll select one of your choice:
while (folders.hasNext()) {
var thisFolder = folders.next();
if (/* condition to check if we found the right folder */) {
thisFolder.createFile(name, objectData.name.join("\n"))
}
}
}

Update named ranges without breaking formula

I have a spreadsheet with many formula referencing named ranges.
The spreadsheet has a script associated with it in a custom menu that imports updated data which can increase the length of the data needed in each range. Once the data are imported the script updates the named ranges to be the length of the new data.
Here is the code block that imports new data and then updates the named ranges:
// add the CSV menu. Might change this to be an automatic update base don date
function onOpen() {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var csvMenuEntries = [{name: "Update Data", functionName: "importFromCSV"}];
ss.addMenu("Update", csvMenuEntries);
}
function importFromCSV() {
var file = DriveApp.getFilesByName("double_leads_data.csv");// get the file object
var csvFile = file.next().getBlob().getDataAsString();// get string content
var csvData = Utilities.parseCsv(csvFile);
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheet = ss.getSheetByName('DataImport'); // only add data to the tab DataImport to prevent overwriting other parts of the spreadsheet
sheet.getRange(2,1, csvData.length, csvData[0].length).setValues(csvData);// write to sheet in one single step. Start at row 2 (getRange(2... )
SpreadsheetApp.getUi().alert('Data Updated');
//now update the named ranges if they have changed in length
var openEnded = ["business_unit", "channel", "date", "leads", "medium", "region" ];
for(i in openEnded) {
var r = ss.getRangeByName(openEnded[i]);
var rlr = r.getLastRow();
var s = r.getSheet();
var slr = s.getMaxRows();
if(rlr==slr ) continue; // ok as is-skip to next name
var rfr = r.getRow();
var rfc = r.getColumn();
var rnc = r.getNumColumns();
var rnr = slr - rfr + 1;
ss.removeNamedRange(openEnded[i]);
ss.setNamedRange( openEnded[i], s.getRange(rfr, rfc, rnr, rnc ));
}
}
All works well - the data import and the named ranges update. However, after the update all the formula referencing the named ranges break and show #REF where they previously referenced the corresponding named range.
Reading some documentation here there is a sentence
When you delete a named range, any formulas referencing this named
range will no longer work. However, protected ranges that reference a
named range will swap out the named range for the cell values
themselves and continue to work.
I'm not really sure what that means. If I use a protected range instead will it all work? I tried editing the code above? I read about getProtections() here so tried making a small edit:
var openEnded = ["businessunit2", "date2", "leads2", "medium2", "region2", "saleschannel2" ];
var openEnded = ss.getProtections(SpreadsheetApp.ProtectionType.RANGE)
I didn't really expect this to work but was worth a try.
Is there a solution here? How can I update a named range with a script without breaking existing formula which references those ranges? Will using getProtections() lead to a solution or is that just a diversion?
Use INDIRECT("rangeName") in formulas instead of just rangeName. The only way to extend the range programmatically is by removing it and then adding it back with a new definition. This process breaks the formula and returns #ref instead of the range name.
=sum(indirect("test1"),indirect("test3"))
This is a messy and should be unnecessary workaround. If you agree please star the item in the issue tracker.
https://code.google.com/p/google-apps-script-issues/issues/detail?id=5048
the detail you mention about protected ranges wont help you. That refers to the protected range definition in their details pane. You can define a protected range to be a named range and if the named range is deleted it won't break the protected range definition.i also ran into this a while ago and consider it to be a serious bug in their named ranges api. Its ridiculous that their api doesnt have a way to modify them (instead of deleting and recreating). I mean obviously if we use named ranges is because we expect them to change. Sorry for the rant but this is a very old issue that is still broken.
edit: see
https://code.google.com/p/google-apps-script-issues/issues/detail?id=1040 (i'm #7 there from 1.5 years ago)
and
https://code.google.com/p/gdata-java-client/issues/detail?id=196 (im #4 and just added #5 there)
Please star both.
formulas that reference the NamedRange are being broken by the line
ss.removeNamedRange(openEnded[i]);
I believe you can simply omit this line, and go directly to
ss.setNamedRange( openEnded[i], s.getRange(rfr, rfc, rnr, rnc ));
this approach appears to be working for me in a GAS script that adds a column to a NamedRange in Google Sheets. Formulas in other cells reference this named range and are not broken when my script executes
I read the three issue tracker postings and I understand the concern is generating duplicate entries in the set of named ranges. So far I have not seen this behavior so perhaps this bug was fixed.
The following code will update a named range without deleting it or create the range if it doesn't exist.
function fixNamedRange (ss, name, range) {
var ssNamedRanges = ss.getNamedRanges();
var ssRangeNames = ssNamedRanges.map (function (ssRange) {
return ssRange.getName();
}
);
var myRange = ssNamedRanges[ssRangeNames.indexOf(name)];
if (myRange) {
return myRange.setRange(range);
} else {
ss.setNamedRange(name, range);
return -1;
}
}
Edited to employ .map method instead of iterating through array of named ranges.