Need to optimize Apps Script function [duplicate] - google-apps-script

I've just written my first google apps scripts, ported from VBA, which formats a column of customer order information (thanks to you all of your direction).
Description:
The code identifies state codes by their - prefix, then combines the following first name with a last name (if it exists). It then writes "Order complete" where the last name would have been. Finally, it inserts a necessary blank cell if there is no gap between the orders (see image below).
Problem:
The issue is processing time. It cannot handle longer columns of data. I am warned that
Method Range.getValue is heavily used by the script.
Existing Optimizations:
Per the responses to this question, I've tried to keep as many variables outside the loop as possible, and also improved my if statements. #MuhammadGelbana suggests calling the Range.getValue method just once and moving around with its value...but I don't understand how this would/could work.
Code:
function format() {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var s = ss.getActiveSheet();
var lastRow = s.getRange("A:A").getLastRow();
var row, range1, cellValue, dash, offset1, offset2, offset3;
//loop through all cells in column A
for (row = 0; row < lastRow; row++) {
range1 = s.getRange(row + 1, 1);
//if cell substring is number, skip it
//because substring cannot process numbers
cellValue = range1.getValue();
if (typeof cellValue === 'number') {continue;};
dash = cellValue.substring(0, 1);
offset1 = range1.offset(1, 0).getValue();
offset2 = range1.offset(2, 0).getValue();
offset3 = range1.offset(3, 0).getValue();
//if -, then merge offset cells 1 and 2
//and enter "Order complete" in offset cell 2.
if (dash === "-") {
range1.offset(1, 0).setValue(offset1 + " " + offset2);
//Translate
range1.offset(2, 0).setValue("Order complete");
};
//The real slow part...
//if - and offset 3 is not blank, then INSERT CELL
if (dash === "-" && offset3) {
//select from three rows down to last
//move selection one more row down (down 4 rows total)
s.getRange(row + 1, 1, lastRow).offset(3, 0).moveTo(range1.offset(4, 0));
};
};
}
Formatting Update:
For guidance on formatting the output with font or background colors, check this follow-up question here. Hopefully you can benefit from the advice these pros gave me :)

Issue:
Usage of .getValue() and .setValue() in a loop resulting in increased processing time.
Documentation excerpts:
Minimize calls to services:
Anything you can accomplish within Google Apps Script itself will be much faster than making calls that need to fetch data from Google's servers or an external server, such as requests to Spreadsheets, Docs, Sites, Translate, UrlFetch, and so on.
Look ahead caching:
Google Apps Script already has some built-in optimization, such as using look-ahead caching to retrieve what a script is likely to get and write caching to save what is likely to be set.
Minimize "number" of read/writes:
You can write scripts to take maximum advantage of the built-in caching, by minimizing the number of reads and writes.
Avoid alternating read/write:
Alternating read and write commands is slow
Use arrays:
To speed up a script, read all data into an array with one command, perform any operations on the data in the array, and write the data out with one command.
Slow script example:
/**
* Really Slow script example
* Get values from A1:D2
* Set values to A3:D4
*/
function slowScriptLikeVBA(){
const ss = SpreadsheetApp.getActive();
const sh = ss.getActiveSheet();
//get A1:D2 and set it 2 rows down
for(var row = 1; row <= 2; row++){
for(var col = 1; col <= 4; col++){
var sourceCellRange = sh.getRange(row, col, 1, 1);
var targetCellRange = sh.getRange(row + 2, col, 1, 1);
var sourceCellValue = sourceCellRange.getValue();//1 read call per loop
targetCellRange.setValue(sourceCellValue);//1 write call per loop
}
}
}
Notice that two calls are made per loop(Spreadsheet ss, Sheet sh and range calls are excluded. Only including the expensive get/set value calls). There are two loops; 8 read calls and 8 write calls are made in this example for a simple copy paste of 2x4 array.
In addition, Notice that read and write calls alternated making "look-ahead" caching ineffective.
Total calls to services: 16
Time taken: ~5+ seconds
Fast script example:
/**
* Fast script example
* Get values from A1:D2
* Set values to A3:D4
*/
function fastScript(){
const ss = SpreadsheetApp.getActive();
const sh = ss.getActiveSheet();
//get A1:D2 and set it 2 rows down
var sourceRange = sh.getRange("A1:D2");
var targetRange = sh.getRange("A3:D4");
var sourceValues = sourceRange.getValues();//1 read call in total
//modify `sourceValues` if needed
//sourceValues looks like this two dimensional array:
//[//outer array containing rows array
// ["A1","B1","C1",D1], //row1(inner) array containing column element values
// ["A2","B2","C2",D2],
//]
//#see https://stackoverflow.com/questions/63720612
targetRange.setValues(sourceValues);//1 write call in total
}
Total calls to services: 2
Time taken: ~0.2 seconds
References:
Best practices
What does the range method getValues() return and setValues() accept?

