Dynamic grouping in AppSript - google-apps-script

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()

Related

Sheets API Import sheet with formating

Using the script here I am able to import the sheet to another document however it does not work if I remove the columns from the srcRange. My goal is to copy the entire sheets formating over to the other sheet and the number of columns occasionally changes;
This works but the columns have to be specified:
const srcSpreadsheetId = "1mVlva8Dyxxxxxxxxxxxxxx"; // Please set source Spreadsheet ID.
const dstSpreadsheetId = "1a2Eb7fQOxxxxxxxxxxxxxx"; // Please set destination Spreadsheet ID.
const srcRange = "Database!A:I";
const dstRange = "Database";
// Here, the date object is retrieved as the serial number.
const values = Sheets.Spreadsheets.Values.get(srcSpreadsheetId, srcRange, { dateTimeRenderOption: "SERIAL_NUMBER", valueRenderOption: "UNFORMATTED_VALUE" }).values;
const dstSheet = SpreadsheetApp.openById(dstSpreadsheetId).getSheetByName(dstRange);
const sheetId = dstSheet.getSheetId();
Sheets.Spreadsheets.batchUpdate({ requests: [{ repeatCell: { range: { sheetId }, fields: "userEnteredValue" } }] }, dstSpreadsheetId);
Sheets.Spreadsheets.Values.update({ values }, dstSpreadsheetId, dstRange, { valueInputOption: "USER_ENTERED" });
// Here, the number format is copied.
const numberFormats = SpreadsheetApp.openById(srcSpreadsheetId).getRange(srcRange).getNumberFormats();
dstSheet.getRange(1, 1, numberFormats.length, numberFormats[0].length).setNumberFormats(numberFormats);
This does not work:
const srcSpreadsheetId = "1mVlva8Dyxxxxxxxxxxxxxx"; // Please set source Spreadsheet ID.
const dstSpreadsheetId = "1a2Eb7fQOxxxxxxxxxxxxxx"; // Please set destination Spreadsheet ID.
const srcRange = "Database"; // <<<<<<<<<<<<<<<<< Columns not specified
const dstRange = "Database";
// Here, the date object is retrieved as the serial number.
const values = Sheets.Spreadsheets.Values.get(srcSpreadsheetId, srcRange, { dateTimeRenderOption: "SERIAL_NUMBER", valueRenderOption: "UNFORMATTED_VALUE" }).values;
const dstSheet = SpreadsheetApp.openById(dstSpreadsheetId).getSheetByName(dstRange);
const sheetId = dstSheet.getSheetId();
Sheets.Spreadsheets.batchUpdate({ requests: [{ repeatCell: { range: { sheetId }, fields: "userEnteredValue" } }] }, dstSpreadsheetId);
Sheets.Spreadsheets.Values.update({ values }, dstSpreadsheetId, dstRange, { valueInputOption: "USER_ENTERED" });
// Here, the number format is copied.
const numberFormats = SpreadsheetApp.openById(srcSpreadsheetId).getRange(srcRange).getNumberFormats();
dstSheet.getRange(1, 1, numberFormats.length, numberFormats[0].length).setNumberFormats(numberFormats);
Have tried multiple variations to no avail. Keep getting > Exception: Range not found.
If you want to achieve The source range only works right now if you put in the columns but I want to copy the format for the entire sheet. and your actual error is No error > const srcRange = "Database!A:IX";, error happens if I use > const srcRange = "Database";, how about the following modification?
From:
const srcRange = "Database!A:IX";
To:
const srcRange = "Database";
And,
From:
const numberFormats = SpreadsheetApp.openById(srcSpreadsheetId).getRange(srcRange).getNumberFormats();
To:
const numberFormats = SpreadsheetApp.openById(srcSpreadsheetId).getSheetByName(srcRange).getDataRange().getNumberFormats();
or
const srcSheet = SpreadsheetApp.openById(srcSpreadsheetId).getSheetByName(srcRange);
const numberFormats = srcSheet.getRange(1, 1, srcSheet.getMaxRows(), srcSheet.getMaxColumns()).getNumberFormats();
Added:
From I tested both the first and second solution you suggested with a smaller amount of data and both work. The only wierd part is that for numbers stored as text they are in a white font and you cannot change the color., when I saw your Spreadsheet, I understood that the reason for this issue is due to plain text format. When the cells of plain text are retrieved with getNumberFormats(), it seems that null` is returned. By this, the number is not displayed. In order to reflect this issue, please modify it as follows.
Modified script:
Please set the Spreadsheet IDs and sheet names for your test situation.
function sample() {
const srcSpreadsheetId = "1mVlva8Dyxxxxxxxxxxxxxx"; // Please set source Spreadsheet ID.
const dstSpreadsheetId = "1a2Eb7fQOxxxxxxxxxxxxxx"; // Please set destination Spreadsheet ID.
const srcRange = "Database"; // <<<<<<<<<<<<<<<<< Columns not specified
const dstRange = "Database";
// Here, the date object is retrieved as the serial number.
const values = Sheets.Spreadsheets.Values.get(srcSpreadsheetId, srcRange, { dateTimeRenderOption: "SERIAL_NUMBER", valueRenderOption: "UNFORMATTED_VALUE" }).values;
const dstSheet = SpreadsheetApp.openById(dstSpreadsheetId).getSheetByName(dstRange);
const sheetId = dstSheet.getSheetId();
Sheets.Spreadsheets.batchUpdate({ requests: [{ repeatCell: { range: { sheetId }, fields: "userEnteredValue" } }] }, dstSpreadsheetId);
Sheets.Spreadsheets.Values.update({ values }, dstSpreadsheetId, dstRange, { valueInputOption: "USER_ENTERED" });
// Here, the number format is copied.
const srcSheet = SpreadsheetApp.openById(srcSpreadsheetId).getSheetByName(srcRange);
const range = srcSheet.getRange(1, 1, srcSheet.getMaxRows(), srcSheet.getMaxColumns());
const numberFormats = range.getNumberFormats().map(r => r.map(c => c || "#"));
const styles = range.getTextStyles();
dstSheet.getRange(1, 1, numberFormats.length, numberFormats[0].length).setNumberFormats(numberFormats).setTextStyles(styles);
}

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

How to find the correct rowId when using a limited range from Google Sheet?

I have the below code that checks through a sheet for the words "CASH". Once it's found, it will copy the row to a new sheet then delete the row.
Prior, my range for the values was set up as so:
var range = ss1.getRange(2, 1, lr, lc);
But now that my sheet has 6000+ rows, this is highly inefficient. I've changed it so that it only looks through a range that is lr-100 so that it doesn't have to dig so deep.
Since making that change, my old code to delete rows ss1.deleteRow(i+2) is no longer valid, because i references the row only within that particular range (i.e., if it's the 90th row out of that 100, i = 90, and that would end up deleting the 90th row in my sheet when it should have been, for example, 6500.
question How do I find the correct row# in this new way my script is setup?
var etfar = ["BOTZ"]
function doCash() {
for (var i = 0; i < etfar.length; i++) {
cashMoney(etfar[i])
}
};
function cashMoney(etf) {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var ss1 = ss.getSheetByName(etf);
var ss2 = ss.getSheetByName("Cash");
var lr = ss1.getLastRow();
var lc = ss1.getLastColumn();
var range = ss1.getRange(lr-100, 1, lr, lc);
var data = range.getValues();
for (var i = data.length - 1; i >= 0; i--)
{
var check = data[i][4] // ith row, 3rd column
if (check.includes("CASH")) {
var rowUsd = data[i];
// something has to happen here to find the exact row
ss2.appendRow(rowUsd);
//ss1.deleteRow(i+2); <-- this is old code. Prior, in my 'range' I used to start my rows from "2", but that was highly inefficient and I've changed it to "lr-100" so that it begins checking from the last 100 rows instead
}
};
};
From now that my sheet has 6000+ rows, this is highly inefficient., in your situation, in order to reduce the process cost of the script, how about using Sheets API? When Sheets API is used for your situation, all rows might be able to be processed. When Sheets API is used for your script, it becomes as follows.
Modified script:
This script uses Sheets API. So, please enable Sheets API at Advanced Google services. And, please set etfar.
function sample() {
var etfar = ["Sheet1", "Sheet2",,,]; // Please set your sheet names.
var ss = SpreadsheetApp.getActiveSpreadsheet();
var dstSheet = ss.getSheetByName("Cash");
var dstSheetId = dstSheet.getSheetId();
var lastRow = dstSheet.getLastRow();
var { copyRows, deleteRows } = etfar.reduce((o, e) => {
var srcSheet = ss.getSheetByName(e);
var srcSheetId = srcSheet.getSheetId();
srcSheet.getRange("E2:E" + srcSheet.getLastRow()).getValues().forEach(([v], i) => {
if (v == "CASH") {
o.copyRows.push({ copyPaste: { source: { sheetId: srcSheetId, startRowIndex: i + 1, endRowIndex: i + 2, startColumnIndex: 0 }, destination: { sheetId: dstSheetId, startRowIndex: lastRow, endRowIndex: lastRow + 1, startColumnIndex: 0 } } });
o.deleteRows.push({ deleteDimension: { range: { sheetId: srcSheetId, dimension: "ROWS", startIndex: i + 1, endIndex: i + 2 } } });
lastRow++;
}
});
return o;
}, { copyRows: [], deleteRows: [] });
var requests = [...copyRows, ...deleteRows.reverse()];
if (requests.length == 0) return;
Sheets.Spreadsheets.batchUpdate({ requests }, ss.getId());
}
When this script is run, the values searched by the column "E" are retrieved from the sheets of etfar, and those values are appended to the destination sheet "Cash". And also, the copied rows of each sheet are removed.
In this script, these processes can be run by one API call.
Note:
When I saw your script, from var check = data[i][4] // ith row, 3rd column, I thought that data[i][4] is the coumn "E". But you say 3rd column. It's the column "C". If you want to search the value of "CASH" from the column "C", please modify getRange("E2:E" + srcSheet.getLastRow()) to getRange("C2:C" + srcSheet.getLastRow()).
References:
Method: spreadsheets.batchUpdate
CopyPasteRequest
DeleteDimensionRequest
I think this is what you want
function cashMoney(etf) {
var ss = SpreadsheetApp.getActive();
var sh1 = ss.getSheetByName(etf);
var sh2 = ss.getSheetByName("Cash");
var lr = sh1.getLastRow();
var lc = sh1.getLastColumn();
var range = sh1.getRange(lr - 100, 1, 101, lc);
var data = range.getValues();
var d = 0;
for (var i = data.length - 1; i >= 0; i--) {
var check = data[i][4] // ith row, 3rd column
if (check.includes("CASH")) {
var rowUsd = data[i];
sh2.appendRow(rowUsd);
sh.deleteRow(lr - 100 + i - d++);The row is data start row + i
}
};
}
The d keeps track of the rows that have already been deleted from the spreadsheet which is necessary because they have not been deleted from the data set.

Group rows with appscript

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

Sheets API: Append Row with hidden columns

I have a "appendRow" function for insert rows in the Google sheet called "Data", and this sheet has a column called "Id" (**A** column) which is hidden by client request.
function appendRow() {
var spreadsheetId = "SPREADSHEET_ID";
var range = "Data!A2:C";
var resource = {
values: [
["1", "James", "jam10#gmail.com"]
]
};
Sheets.Spreadsheets.Values.append(resource, spreadsheetId, range, {
valueInputOption: "USER_ENTERED"
});
}
However, when I execute this function, the result is not the expected, as seen in the image.
I appreciate any information, idea or solution that can help me to resolve this incident. Regards.
function appendRow() {
const ss = SpreadsheetApp.openById("SS_ID");
const sh = ss.getSheetByName('Data');
sh.appendRow(["1", "James", "jam10#gmail.com"]);
}
//row must be 2d array
function appendRows(rows) {
var spreadsheetId = "SPREADSHEET_ID";
const ss = SpreadsheetApp.openById("SSID");
const sh = ss.getSheetByName('Data');
sh.getRange(sh.getLastRow() + 1, 1, rows.length, rows[0].length).setValues(rows);
}