Google scripts code running slow when batching functions - google-apps-script

I use a cell to help trigger functions in a spreadsheet I made
ie: when cell A13 = "loading old orders" do this
It should run for around 4 mins then end
then it repeats this until the code finds "2015-11-09T22:40:56" in col C
I am finding that some of these calls are taking a lot longer now that the sheet is growing in size.
I am after some ideas to make the code more efficient
Thanks in advance
function getOldOrders(){
var ss = SpreadsheetApp.getActiveSpreadsheet();
var status = ss.getSheetByName('Main');
var sheet = ss.getSheetByName('Summary');
var StatusCellToWatch = status.getRange("A13");
var offset = 0;
if (StatusCellToWatch.getValue() === "Loading Old Orders") {
try {
var beforedate = sheet.getRange(sheet.getLastRow() - 1,21).getValue();
}
catch(e) {
var beforedate = "2999-03-22T19:28:02";
}
//StatusCellToWatch.setValue(beforedate);
getOrders('&before='+beforedate,offset);
StatusCellToWatch.setValue('Loading Old Orders');
//Set sheet status so that the dataset just updates and grabs new orders
var data = sheet.getRange('C30000:C').getValues();
var search = "2015-11-09T22:40:56";
for (var i=0; i < data.length; i++) {
if (data[i][0] == search) {
StatusCellToWatch.setValue('');
FormatSummary();
}
}

Related

Google Apps Script: Prevent duplicate copies based on two columns

I am working with some colleagues who want the following to happen within a Google Sheet:
A Google Form contains a question that asks which Counselor a student is assigned to (among other questions)
Forms are submitted throughout the year by students
When a form is submitted, the form data goes into a Google Sheet in a Responses sheet
The Counselors would like a copy of each row to appear in another sheet within the main Sheet, based on the name of the Counselor
In their own sheets, each Counselor needs to be able to manipulate the data (sorting, highlighting rows, adding notes to the row/submission) ←hence a copy is needed instead of a query
I have the following script that copies the rows in the correct Counselor sheet, and does not copy a row into a Counselor sheet if it already appears. However, if a Counselor modifies anything in the row, the script will make a duplicate row (with the original data) the next time it is run, perhaps because it sees the modified row as not an exact match.
Is there a way to modify my script so it can check against a unique part of a row in the Responses sheet (the columns at indexes 0 and 1 together in the same row create a unique entry) in any part of a Counselor sheet before it creates a copy? In other words, it would not create a duplicate row if the Counselor modifies anything except for columns 0 and 1.
function copyData() {
var formResponses = SpreadsheetApp.getActive().getSheetByName("Form Responses 1");
var formValues = formResponses.getDataRange().getValues();
formValues.shift(); // remove the header row
formValues.forEach(function(row) {
var sheetName = row[4]; // the value of "My College Counselor is" column
var sheet = SpreadsheetApp.getActive().getSheetByName(sheetName);
var range = sheet.getDataRange();
var data = range.getValues();
var duplicate = false;
for (var i = 0; i < data.length; i++) {
var currentRow = data[i];
if (currentRow.join() == row.join()) {
duplicate = true;
break;
}
}
if (!duplicate) {
sheet.appendRow(row);
}
});
}
I'm stuck at this point and am not sure how to proceed.
NOTE: I have code to add a button to the menu list for the Counselors to run this script as needed since the forms can be submitted at any time. Using "onFormSubmit" does not work because there is a potential for multiple students to submit the form at the same time, which I've seen can cause a row or two to not be copied over.
If I understand your question correctly, you want to find a way to avoid duplicated rows, even if you edit them.
In order to do that, you have to define a value for each row that won't change and that is unique. My suggestion would be the following :
Installable trigger with the function custom_onFormSubmit
In the function get Uid (unique ID), and add it to each row submitted
Edit your code in order to search duplicate only with this Uid
First, add this function your Google Form Apps Script:
//add unique ID at a defined column each time a google form is submitted
function custom_onFormSubmit(e){
var uuid = e.triggerUid;
//alternatily you can use:
//var uuid = Utilities.getUuid();
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheet = ss.getSheetByName([SHEETNAME]);
var range = sheet.getDataRange();
var row = range.getLastRow();
sheet.getRange(row, 10).setValue(uuid); //column 10 is for example, adapt to your need
}
------ EDIT: alternative function without trigger onFormSubmit, add this function before
function check_insert_uuid(){
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheet = ss.getSheetByName([SHEETNAME]);
var range = sheet.getDataRange();
var values = range.getValues();
for (var x = 0; x < values.length; x++) {
if (values[x][10] == "") {
let uuid = Utilities.getUuid();
range.offset(x, 10, 1, 1).setValue(uuid);
}
}
SpreadsheetApp.flush(); //force new data to sync before copyData
copyData(); //call copy function
}
------ END EDIT -------
Then you just have to edit your function copyData
FROM:
for (var i = 0; i < data.length; i++) {
var currentRow = data[i];
if (currentRow.join() == row.join()) {
duplicate = true;
break;
}
}
TO:
for (var i = 0; i < data.length; i++) {
var currentRow = data[i];
if (currentRow[10] == row[10]) { //same example of column index 10
duplicate = true;
break;
}
}
References:
Installable Triggers
Google Form Events
Apps Script getuuid (Unique ID are not 100% unique in time and space, but will certainly answer your project)
Based on the help from #waxim-corp, here is the final script that accomplishes my goal:
function onOpen(e) {
let ui = SpreadsheetApp.getUi();
ui.createMenu("🤖 Copy Data 🤖")
.addItem("Let's Do This!", 'checkForID')
.addToUi();
};
function checkForID(){
var ss = SpreadsheetApp.getActive().getSheetByName("Form Responses 1");
var range = ss.getDataRange();
var values = range.getValues();
for (var x = 0; x < values.length; x++) {
if (values[x][0] == "") {
let uuid = Utilities.getUuid();
range.offset(x, 0, 1, 1).setValue(uuid);
}
}
SpreadsheetApp.flush(); //force new data to sync before copyData
copyData(); //call copy function
}
function copyData(){
var formResponses = SpreadsheetApp.getActive().getSheetByName("Form Responses 1");
var formValues = formResponses.getDataRange().getValues();
formValues.shift(); // remove the header row
formValues.forEach(function(row) {
var sheetName = row[5]; // the value of "My College Counselor is" column
var sheet = SpreadsheetApp.getActive().getSheetByName(sheetName);
var rangeC = sheet.getDataRange();
var data = rangeC.getValues();
var duplicate = false;
for (var i = 0; i < data.length; i++) {
var currentRow = data[i];
if (currentRow[0] == row[0]) {
duplicate = true;
break;
}
}
if (!duplicate) {
sheet.appendRow(row);
}
});
}
I'm sure it could be more efficient, but it works well.

Loop the recored macro in google sheet

Google sheet linkSheet 1 imageI have created a crypto analysis sheet in Google sheets, where it takes the data from Investing.com.
In a sheet1 I have all the info for a single coin, now I don't want to check each one at a time.
So in sheet2, i placed the complete Coins list. I recorded a macro with copy pasting the required data from sheet1 to sheet2. Now I want to do the same for all remaining 100 coins. How to loop this. Kindly answer. I don't have knowledge in Codes but trying my best.
I tried with this below code. But while running macro, this below function not run.
function loop() {
var spreadsheet = SpreadsheetApp.getActive();
var sheet = ss.getSheetByName('Analysis_USD');
var data = ss.getSheetByName('Screener').getRange('B3:B102').getValues();
var output = [];
for (var i = 0; i < data.length; i++) {
sheet.getRange('A2').setValue(data[i][0]);
SpreadsheetApp.flush();
Utilities.sleep(10000);
if (sheet.getRange('B102').getValue() == 'Yes'){
output.push([data[i][0]]);
}
}
sheet.getRange(2,12,output.length, 1).setValues(output);
}
From the Coin list(sheet2) i am pasting one coin to the A2 of sheet 1 and it calculates and the result has to be paste in the sheet 2, for 1st coin i have recorded the macro
Please try to following code modification and check if it does work, as it need to Transpose your column value into row value to be able to write to Screener sheet, it work when I test it for single coin execution, the looping I did not test for it but it work in the same logic:
function loop() {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheet = ss.getSheetByName('Analysis_USD');
var data = ss.getSheetByName('Screener').getRange('B3:B102').getValues();
for (var i = 0; i < data.length; i++) {
sheet.getRange('A2').setValue(data[i][0]);
SpreadsheetApp.flush();
Utilities.sleep(10000);
if (sheet.getRange('B102').getValue() == 'Yes'){
var result = sheet.getRange(10,2,13,1).getValues();
}
//Transpose Column to row value
var newArray = result[0].map(function(col, i) {
return result.map(function(row) {
return row[i]
})
})
ss.getSheetByName('Screener').getRange(3 + i,4, 1,result.length).setValues(newArray);
}
}
Let's try with single row update on the Bitcoin first, because I can perform the entire row value updating using Column Value from Analysis, I am doing the testing based same data from your worksheet :
function loop() {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheet = ss.getSheetByName('Analysis_USD');
var data = ss.getSheetByName('Screener').getRange('B3:B102').getValues();
var result = sheet.getRange(10,2,13,1).getValues();
var newArray = result[0].map(function(col, i) {
return result.map(function(row) {
return row[i]
})
})
ss.getSheetByName('Screener').getRange(3 ,4, 1,result.length).setValues(newArray);
}

Optimizing .gs search & replace script?

Hoping someone could just point out my probably obvious mistakes.
Code searches a Data page & copies matched info to a Results page. This is fine & works quickly.
What happens is when I add an overwrite section to the script. It overwrites matched entries as "Done" on the data page, so if the Data page gets added to, the previous entries won't match again.
I have 2 variants I've tried that work but are quite slow...as in you can sit & watch each match change to "Done" once every few seconds.
Appreciate any insight.
Here's the code:
function cellMatch() {
// Sheet Import
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheet1 = ss.getSheetByName("Raw_Data"); // Data
var sheet2 = ss.getSheetByName("Pallet_Data");// Result
var sheet3 = ss.getSheetByName("Close");// Search
// Data Import
var lr = sheet1.getLastRow();
var data = sheet1.getRange(2,1,lr-1,2).getValues();
var lc = sheet2.getLastColumn()+1;
var key = sheet3.getRange("A2").getValue();
var matched = [["Pallet "+lc+" ("+key+")"]];
//Start
for (var i=0; i<lr-1; i++) {
if (data[i][1] == key) {
var temp = [];
temp.push(data[i][0]);
matched.push(temp);
// Slow Overwrite-----------------------------------
/*
if(i>0){
var temp1 = sheet1.getRange(i,2).getValue();
var temp2 = sheet1.getRange(i+1,2).getValue();
var temp3 = sheet1.getRange(i+2,2).getValue();
// Testing1
if(temp1 == key ){
sheet1.getRange(i,2).setValue("Done");
}
if (i== lr-2){
if (temp2 == key){
sheet1.getRange(i+1,2).setValue("Done");
}
if (temp3 == key){
sheet1.getRange(i+2,2).setValue("Done");
}
}
}
*/
}
// Shorter code but even slower overwrite----------------
/*
for(var i=1; i<=lr; i++){
var temp1 = sheet1.getRange(i,2).getValue();
if(temp1 == key){
sheet1.getRange(i,2).setValue("Done");
}
}
*/
// Location Update ---------------------------------
var A7 = sheet3.getRange("A7");
A7.setValue(matched);
// Data Write -----------------------------------------
var result = sheet2.getRange(1,lc,matched.length);
result.setValues(matched);
}
Link to the Test Sheet
Solution:
The script runs slow because there are API calls in for loops. Best practice is to manipulate only the array inside the loop and do the API calls before and after the loop.
In your code it would look like this:
function cellMatch() {
// Sheet Import
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheet1 = ss.getSheetByName("Raw_Data"); // Data
var sheet2 = ss.getSheetByName("Pallet_Data");// Result
var sheet3 = ss.getSheetByName("Close");// Search
// Data Import
var lr = sheet1.getLastRow();
var datarange = sheet1.getRange(2,1,lr-1,2);
var data = datarange.getValues();
var lc = sheet2.getLastColumn()+1;
var key = sheet3.getRange("A2").getValue();
var matched = [["Pallet "+lc+" ("+key+")"]];
//Start
for (var i=0; i<lr-1; i++) {
if (data[i][1] == key) {
var temp = [];
temp.push(data[i][0]);
matched.push(temp);
data[i][1] = "Done";
}
// Location Update ---------------------------------
var A7 = sheet3.getRange("A7");
A7.setValue(matched);
// Data Write -----------------------------------------
datarange.setValues(data);
var result = sheet2.getRange(1,lc,matched.length);
result.setValues(matched);
}
Note that the for loop does not have any getValue()/setValue() methods.
This has the same effect on your sample sheet, only way faster.

Google Script Protection Issues with disjoint Ranges

I have an Array of Ranges that I am trying to loop through to protect, but it is not working anymore. I do not know why. It was working yesterday just fine, but the protections are not happening on the sheet. I have tried removing all editors, but we are still able to edit. Basically, I have a sheet of Named ranges; I create an array based on those ranges; then get a rangelist from that array; I then loop through the rangelist to protect each range.
function protectTU(){
var ss = SpreadsheetApp.getActiveSpreadsheet();
var s1 = ss.getSheetByName("Named Ranges");
var s2 = ss.getSheetByName("Copy of TRUCK UPDATES");
var lr1 = s1.getRange("A3:A").getValues(); //Find last rowv
var lr2 = lr1.filter(String).length; //Find last row
var lr3 = parseInt(lr2) - 1;
var data1 = s1.getRange("A3:E" + lr2).getValues();
var dis = 2;
var i = 1;
var v1 = [];
for (var j = i; j < lr3 - 1; j++)
{
if(data1[j][4] == dis)
{
v1.push(data1[j][1]);
}
if(dis == 1 && j == 43)
{break;}
if(dis == 2 && j == 81)
{break;}
if(dis == 3 && j == 135)
{break;}
}
var ranges = s2.getRangeList(v1);
**for (i = 0; i < ranges.length; i++)
{
var protection = ranges[i].protect().setDescription('Please see the office for permission');
}**
}
Try this:
I would like to see an image of your spreadsheet and some data to confirm so this is pretty much a guess.
function protectTU(){
var ss = SpreadsheetApp.getActiveSpreadsheet();
var s1 = ss.getSheetByName("Named Ranges");
var s2 = ss.getSheetByName("Copy of TRUCK UPDATES");
var lr3 = s1.getRange(3,1,s1.getLastRow()-2,1).getValues();//better not to use A1Notation for setting up range here
var data1 = s1.getRange(3,1,s1.getLastRow()-2,5).getValues();//and here
var dis = 2;
var i = 1;
var v1 = [];
for (var j=0;j<data1.length;j++) {
if(data1[j][4] == dis) {
v1.push(data1[j][1]);//so hopefully column2 has A1Notation
}
if(dis==1 && j==43){break;}
if(dis==2 && j==81){break;}
if(dis==3 && j==135){break;}
}
var ranges = s2.getRangeList(v1).getRanges();
//I don't use protection much so I don't know about this
for (i = 0; i < ranges.length; i++){
var protection = ranges[i].protect().setDescription('Please see the office for permission');
}
}
Here is a link to a basic version of the spreadsheet. The answer above helped a lot by the way, but it is still too slow. The answer was what I needed. Now I have to fix that. So my people are divided by truck numbers. However, there is one person that has random numbers. I cannot lock a huge chunk at time because another editor needs to edit their trucks too.
So I need to Lock the rows for trucks 301/302/303/305/310.. while not locking the numbers in between them which is why I have to loop it. I need to be able to lock out one person at a time. The divisions are 1:301-350, 2:351-394, 3:395-335..., 4:Random numbers.
https://docs.google.com/spreadsheets/d/1HJROXH1fhkSMtdgQQqUUrKxi0hj7s42I9rjCoz-Vswc/edit?usp=sharing
Actually, it was s2.getRangeList(v1).GETRANGES() part.
So, everyone views the same spreadsheet, but if I need for x person to see me urgently I want to be able to freeze what they are doing until they see me. Additionally everyone can keep working on their portion of the spreadsheet.

I need to split a Google Sheet into multiple tabs (sheets) based on column value

I have searched many possible answers but cannot seem to find one that works. I have a Google Sheet with about 1600 rows that I need to split into about 70 different tabs (with about 20-30 rows in each one) based on the value in the column titled “room”. I have been sorting and then cutting and pasting but for 70+ tabs this is very tedious.
I can use the Query function but I still need to create a new tab, paste the function and update the parameter for that particular tab.
This script seemed pretty close:
ss = SpreadsheetApp.getActiveSpreadsheet();
itemName = 0;
itemDescription = 1;
image = 2;
purchasedBy = 3;
cost = 4;
room = 5;
isSharing = 6;
masterSheetName = "Master";
function onOpen() {
var ui = SpreadsheetApp.getUi();
ui.createMenu('Update Purchases')
.addItem('Add All Rows To Sheets', 'addAllRowsToSheets')
.addItem('Add Current Row To Sheet', 'addRowToNewSheet')
.addToUi();
}
function addRowToNewSheet() {
var s = ss.getActiveSheet();
var cell = s.getActiveCell();
var rowId = cell.getRow();
var range = s.getRange(rowId, 1, 1, s.getLastColumn());
var values = range.getValues()[0];
var roomName = values[room];
appendDataToSheet(s, rowId, values, roomName);
}
function addAllRowsToSheets(){
var s = ss.getActiveSheet();
var dataValues = s.getRange(2, 1, s.getLastRow()-1, s.getLastColumn()).getValues();
for(var i = 0; i < dataValues.length; i++){
var values = dataValues[i];
var rowId = 2 + i;
var roomName = values[room];
try{
appendDataToSheet(s, rowId, values, roomName);
}catch(err){};
}
}
function appendDataToSheet(s, rowId, data, roomName){
if(s.getName() != masterSheetName){
throw new Error("Can only add rows from 'Master' sheet - make sure sheet name is 'Master'");
}
var sheetNames = [sheet.getName() for each(sheet in ss.getSheets())];
var roomSheet;
if(sheetNames.indexOf(roomName) > -1){
roomSheet = ss.getSheetByName(roomName);
var rowIdValues = roomSheet.getRange(2, 1, roomSheet.getLastRow()-1, 1).getValues();
for(var i = 0; i < rowIdValues.length; i++){
if(rowIdValues[i] == rowId){
throw new Error( data[itemName] + " from row " + rowId + " already exists in sheet " + roomName + ".");
return;
}
}
}else{
roomSheet = ss.insertSheet(roomName);
var numCols = s.getLastColumn();
roomSheet.getRange(1, 1).setValue("Row Id");
s.getRange(1, 1, 1, numCols).copyValuesToRange(roomSheet, 2, numCols+1, 1, 1);
}
var rowIdArray = [rowId];
var updatedArray = rowIdArray.concat(data);
roomSheet.appendRow(updatedArray);
}
But I always get an unexpected token error on line 51 or 52:
var sheetNames = [sheet.getName() for each(sheet in ss.getSheets())];
(And obviously the column names, etc. are not necessarily correct for my data, I tried changing them to match what I needed. Not sure if that was part of the issue.)
Here is a sample of my data: https://docs.google.com/spreadsheets/d/1kpD88_wEA5YFh5DMMkubsTnFHeNxRQL-njd9Mv-C_lc/edit?usp=sharing
This should return two separate tabs/sheets based on room .
I am obviously not a programmer and do not know Visual Basic or Java or anything. I just know how to google and copy things....amazingly I often get it to work.
Let me know what else you need if you can help.
Try the below code:
'splitSheetIntoTabs' will split your master sheet in to separate sheets of 30 rows each. It will copy only the content not the background colors etc.
'deleteTabsOtherThanMaster' will revert the change done by 'splitSheetIntoTabs'. This function will help to revert the changes done by splitSheetIntoTabs.
function splitSheetIntoTabs() {
var sheet = SpreadsheetApp.getActiveSheet();
var rows = SpreadsheetApp.getActiveSheet().getDataRange().getValues();
var header = rows[0];
var contents = rows.slice(1);
var totalRowsPerSheet = 30; // This value will change no of rows per sheet
//below we are chunking the toltal row we have into 30 rows each
var contentRowsPerSheet = contents.map( function(e,i){
return i%totalRowsPerSheet===0 ? contents.slice(i,i+totalRowsPerSheet) : null;
}).filter(function(e){ return e; });
contentRowsPerSheet.forEach(function(e){
//crate new sheet here
var currSheet = SpreadsheetApp.getActiveSpreadsheet().insertSheet();
//append the header
currSheet.appendRow(header);
//populate the rows
e.forEach(function(val){
currSheet.appendRow(val);
});
});
}
// use this function revert the sheets create by splitSheetIntoTabs()
function deleteTabsOtherThanMaster() {
var sheetNotToDelete ='Master';
var ss = SpreadsheetApp.getActive();
ss.getSheets().forEach(function(sheet){
if(sheet.getSheetName()!== sheetNotToDelete)
{
ss.deleteSheet(sheet);
}
});
}
I was using Kessy's nice script, but started having trouble when the data became very large, where the script timed out. I started looking for ways to reduce the amount of times the script read/wrote to the spreadsheet (rather than read/write one row at a time) and found this post https://stackoverflow.com/a/42633934
Using this principle and changing the loop in the script to have a loop within the loop helped reduce these calls. This means you can also avoid the second call to append rows (the "else"). My script is a little different to the examples, but basically ends something like:
`for (var i = 1; i < theEmails.length; i++) {
//Ignore blank Emails and sheets created
if (theEmails[i][0] !== "" && !completedSheets.includes(theEmails[i][0])) {
//Set the Sheet name = email address. Index the sheets so they appear last.
var currentSheet = theWorkbook.insertSheet(theEmails[i][0],4+i);
//append the header
//To avoid pasting formulas, we have to paste contents
headerFormat.copyTo(currentSheet.getRange(1,1),{contentsOnly:true});
//Now here find all the rows containing the same email address and append them
var theNewRows =[];
var b=0;
for(var j = 1; j < rows.length; j++)
{
if(rows[j][0] == theEmails[i][0]) {
theNewRows[b]=[];//Initial new array
theNewRows[b].push(rows[j][0],rows[j][1],rows[j][2],rows[j][3],rows[j][4],rows[j][5],rows[j][6],rows[j][7]);
b++;
}
}var outrng = currentSheet.getRange(2,1,theNewRows.length,8); //Make the output range the same size as the output array
outrng.setValues(theNewRows);
I found a table of ~1000 rows timed out, but with the new script took 6.5 secs. It might not be very neat, as I only dabble in script, but perhaps it helps.
I have done this script that successfully gets each room and creates a new sheet with the corresponding room name and adding all the rows with the same room.
function myFunction() {
var sheet = SpreadsheetApp.getActiveSheet();
// This var will contain all the values from column C -> Room
var columnRoom = sheet.getRange("C:C").getValues();
// This var will contain all the rows
var rows = SpreadsheetApp.getActiveSheet().getDataRange().getValues();
//Set the first row as the header
var header = rows[0];
//Store the rooms already created
var completedRooms = []
//The last created room
var last = columnRoom[1][0]
for (var i = 1; i < columnRoom.length; i++) {
//Check if the room is already done, if not go in and create the sheet
if(!completedRooms.includes(columnRoom[i][0])) {
//Set the Sheet name = room (except if there is no name, then = No Room)
if (columnRoom[i][0] === "") {
var currentSheet = SpreadsheetApp.getActiveSpreadsheet().insertSheet("No Room");
} else {
var currentSheet = SpreadsheetApp.getActiveSpreadsheet().insertSheet(columnRoom[i][0]);
}
//append the header
currentSheet.appendRow(header);
currentSheet.appendRow(rows[i]);
completedRooms.push(columnRoom[i][0])
last = columnRoom[i][0]
} else if (last == columnRoom[i][0]) {
// If the room's sheet is created append the row to the sheet
var currentSheet = SpreadsheetApp.getActiveSpreadsheet()
currentSheet.appendRow(rows[i]);
}
}
}
Please test it and don't hesitate to comment for improvements.