Optimizing google app script for implementing auto-increment columns - javascript

There are two ways that i am able to add an auto increment column. By auto-increment, i mean that if column B has a value, column A will be incremented by a numeric value that increments based on the previous rows value.
The first way of doing this is simple, which is to paste a formula like the one below in my first column:
=IF(ISBLANK(B1),,IF(ISNUMBER(A1),A1,0)+1)
The second way i have done this is via a GA script. What i found however is performance using a GA script is much slower and error prone. For example if i pasted values quickly in the cells b1 to b10 in that order, it will at times reset the count and start at 1 again for some rows. This is because the values for the previous rows have not yet been calculated. I assume that this is because the GA scripts are probably run asynchronously and in parallel. My question is..is there a way to make sure each time a change happens, the execution of this script is queued and executed in order?
OR, is there a way i should write this script to optimize it?
function auto_increment_col() {
ID_COL = 1;
var spreadsheet = SpreadsheetApp.getActiveSpreadsheet();
//only increment column 1 for sheets in this list
var auto_inc_sheets = SpreadsheetApp.getActiveSpreadsheet().getRangeByName("auto_inc_sheets").getValues();
auto_inc_sheets = auto_inc_sheets.map(function(row) {return row[0];});
var is_auto_inc_sheet = auto_inc_sheets.indexOf(spreadsheet.getSheetName()) != -1;
if (!is_auto_inc_sheet) return;
var worksheet = spreadsheet.getActiveSheet();
var last_row = worksheet.getLastRow();
var last_inc_val = worksheet.getRange(last_row, ID_COL).getValue();
//if auto_inc column is blank and the column next to auto_inc column (col B) is not blank, then assume its a new row and increment col A
var is_new_row = last_inc_val == "" && worksheet.getRange(last_row, ID_COL+1).getValue() != "";
Logger.log("new_row:" + is_new_row + ", last_inc_val:" + last_inc_val );
if (is_new_row) {
var prev_inc_val = worksheet.getRange(last_row-1, ID_COL).getValue();
worksheet.getRange(last_row, ID_COL).setValue(prev_inc_val+1);
}
}

There is my vision of auto increment https://github.com/contributorpw/google-apps-script-snippets/tree/master/snippets/spreadsheet_autoincrement
The main function of this is
/**
*
* #param {GoogleAppsScript.Spreadsheet.Sheet} sheet
*/
function autoincrement_(sheet) {
var data = sheet.getDataRange().getValues();
if (data.length < 2) return;
var indexCol = data[0].indexOf('autoincrement');
if (indexCol < 0) return;
var increment = data.map(function(row) {
return row[indexCol];
});
var lastIncrement = Math.max.apply(
null,
increment.filter(function(e) {
return isNumeric(e);
})
);
lastIncrement = isNumeric(lastIncrement) ? lastIncrement : 0;
var newIncrement = data
.map(function(row) {
if (row[indexCol] !== '') return [row[indexCol]];
if (row.join('').length > 0) return [++lastIncrement];
return [''];
})
.slice(1);
sheet.getRange(2, indexCol + 1, newIncrement.length).setValues(newIncrement);
}
But you have to open the snippet for details because this doesn't work without locks.

Related

Rearranging information on spreadsheet using Google Apps Script arrays

