Group rows with appscript - google-apps-script

I have a sheet with more than 3000 rows and I want to group those rows by a parameter in ColA. So all rows having '1' in colA should be grouped under the row above with '0' in colA. Once groups are created I want them to collapse.
Since the script will be triggered daily upon data update, I also need the before-created groups to be removed so the new ones can be properly created.
I have several scrips doing what I need but it takes forever for them to go through all the rows. Is it possible to optimize them in some way or perhaps a different approach can be used for my needs? Thanks in advance for your help!
function removeAllGroups1() {
const ss = SpreadsheetApp.getActive();
const sh = ss.getSheetByName("Pipeline");
const rg = sh.getDataRange();
const vs = rg.getValues();
vs.forEach((r, i) => {
let d = sh.getRowGroupDepth(i + 1);
if (d >= 1) {
sh.getRowGroup(i + 1, d).remove()
}
});
}
function groupRows1() {
const ss = SpreadsheetApp.getActive();
const sh = ss.getSheetByName('Pipeline');
const levels = sh.getRange(2, 1, sh.getLastRow() - 1).getValues().flat();
levels.forEach((e, i) => sh.getRange(i + 2, 1).shiftRowGroupDepth(e));
}
function collapse() {
const ss = SpreadsheetApp.getActive();
const sh = ss.getSheetByName('Pipeline');
let lastRow = sh.getDataRange().getLastRow();
for (let row = 1; row < lastRow; row++) {
let depth = sh.getRowGroupDepth(row);
if (depth < 1) continue;
sh.getRowGroup(row, depth).collapse();
}
}
Data Sample: https://docs.google.com/spreadsheets/d/10BNrnAyQw89gy-Sj3CLiz4AgVtFI0AjXTvc0REGGTfY/edit#gid=113574154

I believe your goal is as follows.
You want to group the rows with high process speed.
You want to delete all groups with high process speed.
You want to collapse all groups with high process speed.
From the above goal, I thought that when Sheets API is used, the process cost can be reduced. And, Sheets API can group rows, delete all groups and collapse all groups.
Sample script:
Before you run this script, please enable Sheets API at Advanced Google services.
function removeAllGroups2() {
const sheetName = "sample";
const ss = SpreadsheetApp.getActive();
const ssId = ss.getId();
const sheetId = ss.getSheetByName(sheetName).getSheetId();
const n = Sheets.Spreadsheets.get(ssId, { ranges: [sheetName] }).sheets[0].rowGroups.reduce((n, { depth }) => n < depth ? depth : n, 0);
const requests = Array(n).fill("").map(_ => ({ deleteDimensionGroup: { range: { sheetId, dimension: "ROWS" } } }));
Sheets.Spreadsheets.batchUpdate({ requests }, ssId);
}
function groupRows2() {
const sheetName = "sample";
const ss = SpreadsheetApp.getActive();
const sheet = ss.getSheetByName(sheetName);
const levels = sheet.getRange("A2:A" + sheet.getLastRow()).getValues();
const sheetId = sheet.getSheetId();
const requests = levels.flatMap(([a], i) => Array(a).fill("").map(_ => ({ addDimensionGroup: { range: { sheetId, startIndex: i + 1, endIndex: i + 2, dimension: "ROWS" } } })));
Sheets.Spreadsheets.batchUpdate({ requests }, ss.getId());
}
function collapse2() {
const ss = SpreadsheetApp.getActive();
const requests = Sheets.Spreadsheets.get(spreadsheetId, {ranges: ["sample"]}).sheets[0].rowGroups.map(r => {
r.collapsed = true;
return { updateDimensionGroup: { fields: "*", dimensionGroup: r }};
});
Sheets.Spreadsheets.batchUpdate({ requests }, ss.getId());
}
Note:
I tested these sample scripts using your sample Spreadsheet. So when the structure of the Spreadsheet is different from your sample Spreadsheet, these scripts might not be able to be used. Please be careful about this. At first, please test them using your sample script.
References:
Method: spreadsheets.get
Method: spreadsheets.batchUpdate
DeleteDimensionGroupRequest
AddDimensionGroupRequest
UpdateDimensionGroupRequest