Using methods like .getValue() and .moveTo() can be very expensive on execution time. An alternative approach is to use a batch operation where you get all the column values and iterate across the data reshaping as required before writing to the sheet in one call. When you run your script you may have noticed the following warning:
The script uses a method which is considered expensive. Each
invocation generates a time consuming call to a remote server. That
may have critical impact on the execution time of the script,
especially on large data. If performance is an issue for the script,
you should consider using another method, e.g. Range.getValues().
Using .getValues() and .setValues() your script can be rewritten as:
function format() {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var s = ss.getActiveSheet();
var lastRow = s.getLastRow(); // more efficient way to get last row
var row;
var data = s.getRange("A:A").getValues(); // gets a [][] of all values in the column
var output = []; // we are going to build a [][] to output result
//loop through all cells in column A
for (row = 0; row < lastRow; row++) {
var cellValue = data[row][0];
var dash = false;
if (typeof cellValue === 'string') {
dash = cellValue.substring(0, 1);
} else { // if a number copy to our output array
output.push([cellValue]);
}
// if a dash
if (dash === "-") {
var name = (data[(row+1)][0]+" "+data[(row+2)][0]).trim(); // build name
output.push([cellValue]); // add row -state
output.push([name]); // add row name
output.push(["Order complete"]); // row order complete
output.push([""]); // add blank row
row++; // jump an extra row to speed things up
}
}
s.clear(); // clear all existing data on sheet
// if you need other data in sheet then could
// s.deleteColumn(1);
// s.insertColumns(1);
// set the values we've made in our output [][] array
s.getRange(1, 1, output.length).setValues(output);
}
Testing your script with 20 rows of data revealed it took 4.415 seconds to execute, the above code completes in 0.019 seconds

Related

How to find an empty cell and paste data in that one [duplicate]

