Spreadsheet formula taking too long to run - script advise - google-apps-script

The following formula is taking far to long to run.
= TRANSPOSE (
IFERROR (
INDEX (
FILTER(Students!B:B;
REGEXMATCH(Students!B:B; C50),
REGEXMATCH(Students!B:B; B50),
REGEXMATCH(Students!C:C; F50)
));"NO MATCH"
))
Any suggestions on the coding would be great, as I know very little programming.
Thanks
T

Here is a custom function that can replace the formula you're using. For example:
=listStudents(C50,B50,F50)
If used that way, you'll still have constant recalculation, but it should be much faster than the regex tests. Alternatively, the same function could be invoked from a menu item, and used to populate a given target range in the sheet, thereby avoiding automatic recalculation altogether.
Code:
/**
* Custom spreadsheet function to produce a list of names of
* students that match the given criteria.
*/
function listStudents( givenName, surname, employer ) {
var matches = []; // matching students will be placed in this array
var HEADERS = 1; // # rows of header info at top of sheet
var FULLNAME = 1; // Column containing full names (B)
var EMPLOYER = 2; // employers (C)
// Array filter function - returns true if conditions match
function test4match( row ) {
return ( row[FULLNAME].indexOf(givenName) !== -1 &&
row[FULLNAME].indexOf(surname) !== -1 &&
row[EMPLOYER].indexOf(employer) !== -1)
}
var sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('Students');
var range = sheet.getDataRange();
var data = range.getValues().slice(HEADERS); // All data from sheet, without headers
var filteredData = data.filter(test4match); // Get matching rows
for (var i=0; i<filteredData.length; i++) {
matches.push(filteredData[i][FULLNAME]); // Then produce list of names
}
return [matches]; // Return a 2-d array, one row
}

Related

Google Apps Script - Better way to do a Vlookup

I am doing a kind of VLOOKUP operation in a column with about 3K cells. I am using the following function to do it. I commented on what the code is doing in the function, but to summarize:
It creates a map from values to search for from a table with metadata
It iterates each value of a given range, and searches for coincidences in the previous map
If coincidences are found, it uses the index to capture the second column of the metadata table
Finally, sets the value captured in another cell
This is the code:
function questions_categories() {
var ss = SpreadsheetApp.getActive();
var sheet = ss.getSheetByName("data_processed");
// get metadata. This will work as the table to look into
// Column B contains the matching element
// Column C contains the string to return
var metadata = ss.getSheetByName("metadata").getRange('B2:C').getValues()
// Just get the different values from the column B
var dataList = metadata.map(x => x[0])
// Used to define the last cell where to apply the vlookup
var Avals = sheet.getRange("A1:A").getValues();
var Alast = Avals.filter(String).length;
// define the range to apply the "vlookup"
const questions_range = sheet.getRange("Q2:Q" + Alast);
forEachRangeCell(questions_range, (cell) => {
var searchValue = cell.getValue();
// is the value to search in the dataList we defined previously?
var index = dataList.indexOf(searchValue);
if (index === -1) {
// if not, throw an error
throw new Error('Value not found')
} else {
// if the value is there, use the index in which that appears to get the value of column C
var foundValue = metadata[index][1]
// set the value in two columns to the right
cell.offset(0, 2).setValue(`${foundValue}`);
}
})
}
forEachRangeCell() is a helper function to iterate through the range.
This works very well, but it resolves 3-4 cells per second, which is not very efficient if I need to check thousands of data. I was wondering if there is a more performant way to achieve the same result.
To improve performance, use Range.setValues() instead of Range.setValue(), like this:
function questions_categories() {
const ss = SpreadsheetApp.getActive();
const source = { values: ss.getRange('metadata!B2:C').getValues() };
const target = { range: ss.getRange('data_processed!Q2:Q') };
source.keys = source.values.map(row => row[0]);
target.keys = target.range.getValues().flat();
const result = target.keys.map(key => [source.values[source.keys.indexOf(key)]?.[1]]);
target.range.offset(0, 2).setValues(result);
}
See Apps Script best practices.

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
}

Dependent Data Validation in Google Apps Script