Related

How to delete blank rows for multiple sheets using google API

I have a case that i want to delete blank rows for multiple sheets. I want to use Google Sheet API because the SpreadsheetApp method takes too long and i get the timeout error because of it. I have 10k+ rows.
I already made my own code (Spreadsheet method version):
function myFunction() {
const sheet = ['', '', '', '']; //sheets name
for (let i in sheet) {
const ss = SpreadsheetApp.getActiveSpreadsheet().getSheetByName(sheet[i]);
const maxrow = sheet.getMaxRows();
const lastrow = sheet.getLastRow();
if (maxrow - lastrow > 10) {
ss.deleteRows(lastrow + 10, maxrow - lastrow - 10);
}
}
}
I believe your goal is as follows.
You want to modify your showing script using Sheets API.
In your script, it seems that sheet is an array. So, I think that an error occurs at const maxrow = sheet.getMaxRows();. Please be careful about this.
In this case, how about the following modification?
Modified script:
Please enable Sheets API at Advanced Google services. Ref
function myFunction() {
const sheet = ['Sheet1', 'Sheet2',,,]; // Please set sheet names you want to use.
const ss = SpreadsheetApp.getActiveSpreadsheet();
const requests = sheet.reduce((ar, s) => {
const sheet = ss.getSheetByName(s);
const maxrow = sheet.getMaxRows();
const lastrow = sheet.getLastRow();
if (maxrow - lastrow > 10) {
ar.push({ deleteDimension: { range: { sheetId: sheet.getSheetId(), startIndex: lastrow + 10, endIndex: maxrow, dimension: "ROWS" } } });
}
return ar;
}, []);
if (requests.length == 0) return;
Sheets.Spreadsheets.batchUpdate({ requests }, ss.getId());
}
References:
Method: spreadsheets.batchUpdate
DeleteDimensionRequest

Google Sheet App Script Protect Range & Allow Editors Based on Email But Exclude Few Columns From Edit Rights