I've made a script that every few hours adds a new row to a Google Apps spreadsheet.
This is the function I've made to find the first empty row:
function getFirstEmptyRow() {
var spr = SpreadsheetApp.getActiveSpreadsheet();
var cell = spr.getRange('a1');
var ct = 0;
while ( cell.offset(ct, 0).getValue() != "" ) {
ct++;
}
return (ct);
}
It works fine, but when reaching about 100 rows, it gets really slow, even ten seconds.
I'm worried that when reaching thousands of rows, it will be too slow, maybe going in timeout or worse.
Is there a better way?
This question has now had more than 12K views - so it's time for an update, as the performance characteristics of New Sheets are different than when Serge ran his initial tests.
Good news: performance is much better across the board!
Fastest:
As in the first test, reading the sheet's data just once, then operating on the array, gave a huge performance benefit. Interestingly, Don's original function performed much better than the modified version that Serge tested. (It appears that while is faster than for, which isn't logical.)
The average execution time on the sample data is just 38ms, down from the previous 168ms.
// Don's array approach - checks first column only
// With added stopping condition & correct result.
// From answer https://stackoverflow.com/a/9102463/1677912
function getFirstEmptyRowByColumnArray() {
var spr = SpreadsheetApp.getActiveSpreadsheet();
var column = spr.getRange('A:A');
var values = column.getValues(); // get all data in one call
var ct = 0;
while ( values[ct] && values[ct][0] != "" ) {
ct++;
}
return (ct+1);
}
Test results:
Here are the results, summarized over 50 iterations in a spreadsheet with 100 rows x 3 columns (filled with Serge's test function).
The function names match the code in the script below.
"First empty row"
The original ask was to find the first empty row. None of the previous scripts actually deliver on that. Many check just one column, which means that they can give false positive results. Others only find the first row that follows all data, meaning that empty rows in non-contiguous data get missed.
Here's a function that does meet the spec. It was included in the tests, and while slower than the lightning-fast single-column checker, it came in at a respectable 68ms, a 50% premium for a correct answer!
/**
* Mogsdad's "whole row" checker.
*/
function getFirstEmptyRowWholeRow() {
var sheet = SpreadsheetApp.getActiveSheet();
var range = sheet.getDataRange();
var values = range.getValues();
var row = 0;
for (var row=0; row<values.length; row++) {
if (!values[row].join("")) break;
}
return (row+1);
}
Complete script:
If you want to repeat the tests, or add your own function to the mix as a comparison, just take the whole script and use it in a spreadsheet.
/**
* Set up a menu option for ease of use.
*/
function onOpen() {
var menuEntries = [ {name: "Fill sheet", functionName: "fillSheet"},
{name: "test getFirstEmptyRow", functionName: "testTime"}
];
var sh = SpreadsheetApp.getActiveSpreadsheet();
sh.addMenu("run tests",menuEntries);
}
/**
* Test an array of functions, timing execution of each over multiple iterations.
* Produce stats from the collected data, and present in a "Results" sheet.
*/
function testTime() {
var ss = SpreadsheetApp.getActiveSpreadsheet();
ss.getSheets()[0].activate();
var iterations = parseInt(Browser.inputBox("Enter # of iterations, min 2:")) || 2;
var functions = ["getFirstEmptyRowByOffset", "getFirstEmptyRowByColumnArray", "getFirstEmptyRowByCell","getFirstEmptyRowUsingArray", "getFirstEmptyRowWholeRow"]
var results = [["Iteration"].concat(functions)];
for (var i=1; i<=iterations; i++) {
var row = [i];
for (var fn=0; fn<functions.length; fn++) {
var starttime = new Date().getTime();
eval(functions[fn]+"()");
var endtime = new Date().getTime();
row.push(endtime-starttime);
}
results.push(row);
}
Browser.msgBox('Test complete - see Results sheet');
var resultSheet = SpreadsheetApp.getActive().getSheetByName("Results");
if (!resultSheet) {
resultSheet = SpreadsheetApp.getActive().insertSheet("Results");
}
else {
resultSheet.activate();
resultSheet.clearContents();
}
resultSheet.getRange(1, 1, results.length, results[0].length).setValues(results);
// Add statistical calculations
var row = results.length+1;
var rangeA1 = "B2:B"+results.length;
resultSheet.getRange(row, 1, 3, 1).setValues([["Avg"],["Stddev"],["Trimmed\nMean"]]);
var formulas = resultSheet.getRange(row, 2, 3, 1);
formulas.setFormulas(
[[ "=AVERAGE("+rangeA1+")" ],
[ "=STDEV("+rangeA1+")" ],
[ "=AVERAGEIFS("+rangeA1+","+rangeA1+',"<"&B$'+row+"+3*B$"+(row+1)+","+rangeA1+',">"&B$'+row+"-3*B$"+(row+1)+")" ]]);
formulas.setNumberFormat("##########.");
for (var col=3; col<=results[0].length;col++) {
formulas.copyTo(resultSheet.getRange(row, col))
}
// Format for readability
for (var col=1;col<=results[0].length;col++) {
resultSheet.autoResizeColumn(col)
}
}
// Omiod's original function. Checks first column only
// Modified to give correct result.
// question https://stackoverflow.com/questions/6882104
function getFirstEmptyRowByOffset() {
var spr = SpreadsheetApp.getActiveSpreadsheet();
var cell = spr.getRange('a1');
var ct = 0;
while ( cell.offset(ct, 0).getValue() != "" ) {
ct++;
}
return (ct+1);
}
// Don's array approach - checks first column only.
// With added stopping condition & correct result.
// From answer https://stackoverflow.com/a/9102463/1677912
function getFirstEmptyRowByColumnArray() {
var spr = SpreadsheetApp.getActiveSpreadsheet();
var column = spr.getRange('A:A');
var values = column.getValues(); // get all data in one call
var ct = 0;
while ( values[ct] && values[ct][0] != "" ) {
ct++;
}
return (ct+1);
}
// Serge's getFirstEmptyRow, adapted from Omiod's, but
// using getCell instead of offset. Checks first column only.
// Modified to give correct result.
// From answer https://stackoverflow.com/a/18319032/1677912
function getFirstEmptyRowByCell() {
var spr = SpreadsheetApp.getActiveSpreadsheet();
var ran = spr.getRange('A:A');
var arr = [];
for (var i=1; i<=ran.getLastRow(); i++){
if(!ran.getCell(i,1).getValue()){
break;
}
}
return i;
}
// Serges's adaptation of Don's array answer. Checks first column only.
// Modified to give correct result.
// From answer https://stackoverflow.com/a/18319032/1677912
function getFirstEmptyRowUsingArray() {
var sh = SpreadsheetApp.getActiveSpreadsheet();
var ss = sh.getActiveSheet();
var data = ss.getDataRange().getValues();
for(var n=0; n<data.length ; n++){
if(data[n][0]==''){n++;break}
}
return n+1;
}
/**
* Mogsdad's "whole row" checker.
*/
function getFirstEmptyRowWholeRow() {
var sheet = SpreadsheetApp.getActiveSheet();
var range = sheet.getDataRange();
var values = range.getValues();
var row = 0;
for (var row=0; row<values.length; row++) {
if (!values[row].join("")) break;
}
return (row+1);
}
function fillSheet(){
var sh = SpreadsheetApp.getActiveSpreadsheet();
var ss = sh.getActiveSheet();
for(var r=1;r<1000;++r){
ss.appendRow(['filling values',r,'not important']);
}
}
// Function to test the value returned by each contender.
// Use fillSheet() first, then blank out random rows and
// compare results in debugger.
function compareResults() {
var a = getFirstEmptyRowByOffset(),
b = getFirstEmptyRowByColumnArray(),
c = getFirstEmptyRowByCell(),
d = getFirstEmptyRowUsingArray(),
e = getFirstEmptyRowWholeRow(),
f = getFirstEmptyRowWholeRow2();
debugger;
}
The Google Apps Script blog had a post on optimizing spreadsheet operations that talked about batching reads and writes that could really speed things up. I tried your code on a spreadsheet with 100 rows, and it took about seven seconds. By using Range.getValues(), the batch version takes one second.
function getFirstEmptyRow() {
var spr = SpreadsheetApp.getActiveSpreadsheet();
var column = spr.getRange('A:A');
var values = column.getValues(); // get all data in one call
var ct = 0;
while ( values[ct][0] != "" ) {
ct++;
}
return (ct);
}
If the spreadsheet gets big enough, you might need to grab the data in chunks of 100 or 1000 rows instead of grabbing the entire column.
It's already there as the getLastRow method on the Sheet.
var firstEmptyRow = SpreadsheetApp.getActiveSpreadsheet().getLastRow() + 1;
Ref https://developers.google.com/apps-script/class_sheet#getLastRow
Seeing this old post with 5k views I first checked the 'best answer' and was quite surprised by its content... this was a very slow process indeed ! then I felt better when I saw Don Kirkby's answer, the array approach is indeed much more efficient !
But how much more efficient ?
So I wrote this little test code on a spreadsheet with 1000 rows and here are the results : (not bad !... no need to tell which one is which...)
and here is the code I used :
function onOpen() {
var menuEntries = [ {name: "test method 1", functionName: "getFirstEmptyRow"},
{name: "test method 2 (array)", functionName: "getFirstEmptyRowUsingArray"}
];
var sh = SpreadsheetApp.getActiveSpreadsheet();
sh.addMenu("run tests",menuEntries);
}
function getFirstEmptyRow() {
var time = new Date().getTime();
var spr = SpreadsheetApp.getActiveSpreadsheet();
var ran = spr.getRange('A:A');
for (var i= ran.getLastRow(); i>0; i--){
if(ran.getCell(i,1).getValue()){
break;
}
}
Browser.msgBox('lastRow = '+Number(i+1)+' duration = '+Number(new Date().getTime()-time)+' mS');
}
function getFirstEmptyRowUsingArray() {
var time = new Date().getTime();
var sh = SpreadsheetApp.getActiveSpreadsheet();
var ss = sh.getActiveSheet();
var data = ss.getDataRange().getValues();
for(var n =data.length ; n<0 ; n--){
if(data[n][0]!=''){n++;break}
}
Browser.msgBox('lastRow = '+n+' duration = '+Number(new Date().getTime()-time)+' mS');
}
function fillSheet(){
var sh = SpreadsheetApp.getActiveSpreadsheet();
var ss = sh.getActiveSheet();
for(var r=1;r<1000;++r){
ss.appendRow(['filling values',r,'not important']);
}
}
And the test spreadsheet to try it yourself :-)
EDIT :
Following Mogsdad's comment, I should mention that these function names are indeed a bad choice... It should have been something like getLastNonEmptyCellInColumnAWithPlentyOfSpaceBelow() which is not very elegant (is it ?) but more accurate and coherent with what it actually returns.
Comment :
Anyway, my point was to show the speed of execution of both approaches, and it obviously did it (didn't it ? ;-)
I know this is an old thread and there have been some very clever approaches here.
I use the script
var firstEmptyRow = SpreadsheetApp.getActiveSpreadsheet().getLastRow() + 1;
if I need the first completely empty row.
If I need the first empty cell in a column I do the following.
My first row is usually a title row.
My 2nd row is a hidden row and each cell has the formula
=COUNTA(A3:A)
Where A is replaced with the column letter.
My script just reads this value. This updates pretty quickly compared to script approaches.
There is one time this does not work and that is when I allow empty cells to break up the column. I have not needed a fix for this yet, I suspect one may be derived from COUNTIF, or a combined function or one of the many other inbuilt ones.
EDIT: COUNTA does cope with blank cells within a range, so the concern about the "one time this does not work" is not really a concern. (This might be a new behavior with "new Sheets".)
And why don't use appendRow?
var spreadsheet = SpreadsheetApp.getActiveSpreadsheet();
spreadsheet.appendRow(['this is in column A', 'column B']);
I have a similar issue. Right now it's a table with many hundreds of rows, and I'm expecting it to grow to many many thousands. (I haven't seen whether a Google spreadsheet will handle tens of thousands of rows, but I'll get there eventually.)
Here's what I'm doing.
Step forward through the column by hundreds, stop when I'm on an empty row.
Step backward through the column by tens, looking for the first non-empty row.
Step forward through the column by ones, looking for the first empty row.
Return the result.
This depends of course on having contiguous content. Can't have any random blank lines in there. Or at least, if you do, results will be sub-optimal. And you can tune the increments if you think it's important. These work for me, and I find that the difference in duration between steps of 50 and steps of 100 are negligible.
function lastValueRow() {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var r = ss.getRange('A1:A');
// Step forwards by hundreds
for (var i = 0; r.getCell(i,1).getValue() > 1; i += 100) { }
// Step backwards by tens
for ( ; r.getCell(i,1).getValue() > 1; i -= 10) { }
// Step forwards by ones
for ( ; r.getCell(i,1).getValue() == 0; i--) { }
return i;
}
This is much faster than inspecting every cell from the top. And if you happen to have some other columns that extend your worksheet, it may be faster than inspecting every cell from the bottom, too.
I tweaked the code ghoti supplied so that it searched for an empty cell. Comparing values did not work on a column with text (or I could not figure out how) instead I used isBlank(). Notice the value is negated with ! (in front of the variable r) when looking forward since you want i to increase until a blank is found. Working up the sheet by ten you want to stop decreasing i when you find a cell that is not blank (! removed). Then, back down the sheet by one to the first blank.
function findRow_() {
var ss = SpreadsheetApp.getActiveSpreadsheet();
ss.setActiveSheet(ss.getSheetByName("DAT Tracking"));
var r = ss.getRange('C:C');
// Step forwards by hundreds
for (var i = 2; !r.getCell(i,1).isBlank(); i += 100) { }
// Step backwards by tens
for ( ; r.getCell(i,1).isBlank(); i -= 10) { }
// Step forwards by ones
for ( ; !r.getCell(i,1).isBlank(); i++) { }
return i;
Just my two cents, but I do this all the time. I just write the data to the TOP of the sheet. It's date reversed (latest on top), but I can still get it to do what I want. The code below has been storing data it scrapes from a realtor's site for the past three years.
var theSheet = SpreadsheetApp.openById(zSheetId).getSheetByName('Sheet1');
theSheet.insertRowBefore(1).getRange("A2:L2").setValues( [ zPriceData ] );
This chunk of the scraper function inserts a row above #2 and writes the data there. The first row is the header, so I don't touch that. I haven't timed it, but the only time I have an issue is when the site changes.
Indeed the getValues is a good option but you can use the .length function to get the last row.
function getFirstEmptyRow() {
var spr = SpreadsheetApp.getActiveSpreadsheet();
var array = spr.getDataRange().getValues();
ct = array.length + 1
return (ct);
}
Using indexOf is one of the ways to achieve this:
function firstEmptyRow() {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sh = ss.getActiveSheet();
var rangevalues = sh.getRange(1,1,sh.getLastRow(),1).getValues(); // Column A:A is taken
var dat = rangevalues.reduce(function (a,b){ return a.concat(b)},[]); //
2D array is reduced to 1D//
// Array.prototype.push.apply might be faster, but unable to get it to work//
var fner = 1+dat.indexOf('');//Get indexOf First empty row
return(fner);
}
I have gone through way too many of these implementations of last-row for a specific column. Many solutions work but are slow for large or multiple datasets. One of my use cases requires me to check the last row in specific columns across multiple spreadsheets. What I have found is that taking the whole column as a range and then iterating through it is too slow, and adding a few of these together makes the script sluggish.
My "hack" has been this formula:
=ROW(index(sheet!A2:A,max(row(sheet!A2:A)*(sheet!A2:A<>""))))-1
Example: Add this to Cell A1, to find the last row in column A. Can be added anywhere, just make sure to manage the "-1" at the end depending on which row the formula is placed. You can also place this is another col, rather than the one you're trying to count, and you don't need to manage the -1. You could also count FROM a starting Row, like "C16:C" - will count values C16 onwards
This formula is reliably giving me the last row, including blanks in the middle of the dataset
To use this value in my GS code, I am simply reading the cell value from A1. I understand that Google is clear that spreadsheet functions like read/write are heavy (time-consuming), but this is much faster than column count last-row methods in my experience (for large datasets)
To make this efficient, I am getting the last row in a col once, then saving it as a global variable and incrementing in my code to track which rows I should be updating. Reading the cell every-time your loop needs to make an update will be too inefficient. Read once, iterate the value, and the A1 cell formula (above) is "storing" the updated value for the next time your function runs
This also works if the data has filters turned on. Actual last row is maintained
Please let me know if this was helpful to you! If I encounter any issues I will comment on this answer.
combo of DON and Ghoti.
function getLastRowNumber(sheet, columnLabel) {
var columnLabel = sheet.getRange(`${columnLabel}:${columnLabel}`);
var values = columnLabel.getValues(); // get all data in one call
var ct = 0;
for (; values.length > ct && values[ct][0] != ""; ct += 100);
// Step backwards by tens
for ( ; ct > 0 && values[ct][0] == ""; ct -= 10);
// Step forwards by ones
for ( ; values.length > ct && values[ct][0] != ""; ct ++);
return ct;
}
I keep an extra "maintenance" sheet, on my spreadsheets, where I keep such data.
To get the next free row of a range I just examine the relevant cell. I can get the value instantly, because the work of finding the value happens when the data is changed.
The formula in the cell is usually something like :
=QUERY(someSheet!A10:H5010,
"select min(A) where A > " & A9 & " and B is null and D is null and H < 1")
The value in A9 can be set periodically to some row that is near "enough" to the end.
Caveat : I have never checked if this is viable for huge data sets.
Finally I got a single line solution for it.
var sheet = SpreadsheetApp.getActiveSpreadsheet();
var lastEmptyOnColumnB = sheet.getRange("B1:B"+sheet.getLastRow()).getValues().join(",").replace(/,,/g, '').split(",").length;
It works fine for me.
Here is a list of what the code should do:
Give a correct answer if there are no empty cells
Be fast
Return the correct row number - not the index number of the array
Get the correct row number of the empty cell even when other columns in the sheet tab have more rows with data
Have good variable names
Answer the original question
Avoid unnecessary data processing
Provide comment explanations for what the code does
Be generic enough to adapt to the readers conditions
This solution uses the array method some which will stop iterating the loop when the condition is true. This avoids wasting time spent looping through every element of the array, and yet uses an array method rather than a for or while loop.
The some method only returns true or false, but there is a way to capture the index number because the some method halts looping when the condition is true.
The index number is assigned to a variable in the scope outside of the array function. This does not slow down the processing.
Code:
function getFirstEmptyCellIn_A_Column(po) {
var foundEmptyCell,rng,sh,ss,values,x;
/*
po.sheetTabName - The name of the sheet tab to get
po.ssID - the file ID of the spreadsheet
po.getActive - boolean - true - get the active spreadsheet -
*/
/* Ive tested the code for speed using many different ways to do this and using array.some
is the fastest way - when array.some finds the first true statement it stops iterating -
*/
if (po.getActive || ! po.ssID) {
ss = SpreadsheetApp.getActiveSpreadsheet();
} else {
ss = SpreadsheetApp.openById(po.ssID);
}
sh = ss.getSheetByName(po.sheetTabName);
rng = sh.getRange('A:A');//This is the fastest - Its faster than getting the last row and getting a
//specific range that goes only to the last row
values = rng.getValues(); // get all the data in the column - This is a 2D array
x = 0;//Set counter to zero - this is outside of the scope of the array function but still accessible to it
foundEmptyCell = values.some(function(e,i){
//Logger.log(i)
//Logger.log(e[0])
//Logger.log(e[0] == "")
x = i;//Set the value every time - its faster than first testing for a reason to set the value
return e[0] == "";//The first time that this is true it stops looping
});
//Logger.log('x + 1: ' + (x + 1))//x is the index of the value in the array - which is one less than the row number
//Logger.log('foundEmptyCell: ' + foundEmptyCell)
return foundEmptyCell ? x + 1 : false;
}
function testMycode() {
getFirstEmptyCellIn_A_Column({"sheetTabName":"Put Sheet tab name here","ssID":"Put your ss file ID here"})
}
this is my very first post on stackOverflow, I hope to meet all your netiquette needs, so please be nice to me.
considerations
I think the fastest way to find the first blank cell in a column (I couldn't run the performance checks, anyway) is to let the Google engine do sequential tasks itself; it is simply much more efficient. From a programmer's point of view, this translates into NOT using any kind of iteration/loops, i.e. FOR, WHILE, etc. (By the way, this is the same programming approach on database engines - any activity should NOT use loops to find information.)
the idea
Go all way DOWN and find the cell in last row of the Sheet (considering all columns),
from there, go UP find the first cell containing data in the specified column (selecting the column),
shift down one cell to find a free place.
The following function does this in just one command (neglecting the var declarations, here just to improve readability):
code
function lastCell() {
var workSheet = SpreadsheetApp.getActiveSpreadsheet().getActiveSheet();
var lastRow = workSheet.getLastRow();
var columnToSearch = 1; //index of the column to search. 1 is 'A'.
workSheet.getRange(lastRow, columnToSearch).activateAsCurrentCell().
getNextDataCell(SpreadsheetApp.Direction.UP).activate();
workSheet.getCurrentCell().offset(1, 0).activate(); // shift one cell down to find a free cell
}

How I can speed up Google Apps Script code for Google Spreadsheet

My code works "if AM1 > 0 show AM:AM else hide AM:AM". AM to CM
This works but is very very slow. Each step takes 1-3 seconds, and having about 53 columns, takes 50-150 seconds overall.
How I can speed this up?
function myFunction() {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheet = ss.getSheetByName("Ürünler");
var range = sheet.getRange("AP1");
var values = range.getValues();
var row, col;
var data0 = ["AL:AL", "AM:AM", "AN:AN", "AO:AO", "AP:AP", "AQ:AQ", "AR:AR", "AS:AS", "AT:AT", "AU:AU",
"AV:AV", "AW:AW", "AX:AX", "AY:AY", "AZ:AZ", "BA:BA", "BB:BB", "BC:BC", "BD:BD", "BE:BE",
"BF:BF", "BG:BG", "BH:BH", "BI:BI", "BJ:BJ", "BK:BK", "BL:BL", "BM:BM", "BN:BN", "BO:BO",
"BP:BP", "BQ:BQ", "BR:BR", "BS:BS", "BT:BT", "BU:BU", "BV:BV", "BW:BW", "BX:BX", "BY:BY",
"BZ:BZ", "CA:CA", "CB:CB", "CC:CC", "CD:CD", "CE:CE", "CF:CF", "CG:CG", "CH:CH", "CI:CI",
"CJ:CJ", "CK:CK", "CL:CL", "CM:CM", "CN:CN"];
col = 38;
for (col = 38; col < 93; col++) { //start 38 items 53
range = sheet.getRange(1, col);
if (range.getValue() > 0) {
sheet.getRange(data0[col-38]).activate();
ss.getActiveSheet().showColumns(col);
} else {
sheet.getRange(data0[col-38]).activate();
ss.getActiveSheet().hideColumns(col);
}
}
You call functions getActiveSheet(), getRange() and getValue() way too often.
Try minimizing the number of reads and writes. Consider using the getValues() method.
You can write scripts to take maximum advantage of the built-in caching, by minimizing the number of reads and writes. Alternating read and write commands is slow. To speed up a script, read all data into an array with one command, perform any operations on the data in the array, and write the data out with one command.
Check it out for more
https://developers.google.com/apps-script/guides/support/best-practices

How do I write data in every last column?

I need to add a new column everyday and the data insertion will be done individually for the specific row comparing with the input... please tell me if it is correct or not.
will this piece of code work for adding new columns ever day and initialize all the entries by 0?
function trig(){
var builder = ScriptApp.newTrigger("addcol").timeBased().everyDays(1);
var trigger = builder.create();
}
function addcol(){
var cname = builder.atDate( day, month, year)
var column = eventRange.getLastColumn();
sheet.insertColumnAfter(column).setName(cname);
sheet.getRange("E1").setValue(new Date()).setNumberFormat('d/M/yyyy');
var col = [];
for(var n=0 ; n<s.getMaxRows();n++){
col.getLastColumn().push(['0']);
}
ss.getRange('N:N').setValues(col);
}
// now for the insertion part
here the sr will be compared to SRN from the sheet (E) and if it matches it will replace 0 with 1 in the last column added everyday. plese tell me will this work?
function doPost(e){
var action = e.parameter.action;
if(action == 'scanner'){
return scanner(e);
}
}
function scanner(e){
var srn = e.parameter.sr;
var C = sheet.getLastColumn();
var R = sheet.getLastRow();
for(i=1; i<=R; i++)
{
if (srn == sheet.getDataRange([i][2]))
{
sheet.getDataRange([i],[C]).push[(1)];
sheet.append([i],[C]);
return ContentService.createTextOutput("Success").setMimeType(ContentService.MimeType.TEXT);
break;
}
}
}
Time-based trigger:
There are no event objects associated with time-based triggers, so variables like eventRange cannot work. It seems like you want to use variables in addcol that are defined in trig (e.g. builder). That is not possible. Also, if you want your function to run once a day, there is no need for lines like this: builder.atDate(day, month, year)). The trigger will be created by running this function once:
function createTrigger(){
var builder = ScriptApp.newTrigger("addcol").timeBased().everyDays(1).create();
}
Adding column with 0's:
There are many problems with the function addcol:
Several uninitialized variables are being used (s, builder, eventRange).
Unexisting methods are being used: e.g.: setNumberFormat is a method of the Range class, not of the Date object. You should use Utilities.formatDate(date, timeZone, format) to format dates. Also, you are using setName when inserting a new column, but that changes the sheet name. Is that what you want to do? And also, cname is assigned a trigger builder as value, which I seriously doubt is your purpose. The same way, an array col does not have a method getLastColumn().
You could use this addcol function instead (change your sheet name, currently set to Your sheet name, and the timeZone in formatDate, currently set to GMT:
function addcol() {
var sheet = SpreadsheetApp.getActive().getSheetByName("Your sheet name"); // Change accordingly
var lastCol = sheet.getLastColumn();
var lastRow = sheet.getLastRow();
if (sheet.getMaxColumns() === lastCol) sheet.insertColumnAfter(lastCol);
var newCol = sheet.getRange(1, lastCol + 1, lastRow, 1);
var values = [];
values.push([Utilities.formatDate(new Date(), "GMT", "d/M/yyyy")]); // Change accordingly
for (var i = 1; i < sheet.getLastRow(); i++) {
values.push([0]);
}
newCol.setValues(values);
}
Replacing 0's with 1's:
Assuming that you are getting the function scanner to run correctly and that the parameter e.parameter.sr is getting populated correctly, you can do the following:
function scanner(e){
var srn = e.parameter.sr;
var C = sheet.getLastColumn();
var R = sheet.getLastRow();
for (i=1; i<=R; i++) {
if (srn == sheet.getRange(i, 2).getValue()) {
sheet.getRange(i, C).setValue(1);
}
}
}
Here you were also using unexisting methods or providing incorrect parameters:
The method getDataRange doesn't allow any argument, you should be using getRange(row, column), and provide the row and column indexes separated by commas, not as if trying to access a 2D array.
break terminates the current loop, so only use it if you only want to update 1 cell. The same goes for return which finishes current function execution.
Reference:
Spreadsheet Service
Installable Triggers
Short
No
Long
There are several problems with the script:
getDataRange() expects no arguments passed (docs only say it is the same as using getRange(yourSheet.getLastRow(), yourSheet.getLastColumn()), not that you should do it). Certainly it does not expect instances of Array (bracket [] notation wraps C and i, which are of type Number into one). Moreover, it returns a Range, which at the time of writing does not have push() method.
getLastColumn() returns an instance of Number, and thus does not have a push() method as well. You are on the right track, though, since col is an Array, and you need to push() into it.
If you want the script to add a zero-filled column, don't get constant ranges: in current state, getRange('N:N') guarantees that each time you will re-initialize column N. Btw, same goes for getRange("E1").
You still haven't addressed issues listed in comments to your previous question.
Also, in your scanner function there is a syntax error: push[(1)] should be push([1]).
Also, the sheet variable is either undeclared or is declared globally, which is bad.
Notes
If you don't expect number of students to change dynamically, you can switch from getMaxRows() to getLastRow() to only zero-fill cells that are in range of cureent student info grid.
This question is a direct continuation of a currently closed one (please, always disclose that for reference at least).
How about skipping init to zero step at all? If cell is empty, getValue() / getValues() will return its value as an empty string, which is a falsy value, just as 0 is. If you want to count attendance at the end of period, a simple conditional will suffice to sum up.
The default MIME type for TextOutput instance obtained by createTextOutput() is plain text, so setting it to ContentService.MimeType.TEXT is an overkill in your case.
Reference
getDataRange() docs
getLastColumn() docs
getValue() docs
getValues() docs
Range docs
createTextOutput() docs
Falsy values explanation on MDN

Script to copy from one sheet to another, needs edit

I have this script which is working well, but i need to edit it to
a) only return new rows since last run
b) only return certain cells instead of whole row
any guidance would be greatly appreciated
function Copy() {
var sourceSheet = SpreadsheetApp.openById('1WAtRDYhfVXcBKQoUxfTJORXwAqYvVG2Khl4GuJEYSIs')
.getSheetByName('Jobs Log');
var range = sourceSheet.getRange(1, 1, sourceSheet.getLastRow(), sourceSheet.getLastColumn());
var arr = [];
var rangeval = range.getValues()
.forEach(function (r, i, v) {
if (r[1] == 'Amber') arr.push(v[i]);
});
var destinationSheet = SpreadsheetApp.openById('137xdyV8LEh6GAhAwSx4GmRGusnjsHQ0VGlWbsDLXf2c')
.getSheetByName('Sheet1');
destinationSheet.getRange(destinationSheet.getLastRow() + 1, 1, arr.length, arr[0].length)
.setValues(arr);
}
In order to only check new data added after last runtime we have to store .getLastRow() value in properties and retrieve it every runtime. We would also have to work under a few assumptions:
In the input data new values are only appended at the bottom and never inserted between other data
Data is never deleted from the input sheet (if you ignore this, then you must also have an update script for the last row that runs after deleting data)
The sheet is not sorted after new data is added but before this script is run.
So you would need something along the lines of
var sourceSheet = SpreadsheetApp.openById('1WAtRDYhfVXcBKQoUxfTJORXwAqYvVG2Khl4GuJEYSIs')
.getSheetByName('Jobs Log');
var lastRow = sourceSheet.getLastRow();
// note that you need to hav the script property initialized and stored
// or adjust the if to also check if prevLastRow gets a value
var prevLastRow = PropertiesService.getScriptProperties().getProperty('lastRow')
if (lastRow <= prevLastRow) {
return; // we simply stop the execution right here if we don't have more data
}
// then we simply start the range from the previous last row
// and take the amount of rows added afterwards
var range = sourceSheet.getRange(prevLastRow,
1,
lastRow - prevLastRow,
sourceSheet.getLastColumn()
);
As for the second question, inside the forEach you need to simply push an array into arr that will contain only the columns you want. So for example
if (r[1] == 'Amber') arr.push(v[i]);
changes into
if (r[1] == 'Amber') arr.push([v[i][0], v[i][3], v[i][2]]);
which will output A D C columns (in that order) for each row.
Finally, the last thing you need to run before the script ends is
PropertiesService.getScriptProperties().setProperty('lastRow', lastRow)
which will let us know where we stopped the next time we run the script. Again, keep in mind that this works only if new data will always be in new rows. Otherwise, you need to do a different method and retrieve 2 arrays of data. 1 for the entire input sheet and 1 for the output sheet. Then you would have to perform 2 if checks. First one to see if your criteria are met and a second one to see if it already exists in the output data.

Reducing Google Apps Script execution time with using an array?

I wrote a script to periodically copy data from one column depending on if each cell was determined to have current data (Designated as ALIVE in another column), and place that data in another column in a different sheet. The script doesn't exceed the execution time, however I was wondering if there was a way to make it faster by utilizing Arrays.
I appreciate the help, I'm new to Google Apps Script programming but plugging along. Many thanks in advance for the advice.
function copyFunctionDATA() {
var defSheet1 = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("(DATA)")
var defSheet2 = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("(DATAdead)")
var numLastRow = 60
for (var x=11; x<=numLastRow; x++) {
var srcRange = defSheet1.getRange(x,1);
var srcRange2 = defSheet1.getRange(x,1);
var value = srcRange.getValue();
var value2 = srcRange2.getValue();
if (value2.indexOf("ALIVE") !== -1) {
defSheet2.getRange(x,1).setValue(value);
}
}}
Transposing in 2D array is very simple. The main difference is the way data is indexed : ranges count from 1 and arrays count from 0.
So to transpose your code you should get 2 arrays (one for each sheet) and iterate the corresponding cells, change the value depending on your condition and write back the array to the spreadsheet to update it.
Here is a rough transpose of your code with a couple of comments to explain : (some variables ought to be renamed for clarity)
function copyFunctionDATA() {
var defSheet1 = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("(DATA)").getDataRange().getValues();// read the whole sheet in a 2D array
var defSheet2 = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("(DATAdead)").getDataRange().getValues();// read the whole sheet in a 2D array
var numLastRow = 59 ; // I suppose you intentionally limit to the 60 first rows ?
for (var x=10; x<=numLastRow; x++) { // starting from row 11 >> becomes 10
var value = defSheet1[x][0];
var value2 = defSheet1[x][0]; // you made a mistake in your code : you define 2 identical ranges !! change it to your need : 0 is column A, 1 is B etc...
if (value2.indexOf("ALIVE") !== -1) {
defSheet2[x][0] = defSheet1[x][0];
}
}
SpreadsheetApp.getActiveSpreadsheet().getSheetByName("(DATAdead)").getRange(1,1,defSheet2.length,defSheet2[0].length).setValues(defSheet2);// write back defSheet2 array to sheet (DATAdead)
}
EDIT : if you want to overwrite only the first column in defSheet2 change simply the range definition for this sheet, for example like this :
var defSheet2 = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("(DATAdead)").getRange('A1:A').getValues();// read the whole sheet in a 2D array