Google Spreadsheets replace cell range with data minus empty cells - google-app-engine

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; ...

Related

Copy and Pasting a Moving Range in Google Sheets

I'm very much still learning the basics of script writing, but I have a rather specific question that I've not been able to find a solution for - perhaps because I might be phrasing it wrong.
Essentially, In sheet MS, I have a range of data (A2:O23) with formulas that I want to stay fixed (as in, always reference the same range in a different sheet).
My aim is, at the run of a script, that this block of formulas will copy and paste from the next empty row (in the same columns [A:O]) and that the previous block (A2:O23) will copy and paste over itself as values.
Then, each time the script runs, it does the same thing, copying the latest block of 22 rows beneath itself and pasting itself as values. Essentially having the effect of building a long list of data with the latest (read lowest) range being the formulas and all above being values.
For example, the results of the first time it runs should be that, A24:O45now show the same formula and that A2:O23 are showing as values only. The next time, A24:O45 would be frozen as values and A46:O67 are now the formulas.
I'm not sure if this requires the use of loops or there is a different way to achieve the effect, but the former is something I've not used and the latter is still a mystery to me.
I realise that this might not be especially clear, but I'm happy to field any questions and greatly appreciate any efforts made to help.
Thanks
If I understand you correctly, you want to:
Copy the last 22 rows of your sheet (columns A to O) to the next 22 rows (formulas included).
Copy/paste to the original range as values only.
If the above is correct, then you can copy and run the following code to the script bound to your spreadsheet (see inline comments for detail on what the code is doing, step by step):
function copyPasteValues() {
var ss = SpreadsheetApp.getActive();
var sheet = ss.getSheetByName("MS"); // Name of your sheet (change if that's necessary)
var lastRow = sheet.getLastRow();
var firstCol = 1; // First column to copy
var numCols = 15; // Number of columns to copy
var numRows = 22; // Number of rows to copy
if (lastRow > numRows) { // Check if the sheet has at least 23 rows with content (otherwise it would give error when copying the range)
var originRange = sheet.getRange(lastRow - numRows + 1, firstCol, numRows, numCols);
var destRange = sheet.getRange(lastRow + 1, firstCol, numRows, numCols);
originRange.copyTo(destRange); // Copying with formulas to next 22 rows
originRange.copyTo(originRange, {contentsOnly:true}); // Copying only values to same range
}
}
The main method to notice here is copyTo, which can be used to copy the data as it is, with formulas included, or values only, by setting the parameter {contentsOnly:true} (copyValuesToRange could be used too).
I hope this is of any help.
function copypastepaste() {
var ss=SpreadsheetApp.getActive();
var sh=ss.getActiveSheet();
var rows=sh.getLastRow();
if(rows>22) {
var rg=sh.getRange(sh.getLastRow()-21,1,22,16);
var vA=rg.getDisplayValues();
var fA=rg.getFormulas();
var nrg=sh.getRange(sh.getLastRow()+1,1,22,16);
nrg.setFormulas(fA);
rg.setValues(vA);
}
}

Pass values in array to a range of cells on Google Spreadsheet

I'm tryin to read in a column of cells its numbers and backgrounng colors ,and copy it to another columuns.
I can read both and put values in arrays, so what I need is pass the values in these arrays to the range that should receive it.
That's what I'm trying, reading values in column A and pass it to column C:
var origin =["A1:A5"]
var destiny=["C1:C5"]
var ColCell = SpreadsheetApp.getActiveSpreadsheet().getRange(origin).getBackgrounds();
var ValCell=SpreadsheetApp.getActiveSheet().getRange(origin).getValues();
// now i have two arrays, ColCell and ValCell,
// that have backgound colors and Values of original
// cells in the range "A1:A5".
I can't figure how to pass values in ColCell and ValCell to ["C1:C5"]. Have anyone had this problem before?
You want to copy the values (background colors and cell values) of "A1:A5" of the active sheet to the range of "C1:C5" of the active sheet using Google Apps Script.
If my understanding is correct, how about this modification?
Modified script:
var origin = ["A1:A5"];
var destiny = ["C1:C5"];
// Retrieve values.
var sheet = SpreadsheetApp.getActiveSheet();
var range = sheet.getRange(origin);
var ColCell = range.getBackgrounds();
var ValCell = range.getValues();
// Set values.
sheet.getRange(destiny).setBackgrounds(ColCell).setValues(ValCell);
References:
setBackgrounds(color)
setValues(values)
If I misunderstood your question and this was not the result you want, I apologize.

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

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.

Store big structure in Matlab

I am fitting a statistical model in matlab using fitglm which returns a structure mdl. I would like to store many such structures in an array of cells to reuse them later but this seems not to work. Here is the code:
models = cell(size(quarterList,1)-lag-1,1);
for i=1:size(quarterList,1)-lag-1
%indicesTemp = find(and(annQuarters(:,2) <= quarterList(i+11,2),annQuarters(:,2) >= quarterList(i,2)));
memberTemp = ismember(annQuarters(:,:), quarterList(i:i+lag,:));
indicesTemp = find(memberTemp(:,2));
fprintf('Perdiod: Q%i %i to Q%i %i - Nb samples: %i \n',annQuarters(i,1),annQuarters(i,2),annQuarters(i+lag,1),annQuarters(i+lag,2),size(indicesTemp,1));
[Xtemp Ytemp] = categorizeVariables(X(indicesTemp,:),Y(indicesTemp,:));
mdl = fitglm(Xtemp,Ytemp-1,'Distribution','binomial', 'Link','logit');
models(i,1) = mdl;
end
Now when I try to assign such structure to a single cell, it works:
temp = cell(1,1);
mdl = fitglm(Xtemp,Ytemp-1,'Distribution','binomial', 'Link','logit');
temp = mdl;
Why is the assignment in the array of cells not working in that case? Any suggestion on how to go around this?
This doesn't work because using models(index) assignment (with ()) assumes that the thing on the right side is a cell. You instead want to use curly brackets which will copy the item on the right (of any type) into the cell array at the specified element.
models{i,1} = mdl;
If you really wanted to use (), you could instead convert the thing on the right to a cell first.
models(i,1) = {mdl};
The reason that your second example (with a scalar cell array) doesn't result in an error is because you aren't putting the output of fitglm into the cell array but rather overwriting the variable temp to point to mdl instead of the cell array.
temp = cell(1,1);
% Check if temp is a cell
iscell(temp)
%// TRUE
mdl = fitglm(Xtemp,Ytemp-1,'Distribution','binomial','Link','logit');
temp = mdl;
% Check if temp is still a cell (it isn't)
iscell(temp)
%// FALSE
All of that aside, you can actually store structs within an array themselves. You don't actually need a cell array unless the fields are different.
for i = 1:N
mdl(i) = fitglm(Xtemp, Ytemp - 1, 'Distribution', 'binomial', 'Link', 'logit');
end

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);

Resources