I am using below script to protect data upto last row on opening the sheet. The script will add protection on the range and will allow the sheet owner and email id specified in the code to edit the protected range. The script performs its work, however, I want to update below in the script.
E.g. There are 5 columns in the sheet and data upto row no. 100, so on opening the sheet the script will protect all the columns upto row no. 100, only the sheet owner and the specified email id will be allowed to edit this protected range. I want a permanent protection on Col A & Col D so in this case the specific email id covered in the script will get access to protected range but excluding column A & col D.
function installedOnOpen(e) {
const editors = ["###"]; // Added: Please set the email addresses you want to give
the permission as the editor.
const sheetNames = ["Sheet1"]; // Please set the sheet names you want to protect.
const sheets = e.source.getSheets().filter(s =>
sheetNames.includes(s.getSheetName()));
if (sheets.length == 0) return;
sheets.forEach(s => {
const p = s.getProtections(SpreadsheetApp.ProtectionType.RANGE);
if (p.length > 0) {
p.forEach(pp => pp.remove());
}
const lastRow = s.getLastRow();
if (lastRow != 0) {
const newProtect = s.getRange(1, 1, lastRow, s.getMaxColumns()).protect();
newProtect.removeEditors(newProtect.getEditors());
newProtect.addEditors(editors); // Added
if (newProtect.canDomainEdit()) newProtect.setDomainEdit(false);
}
});
}
Any help on above will be greatly appreciated.
I believe your goal is as follows.
You want to protect the rows from 1st row to the last row. In this case, you want to give the edit permission to the specific user. And, for the specific columns, you don't want to make all users edit.
Added: I want these columns to be protected in full i.e. entire column should be protected.
In this case, how about the following modified script?
Modified script 1:
Please install OnOpen trigger to the function. When you reopen the Spreadsheet, the script is automatically run.
function installedOnOpen(e) {
const editors = ["###"]; // Added: Please set the email addresses you want to give the permission as the editor.
const permanentProtectedColumns = [1, 4]; // Added: Please set the permanent protected columns.
const sheetNames = ["Sheet1"]; // Please set the sheet names you want to protect.
const sheets = e.source.getSheets().filter(s => sheetNames.includes(s.getSheetName()));
if (sheets.length == 0) return;
sheets.forEach(s => {
const p = s.getProtections(SpreadsheetApp.ProtectionType.RANGE);
if (p.length > 0) {
p.forEach(pp => pp.remove());
}
const lastRow = s.getLastRow();
if (lastRow != 0) {
const newProtect = s.getRange(1, 1, lastRow, s.getMaxColumns()).protect();
newProtect.removeEditors(newProtect.getEditors());
newProtect.addEditors(editors);
if (newProtect.canDomainEdit()) newProtect.setDomainEdit(false);
// I added below script.
const maxRow = s.getMaxRows();
permanentProtectedColumns.forEach(r => {
const pp = s.getRange(1, r, maxRow).protect();
pp.removeEditors(pp.getEditors());
if (pp.canDomainEdit()) pp.setDomainEdit(false);
});
}
});
}
When this script is run, the 1st row to the last row is protected. In this case, the owner and specific users can edit the protected ranges. But, the columns of const permanentProtectedColumns = [1, 4]; (in this case, columns "A" and "D") can be edited by only the owner.
Modified script 2:
I think that the above modified script can be used. But, in your situation, the process cost might be high a little. So, if you want to reduce the process cost, how about using Sheets API as follows?
In this case, please enable Sheets API at Advanced Google services.
function installedOnOpen(e) {
const editors = ["###"]; // Added: Please set the email addresses you want to give the permission as the editor.
const permanentProtectedColumns = [1, 4]; // Added: Please set the permanent protected columns.
const sheetNames = ["Sheet1"]; // Please set the sheet names you want to protect.
const sheets = e.source.getSheets().filter(s => sheetNames.includes(s.getSheetName()));
if (sheets.length == 0) return;
const id = e.source.getId();
const obj = Sheets.Spreadsheets.get(id, { ranges: sheets.map(s => s.getSheetName()), fields: "sheets(protectedRanges)" }).sheets;
const protectedIds = obj.flatMap(({ protectedRanges }) => protectedRanges ? protectedRanges.map(({ protectedRangeId }) => protectedRangeId) : []);
const requests1 = [];
if (protectedIds.length > 0) {
protectedIds.forEach(s => requests1.push({ deleteProtectedRange: { protectedRangeId: s } }));
}
const owner = Session.getEffectiveUser().getEmail();
const requests2 = sheets.map(s => {
const sheetId = s.getSheetId();
const lastRow = s.getLastRow();
const maxCol = s.getMaxColumns();
return { addProtectedRange: { protectedRange: { range: { sheetId, startRowIndex: 0, endRowIndex: lastRow, startColumnIndex: 0, endColumnIndex: maxCol }, editors: { domainUsersCanEdit: false, users: [owner, ...editors] } } } };
});
const requests3 = sheets.flatMap(s => {
const sheetId = s.getSheetId();
const maxRow = s.getMaxRows();
return permanentProtectedColumns.map(t => ({ addProtectedRange: { protectedRange: { range: { sheetId, startRowIndex: 0, endRowIndex: maxRow, startColumnIndex: t - 1, endColumnIndex: t }, editors: { domainUsersCanEdit: false, users: [owner] } } } }));
});
Sheets.Spreadsheets.batchUpdate({ requests: [...requests1, ...requests2, ...requests3] }, id);
}
In this sample script, the same result with the above script is obtained.

Google Sheets deleteRow not working in my Macro