I need to add Data Validation to a range of cells based on the State chosen in the previous cell.
I've run this code which works ok for a limited amount of data but is not working on the actual spreadsheet.
function onEdit(e) { // Runs automatically when the user edits the sheet
var value = e.value; // Get the new value entered into the edited cell
var col = e.range.getColumn(); // Get the column number of the edited cell
var row = e.range.getRow(); // Get the row number of the edited cell
if (col == 6 && row >= 10 && row <= 854) { // Make sure that the edited cell is part of the table
var sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('Hoja 1');
var sheet2 = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('Localidades'); // Get the sheet that has the table with the list of cities
var dropdownData = sheet2.getRange("A1:X594").getValues(); // Get the table with the list of cities.
var listOfCountries = dropdownData[0]; // The top row is the list of countries
var countryColumn = listOfCountries.indexOf(value); // Find the column in which the country name appears
if (countryColumn != -1) { // If the new country name is in the list
var cityList = [];
for (var dataRow = 1; dataRow < dropdownData.length; dataRow++) {
cityList.push(dropdownData[dataRow][countryColumn]);
}
var cityCell = sheet.getRange(row, col + 1);
cityCell
.clearDataValidations() // Remove any existing data validation in the target cell
.clearContent(); // Clear the cell
var rule = SpreadsheetApp.newDataValidation().requireValueInList(cityList, true).build();
cityCell.setDataValidation(rule);
}
}
}
I've debugged and it gets the CityList all right so don't know what's the problem really. Any help??
There is a limited amount of allowed dropdown options
Through testing you can easily verify that it is 500 options.
So, if you hardcode "A1:X594" you are above the limit.
However, for most of the provinces in your data the amount of options is less than 594 and your array contains many empty values.
You can remove all duplicates inlcuding emoty values by filtering, e.g.
cityList = cityList.filter(function (value, index, self) {
return self.indexOf(value) === index;
})
As for the provinces where you have many entries (e.g. Buenos Aires), maybe you can try to subdivide it into smaller regions so you have less than 500 dropdown options per dropdown?

Update named range automatically

