How to get Row where two Columns equal specific values - google-apps-script

I'm very new and I'm trying to create a Time in/Time out sheet. I have 2 separate sheets, first(ACTIVE) is where the trigger happens that starts the onEdit(e) script. All the functions that start onEdit(e) affects the second sheet(PASSIVE) to fill out Columns A(Last Name), B(First Name), C(Location), D(Time Out). I finished making the Time out functions by getting value of A, B, C + Active Row(this isn't the code). The trigger is always on the same row as the values being copied, so it was relatively simple. On the PASSIVE sheet I have all the values being stored using a code someone made called addRecord where it gets last row + 1 of the PASSIVE sheet and installs the values grabbed from the ACTIVE sheet and plugs them in. So it adds records without overwriting anything. Works beautifully. However making a "time in" function has been difficult. E(Time In) My idea is to getRow of the PASSIVE sheet by searching PASSIVE!A for the Value grabbed from (ACTIVE!A + Active Row) once it finds a match, it sees if (PASSIVE!E + the matched row) is empty. If it is, it adds new.Date and finishes. If it isn't empty, it ignores this row and continues searching down the line for the next Row that has PASSIVE!A match the grabbed value. Once it finds this Row, getRow. setValue of (PASSIVE!E + grabbed row, new Date())
I did find a function online to find the first row that matched the ACTIVE!A with PASSIVE!A. But it kept overwriting the date on the first match. It never ignored row with nonempty cell to the next match row. Maybe I was just slightly off, which is why I'm asking for a lot of detail and explanation in the Answers.
This was the Code I used from another answer.
function getCurrentRow() {
var currentRow = SpreadsheetApp.getActiveSheet().getActiveSelection().getRowIndex();
return currentRow;
}
function onSearch1()
{
What I added
var row = getCurrentRow();
var activeLocation = getValue('ACTIVE!A' + row);
Continued Other Code
var searchString = activeLocation;
var sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("PASSIVE");
var column =1; //column Index
var columnValues = sheet.getRange(2, column, sheet.getLastRow()).getValues(); //1st is header row
var searchResult = columnValues.findIndex(searchString); //Row Index - 2
What I added
setValue(PASSIVE!E + searchResult, new Date().toLocaleString())
It worked if everyone has a different name, but the search Result always found the first row of the match, I tried adding an if ACTIVE!A == PASSIVE!A
&& PASSIVE!E =="", grabRow (I know this isn't proper code) But I didn't even know where to put this if function or if it would work or if it would just keep coming up false after it runs the first time true.
Continued Other Code
if(searchResult != -1)
{
//searchResult + 2 is row index.
SpreadsheetApp.getActiveSpreadsheet().getSheetByName("PASSIVE").setActiveRange(sheet.getRange(searchResult + 2, 1))
}
if(searchResult = searchResult2) {
setValue('PASSIVE!E' + searchResult, new Date().toLocaleString())
}
}
Array.prototype.findIndex = function(search){
if(search == "") return false;
for (var i=0; i<this.length; i++)
if(this[i] == search) return i;
return -1;
}
So this is what I used, but not sure if it's the right way to go about this. Every time I used it, it would only set the SearchResult to the first row it found that had the searchString I'd actually prefer if it found the last row, considering the add record goes down over time and signing in should be the most recent name. But I'm guessing if I can just get a function that searches a range and finds the row for two values in specific columns, I can then just setValue('PASSIVE!E' + foundRow, new Date().toLocaleString())
Edit 5/9/2019 17:34 PST
Thank you to those Answering. I'm expanding on the question.
function rowWhereTwoColumnsEqual(value1,col1,value2,col2) {
var value1=value1 || 'A1';//testing
var value2=value2 || "";
The idea I'm having is to search Column1 of another sheet for, let's say, 'SheetA1' (the first sheet). And Column3 of another sheet for "" (cellisempty).
var value1= 'Sheet1!A1';
var value2= "";
var col1='Sheet2!A';
var col2='Sheet2!C';
var ss=SpreadsheetApp.getActive();
var sh=ss.getSheetByName('Sheet2');
var rg=sh.getDataRange();
var vA=rg.getValues();
However, I don't know how the vA works. I also want to getRow() of the Row that is found in order to use that number in another function.

Try this:
function rowWhereTwoColumnsEqual(value1,col1,value2,col2) {
var value1=value1 || 9;//testing
var value2=value2 || 8;
var col1=col1 || 1;//testing
var col2=col2 || 2;//testing
var ss=SpreadsheetApp.getActive();
var sh=ss.getActiveSheet();
var rg=sh.getDataRange();
var vA=rg.getValues();
var rA=[];
for(var i=0;i<vA.length;i++) {
if(vA[i][col1-1]==value1 && vA[i][col2-1]==value2) {
rA.push(i+1);
}
}
SpreadsheetApp.getUi().alert(rA.join(','));
//return rA;//as an array
//return rA.join(',');//as a string
}

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
}

Check number of rows in named range and if more than 2, execute different script

Currently I have a script that will add a specified number of rows into a named range. Due to the fact that the named range will always have 1 row to start, adding in "3" rows will really only add 2. This is the expected outcome for the first time you run the script. Obviously if the script has been run already, and you are expecting to add 3 more rows, having it only add 2 is frustrating. I cannot seem to wrap my head around how this works.
Here is the functional code:
//Take the Account and number of holes from
// the sidebar and insert rows to the proper named range
function insertRowNext(account,n_rows) {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var ws = ss.getSheetByName('Next Week'); // Change to your sheet name
//Replace space with underscore
account=account.replace(/ /g,"_");
var nameRange = 'Next Week!'+account;
n_rows-=1;
if(n_rows>0){
//add row
var range = ss.getRangeByName(nameRange);
ws.insertRowsBefore(range.getLastRow(),n_rows);
//Show all rows in the namedRange
ws.showRows(range.getRow(),range.getNumRows()+n_rows);
}
}
Here is the code that I modified but it broke the script.
// Take the Account and number of holes from
// the sidebar and insert rows to the proper named range
function insertRowNext(account,n_rows) {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var ws = ss.getSheetByName('Next Week');
//Replace space with underscore
account=account.replace(/ /g,"_");
if (n_rows == 2){
n_rows -= 1
var range = ss.getRangeByName(nameRange);
ws.insertRowsBefore(range.getLastRow(),n_rows);
//Show all rows in the namedRange
ws.showRows(range.getRow(),range.getNumRows()+n_rows);
}else if (n_rows < 2){
n_rows = 0
var range = ss.getRangeByName(nameRange);
ws.insertRowsBefore(range.getLastRow(),n_rows);
//Show all rows in the namedRange
ws.showRows(range.getRow(),range.getNumRows()+n_rows);
}else{
alert("unexpected number of rows -- please check template");
//throw error
}
}
I just need the script to check how many rows are in the selected named range, and if it is 2, subtract one from the amount to add. If the amount of rows in the named range is >2, do not subtract the amount of holes.
I don't know how your sheet is organized to produce such kind of behavior when inserting rows, but if you really want to check, you can use this code block:
var range = ss.getRangeByName(nameRange);
var rangeRows = range.getNumRows();
if (rangeRows == 2) {
ws.insertRowsBefore(range.getRow(),n_rows-1);
}
else if (rangeRows > 2) {
ws.insertRowsBefore(range.getRow(),n_rows);
}
This should be the output if n_rows = 3:

How to increment simple formula by 1 in Google Apps Script?

Disclaimer: I'm a Google Apps Script newbie.
I'm trying to create a timesheet in Google Sheets that lets a user clock in & clock out to log hours on a given project. I've borrowed code from a YouTube video on the general structure of setting the whole thing up.
Here's what the blank time sheet looks like. It's pretty basic:
I've created a user button (off to the right) where the user presses "Start" and cell A2 will input a timestamp. Then the user can press an "End" button, and a second timestamp, this time in B2, will appear, along with a simple calculation in C2 that measures the delta in the two timestamps, thus giving a duration of time spent on a given task or project. Here's what it looks like:
When the user needs to press "Start" again, a new timestamp appears in cell A3, and so on so forth, along with a new delta calculation for each new row.
Problem: I'm unable to get the simple delta calculation in column C to increment down each new rows so that the setFormula function doesn't contain hardcoded references to cells A2 & B2. See below code for what I have so far:
function setValue(cellName, value) {
SpreadsheetApp.getActiveSpreadsheet().getRange(cellName).setValue(value);
}
function getValue(cellName) {
return SpreadsheetApp.getActiveSpreadsheet().getRange(cellName).getValue();
}
function getNextRow() {
return SpreadsheetApp.getActiveSpreadsheet().getLastRow() + 1;
}
function addStartRecord (a) {
var row = getNextRow();
setValue('A' + row, a);
}
function addEndRecord (b, c) {
var row = getNextRow()-1;
setValue('B' + row, b);
setValue('C' + row, c);
}
function punchIn() {
addSRecord(new Date());
}
function punchOut() {
addERecord(new Date(), '=B2-A2');
}
The problem is with the punchOut() function there at the bottom. Any idea on the best way to increment this delta calculator down each new row?
Note: I saw a pretty good answer to a similar question here, but the code is throwing an error in the script editor after the line containing data[i] = ['=A' + i+1.toString() + ' + 1 ' ]. Also, I don't want to set a definitive last row for the delta calculation (such as 20 in this example). I'd want the user to be able to record as many new start/end times for a project as they'd want.
Edit: Here's a link to the timesheet so you can test the code.
Try modifying your punchOut method like this:
function punchOut() {
var ss = SpreadsheetApp.getActiveSheet();
var row = ss.getLastRow();
addEndRecord(new Date(), '=B' + row + '-A' + row);
}
I tested it in the sheet and it worked well.
setFormula() - this enables you to describe the formula to be inserted into column C.
The following is two simple functions that handle "Punch in" and "Punch Out" (with its calculation).
function so5695101401in() {
// punchin
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheet = ss.getActiveSheet();
var lR = sheet.getLastRow();
// Logger.log("DEBUG: the last row is "+lR);
var punchinRange = sheet.getRange(lR+1, 1);
// Logger.log("DEBUG: the punchinRange = "+punchinRange.getA1Notation());
punchinRange.setValue(new Date());
}
function so5695101401out() {
// punchout
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheet = ss.getActiveSheet();
var lR = sheet.getLastRow();
//Logger.log("DEBUG: the last row is "+lR);
var punchoutRange = sheet.getRange(lR, 2);
// Logger.log("DEBUG: the punchoutRange = "+punchoutRange.getA1Notation());
punchoutRange.setValue(new Date());
var timeElapsed = sheet.getRange(lR, 3).setNumberFormat("hh:mm:ss");
timeElapsed.setFormula("=B2-A2");
}
setFormula
I use a workaround for this problem, via app script copy the cell with the formula to de new row or range!.
for you problem:
var formula1 = sheetDatos.getRange(lastRow, 3); //get the formula
var copyRange = sheetDatos.getRange(lastRow+1, 3);
formula1.copyTo(copyRange);
for me is more easy in this way, try to do in sheet to understand how this work.
you need a initial formula to go in this way ;)

Auto insert row above, based on single cell value

I am trying to auto-insert a row above the current top row (row 2) based on a cell's value.
I cannot figure out the variables I need to put into the script editor.
I tried googling for help and modified other peoples script to see if I could do it, and the answer is no, no I do not possess the knowledge/skill.
function conditionalNewRow() {
var ss = SpreadsheetApp.getActive().getSheetByName('NEW INV');
var sh = ss.getActiveSheet();
var headerRow = 1;
var firstRow = headerRow + 1;
var range = sh.getRange("A2"); // Get range of row 2.
var futureRange = sh.getRange("A3"); // Get range of row 3.
var numCols = range.getNumColumns(); // Get the number of columns for row 2.
var contentVal = false;
for (i = 1; i <= numCols; i++) {
var currentValue = range.getCell(1,i).getValue();
if (currentValue == "NO"){
contentVal = true;
break;
}
}
if (contentVal == true) {
sh.insertRowBefore(firstRow); // Insert an empty row into row 2.
futureRange.copyTo(sh.getRange(firstRow, 1, firstRow, numCols), {contentsOnly:false}); // Copy row 3 to row 2.
}
}
All I want is a blank row above the previous row when a cell meets certain criteria;
e.g. Cell A2 contains any of the following; YES, NO, LOADED, UNLOADED
If it contains any of those values, it will auto-insert a row above.
I am a little unclear on your intent and what issues you are having.
A few things I see:
1) Since your range is a single cell, var numCols = range.getNumColumns(); should always return 1, so your loop, for (i = 1; i <= numCols; i++), should run exactly once. It feels like there is a lot of redundant code here.
To get the value of cell A2, you could just write:
var range = sh.getRange("A2");
var currentValue = range.getValue();
2) From the if statements, it looks like you only want to add a new row if the value of A2 is something other than "NO". Is that correct? (This is not what your question states). If so, I think you're pretty close with the sh.insertRowBefore(firstRow); statement, it just might be getting lost in some convoluted logic.
3) Do you really want to copy everything from row 3 into row 2? In the question you state "All I want is a blank row above the previous row when a cell meets certain criteria".
Perhaps something like this is closer to what you want?
function conditionalNewRow() {
var ss = SpreadsheetApp.getActive().getSheetByName("NEW INV");
var sh = ss.getActiveSheet();
var range = sh.getRange("A2"); // Get range of row 2.
var currentValue = range.getValue();
if (currentValue !== "NO") {
sh.insertRowBefore(firstRow); // Insert an empty row into row 2.
}
}
Edit
From your comment, sounds like you want to insert a new row whenever A2 is edited to one of the selectable values. You may want to check out simple triggers, such as onEdit, which runs whenever a cell is edited. For instance, something like this:
/**
* The event handler triggered when editing the spreadsheet.
* #param {Event} e The onEdit event.
*/
function onEdit(e) {
// get sheet
var sheet = SpreadsheetApp.getActive().getSheetByName("NEW INV");
// Get the edited range
var editedRange = e.range;
// check if cell A2 was edited
if (editedRange.getA1Notation() === "A2") {
// check if value is a desired value
if (editedRange.getValue() === "YES" || "NO" || "LOADED" || "UNLOADED") {
// if yes to both, insert new row
sheet.insertRowBefore(2);
}
}
}