I'm trying to build a Macro to erase all the rows that have empty values on column D. Originally, I was using this code that I found:
function deleteRows() {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var s = ss.getSheetByName('Datos Competidor 2 - EV');
var r = s.getRange('D:D');
var v = r.getValues();
for(var i=v.length-1;i>=0;i--)
if(v[0,i]=='')
s.deleteRow(i+1);
};
However the excessive number of calls to the API made this really slow and some times even fail due to a timeout.
I decided to just add all the rows that met the condition to a list and then just pass that to the deleteRow() in order to only call the API once using this code:
function deleteBlankRows() {
emptyRange=[]
var ss = SpreadsheetApp.getActiveSpreadsheet();
var s = ss.getSheetByName('Datos Competidor 2 - EV');
var r = s.getRange('D:D');
var v = r.getValues();
for(var i=v.length-1;i>=0;i--)
if(v[0,i]=='')
emptyRange.push((i)+":"+(i));
ss.getRangeList(emptyRange).activate();
ss.getActiveSheet().deleteRows(ss.getActiveRange().getRow(), ss.getActiveRange().getNumRows());
};
The execution seems to work just fine, completing in 1 to 2 seconds, however, rows aren't erased as much as selected by the end of the execution.
This is what I see:
Final result
Any ideas why this is happening?
Thanks!
I believe your goal is as follows.
You want to reduce the process cost of your script.
In this case, how about using Sheets API? When Sheets API is used, I thought that the process cost for deleting the rows can be reduced a little. When the Sheets API is reflected in your script, it becomes as follows.
Modified script:
Before you use this script, please enable Sheets API at Advanced Google services.
function deleteRows() {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var s = ss.getSheetByName('Datos Competidor 2 - EV');
var values = s.getRange('D1:D' + s.getLastRow()).getDisplayValues();
var sheetId = s.getSheetId();
var requests = values.reduce((ar, [d], i) => {
if (!d) ar.push({ deleteDimension: { range: { sheetId, startIndex: i, endIndex: i + 1, dimension: "ROWS" } } });
return ar;
}, []).reverse();
Sheets.Spreadsheets.batchUpdate({ requests }, ss.getId());
}
References:
Method: spreadsheets.batchUpdate
DeleteDimensionRequest
Delete Rows with empties on column D
function deleteBlankRows() {
const ss = SpreadsheetApp.getActiveSpreadsheet();
const s = ss.getSheetByName('Datos Competidor 2 - EV');
const r = s.getRange('D1:D' + s.getLastRow());
const v = r.getValues().flat();
let d = 0;
v.forEach((e, i) => {
if (!e) {
s.deleteRow(i + 1 - d++)
}
})
}

Dynamic grouping in AppSript