Been dealing with this code for a couple of days now. What I want to achieve in the spreadsheet is, move some columns of a single row to another row based on some criteria, and leave the older rows clear of content.
To put it other way, I check at which row columns B, C and H (let's call this row N° '3') match exactly with values at columns B, C and N (let's call this row N° '9'). Then I need to move information from J9:N9 to J3:N3.
So this is the code I've come up with. Problem is, at the end no changes are being made to the spreadsheet nor the 'rows' array when I log it.
function reordenarFacturas(){
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheet = ss.getSheetByName('AP que esperan');
var lastRow = sheet.getLastRow(); //get the last Row with values
var filteredArray = [];
const rows = sheet.getDataRange().getValues(); // get data as an array
const nombresAP = sheet.getSheetValues(3,2,lastRow-2,2); //get columns 2 and 3 as an array
//Clean nombresAP array from special characters so that later filterLogic works correctly (not using filter anymore, but still wanted to clear names list).
for (h=0; h<lastRow-2; h++){ //rows
for(j=0; j<2;j++){ //columns
nombresAP[h][j] = nombresAP[h][j].toString().toUpperCase();
nombresAP[h][j] = nombresAP[h][j].toString().replace(/á/ig, "A");
nombresAP[h][j] = nombresAP[h][j].toString().replace(/é/ig, "E");
nombresAP[h][j] = nombresAP[h][j].toString().replace(/í/ig, "I");
nombresAP[h][j] = nombresAP[h][j].toString().replace(/ó/ig, "O");
nombresAP[h][j] = nombresAP[h][j].toString().replace(/ú/ig, "U");
nombresAP[h][j] = nombresAP[h][j].toString().replace(" ", " ");
}
}
sheet.getRange("B3:C" + lastRow).setValues(nombresAP);
//End of the cleaning section
//NOT IN USE CURRENTLY, SAVED JUST IN CASE
//Filterlogic works as a way to input several parameters to the filter method used below (now erased).
/*
var filterLogic = function(item){ //function to pass filter data later
if (item[1] === apellidoAP && item[2] === nombreAP && item[7] === periodoFacturado){
return true;
}
else{
return false;
}
}
*/
//Here I check whether certain columns have different values. If they do, then I retrieve some values on variables and push them to an array for later use.
for (var h = 2; h<lastRow; h++){ // h starts at 2 because data starts at row N° 3
if (rows[h][9] == '') continue; // If cells on this columns are empty, I don't need the row and can evaluate the next
else if (rows[h][7] != rows[h][13]){ // If this two different cells of the same row have different values, I then
//assign some data to variables, for later filtering other rows with said data
var apellidoAP = rows[h][1];
var nombreAP = rows[h][2];
var periodoInformado = rows[h][7];
var factura = rows[h][9];
var fechaFactura = rows[h][10];
var nroFactura = rows[h][11];
var montoFactura = rows[h][12];
var periodoFacturado = rows[h][13];
filteredArray.push([[apellidoAP], [nombreAP], [periodoFacturado], [factura], [fechaFactura], [nroFactura], [montoFactura], [periodoFacturado]]);
// Here I'm 'cleaning' some fields in the rows where I've found the differences (doesn't seem to be working)
rows[h][9] == '';
rows[h][10] == '';
rows[h][11] == '';
rows[h][12] == '';
rows[h][13] == '';
}
}
//And finally here, I check whether some filteredArray values match some values in the 'rows' array. If they do match, then I replace some more values in rows array with values from filteredArray. This doesn't seem to be working either.
for (p = 2; p<rows.length; p++){
for (r = 0; r<filteredArray.length; r++){
if (rows[p][1] == filteredArray [r][0] && rows[p][2] == filteredArray[r][1] && rows[p][7] == filteredArray[r][2]){
rows[p][9] == filteredArray[r][3];
rows[p][10] == filteredArray[r][4];
rows[p][11] == filteredArray[r][5];
rows[p][12] == filteredArray[r][6];
rows[p][13] == filteredArray[r][7];
}
}
}
//Finally I set rows back at the spreadsheet with all changes made, but no changes are shown in the spreadsheet once the script ends running.
sheet.getRange("A1:U" + lastRow).setValues(rows);
}
```

Google sheets script, Every function combined with && operator and setting range of values to check array against

Follow up to Sending duplicate data to array, then checking if said array is empty or not to decide what code to run next, if statement not working correctly.
I'm pretty much trying to copy the conditional formatting that I put to notify users of "bad" data, but with script to prevent sending bad data and giving unique error messages. With the answer from my last question, the checks and preventing sending no data or data with duplicates works, but I'm now stuck on preventing sending of data that does not match the format of a four digit number.
My conditional formatting was to set the background color to orange on anything that was not equal to or in between 1000 and 9999, but I'm having trouble getting the scripting to work--I'm trying to use negation of anything in between those two values as the "false" that will prompt an error message, and the "true" to let the rest of the script run that will send the data and notification emails out. However, this makes it say that there are bad values even if I do have the correct data in. Removing the negation lets anything go through, like it's not actually checking it. Any ideas?
The section I'm asking about is the last else if statement:
else if (!data.every(function(num) {return num >= 1000 && num <= 9999})) {
SpreadsheetApp.getUi().alert("You have incorrectly formatted tallies, tallies must be four digits.", SpreadsheetApp.getUi().ButtonSet.OK);
}
Total code is below.
var ss = SpreadsheetApp.getActiveSpreadsheet();
var ssSheet = ss.getActiveSheet();
//https://techyesplease.com/code/google-apps-script-find-duplicates-sheets/
function readData() {
var dataColumn = 3;
var firstRow = 6;
var lastRow = ssSheet.getLastRow();
var numRows = lastRow - firstRow + 1;
var columnRange = ssSheet.getRange(firstRow, dataColumn, numRows);
//var rangeArray = columnRange.getValues();
// Convert to one dimensional array
//rangeArray = [].concat.apply([], rangeArray);
var rangeArray = columnRange.getValues().flat().filter(String);
return rangeArray;
}
// Sort data and find duplicates
function findDuplicates(dataAll) {
var sortedData = dataAll.slice().sort();
var duplicates = [];
for (var i = 0; i < sortedData.length - 1; i++) {
if (sortedData[i + 1] == sortedData[i]) {
duplicates.push(sortedData[i]);
}
}
return duplicates;
}
//Use the same string for this variable as the name for the requestor for his or her column of data. E.g., John Doe
//****GLOBALS****
var targetSheet = 'All Tallies'; //replace with sheet/tab name
var targetSpreadsheetID = 'id' //replace with destination ID
var targetURL = 'url'
//var dataNotificationReceivingEmailAddresses =
//Set up to be able to easily change what emails the data notification goes to?
function sendDataAndTimestamp2() {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var ssSheet = ss.getActiveSheet();
var sourceRange = ssSheet.getRange('C6:C401');
//assign the range you want to copy, could make C6:C? No, would like dynamic.
var data = sourceRange.getValues();
var nameRange = ssSheet.getRange('C4:D4');
var nameValue = nameRange.getDisplayValue();
var tallyDateRange = ssSheet.getRange('C2');
var tallyDateValue = tallyDateRange.getDisplayValue();
var tallyDateText = 'Tallies to run on '+ tallyDateValue;
var tallyAmountRange = ssSheet.getRange(8,1);
var tallyAmount = tallyAmountRange.getDisplayValue();
var tallyAmountNumberOnly = data.filter(String).length;
//Used as tallyAmount includes text, for some cases need the number only
var thisDocumentUrl = ss.getUrl();
//Variables for the sending/source spreadsheet above
//Initial confirmation alert, need checks for blank or error tallies first. First condition needs to loop through data variable and check that if any values are numbers not between 1000 and 9999, throw up Ui alert error message. Second condition goes to result variable?
//Reference the earlier functions
var dataArray = readData();
var duplicates = findDuplicates(dataArray);
//Need to check data and have error message if duplicates.length >=1, if 0 allow, refuse if data length less than 1
if (duplicates.length !== 0) {
SpreadsheetApp.getUi().alert("Your tallies include duplicates, please remove them then try again.", SpreadsheetApp.getUi().ButtonSet.OK);
Logger.log(duplicates);
}
else if (dataArray.length ===0) {
SpreadsheetApp.getUi().alert("You have not input any tallies.", SpreadsheetApp.getUi().ButtonSet.OK);
}
else if (!data.every(function(num) {return num >= 1000 && num <= 9999})) {
SpreadsheetApp.getUi().alert("You have incorrectly formatted tallies, tallies must be four digits.", SpreadsheetApp.getUi().ButtonSet.OK);
}
/*https://www.youtube.com/watch?v=gaC290XzPX4&list=PLv9Pf9aNgemvD9NFa86_udt-NWh37efmD&index=10
Every method
var arr = [1,2,3,4];
var allTalliesGood = data.every(function(num){
return num < 9
});
//Use with num >= 1000, num <=9999, and then if true continue, if false stop and give error msg
*/
/*
dataFiltered = data.filter(filterlogic);
var filterlogic = function(tally){
if (tally >= 1000 && tally <= 9999){
return true;
} else {
return false
}
}
//if use false for good tallies, and rename dataFiltered to something like "dataBad", then check if dataBad has true matches in it...
//need to check for strings? what happens if letters/symbols in tally range?
//var dataBad = data.filter(function(tally){ return tally < 1000 || tally > 9999;});
// use OR (||) for getting tallies great than or less than, what about letters/symbols? Use NOT good range?
//https://www.youtube.com/watch?v=hPCIOohF0Fg&list=PLv9Pf9aNgemvD9NFa86_udt-NWh37efmD&index=8
//sort method link above
*/
else {
// rest of code
var result = SpreadsheetApp.getUi().alert("You're about to notify scheduler of the following number of tallies: " + tallyAmountNumberOnly, SpreadsheetApp.getUi().ButtonSet.OK_CANCEL);
if(result === SpreadsheetApp.getUi().Button.OK) {
//Code to send out emails and data
As far as I can tell in the function sendDataAndTimestamp2() you need to get rid of empty values from data. It can be done with filter(String) method:
var data = sourceRange.getValues().filter(String); // <-- here
. . .
else if (!data.every(function(num) {return num >= 1000 && num <= 9999}))
. . .
Or, I don't know, perhaps you meant dataArray instead of data:
else if (!dataArray.every(function(num) {return num >= 1000 && num <= 9999}))
Btw, the line can be shortened a bit:
else if (!data.every(x => (x >= 1000) && (x <= 9999)))

Google Script working as 2 separate scripts but not inside the same function

Basically I have a script that is in 4 blocks:
1. Copies within a range each row provided it meets a criteria
2. Removes all empty rows
3. Sets all numbers as percentage
4. Applies conditional cell formatting to one of the columns
The 4th part is the one that is causing me issues. The script runs without any error message AND block 4 works perfectly fine if it's in another script alone with the same variables defined but as soon as it is inside the same function as the others it simply doesn't run without any error message of any kind.
Tried changing the name of the variables to single use ones to ensure it wasn't because one of the "var" was modified above it, removing the "else if" to keep only an "if" in the loop, moving it around to other parts of the script but if the block 1 is in the script then block 4 won't apply (will apply if it is only with 2 & 3.
2 & 3 which follow the same structure work well with 1.
Does any one have any clue what's wrong with my script ? :)
Each block is commented with what it does
function copy() {
//Set variables & criterion to choose which rows to copy
var s = SpreadsheetApp.openByUrl('https://docs.google.com/spreadsheets/d/1bEiLWsbFszcsz0tlQudMBgTk5uviyv_wDx7fFa8txFM/edit');
var ssSource = s.getSheetByName('Variations');
var ssDest = s.getSheetByName('Email');
var lastRowSource = ssSource.getLastRow();
var lastRowDest = ssDest.getLastRow();
var lastColSource = ssSource.getLastColumn()
var criteria = 0;
var titles = ssSource.getRange(1,1,1, lastColSource).getValues()
//Copies the range
ssDest.getRange(1,1,1, lastColSource).setValues(titles)
for (var i = 2; i < lastRowSource; i++ ) {
var test = ssSource.getRange(i ,1);
Logger.log(test.getValue()+ ' <? ' + criteria);
if (ssSource.getRange(i ,6).getValue() > criteria) {
ssSource.getRange(i ,1,1,ssSource.getLastColumn()).copyTo(ssDest.getRange(i ,1,1,ssSource.getLastColumn()), {contentsOnly:true}); // copy/paste content only
}
}
//Removes empty rows
var data = ssDest.getDataRange().getValues();
var targetData = new Array();
for(n=0;n<data.length;++n){
if(data[n].join().replace(/,/g,'')!=''){ targetData.push(data[n])};
Logger.log(data[n].join().replace(/,/g,''))
}
ssDest.getDataRange().clear();
ssDest.getRange(1,1,targetData.length,targetData[0].length).setValues(targetData);
//Formats numbers as percentages
var rangePercent = ssDest.getRange(1,1,ssDest.getLastRow(),ssDest.getLastColumn());
var rowsPercent = rangePercent.getNumRows();
var colsPercent = rangePercent.getNumColumns();
for(var rowPercent = 1; rowPercent <= rowsPercent; rowPercent++) {
for(var colPercent = 1; colPercent <= colsPercent; colPercent++) {
var cellPercent = rangePercent.getCell(rowPercent, colPercent);
var valuePercent = cellPercent.getValue();
if(typeof(valuePercent) == 'number') {
cellPercent.setNumberFormat("##.#%");
}
}
}
//Adds conditional background colours
for (var z = 2; z < lastRowDest+1;z++) {
var avgCpc = 4;
var rangeColour = ssDest.getRange(z,avgCpc);
var dataColour = rangeColour.getValue()
if (dataColour < 0) {
ssDest.getRange(z,avgCpc).setBackground('#d9ead3')
}
else if (dataColour > 0) {
ssDest.getRange(z,avgCpc).setBackground('#f4cccc')
}
}
//Centers Values
}
The problem you're having is your code has performance issues because you're calling too many times methods such as getRange() and getValue() inside various loops, therefore Apps Script can't keep up with all those calls. Please check Best Practices.
Having said that, I modified your code in order to make it more efficient. Besides your copy function, I added two more functions to make the code more readable.
function copy
As before this function sets the variables, but now it calls two other functions, which are setPositiveCostValues and formatCells
function copy() {
//Set variables & criterion to choose which rows to copy
var ss = SpreadsheetApp.openByUrl('your-url');
var ssSource = ss.getSheetByName('Variations');
var ssDest = ss.getSheetByName('Email');
// set the title
var titles = ssSource.getRange(1,1,1, ssSource.getLastColumn()).getValues();
ssDest.getRange(1,1,1, ssSource.getLastColumn()).setValues(titles);
// get the positive values you want from the cost col
var positiveValues = setPositiveCostValues(ssSource, ssDest, ssSource.getLastRow());
// fomrat the cells you want as percentage and set the color
formatCells(ssDest, positiveValues);
}
function setPositiveCostValues
This will take the values where the cost is positive and it will get rip off of the cells with empty values and "n/a" values.
function setPositiveCostValues(ssSource,ssDest, lastRowSource){
var postiveCost = ssSource.getRange(2, 1, lastRowSource, 6).getValues();
// this loop will clean the empty elements and the ones that only have n/a
for (var i = postiveCost.length - 1; i >= 0; i--) {
if (postiveCost[i][0]) {
postiveCost.splice(i + 1, postiveCost.length - (i + 1));
postiveCost = postiveCost.filter(function(el){ return el != 'n/a'})
break;
}
}
return postiveCost;
}
function formatCells
This will format the cells in the cost col as a percentage and will set the right color in your avgCpc col.
function formatCells(ssDest, postiveCost){
var avgCpc = 4, cost = 6, row = 2, criteria = 0;
// iterate over the array and depending on the criteria format the cells
postiveCost.forEach(function(el){
if(el[cost - 1] > criteria){
var ssDestRange = ssDest.getRange(row, 1, 1, cost);
ssDestRange.setValues([el]);
ssDestRange.getCell(1, cost).setNumberFormat("##.#%");
// set the color depending on the avgCpc value condition
if(el[avgCpc - 1] < criteria) ssDest.getRange(row, avgCpc).setBackground('#d9ead3');
else ssDest.getRange(row, avgCpc).setBackground('#f4cccc');
row++;
}
});
}
Code
Your whole code now it will look like this:
function copy() {
//Set variables & criterion to choose which rows to copy
var ss = SpreadsheetApp.openByUrl('your-url');
var ssSource = ss.getSheetByName('Variations');
var ssDest = ss.getSheetByName('Email');
// set the title
var titles = ssSource.getRange(1,1,1, ssSource.getLastColumn()).getValues();
ssDest.getRange(1,1,1, ssSource.getLastColumn()).setValues(titles);
// get the positive values you want from the cost col
var positiveValues = setPositiveCostValues(ssSource, ssDest, ssSource.getLastRow());
// fomrat the cells you want as percentage and set the color
formatCells(ssDest, positiveValues);
}
function setPositiveCostValues(ssSource,ssDest, lastRowSource){
var postiveCost = ssSource.getRange(2, 1, lastRowSource, 6).getValues();
// this loop will clean the empty elements and the ones that only have n/a
for (var i = postiveCost.length - 1; i >= 0; i--) {
if (postiveCost[i][0]) {
postiveCost.splice(i + 1, postiveCost.length - (i + 1));
postiveCost = postiveCost.filter(function(el){ return el != 'n/a'})
break;
}
}
return postiveCost;
}
function formatCells(ssDest, postiveCost){
var avgCpc = 4, cost = 6, row = 2, criteria = 0;
// iterate over the array and depending on the criteria format the cells
postiveCost.forEach(function(el){
if(el[cost - 1] > criteria){
var ssDestRange = ssDest.getRange(row, 1, 1, cost);
ssDestRange.setValues([el]);
ssDestRange.getCell(1, cost).setNumberFormat("##.#%");
// set the color depending on the avgCpc value condition
if(el[avgCpc - 1] < criteria) ssDest.getRange(row, avgCpc).setBackground('#d9ead3');
else ssDest.getRange(row, avgCpc).setBackground('#f4cccc');
row++;
}
});
}

Optimizing Code - getValue() and looping

I'm a bit of newbie at coding, especially Javascript/Google-script language. I've created the code below, and it works, but now that I've got a working code I'd like to see how I can optimize it. It seems to me that all of the getValue() calls are a major performance hit, and I've never really been good at optimizing loops. Anyone know a better way to accomplish the same as this code?
What it does: Checks each spreadsheet in one of my folders to see if it needs to have the rest of the script run. If true, it opens that sheet and counts the number of rows that have data, using that to limit the amount of rows it checks in the loop. It then looks for any row marked for push and copies that range to another spreadsheet in my drive. It then continues to the next file in the folder and does the same.
Here's my code:
function myVupdate() {
try {
var folder = DriveApp.getFolderById("123abc"),
files = folder.getFiles();
while (files.hasNext()) {
var file = files.next(),
sss = SpreadsheetApp.open(file);
SpreadsheetApp.setActiveSpreadsheet(sss);
//Work orders update
var ss = sss.getSheetByName("Sheet2"),
refresh = ss.getRange("W3").getValue();
if (refresh == 0) {continue};
var avals = ss.getRange("D5:D").getValues(),
count = avals.filter(String).length,
rows = count + 5
var val = ss.getDataRange().getValues();
for (var row=5; row < rows; row++) {
var cell = ss.getDataRange().getCell(row, 23).getValue();
if (cell == 0) {
var cells = [["v" + "WO-" + val[row-1][3] + "_" + val[row-1][2],val[row-1][13],val[row-1][14],val[row-1][15],new Date()]];
var tss = SpreadsheetApp.openById("target_spreadsheet"),
ts = tss.getSheetByName("Sheet5");
ts.insertRowBefore(2);
var last_hmy = ts.getRange(3,1).getValue();
ts.getRange(2,1).setValue(last_hmy+1);
ts.getRange(2,2,cells.length,cells[0].length).setValues(cells);
ts.getRange(2,7).setValue(sss.getName());
ss.getRange(row,17).setValue(last_hmy+1);
ss.getRange(row,18,cells.length,cells[0].length).setValues(cells);
//Turnover update
var ss = sss.getSheetByName("Sheet1"),
avals = ss.getRange("D5:D").getValues(),
count = avals.filter(String).length,
rows = count + 5
var val = ss.getDataRange().getValues();
}
}
for (var row=5; row < rows; row++) {
var cell = ss.getDataRange().getCell(row, 24).getValue();
if (cell == 0) {
var cells = [["v" + val[row-1][3] + "_" + val[row-1][2],val[row-1][12],val[row-1][15],val[row-1][16],new Date()]];
var tss = SpreadsheetApp.openById("target_spreadsheet"),
ts = tss.getSheetByName("Sheet5");
ts.insertRowBefore(2);
var last_hmy = ts.getRange(3,1).getValue();
ts.getRange(2,1).setValue(last_hmy+1);
ts.getRange(2,2,cells.length,cells[0].length).setValues(cells);
ts.getRange(2,7).setValue(sss.getName());
ss.getRange(row,18).setValue(last_hmy+1);
ss.getRange(row,19,cells.length,cells[0].length).setValues(cells);
}
}
}
}
catch(e) {
// Browser.msgBox("An error occured. A log has been sent for review.");
var errorSheet = SpreadsheetApp.openById ("target_sheet").getSheetByName("Error Log"),
source = sss.getName();
lastRow = errorSheet.getLastRow();
var cell = errorSheet.getRange('A1');
cell.offset(lastRow, 0).setValue(e.message);
cell.offset(lastRow, 1).setValue(e.fileName);
cell.offset(lastRow, 2).setValue(e.lineNumber);
cell.offset(lastRow, 3).setValue(source);
cell.offset(lastRow, 4).setValue(new Date());
MailApp.sendEmail("my#email.com", "Error report - " + new Date(),
"\r\nSource: " + source + "\r\n"
+ "\r\nMessage: " + e.message
+ "\r\nFile: " + e.fileName
+ "\r\nLine: " + e.lineNumber
);
}
}
Hello and welcome to Stack Overflow,
first of all, you are correct. The more getValue(), or setValue() calls you do the worse the performance, read more on best practices here. Google recommends you batch these as much as possible. One thing that immediately springs to attention is the following:
var val = ss.getDataRange().getValues();
so now you have all the values on the sheet in a 2D array. That means that in the following bit
var ss = sss.getSheetByName("Sheet2"),
refresh = ss.getRange("W3").getValue();
if (refresh == 0) {continue};
var avals = ss.getRange("D5:D").getValues(),
count = avals.filter(String).length,
rows = count + 5
var val = ss.getDataRange().getValues();
for (var row=5; row < rows; row++) {
var cell = ss.getDataRange().getCell(row, 23).getValue();
every single getValue() or getValues() is no longer necessary. Instead, you know that refresh = val[2][22] because you need the 3rd row and 23rd column, as you already have the entire range that has data from that sheet.
Same with avals as all values in range D5:D are in vals[n][3], where n starts from 4. Remember, the array index starts from 0 (so first row and first column is vals[0][0].
So anywhere you are trying to use getValues() from the ss spreadsheet, you already have that data. What you can also do, is manipulate the array you have, so you always change the values only in that array. Once you are done with it, you use ss.getDataRange().setValues(vals) to push the entire array back to the same range (you can just store the range in a variable like datRange = ss.getDataRange() and then do datRange.setValues(vals).
You will just need to work with a separate data array for any other sheet. I did not go into detail for the rest of the code as the same ideas go throughout. Since you already grab everything with getValues() there is no longer any reason to use getValue() for any cell within that range.

Optimize by getting data as single array versus separate columns

I have a script below which calls separately for column G and I of a spreadsheet and returns the last non-zero value in the specific column.
I am wondering if it is faster to call for an array of data from columns G, H and I (is that called an array?) and retrieve the last non-zero value in column G and I? How can I do that?
// get the latest value for Column G
var lastRow = spreadsheet.getSheetByName("Form responses 1").getLastRow();
var columnGvalues = spreadsheet.getSheetByName("Form responses 1").getRange("G" + "1:" + "G" + lastRow).getValues();
for (; columnGvalues[lastRow - 1] == "" && lastRow > 0; lastRow--) {}
var columnGLast = columnGvalues[lastRow - 1];
// get the latest value for Column I
var lastRow = spreadsheet.getSheetByName("Form responses 1").getLastRow();
var columnIvalues = spreadsheet.getSheetByName("Form responses 1").getRange("I" + "1:" + "I" + lastRow).getValues();
for (; columnIvalues[lastRow - 1] == "" && lastRow > 0; lastRow--) {}
var columnILast = columnIvalues[lastRow - 1];
I'm not sure which is faster, it will strongly depend on how much data you have in those columns (and in H too)
There are 3 possibilities:
1 - You have very few data: using a single array from G to I will be faster (but then, it wouldn't matter at all, everything would be fast)
2 - You have moderate data and one of the columns has a lot of empty cells related to the other. Your approach is a good one.
3 - You have huge data. Then each getValues() will take long, it would be better to go cell by cell (provided you do not have one of the two columns with a great number of empty cells)
If you have really huge data, it will be faster to get the last line and do range offsets upwards.
Repeated getRanges and offsets are usually slower than a getValues(), but if you have tons of data, getValues will be slower. (Because get values would always take the entire column, and going cell by cell would take only the necessary data).
But what would be considered huge data and small data?? Only speed tests could tell...
So, for huge data:
function mainFunction()
{
var spreadsheet = SpreadsheetApp.getActive();
var sheet = spreadsheet.getSheetByName("Form responses 1");
var lastRow = sheet.getLastRow();
var columnGLast = getLast(sheet, "G", lastRow);
var columnILast = getLast(sheet, "I", lastRow);
}
function getLast(sheet, column, row)
{
var currCell = sheet.getRange(column + row);
while (currCell.getValue() === "" && row > 1)
{
row--;
currCell = currCell.offset(-1,0);
}
return currCell.getValue();
}
Now, if your columns do not have that huge data, you can pick just one array:
function mainFunction()
{
var spreadsheet = SpreadsheetApp.getActive();
var sheet = spreadsheet.getSheetByName("Form responses 1");
var lastRow = sheet.getLastRow();
var myRange = sheet.getRange("G1:I" + lastRow).getValues();
var columnGLast = getLast(myRange, 0, lastRow - 1);
var columnILast = getLast(myRange, 2, lastRow - 1);
}
function getLast(array, columnIndex, lastRow)
{
var val = array[lastRow][columnIndex];
while (val === "" && lastRow > 0)
{
lastRow--;
val = array[lastRow][columnIndex];
}
return val;
}
So for this you get the values of each column(G and I) separately to two different array using the getRange(row, column, numRows) by adding the last row value to numRows.
Then you can easily find the last non zero value from those arrays.
Hope this helps!

Categories