Removing invalid named ranges with Sheets API and GAS - javascript

Here is my function:
function deleteNamedRangeWithREF() {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var spreadsheetId = ss.getId();
var spreadsheetObject = Sheets.Spreadsheets.get(spreadsheetId);
var namedRanges = spreadsheetObject.namedRanges;
Logger.log(namedRanges);
var namedRangesRef = [];
for(var i = 0; i < namedRanges.length; i++)
{
if((namedRanges[i].range.endColumnIndex - namedRanges[i].range.startColumnIndex) <= 0 || (namedRanges[i].range.endRowIndex - namedRanges[i].range.startRowIndex) <= 0)
namedRangesRef.push(namedRanges[i].namedRangeId);
}
var BUrequest = Sheets.newBatchUpdateSpreadsheetRequest();
BUrequest.requests = [];
for(var i = 0; i < namedRangesRef.length; i++)
{
var request = {
deleteNamedRange: {
namedRangeId: namedRangesRef[i]
}
}
BUrequest.requests.push(request)
}
if (BUrequest.requests.length)
Sheets.Spreadsheets.batchUpdate(BUrequest, spreadsheetId);
}
I'm using sheets API to delete bad #REF named ranges.
When coding this, this worked, but now looks like it's broken.
You can see by yourself #Logger.log(namedRanges); all listed named ranges are valid, it doesn't retrieve the #REF named ranges.
Did google changed something, is there an alternative?