I've been having a hard time trying to figure this out. I realize this is perhaps more basic than usual for those who follow the GAS tag, however any help much appreciated.
If I'm breaking up my bigger task into component parts, my goal right now with this question is to update several named ranges automatically.
There is a tab on the spreadsheet called "DataImport". DataImport has 10 columns all 1000 rows long. There is a named range for each column e.g. cabbages (A2:A1000), dogs (B2:B1000) etc etc.
There is a script attached to a new menu item "Update Data" that when selected imports a csv file into DataImport tab meaning that the length of the data set will grow.
How can I tell the sheet to update each named range to be the length of data? So if the original named range "cabbages" was A2:A1000 and following an update the data now actually goes as long as A2:A1500, how would I tell the sheet to update the range cabbages?
I found a snippet of code online and started to fiddle with it:
function testNamedRange() {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var range = ss.getRange('DataImport!A:A');
var data_len = range.length;
SpreadsheetApp.getUi().alert(data_len); // "alert just gives "undefined"
ss.setNamedRange('TestRange', range);
var rangeCheck = ss.getRangeByName('TestRange');
var rangeCheckName = rangeCheck.getA1Notation();
}
My thinking was if I could just get the length of data following an update using the custom menu function, I could then use setNamedRange() to update cabbages range.
I'm really lost and I imagine this is simpler than I'm making it out to be.
How can I update the named range cabbages to be the length of data in UpdateData column A?
Edit: IMPORTANT
Use INDIRECT("rangeName") in formulas instead of just rangeName.
The only way to extend the range programmatically is by removing it and then adding it back with a new definition. This process breaks the formula and returns #ref instead of the range name. This should be an unnecessary work around. if you agree please star and the issue tracker at: https://code.google.com/p/google-apps-script-issues/issues/detail?id=5048
=sum(indirect("test1"),indirect("test3"))
Emulates open ended named ranges by checking to see that the last row in the named range is the same as the last row in the sheet. If not, adjusts the named range so the last row in the named range is the same as the last row in the sheet.
should probably be used with on open and on change events.
function updateOpenEndedNamedRanges() {
var ss = SpreadsheetApp.getActiveSpreadsheet();
// names of open-ended ranges
var openEndedRangeNames = ["test1", "test2", "test3", "s2test1" ];
for(i in openEndedRangeNames) {
var rName = openEndedRangeNames[i];
try{
var r = ss.getRangeByName(rName);
}
catch(err) {
GmailApp.sendEmail("me#gmail.com",
rName + " -- Cannot find",
"Trying to update open-ended ranges after rows added. \n"
+ "Unable to find range name-- "+ rName
+ " -- in ss ( " + ss.getName() + " ) "
+ "\n If it is not needed please remove it "
+ "from array \n openEndedRangeNames[] \n in the function \n"
+ "updateOpenEndedNamedRanges()");
continue;
}
var rlr = r.getLastRow();
var s = r.getSheet();
var slr = s.getMaxRows();
if(rlr==slr ) continue;
var rfr = r.getRow();
var rfc = r.getColumn();
var rnc = r.getNumColumns();
var rnr = slr - rfr + 1;
ss.removeNamedRange(rName);
ss.setNamedRange( rName, s.getRange(rfr, rfc, rnr, rnc ));
}
}
function ssChangeEvent(change) {
// changeType (EDIT, INSERT_ROW, INSERT_COLUMN, REMOVE_ROW,
// REMOVE_COLUMN, INSERT_GRID, REMOVE_GRID, or OTHER)
switch(change.changeType) {
case "INSERT_ROW":
updateOpenEndedNamedRanges();
break;
default:
Logger.log(change.changeType + " detected. No action taken ");
}
}
Setup ssChangeEvent(change) to run when rows are added
Resources>this projects triggers
Offering this function I wrote to handle dynamic resize of named ranges:
function resizeNamedRange(rangeName, addRows, addCols) {
/* expands (or decreases) a range of a named range.
rows and columns to add can be negative (to decrease range of name). Params:
rangeName - name of range to resize.
addRows - number of rows to add (subtract) from current range.
addCols - number of columns to add (subtract) from current range.
Call example: resizeNamedRange("Products",1,0);
*/
var sh = SpreadsheetApp.getActiveSpreadsheet();
try {
var oldRange = sh.getRangeByName(rangeName);
var numRows = oldRange.getNumRows() + addRows;
var numCols = oldRange.getNumColumns() + addCols;
if (numRows < 1 || numCols <1) {
Logger.log("Can't resize a named range: minimum range size is 1x1.");
return;
}
sh.setNamedRange(rangeName, oldRange.offset(0,0,numRows, numCols));
} catch (e) {
Logger.log ("Failed resizing named range: %s. Make sure range name exists.", rangeName);
}
}
Maybe I'm missing something, but the function below takes a rangename and the range that it should contain. If the rangename already exists it updates the range to the passed value. If the rangename doesn't exist, it creates it with the passed range values.
Also with regard to the "#REF!" problem in the sheet. You can do a find and replace and tick the box for "find in formulas". Put "#REF!" in find and the named range name in the replace box. This assumes only one named range was deleted and that there were no other unrelated #REF! errors. This approach helped me fix a spreadsheet with the error spread over 8 different sheets and 20+ formulas in just a few minutes.
/**
* Corrects a named range to reflect the passed range or creates it if it doesn't exist.
*
* #param {string} String name of the Named Range
* #param {range} Range (not string, but range type from Sheet class)
* #return {void || range} returns void if Named Range had to be created, returns NamedRange class if just updated. This needs improvement.
* #customfunction
*/
function fixNamedRange (name, range) {
var i = 0;
var ss = SpreadsheetApp
.getActiveSpreadsheet();
var ssNamedRanges = ss.getNamedRanges();
for (i = 0; i<ssNamedRanges.length && ssNamedRanges[i].getName() != name; i++) {};
if (i == ssNamedRanges.length) {
return (ss.setNamedRange(name, range));
} else {
return (ssNamedRanges[i].setRange(range));
}
}
I found the solution!
I have a cell with a drop down list with all the clients that the company has registered on the system, if the name we enter does not appear on the list, then function newClient executes. Basically we use the SpreadsheetApp.getUi() in order to save the new information. Once we have introduced the client data, function creates a new row on the client's sheet and enters the information from the prompts on the last row. Once done, updates the drop down list automatically.
The real function is inside of a big function that calls newClient if it's needed so the real one would be newClient(client, clients), on the example I put the variables in order to make it easier.
I hope it works!
function newClient() {
var ss = SpreadsheetApp.getActive().getSheetByName('Clients'); // Sheet with all the client information, name, city, country...
var client = 'New company';
var clients = ss.getRange('A2:A').getValues();
var ui = SpreadsheetApp.getUi();
ui.alert('Client '+client+' does not exist, enter the next information.');
var city = ui.prompt('Enter city').getResponseText();
var country = ui.prompt('Enter country').getResponseText();
client = client.toUpperCase();
city = city.toUpperCase();
country = country.toUpperCase();
ui.alert('Here is the information you entered about '+client+':'+'\n\n'+'City: '+city+'\n\n'+'Country: '+country)
ss.insertRowAfter(ss.getLastRow()); // Insert a row after the last client
ss.getRange('A'+(clients.length+2)).setValue(client); // Let's suppose we have 150 clients, on the first row we have the titles Client, City, Country, then we have the 150 clients so the last client is on row 151, that's why we enter the new one on the 152
ss.getRange('B'+(clients.length+2)).setValue(city);
ss.getRange('C'+(clients.length+2)).setValue(country);
var namedRanges = SpreadsheetApp.getActive().getNamedRanges(); // We get all the named ranges in an array
for (var i = 0; i < namedRanges.length; i++) {
var name = namedRanges[0].getName();
if (name == 'Clients') { // All the clients are stored on 'Clients' named range
var range = ss.getRange('A2:A'); // Update the range of the 'Clients' named range
namedRanges[i].setRange(range);
}
}
ui.alert('Client created, you can find it on the drop down list.');
}

