Most efficient way to get a column range into a 1D array - arrays

I have a Google form that collects responses in a linked spreadsheet. One of the fields in the form is Username (column B in the sheet). I'm writing a script to notify me when a new user (unique username) makes a submission. To do this, I:
Get all the values in column B except the latest one
Get the latest username
Check if the latest username is in the list of values (do something if not).
Here's what I've got so far:
function isUserNew() {
var spreadsheet = SpreadsheetApp.openById("INSERT ID HERE");
var sheet = SpreadsheetApp.setActiveSheet(spreadsheet.getSheets()[0]);
var last_row_number = sheet.getLastRow()
var penultimate_row_number = (last_row_number-1)
var latest_username = sheet.getRange(last_row_number, 2).getValue()
var usernames = sheet.getRange("B2:B" + penultimate_row_number).getValues();
}
This gives me a 2D array usernames that looks like [[UserA],[UserB],[UserC],[UserC],[UserA],[UserX]]. Then I tried if (usernames.indexOf(latest_username) == -1 but this doesn't work - it only works for a 1D array. I thought of converting the array to 1D using a loop:
for (var i = 0; i < usernames.length; i++) {
Logger.log(usernames[i][0]);
}
This makes the logger correctly log all the usernames I want, but I don't know how to put them into an array.
2 main questions:
How do I get the values into an array instead of the Logger?
Is this the best way to do what I'm trying to do? It seems unnecessary that every time there's a submission, I grab a 2D array, convert to 1D, and compare. The column data is not going to change, it only grows by 1 every time. How will this impact run time as the data grows into the 1000s?

Yep, you could flatten the array and compare with indexOf as you're doing:
var flattened_usernames = [] ;
for (var i = 0; i < usernames.length; i++) {
flattened_usernames.push(usernames[i][0]);
}
But you're better off just doing the check inside the loop:
if (usernames[i][0] === latest_username)
By the way, I assume you know how to grab the username directly from the onFormSubmit event rather than grabbing it from the last row of the sheet. But if not you should learn how to do that!

Instead of:
var usernames = sheet.getRange("B2:B" + penultimate_row_number).getValues();
Do:
var usernames = [].concat(...sheet.getRange("B2:B" + penultimate_row_number).getValues());
This flattens the 2d result of a column into a 1d array. If it's not the shortest way it should be close. I don't know about the efficiency.

Related

Converting 1D array with multiple values into 2D array in google script without Loop in Loop

I'm trying to shorten my script time, as it is reaching the apps script time limit too often (1800s). Therefore, I'm trying to reduce the number of loops the script is performing.
The script goal is to collect all Montecarlo Revenue analysis results, and yo do so it iterates 1000 I (E.g. 1000) times. Each iterations collects the following data points: Total Revenue, # of logos and the same per month.
I've managed to do that through creating a Loop in a Loop (Loopin I for the Montecarlo iterations, and looping J through each data point) and creating a 2D array that later I post in my sheet using SetValues.
for (var I=0; I < Runs; I++) {
MCOutput[I] = new Array(DataLength);
MCOutput[I][0] = I+1;
sheet.getRange(6,18).setValue((I+1)/Runs);
for (var J=1; J<DataLength; J++) {
MCOutput[I][J]=sheet.getRange(5,J+StartCol).getValue();
}
sheet.getRange(Row,StartCol,MCOutput.length,MCOutput[0].length).setValues(MCOutput);
My goal is to reduce the running time, by looping only once and collecting all the data through GetValues. I've managed to do so, but I can't find a way to set these values to a 2D array in the sheet. I'm getting the following error:
Exception: The number of columns in the data does not match the number of columns in the range. The data has 1 but the range has 21.
Here is the script for it:
var MCOutput = [];
for (var I=0; I < Runs; I++) {
MCOutput[I] = new Array(DataLength);
sheet.getRange(6,18).setValue((I+1)/Runs);
sheet.getRange(5,StartCol).setValue(I+1);
MCOutput[I]=sheet.getRange(5,StartCol,1,DataLength).getValues();
}
sheet.getRange(Row,StartCol,I,DataLength).setValues(MCOutput);
I wasn't able to solve it through map or splice, I guess it is because my 1D array has rows and columns of data.
Here are some modification I would suggest.
new Array() can slow down a script and really isn't needed here.
getValues() returns a 2D array so you need to get the first row getValues()[0] of it and push it into the MCOutput array.
sheet.getRange(6,18).setValue((I+1)/Runs); does absolutely nothing that I can see and multiple calls to setValue() can really slow down a script.
you can simple replace the value in MCOutput[I][0] = I+1;
// you are always getting the same row and StartCol or Datalength don't change
let values = sheet.getRange(5,StartCol,1,DataLength).getValues()[0];
let MCOutput = [];
for (let I=0; I < Runs; I++) {
// assuming values is only javascript primitives a shallow copy will do
MCOutput.push(values.map( value => value ));
MCOutput[I][0] = I+1;
}
sheet.getRange(Row,StartCol,I,DataLength).setValues(MCOutput);
Reference:
Best Practices
Array.push()
new Array() vs []

How to create a true new copy of an array in App Script for Google Sheet?

I am having the following problem with App Script in Google Sheet.
I want to make different copies of a row in my sheet base on a table. I want to do something like
input1=[[1,2,"a"]];
input2=[[4,5,"b"],[7,8,"c"]];
function (input1,input2) {
\\ input# is a row, ie. an array with single element, which is another array
\\ The rows input# represent are of equal length
out=[];
copy1=input1[0];//copy1 is a reference to input1[0]
copy2=input1[0];//copy2 is a reference to input1[0]
for (i=0;i<input1.length,i++) {//input1.length is 1
copy1[i]+=input2[0][i];
copy2[i]+=input2[1][i];
}
out.push(copy1,copy2);//copy1=[5,2,a] copy2=[8,2,a]
return out
}
I would expect out to look like
out=[[5,7,"ab"],[8,10,"ac"]];//[[5,2,a],[8,2,a]]
But it doesn't. The output looks like whenever I modified copy1 or copy2, it was input1 itself that was modified.
What is wrong here? How can I create a new array variable, assign its value as equal to an existing array and modify the new array without changing the old? Is it ok to have input arrays that whose elements (of elements) consist of mixed numeric and strings?
Using Slice() to return a copy of an Array
Try it this way:
function myFunction(input1,input2)
{
var input1=[[1,2,"a"]];
var input2=[[4,5,"b"],[7,8,"c"]];
var out=[];
var copy1=input1[0].slice();//slice returns a copy of the array
var copy2=input1[0].slice();
for (var i=0;i<input1[0].length;i++)//looping through all of elements of input1[0];
{
copy1[i]+=input2[0][i];
copy2[i]+=input2[1][i];
}
out.push(copy1,copy2);
Logger.log(out);//out=[[5,7,"ab"],[8,10,"ac"]];
}
For more information on slice look here.
This is a good question. I've struggled with it a few times myself.

How do I incorporate an array to speed up hiding rows?

I have a script that I want to use so that my manager can quickly see which items in the Spreadsheet need parts ordered. The script quickly and easily hides columns containing information not pertinent to ordering parts, and then hides all rows (out of thousands) where the value in column S is FALSE (doesn't need parts ordered). The hiding columns part is almost instant, but the hiding rows part is EXTREMELY slow. I understand that in order to speed it up, the data should be loaded into an array, then the loop will run on the array in memory instead of making many calls to the spreadsheet. I have seen similar questions, but the answers don't seem to explain exactly how to do this. One example I read suggested that this is already using an array, which confused me even more. Any help pointing me in the right direction would be appreciated. Here is the script I'm using:
function showPartsNeeded() {
var sheet = SpreadsheetApp.getActiveSheet();
var data = sheet.getRange('S:S').getValues();
for(var i=1; i< data.length; i++){
if(data[i][0] == false){
sheet.hideRows(i);
}
sheet.hideColumns(2,2);
sheet.hideColumns(5,2);
sheet.hideColumns(8,1);
sheet.hideColumns(10,2);
sheet.hideColumns(13,18);
sheet.hideColumns(47,14);
}
}
I have tried the following to load the rows that have the "Needs Parts" column marked as false into an array, then hide only the rows that are present in the array, but I only get a "Cannot find method getRange()." line 15 error and I don't understand why:
function showPartsNeeded() {
var sheet = SpreadsheetApp.getActiveSheet();
var data = sheet.getRange().getValues();
sheet.hideColumns(2,2);
sheet.hideColumns(5,2);
sheet.hideColumns(8,1);
sheet.hideColumns(10,2);
sheet.hideColumns(13,18);
sheet.hideColumns(47,14);
var temp = [];
for (var i = 1; i< data.length; i++ ) {
if (data[i][19] == false) {
temp.push.data[i];
}
}
if (temp.length > 0 ) {
sheet.hideRows(temp);
}
}
I am fairly sure the sheet.hideRows(temp); line is wrong but I'm still trying to figure out how to use the data in the array with hideRows().
You are looping through thousands of rows and every time you find one row with the condition you call sheet.hideRows(i). Calls to the sheet functions or ranges are very slow and that's why it is recommended to do all the operations in an array and then just insert the changes as one operation in the case of ranges.
In this case you could probably improve your code by grouping consecutive rows that follow the condition and instead of calling sheet.hideRows(i) on each of them, you could call sheet.hideRows(first_row, last_row).
So if you have 10 consecutive rows that have the condition, instead of making 10 calls, you would just make one. eg. sheet.hideRows(20,30);

Google Spreadsheets replace cell range with data minus empty cells

I am making a script allows you to consolidate a todo list after finished items are erased.
I am getting a range, using an if statement to push all non-zero cell values from the specified range to an array, clearing the cell range and then re-pasting the new array (minus the cells with no data) back into the same range. Here is my code: (thanks mogsdad!)
function onOpen()
{
var spreadsheet = SpreadsheetApp.getActive();
var menuItems = [
{name: 'Consolidate To-Do list', functionName: 'consolidatetodolist_'}
];
spreadsheet.addMenu('LB functions', menuItems);
};
function consolidatetodolist_()
{
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheet = ss.getSheets()[0];
var range = sheet.getRange("A2:A19");
var rangeValues = range.getValues();
var todosArray = new Array();
for (var i = 0 ; i < rangeValues.length ; i++ ) {
var row = rangeValues[i];
var cell = row [0];
if (null !== cell && "" !== cell) {
todosArray.push(row);
};
};
Logger.log(todosArray);
range.clearContent();
range.offset(0,0,todosArray.length).setValues(todosArray);
};
Your edit trigger should not be manipulating the menu. You should have an onOpen trigger do that. If you want the consolidation to be automatic, then you could drive THAT from your onEdit.
In your consolidatetodolist_() function, you've got a handful of errors regarding value assignment, Javascript fundamentals.
Using the SpreadsheetApp services to get the value of valueInstance isn't necessary. You've already got it, in rangeValues[i][0]. Note the two indexes; the data stored in rangeValues is a two-dimensional array, even though your range was one column. The first index is rows, the second is columns. (Opposite order from A1Notation.)
if ( range.getLength(valueInstance) !== 0 ) should crash, because range is an instance of Class Range which has no getLength() method. What you intend to check is whether the current cell is blank. Since spreadsheet data can be of three javascript types (String, Number, Date), you need a comparison that can cope with any of them, as well as an empty cell which is null. Try if (null !== cell && "" !== cell)...
When you get to setValues, you will need to supply a two-dimensional array that is the same dimensions as range. First, you need to ensure todosArray is an array of arrays (array of rows, where a row is an array of cells). Easiest way to do that here is to use var row = rangeValues[i]; var cell = row[0];, then later todosArray.push(row);. Since you are clearing the original range first, you could then define a new range relative to it using offset(0,0,todosArray.length).setValues(todosArray).
Other nitpicking:
You've got the idea of using meaningful variable names, good. But instead of valueInstance, cell would make this code clearer.
Use of "magic numbers" should be avoided, because it makes it harder to understand, maintain, and reuse code. Instead of i < 18, for example, you could use i < rangeValues .length. That way, if you modified the size of the range you're consolidating, the for loop would adapt without change.
Declaring var i outside of the for loop. No error with what you've done (although a declaration with no value assigned is bad), and it makes no difference to the machine, but for humans it's clearer to define i in the for loop, unless you need to use it outside of the loop. for (var i = 0; ...

Having trouble understanding multidimensional arrays in as3

Let's say I have some code like:
private function makeGrid():void
{
_grid = new Array();
for(var i:int = 0; i < stage.stageWidth / GRID_SIZE; i++)
{
_grid[i] = new Array();
for(var j:int = 0; j < stage.stageHeight / GRID_SIZE; j++)
{
_grid[i][j] = new Array();
}
}
}
I don't quite understand what's going on. I get that in the first for loop it determines the number of columns needed, and in the second it determines rows, but I don't get why I'm making arrays out of _grid[i] and _grid[i][j].
For instance, _grid[i] = new Array(); get's called 16 times (800px/50px), so that would make 16 arrays right? Why do I need those if the second for loops is already calculating the amount of rows I need?
I'm just going to elaborate on what has already been commented. Let's say that you are creating a 2D grid formed of rows and columns and you wanted to store some sort of data at each "cell" or specified index of the grid.
The first step is to create the first array to hold either the rows or columns (which you choose first doesn't really matter as you can adjust the for loops either way).
The first for loop creates a new row, then in the next inner loop you fill all the columns of that row (if we had chosen columns to be created first then we would fill all the rows of the columns). In this case the inner loop is creating all the columns with another array (making it a 3-dimensional array as mentioned in the comments).
The reason for doing this is for organization and easy look up. If you wanted to see the data stored in the 1st column of the 3rd row it would be as easy as doing _gird[2][0].
Now as to why a 3rd dimension is made as in _grid[i][j] = new Array(); that is specific to what kind of data needs to be stored at that row and column.

Resources