I have a Spreadsheet with about 3000 rows which are grouped by order number. I'm trying to build an macro to:
remove all groups
multisort all rows
recreate groups
collapse all groups marked as finished orders (optional - have no idea how to achieve this )
SHEET_NAME = "PLAN";
SORT_DATA_RANGE = "A2:CJ";
GROUP_DATA_RANGE = "BQ2:BQ";
SORT_ORDER = [
{column: 40, ascending: false},
{column: 2, ascending: true},
{column: 4, ascending: true}
];
function Sortowanie() {
var ss = SpreadsheetApp.getActiveSpreadsheet();
removeAllGroups();
multiSortColumns();
groupRows();
ss.toast('Zakończono.');
}
function multiSortColumns(){
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheet = ss.getSheetByName(SHEET_NAME);
var range = sheet.getRange(SORT_DATA_RANGE + sheet.getLastRow());
range.sort(SORT_ORDER);
ss.toast('Sortowanie zakończone.');
}
function removeAllGroups() {
const ss = SpreadsheetApp.getActive();
const ssId = ss.getId();
const sheet = ss.getSheetByName(SHEET_NAME);
const sheetId = sheet.getSheetId();
sheet.expandAllRowGroups();
const n = Sheets.Spreadsheets.get(ssId, { ranges: [SHEET_NAME] }).sheets[0].rowGroups.reduce((n, { depth }) => n < depth ? depth : n, 0);
const requests = Array(n).fill("").map(_ => ({ deleteDimensionGroup: { range: { sheetId, dimension: "ROWS" } } }));
Sheets.Spreadsheets.batchUpdate({ requests }, ssId);
ss.toast('Usuwanie grup zakończone.');
}
function groupRows() {
const ss = SpreadsheetApp.getActive();
const sheet = ss.getSheetByName(SHEET_NAME);
const levels = sheet.getRange(GROUP_DATA_RANGE + getLastRowSpecial()).getValues();
const sheetId = sheet.getSheetId();
const requests = levels.flatMap(([a], i) => Array(a).fill("").map(_ => ({ addDimensionGroup: { range: { sheetId, startIndex: i + 1, endIndex: i + 2, dimension: "ROWS" } } })));
Sheets.Spreadsheets.batchUpdate({ requests }, ss.getId());
ss.toast('Ponowne grupowanie zakończone.');
}
function getLastRowSpecial() {
const ss = SpreadsheetApp.getActiveSpreadsheet()
const sheet = ss.getSheetByName(SHEET_NAME);
const lastRow = sheet.getRange(GROUP_DATA_RANGE).getNextDataCell(SpreadsheetApp.Direction.DOWN).getRow();
return lastRow
};
Everything works perfectly when I limit range to 1000 rows. When I try to run this for a whole range or more than 1000 and debuging, I'm getting this:
HttpResponseException: Response Code: 413. Message: response too large.
Without debugger after a while I'm getting this:
The JavaScript runtime has unexpectedly terminated.
Most bizarre thing is that script is creating these groups for a whole range and then error comes out after a while.
I think that 3K rows is not a big range for that, maybe someone has an idea what is wrong?
Here it is sample data sheet:
https://docs.google.com/spreadsheets/d/1DLXxZVyrhDxrBe1AX3iy54nQTFVJkoIpeos7M9mEaIo/edit?usp=sharing
Issue:
If I understand your situation correctly:
You are able to remove existing groups and sort the rows according to the groups you want to create.
The groups are defined by a column with 0s and 1s, so that 0 refers to group headers and continguous 1s should be grouped under the same group.
I assume here that, after the column header (first row), all values in your column are 0 or 1.
Solution:
Use slice and findIndex to find the successive group headers, looking iteratively for the values different than 1, so that all the rows in between are part of the same group.
For each iteration, use the indexes of the current and the next header to build each request.
Code sample:
const FIRST_ROW = 2;
function groupRows() {
const ss = SpreadsheetApp.getActiveSpreadsheet();
const sheet = ss.getSheetByName(SHEET_NAME);
const levels = sheet.getRange(GROUP_DATA_RANGE + getLastRowSpecial()).getValues().flat();
const sheetId = sheet.getSheetId();
const requests = [];
let currentHeader = FIRST_ROW;
while (currentHeader < levels.length) {
let groupLength = levels.slice(currentHeader-FIRST_ROW+1).findIndex(l => l !== 1);
if (groupLength < 0) groupLength = levels.length - currentHeader + 1;
const nextHeader = groupLength + currentHeader+1;
const request = { addDimensionGroup: { range: { sheetId, startIndex: currentHeader, endIndex: nextHeader-1, dimension: "ROWS" } } };
requests.push(request);
currentHeader = nextHeader;
}
Sheets.Spreadsheets.batchUpdate({ requests }, ss.getId());
ss.toast('Ponowne grupowanie zakończone.');
}
Note:
Your other functions are not displayed here, but you should use them to remove existing rows and to make sure your data is sorted according to your preferences.
One of you problems but probably not the only problem
Range specified like this generate a lot of nulls between last row and max rows
"BQ2:BQ"
Try rewriting them like this:
"BQ2:BQ" + sheet.getLastRow()