Auto incrementing Job Reference

I am very new at writing code and have a limited understanding so please be gentle!
I have been trying to expand on the code made on this question.
I have had limited success and am now stuck, I was hoping someone would be kind enough to point me in the right direction or tell me what I am doing wrong.
So, the scenario: A need to have an auto-incrementing "Job Reference" for booking in netbook repairs to the IT dept I work for. This booking is made via a Google Form and I can get the code in the link to work perfectly but! I was hoping to have a little more than a simple count - 1,2,3,4,5 and so on. Ideally the job ref would be displayed as JR0001, JR0002 and so on.
So, my attempt at code!
The code which user: oneself submitted which works perfectly.
function onFormSubmit(e) {
// Get the active sheet
var sheet = SpreadsheetApp.getActiveSheet();
// Get the active row
var row = sheet.getActiveCell().getRowIndex();
// Get the next ID value. NOTE: This cell should be set to: =MAX(A:A)+1
var id = sheet.getRange("P1").getValue();
// Check of ID column is empty
if (sheet.getRange(row, 1).getValue() == "") {
// Set new ID value
sheet.getRange(row, 1).setValue(id);
}
}
The first thing I tried was to simply add another variable and add it to the .setValue
function onFormSubmit(e) {
// Get the active sheet
var sheet = SpreadsheetApp.getActiveSheet();
// Get the active row
var row = sheet.getActiveCell().getRowIndex();
// Get the next ID value. NOTE: This cell should be set to: =MAX(A:A)+1
var id = sheet.getRange("X1").getValue();
// Set Job Reference
var jobref = "JR";
// Check of ID column is empty
if (sheet.getRange(row, 1).getValue() == "") {
// Set new ID value
sheet.getRange(row, 1).setValue(jobref+id);
}
}
This worked as far as getting "JR1" instead of 1 but the auto-increment stopped working so for every form submitted I still had "JR1" - not really auto-increment!
I then tried setting up another .getValue from the sheet as the Job Ref
function onFormSubmit(e) {
// Get the active sheet
var sheet = SpreadsheetApp.getActiveSheet();
// Get the active row
var row = sheet.getActiveCell().getRowIndex();
// Get the next ID value. NOTE: This cell should be set to: =MAX(A:A)+1
var id = sheet.getRange("X1").getValue();
// Set Job Reference
var jobref = sheet.getRange("X2").getValue();
// Check of ID column is empty
if (sheet.getRange(row, 1).getValue() == "") {
// Set new ID value
sheet.getRange(row, 1).setValue(jobref+id);
}
}
Same result - a non incrementing "JR1"
I then tried concatenating the working incrementing number with my job ref cell and then calling that in the script.
function onFormSubmit(e) {
// Get the active sheet
var sheet = SpreadsheetApp.getActiveSheet();
// Set Job Reference
var jobref = sheet.getRange("X2").getValue();
// Get the active row
var row = sheet.getActiveCell().getRowIndex();
// Get the next ID value. NOTE: This cell should be set to: =MAX(A:A)+1
var id = sheet.getRange("X1").getValue();
// Check of ID column is empty
if (sheet.getRange(row, 1).getValue() == "") {
// Set new ID value
sheet.getRange(row, 1).setValue(jobref);
}
}
Same result the value doesn't increment
I don't understand why the number stops incrementing if I add other variables and don't understand how to add the leading zeros to the incrementing number. I get the feeling that I am trying to over complicate things!
So to sum up is there a way of getting an auto-incrementing ref that is 6 characters long - in this scenario first form JR0001 second submit JR0002 and so on.
I would really like some pointers on where I am going wrong if possible, I do want to learn but I am obviously missing some key principles.
here is a working solution that uses the "brand new" Utilities.formatString() that the GAS team just added a few days ago ;-)
function OnForm(){
var sh = SpreadsheetApp.getActiveSheet()
var startcell = sh.getRange('A1').getValue();
if(! startcell){sh.getRange('A1').setValue(1);return}; // this is only to handle initial situation when A1 is empty.
var colValues = sh.getRange('A1:A').getValues();// get all the values in column A in an array
var max=0;// define the max variable to a minimal value
for(var r in colValues){ // iterate the array
var vv=colValues[r][0].toString().replace(/[^0-9]/g,'');// remove the letters from the string to convert to number
if(Number(vv)>max){max=vv};// get the highest numeric value in th column, no matter what happens in the column... this runs at array level so it is very fast
}
max++ ; // increment to be 1 above max value
sh.getRange(sh.getLastRow()+1, 1).setValue(Utilities.formatString('JR%06d',max));// and write it back to sheet's last row.
}