Google App Scripts(spreadsheet) - consolidate data into one sheet

Here is the set up
We have a contest with all employees based on project scores. Each project has two categories of employees(4 employees per category) and two scores(one for each category of employee).
I need to grab all the scores for the employees and output it into a spreadsheet. The following spreadsheet has misc. columns removed
Sheet Explanation
The sheet labeled "Example data" is the source we will be pulling data from
We need to match Editor and Editor Score
We need to match Webmaster and webmaster score
The sheet labeled "Example output" is what I want to be generated in another spreadsheet named "Contest Result" with the sheet name from the source sheet(They are named by date ranges).
We need to compile each employee by the categories
We need to compile all scores to the row for a singular employee
I had found this Removing Duplicates Article that seemed to at least process the information and compare it in a manner that I think this can be done, but am failing to make it work due to being inexperienced.
Did not know what Transpose was till someone commented :)
Here is the solution in another article for how to pull it off with Google Apps Script and with using the spreadsheet option.
How to split and transpose results over 2 columns
Here is the actual code I used to make it work(it is a little horrible but I tried) suggestions on how to improve this?:
function createScoreSheet() {
// Get Source spreadsheet
var source = SpreadsheetApp.getActive();
var sourceSheet = source.getActiveSheet();
var SourceActivate = sourceSheet.activate();
// Set Sheet Name
var sheetName = sourceSheet.getSheetName();
// Set Values to transpose and combine
var sourceEditor = sourceSheet.getRange("C1:C51");
var sourceWeb = sourceSheet.getRange("D1:D51");
var editorScores = sourceSheet.getRange("L1:L51");
var webScores = sourceSheet.getRange("K1:K51");
// Used to create a new spreadsheet
var sheetNameNew = sheetName + " Scores";
var createSheet = SpreadsheetApp.getActive().insertSheet(sheetNameNew,0);
var targetSheet = source.getSheetByName(sheetNameNew);
var totalScore = 1;
// s is the the counter we use to stick values into the rows
var s = 3;
// n is the the counter we use to stick values into the columns
var n = 1;
// loops through twice, once for the editor values, once for the webmaster
for (var j = 1; j<3; j++) {
if (j == 1) {
// grab values for the editors and copy to new sheet
sourceEditor.copyTo(targetSheet.getRange("A1"));
editorScores.copyTo(targetSheet.getRange("B1"));
// delete the header row then sort the column ASC by default
targetSheet.deleteRow(n);
targetSheet.sort(1);
// Find the last value to see how many scores we have
var lastRow = targetSheet.getLastRow();
}
if (j == 2) {
// grab values for the webmasters and copy to new sheet
sourceWeb.copyTo(targetSheet.getRange(n,1));
webScores.copyTo(targetSheet.getRange(n,2));
// delete the header row then sort the column ASC by default
targetSheet.deleteRow(n);
lastRow = targetSheet.getLastRow();
targetSheet.getRange(n,1,lastRow,2).sort(1);
lastRow = targetSheet.getLastRow();
}
// this loop will check to see if the value of the cell is equal to the next on the list and move the score
for (var i = 1; i<lastRow+1; i++) {
// Grab the name of the current row and the next
var firstName = targetSheet.getRange(n,1).getValue();
var nextName = targetSheet.getRange(n+1,1).getValue();
// Grab the scores
var oldScore = targetSheet.getRange(n+1,2);
var newScore = targetSheet.getRange(n,s);
// Loop to check to see if the firstname is blank and break to find the next value
if (firstName === "") {
break;
}
// checks to see if name is equal to the next then shifts then copies the score and adjust the horizontal position
if (firstName == nextName) {
totalScore = oldScore + newScore;
oldScore.copyTo(newScore);
s = s+1;
targetSheet.deleteRow(n+1);
}
// resets horizontal position for the score and increases the row
else {
s=3;
n=n+1;
}
}
// kills remaining rows
targetSheet.deleteRows(n,37);
}
}
I would do it like this:
If you want to generate the names automatically as well, then write this to the output sheet A1:
=unique('Example Data'!B2:B) - This function simply generate the editor names to the A2-A5 cells.
Now write this to the B2 cell:
=transpose(filter('Example Data'!E:E,'Example Data'!B:B=A2)) - This function filters the editor points according to the given name in the beginning of the row (in this case its A2). Then transposes the result in a horizontal form. To get the result for the other rows, simply populate this formula down.
I think you can find out the rest. :)
Hope it helps.