How can I check if a numerical value is within a range of cells in google sheets?

I would like to find if a certain value is in a range using app scripts for google sheets.
var sheet = SpreadsheetApp.getActiveSheet();
var rangeBikeNumbers = sheet.getDataRange("A5:A5000");
var values = rangeBikeNumbers.getValues();
If I have my range rangeBikeNumbers, how can I check if the number "42" for example is in that range. I have searched for hours now and have beeb unable to find any answer to this. indexOf only seems to return -1, regardless of whether or not the value is in the range.
var indexDataNumber = values.indexOf(42); for example always ends up being -1
I believe your goal as follows.
You want to check whether the value of 42 is existing in the range of A5:A5000.
In this case, I would like to propose to use TextFinder. Because when TexiFinder is used, the process cost is low. Ref By the way, getDataRange has not arguments. From your script, I thought that you might want var rangeBikeNumbers = sheet.getRange("A5:A5000");.
When this is reflected to your script, it becomes as follows.
Modified script:
function myFunction() {
var sheet = SpreadsheetApp.getActiveSheet();
var rangeBikeNumbers = sheet.getRange("A5:A5000");
var find = rangeBikeNumbers.createTextFinder("42").matchEntireCell(true).findNext();
if (find) {
// In this case, the value of 42 is existing in the range.
} else {
// In this case, the value of 42 is NOT existing in the range.
}
}
Note:
About var indexDataNumber = values.indexOf(42); for example always ends up being -1, I think that the reason of this issue is due to that values is 2 dimensional array. If you want to use this, you can also use the following script.
function myFunction() {
var sheet = SpreadsheetApp.getActiveSheet();
var rangeBikeNumbers = sheet.getRange("A5:A5000");
var values = rangeBikeNumbers.getValues();
var find = values.map(([e]) => e).indexOf(42); // of values.flat().indexOf(42);
if (find > -1) {
// In this case, the value of 42 is existing in the range.
} else {
// In this case, the value of 42 is NOT existing in the range.
}
}
References:
Benchmark: Process Costs for Searching Values in Spreadsheet using Google Apps Script
getDataRange()
getRange(a1Notation)
createTextFinder(findText)
Select any active range that you wish to search and it will search for the seed in that at range. The seed is currently defaulted to 42 but you can change it.
function findSeedInRange(seed = 42) {
const ui = SpreadsheetApp.getUi();
const ss = SpreadsheetApp.getActive();
const sh = ss.getActiveSheet();
const rg = sh.getActiveRange();
const row = rg.getRow();
const col = rg.getColumn();
var found = false;
rg.getValues().forEach((r, i) => {
r.forEach((c, j) => {
if (c == seed) {
let r = sh.getRange(i + row, j + col).getA1Notation();
ui.alert(`Found ${seed} in ${r}`);
found = true;
}
})
})
if(!found) {
ui.alert(`Did not find ${seed}`);
} else {
ui.alert('That is all.')
}
}
Here's another approach:
function findSeedInRange() {
const ui = SpreadsheetApp.getUi();
const ss = SpreadsheetApp.getActive();
const sh = ss.getActiveSheet();
const rg = sh.getActiveRange();
const resp = ui.prompt('Enter Seed', 'Enter Seed', ui.ButtonSet.OK_CANCEL)
if (resp.getSelectedButton() == ui.Button.OK) {
var seed = parseInt(resp.getResponseText());
const row = rg.getRow();
const col = rg.getColumn();
var found = false;
rg.getValues().forEach((r, i) => {
r.forEach((c, j) => {
if (c == seed) {
let r = sh.getRange(i + row, j + col).getA1Notation();
ui.alert(`Found ${seed} in ${r}`);
found = true;
}
});
});
if (!found) {
ui.alert(`Did not find ${seed}`);
} else {
ui.alert('That is all.')
}
} else {
ui.alert('Operation cancelled.')
}
}