At first, I have to apologize and modify my comments in your question. At Sheets API, when there is no value of object, the property is not created. For example, sheetId of gid=0 is not shown in the range object. (But I'm not sure whether it occurs at all response patterns of Sheets API.) About this, I had forgot it. I have to apologize for this situation.
By considering above situation, I investigated your sample Spreadsheet. As the results, it was found as follows.
NamedRange with PlageNommée1 could be retrieved by Sheets API. In this case, although the range is included as the grid range, sheetId is not included in the range object because the sheet ID is 0.
NamedRange with PlageNommée2 couldn't be retrieved by Sheets API. In this case, the range object related to PlageNommée2 couldn't be retrieved, although sheetId of PlageNommée2 is not 0.
I think that when the grid range is #REF!, Sheets API might use this as no namedRange.
From above situation, I thought that Spreadsheet Service instead of Sheets API might be able to retrieve values. So I tested this. The sample script using Spreadsheet Service is as follows.
Sample script:
var id = "### spreadsheetId ###"; // Please set this.
var namedRanges = SpreadsheetApp.openById(id).getNamedRanges();
for (var i = 0; i < namedRanges.length; i++) {
Logger.log("name: %s, range: %s!%s",
namedRanges[i].getName(),
namedRanges[i].getRange().getSheet().getSheetName(),
namedRanges[i].getRange().getA1Notation()
)
}
Result:
name: "PlageNommée1", range: "Feuille 1!A1"
name: "PlageNommée2", range: "Feuille 2!#REF!"
In this case, the range of Feuille 2 can be retrieved as #REF!. From this result, you can check the namedRanges.
It was found that the namedRange of PlageNommée2 could be retrieved by Spreadsheet Service, even if the range is #REF!.
Reference:
getNamedRanges()

Related

Exception: Document is missing (perhaps it was deleted, or you don't have read access?)

I'm working on a project that take "profiles" stored in a Google Sheet, makes a unique Google Doc for each profile, and then updates the unique Google Doc with any new information when you push a button on the Google Sheet.
I have some other automations built into my original code, but I simplified most of it to what's pertinent to the error I'm getting, which is this:
Exception: Document is missing (perhaps it was deleted, or you don't have read access?
It happens on Line 52 of my script in the fileUpdate funtion. Here's the appropriate line for reference:
var file = DocumentApp.openById(fileName);
And this is the rest of my code:
function manageFiles() {
//Basic setup. Defining the range and retrieving the spreadsheet to store as an array.
var date = new Date();
var sheet = SpreadsheetApp.getActiveSpreadsheet().getActiveSheet();
var array = sheet.getDataRange().getValues();
var arrayL = sheet.getLastRow();
var arrayW = sheet.getLastColumn();
for (var i = 1; i < arrayL; i++) {
if (array[i][arrayW-2] == "") {
//Collect the data from the current sheet.
//Create the document and retrieve some information from it.
var docTitle = array[i , 0]
var doc = DocumentApp.create(docTitle);
var docBody = doc.getBody();
var docLink = doc.getUrl();
//Use a for function to collect the unique data from each cell in the row.
docBody.insertParagraph(0 , "Last Updated: "+date);
for (var j = 2; j <= arrayW; j++) {
var colName = array[0][arrayW-j];
var data = array[i][arrayW-j];
if (colName !== "Filed?") {
docBody.insertParagraph(0 , colName+": "+data);
}
}
//Insert a hyperlink to the file in the cell containing the SID
sheet.getRange(i+1 , 1).setFormula('=HYPERLINK("'+docLink+'", "'+SID+'")');
//Insert a checkbox and check it.
sheet.getRange(i+1 , arrayW-1).insertCheckboxes();
sheet.getRange(i+1 , arrayW-1).setFormula('=TRUE');
}
else if (array[i][arrayW-2] !== "") {
updateFiles(i);
}
}
sheet.getRange(1 , arrayW).setValue('Last Update: '+date);
}
//Note: I hate how cluttered updateFiles is. I'm going to clean it up later.
function fileUpdate(rowNum) {
//now you do the whole thing over again from createFiles()
//Basic setup. Defining the range and retrieving the spreadsheet to store as an array.
var date = new Date();
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheet = ss.getActiveSheet();
var array = sheet.getDataRange().getValues();
var arrayL = sheet.getLastRow();
var arrayW = sheet.getLastColumn();
//Collect the data from the current sheet.
var fileName = array[rowNum][0];
var file = DocumentApp.openById(fileName);
//retrieve the body of the document and clear the text, making it blank.
file.getBody().setText("");
//Use a for function to collect the the unique date from every non-blank cell in the row.
file.getBody().insertParagraph(0 , "Last Updated: "+date);
for (var j = 2; j <= arrayW; j++) {
var colName = array[0][arrayW-j];
var data = array[rowNum][arrayW-j];
file.getBody().insertParagraph(0 , colName+": "+data);
}
}
If you'd like to take a look at my sample spreadsheet, you can see it here. I suggest you make a copy though, because you won't have permissions to the Google Docs my script created.
I've looked at some other forums with this same error and tried several of the prescribed solutions (signing out of other Google Accounts, clearing my cookies, completing the URL with a backslash, widening permissions to everyone with the link), but to no avail.
**Note to anyone offended by my janky code or formatting: I'm self-taught, so I do apologize if my work is difficult to read.
The problem (in the updated code attached to your sheet) comes from your URL
Side Note:
In your initial question, you define DocumentApp.openById(fileName);
I assume your realized that this is not correct, since you updated
your code to DocumentApp.openByUrl(docURL);, so I will discuss the
problem of the latter in the following.
The URLs in your sheet are of the form
https://docs.google.com/open?id=1pT5kr7V11TMH0pJea281VhZg_1bOt8YDRrh9thrUV0w
while DocumentApp.openByUrl expects a URL of form
https://docs.google.com/document/d/1pT5kr7V11TMH0pJea281VhZg_1bOt8YDRrh9thrUV0w/
Just adding a / is not enough!
Either create the expected URL manually, or - much easier / use the method DocumentApp.openById(id) instead.
For this, you can extract the id from your URL as following:
var id = docURL.split("https://docs.google.com/open?id=")[1];
var file = DocumentApp.openById(id)

Delete Row if cell starts with certain text - Google Sheets / Apps Script

I have rows of data in column A containing cells starting with AR. I would like any cell that contains AR to be deleted. I have script already but this only deletes exact matches
So example is AR12345 in Column A & A12345. So it should ONLY delete the cell row with AR and not just A
function DeleteAny() {
var sheet = SpreadsheetApp.getActive();
sheet.setActiveSheet(sheet.getSheetByName('MULTI KIT DATA'), true);
var rows = sheet.getDataRange();
var numRows = rows.getNumRows();
var values = rows.getValues();
var rowsDeleted = 0;
for (var i = 0; i <= numRows - 1; i++) {
var row = values[i];
// I cant put AR here because it wont delete anything. the AR numbers keep changing also
if (row[14] == '') {
sheet.deleteRow((parseInt(i)+1) - rowsDeleted);
rowsDeleted++;
}
}
};
I have searched but cannot find anything.
How about using javascript Array#filter ? This is probably simplest for simple data.
// filter values
let values = sheet.getDataRange().getValues()
.filter(row => !row.find(v => v.match(/^AR/)))
// clear range
sheet.getDataRange().clear()
// write back filtered values
sheet.getRange(1, 1, values.length, values[0].length).setValues(values)
How about the following modification?
Pattern 1:
In this pattern, as a simple modification, your script is modified.
From:
if (row[14] == '') {
To:
if (row[0].length > 1 && row[0].substr(0, 2) == "AR") {
In this modification, the top 2 characters are retrieved with row[0].substr(0, 2).
If AR is included in the inner value, if (row[0].length > 1 && row[0].includes("AR")) { might be suitable.
Note:
In your script, row[14] is used. But in your question, the values are in the column "A". So I used row[0]. If you want to check other column, please modify it.
Pattern 2:
In this pattern, as other sample script, TextFinder and Sheets API are used. When you use this, please enable Sheets API at Advanced Google services.
function myFunction() {
const sheetName = "MULTI KIT DATA"; // Please set the sheet name.
const ss = SpreadsheetApp.getActiveSpreadsheet();
const sheet = ss.getSheetByName(sheetName);
const sheetId = sheet.getSheetId();
const requests = sheet
.getRange(`A1:A${sheet.getLastRow()}`)
.createTextFinder("^AR")
.matchCase(true)
.useRegularExpression(true)
.findAll()
.map(r => r.getRow())
.reverse()
.map(r => ({deleteDimension:{range:{sheetId:sheetId,startIndex:r - 1,endIndex:r,dimension:"ROWS"}}}));
Sheets.Spreadsheets.batchUpdate({requests: requests}, ss.getId());
}
In this sample script, the ranges of values which have AR at the top 2 characters are retrieved with TextFinder. And the request body is created using the retrieved ranges, and then, it requests to Sheets API with the request body. By this, the rows you want to delete are deleted.
When there are a lot of rows you want to delete, the process cost of this sample script might be low.
References:
substr()
includes()
TextFinder
Method: spreadsheets.batchUpdate
DeleteDimensionRequest

Google app script exceeded execution time limit

I have a Google sheet with almost 160 sub sheets. All the questions are in "All" sheet and B column is the actual spreadsheet name where they should be. The following code is reading data from "All" spreadsheet and sending them perfectly to the last row of desired spreadsheet perfectly but it is taking very long time! Probably because it has a lot of subsheets and is using getSheetByName again and again. Now I've stored all the sub sheets' name and ID in 'sheets' and 'sheetID' arrays at once. I'm thinking to compare between rangeValues[j][1] and sheetNames[k][0]. Below is the code and screenshot of the spreadsheet.
Is this a suitable way? Please help me to run the script faster!
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheet = ss.getSheetByName("All");
var rangeData = sheet.getDataRange();
var lastRow = rangeData.getLastRow();
var searchRange = sheet.getRange(1,1, lastRow, 8);
var curr_sheet;
function send() {
var rangeValues = searchRange.getValues();
var sheetNames = new Array();
var sheets = SpreadsheetApp.getActiveSpreadsheet().getSheets();
for (var i=0 ; i<sheets.length ; i++) sheetNames.push( [ sheets[i].getName() ] );
//Logger.log (sheetNames.length);
var sheetID = new Array();
var sheetIDs = SpreadsheetApp.getActiveSpreadsheet().getSheets();
for (var i=0 ; i<sheetIDs.length ; i++) sheetID.push( [ sheetIDs[i].getSheetId() ] );
//Logger.log (sheetID.length);
for ( j = 0 ; j < lastRow; j++)
{
for ( k = 0 ; k < sheetNames.length ; k++) //
{
if (rangeValues[j][1] === sheetNames[k][0])
{
var targetSheet = ss.getSheetByName(sheetNames[k][0]); // This line is working but taking very long to complete
var targetSheet = ss.getSheetByID(sheetIDs[k][0]); // This line is not code just to show what I'm thinking to do.
targetSheet.getRange(targetSheet.getLastRow()+1, 1, 1, 8).setValues([rangeValues[j]]);
}
}
}
}
I believe your goal as follows.
You want to reduce the process cost of your script.
Modification points:
There is no method of getSheetByID in Class Spreadsheet. By this, I think that in your case, the following script can be removed.
var sheetID = new Array();
var sheetIDs = SpreadsheetApp.getActiveSpreadsheet().getSheets();
for (var i=0 ; i<sheetIDs.length ; i++) sheetID.push( [ sheetIDs[i].getSheetId() ] );
In your script, getSheetByName and getRange(###).setValues(###) are used in the loop. And also, even when the sheet is the same sheet, each row is put using setValues in the loop every row. In this case, the process cost will be high. I think that these situation might be the reason of your issue.
Fortunately, in your situation, all sheets are in the same Spreadsheet. So for reducing the process cost of your script, here, I would like to propose to put the values to each sheet using Sheets API. The flow of modified script is as follows.
Retrieve Spreadsheet and values.
This script is from your script.
Retrieve all sheets in the Spreadsheet.
Check whether the sheet names from the column "B" are existing.
Create the object for putting to each sheet using Sheets API.
Request the method of spreadsheets.values.batchUpdate of Sheets API using the created object.
When above points are reflected to your script, it becomes as follows.
Modified script:
Please copy and paste the following script. And, please enable Sheets API at Advanced Google services. And then, please run the script.
function send() {
// 1. Retrieve Spreadsheet and values. This script is from your script.
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheet = ss.getSheetByName("All");
var rangeData = sheet.getDataRange();
var lastRow = rangeData.getLastRow();
var searchRange = sheet.getRange(1,1, lastRow, 8);
var curr_sheet;
var rangeValues = searchRange.getValues();
// 2. Retrieve all sheets in the Spreadsheet.
var sheets = ss.getSheets().reduce((o, e) => Object.assign(o, {[e.getSheetName()]: e}), {});
// 3. Check whether the sheet names from the column "B" are existing.
// 4. Create the object for putting to each sheet using Sheets API.
var data = rangeValues.reduce((o, e) => {
if (sheets[e[1]]) {
if (o[e[1]]) {
o[e[1]].values.push(e);
} else {
o[e[1]] = {range: `'${e[1]}'!A${sheets[e[1]].getLastRow() + 1}`, values: [e]};
}
}
return o;
}, {});
// 5. Request the method of spreadsheets.values.batchUpdate of Sheets API using the created object.
Sheets.Spreadsheets.Values.batchUpdate({data: Object.values(data), valueInputOption: "USER_ENTERED"}, ss.getId());
}
Note:
In this case, when you run the script, one API call is used. Because the method of spreadsheets.values.batchUpdate is used.
In my environment, when I tested above script and your script using a sample Spreadsheet, I could confirm the reduction of the process cost of about 70 % from your script.
References:
Advanced Google services
Method: spreadsheets.values.batchUpdate
reduce()
Try this:
Should be a lot faster
function send() {
var ss=SpreadsheetApp.getActive();
var sh=ss.getSheetByName("All");
var rg=sh.getRange(1,1,sh.getLastRow(),8);
var v=rg.getValues();
var names=[];
var sheets=ss.getSheets();
sheets.forEach(function(sh,i){names.push(sh.getName());});
v.forEach(function(r){
let idx=names.indexOf(r[1]);//column2
if(idx!=-1) {
sheets[idx].getRange(sheets[idx].getLastRow()+1,1,1,8).setValues([r]);
}
});
}

Duplicating Template sheet many times at once while keeping the Range Protections

I'm using a code to duplicate a template sheet with multiple range protections within it,
Currently, I'm running the script to create one by one the new tabs. Can anyone help me with the following script to be able to duplicate the template many times at once (Example create from Template Sheet the following tabs: A,B,C,D,E,F) in one go:
Script
function duplicateSheetWithProtections() {
var ss = SpreadsheetApp.getActiveSpreadsheet();
sheet = ss.getSheetByName('Template');
sheet2 = sheet.copyTo(ss).setName('A');
var protections = sheet.getProtections(SpreadsheetApp.ProtectionType.RANGE);
for (var i = 0; i < protections.length; i++) {
var p = protections[i];
var rangeNotation = p.getRange().getA1Notation();
var p2 = sheet2.getRange(rangeNotation).protect();
p2.setDescription(p.getDescription());
p2.setWarningOnly(p.isWarningOnly());
if (!p.isWarningOnly()) {
p2.removeEditors(p2.getEditors());
p2.addEditors(p.getEditors());
}
}
}
Thank you for your help
How about this modification? I think that you can achieve what you want using Sheets API. The flow of this sample script is as follows.
When you use this script, please enable Sheets API at Advanced Google Services and API console. You can see about how to enable Sheets API at here.
Flow:
Set copied sheet names. For example, those are ["A", "B", "C",,]. This is from your question.
Copy template sheet using the sheet names.
Retrieve protected ranges from Template sheet using Sheets API.
Create request body.
Set protected ranges to copied sheets using Sheets API.
By this flow, the protected ranges of Template sheet can be copied to the all copied sheets by only one API call.
Modified script:
function duplicateSheetWithProtections() {
var sheetNames = ["A", "B"]; // Please set copied sheet names here.
var ss = SpreadsheetApp.getActiveSpreadsheet();
var spreadsheetId = ss.getId();
var sheet = ss.getSheetByName('Template');
// Copy template sheet.
var copiedSheetIds = sheetNames.map(function(e) {return sheet.copyTo(ss).setName(e).getSheetId()});
// Retrieve protected ranges from Template sheet.
var sheets = Sheets.Spreadsheets.get(spreadsheetId).sheets;
var templateSheet = sheets.filter(function(e) {return e.properties.title == "Template"});
var protectedRanges = templateSheet[0].protectedRanges;
// Create request body.
var resources = copiedSheetIds.map(function(e) {
return protectedRanges.map(function(f) {
var obj = JSON.parse(JSON.stringify(f));
delete obj.protectedRangeId;
if (obj.warningOnly) delete obj.editors;
obj.range.sheetId = e;
return {"addProtectedRange": {"protectedRange": obj}};
});
});
resources = Array.prototype.concat.apply([], resources);
// Set protected ranges to copied sheets
Sheets.Spreadsheets.batchUpdate({"requests": resources}, spreadsheetId);
}
References:
spreadsheets.get
spreadsheets.batchUpdate
If I misunderstand your question, please tell me. I would like to modify it.

How to set column to plain text format for every sheet?

I've read through this post >> set format as plain text
I'm creating a new thread because that thread is a few years old already and I didn't want to revive something from a few years ago.
the code:
function A1format() {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var mainsheet = ss.getSheetByName("Sheet1");
var G = mainsheet.getRange("C15:BH3000").getGridId();
var illa = mainsheet.getRange("A13");
Logger.log(G);
illa.copyFormatToRange(G, 16, 3,200, 30);
}
This code is supposed to set plain text format for the sheet named Sheet1
I've tried var mainsheet = ss.getSheetByName("Sheet1, Sheet2, Sheet3"); but this doesn't seem to work, I just get an error message.
This is the current code I have, this code works but is both inefficient and a real pain to maintain if something changes:
function setPlainTextDefault() {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheet1 = ss.getSheets()[0];
var sheet2 = ss.getSheets()[2];
var sheet3 = ss.getSheets()[4];
var sheet4 = ss.getSheets()[7];
var sheetColumn1 = sheet1.getRange("A1:A");
var sheetColumn2 = sheet2.getRange("A1:A");
var sheetColumn3 = sheet3.getRange("A1:A");
var sheetColumn4 = sheet4.getRange("A1:A");
sheetColumn1.setNumberFormat("#");
sheetColumn2.setNumberFormat("#");
sheetColumn3.setNumberFormat("#");
sheetColumn4.setNumberFormat("#");
}
Here I am changing each column A in every sheet to plain text by using the index number of the sheet, so I have to manually count the number for every sheet, this is a nightmare as I have a very large number of sheets, it will take too much time for me to manually count the sheets and then add it to my current code. I know there is a better more efficient way of doing this, but I don't know how due to my lack of knowledge in google apps scripting.
How do you do this for every sheet in the document regardless of how many sheets are present? I want to go through every sheet, from sheet1 till x number of sheets and then change every column A to plain text.
var mainsheet = ss.getSheetByName("Sheet1, Sheet2, Sheet3") throws an error because there isn't a sheet named "Sheet1, Sheet2, Sheet3". "There isn't a way to reduce the calls to the Spreadsheet Service on your code, but you could make that it "looks better" by using some JavaScript features like loops and array handling.
Example (untested)
On the following code snippet, arrays, a for and indexOf are used to reduce a bit the number of lines of the original code.
function setPlainTextDefault() {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheets = ss.getSheets();
var sheetsToProcess = [0,2,4,7]; // Array holding the sheets indexes to process
for(var i = 0; i < sheets.length; i++){
/* If i is not in the sheetsToProccess indexOf returns -1 which is parsed as false
otherwise the result is parsed as true */
if(sheetsToProcess.indexOf(i)){
var sheet = ss.getSheets()[i];
var sheetColumn = sheet.getRange("A1:A");
sheetColumn.setNumberFormat("#");
}
}
}
To make the above work for every sheet, comment out or remove if(sheetsToProcess.indexOf(i)){ and the corresponding }.
It's worth to note that if you are looking help to find the "best" way to improve your code, you could try Code Review.
This was the code I needed:
function setPlainText() {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheets = ss.getSheets();
for(var i = 0; i < sheets.length; i++) {
//Logger.log(sheets[i].getName());
var setPlainText = ss.getSheets()[i];
var sheetColumnA = setPlainText.getRange("A1:A");
sheetColumnA.setNumberFormat("#");
var sheetColumnB = setPlainText.getRange("B1:B");
sheetColumnB.setNumberFormat("#");
}
}
It will:
Iterate through a document with x number of sheets however big or small
Then for each iteration, set plain text format for every column A and B for every sheet in the document

Categories

Resources