Iterate through a range and set values - google-apps-script

In Google Sheets, I'm trying to input numbers in sequential order after prompting the user for the starting range and number of rows from that range. If the user types the cell "C5" for example and inputs 5 rows, the result should be from cells C5 - C10 (1, 2, 3, 4, 5)
I found a way to input the same value in all the rows but I can't seem to iterate through the range the user gives and set different values in each cell.
function placeInCell() {
var mySheet = SpreadsheetApp.getActiveSpreadsheet();
var sheet = mySheet.getSheetByName('Second');
var ui = SpreadsheetApp.getUi();
var result = ui.prompt('What cell to start with?',
ui.ButtonSet.OK);
var result2 = ui.prompt('How many cells?', ui.ButtonSet.OK);
// Process the user's response.
var button = result.getSelectedButton();
var cell = result.getResponseText();
var button2 = result2.getSelectedButton();
var numCells = result2.getResponseText();
var cellRange = sheet.getRange(cell);
cellRange = cellRange.offset(0, 0, numCells);
//iterate through the range given by the user and set values
//in each row
for (var i = 0; i < numCells; i++){
cellRange.setValue(i);
};
}

Try it this way
var values = cellRange.getValues();
for (var i = 0; i < numCells; i++) {
values[i][0] = i;
}
cellRange.setValues(values);

In Google Apps Scripts, a Range is addressed with indices 1..n, unlike 0..n-1 for e.g. an Array.
Also, a Range cannot be addressed with [] like an Array: you need to use methods such as offset.
This is why, as you commented, "cellRange[i][0].setValue(i) throws an error. It says cellRange[i][0] is undefined".
Try #SpiderPig's solution, it should work for you. It uses the general good practice or minimizing the number of server-side calls such as getRange, setValue or offset, rather working locally (client-side) on Arrays instead: first use myRange.getValues, then work on your Array, finally myRange.setValues.

Related

Google Apps Script - Conditionally retrieve data from other Google Sheet to Overview sheet

To explain the larger context: there are several forms which generate different sheets. I'm looking for a way to conditionally copy some of the responses sheet to a seperate "Overview" document. Code-wise, I had some ideas for the Overview document, but stranded near the start.
My method was going to be to build functions for all the information I want to retrieve, such as date of birth (example in code block below), date of submission and phone number, when I click on a button. The information may only be copied if the first and surname match the ones in the Overview. The order of the sheets in different docs are not the same and the column length is continually in flux. Furthermore, the amount of rows in the Overview doc is different than the form submission sheets.
In other words: if Anne Annenson would be the twenty-first respondent to a form, I want that information in the overview sheet where they are the first person.
function getDobs() {
var targetSpreadsheet = SpreadsheetApp.getActive();
var targetSheet = targetSpreadsheet.getSheetByName("Overview");
var targetFirstNameCheck = targetSpreadsheet.getRange("A4:A");
var targetSurnameCheck = targetSpreadsheet.getRange("B4:B");
var sourceSpreadsheetDob = SpreadsheetApp.openById("...");
var sourceDob = sourceSpreadsheetDob.getSheetByName("Form responses 1");
var sourceFirstNameCheckDob = sourceSheetDob.getRange("C2:C");
var sourceSurnameCheckDob = sourceSheetDob.getRange("D2:D");
var sourceRangeDob = sourceSheetDobConsent.getRange("E2:E");
if (sourceFirstNameCheckDob==targetFirstNameCheck && sourceSurnameCheckDob==targetSurnameCheck){ //then I want to copy the data
var sourceData = sourceRangePronouns.getValues();
var targetRangeDob = targetSheet.getRange("C4:C");
}
else (//I want it to leave the cells alone, so any text or formatting that might have been put in manually is still there.){
}
}
I would like for the responses to remain in the form response sheets as well.
Any thoughts?
Cooper already explained all the things you need in the comments. And below is what your code would look like following Cooper's comments.
Code
function getDobs() {
var targetSpreadsheet = SpreadsheetApp.getActive();
var targetSheet = targetSpreadsheet.getSheetByName("Overview");
var targetLastRow = targetSheet.getLastRow();
// range equivalent to A4:B
var targetNamesCheck = targetSheet.getRange(4, 1, targetLastRow - 3, 2).getValues();
// tested in same spreadsheet, change "targetSpreadsheet" to openById on your actual script
var sourceSpreadsheetDob = targetSpreadsheet;
var sourceDob = sourceSpreadsheetDob.getSheetByName("Form responses 1");
var sourceLastRow = sourceDob.getLastRow();
// range equivalent to C2:D
var sourceNamesCheckDob = sourceDob.getRange(2, 3, sourceLastRow - 1, 2).getValues();
// range for data to be copied (E2:G in my sample data)
var sourceRangeDob = sourceDob.getRange(2, 5, sourceLastRow - 1, 3).getValues();
var output = [];
targetNamesCheck.forEach(function (targetNames) {
// search sourceNamesCheckDob for targetNames
var index = searchForArray(sourceNamesCheckDob, targetNames);
// if targetNames is in sourceNamesCheckDob, save the data on that row for later
if (index > -1)
output.push(sourceRangeDob[index]);
// append blank cells if data is not found
else
output.push(new Array(sourceRangeDob[0].length));
});
// if there were names that were found, write the data beside the targetNames
if (output.length > 0) {
targetSheet.getRange(4, 3, output.length, output[0].length).setValues(output);
}
}
// function to search the array for the object
function searchForArray(haystack, needle) {
var i, j, current;
for(i = 0; i < haystack.length; ++i) {
if(needle.length === haystack[i].length) {
current = haystack[i];
for(j = 0; j < needle.length && needle[j] === current[j]; ++j);
if(j === needle.length)
return i;
}
}
return -1;
}
Overview:
Form responses 1:
Overview after running getDobs:
EDIT:
Since there are no methods that includes the apostrophe when the cell value is being fetched, easiest way is to have the sheets identify the phone number as text so it won't remove the 0 in the beginning. I've thought of 3 ways to have the 0 included in the output:
Add the apostrophe manually on the specific output column via script
Add dashes on the number so it is treated as text (09395398314 -> 093-9539-8314) (just an example, not sure if that is the right format)
Format the output column into number -> plain text instead of number -> automatic
I prefer formatting the output column as that will be the fastest and easiest thing to do.
Format:
Output:
Note:
This function will fill up rows where names in Overview are present in Form responses 1.
References:
Check whether an array exists in an array of arrays?
javascript create empty array of a given size

Google Script For Loop Issues

So this question seems to be beaten to death on the boards, but with all my reading and googling, I just can't figure out what I'm doing wrong.
I'm trying to adapt the code from this link
How to loop a google spreadsheet column values and set result in column B?
Below is what I've adapted it to
function EquationIterationTest(){
var s = SpreadsheetApp.getActiveSpreadsheet();
var sht = s.getSheetByName('Heath, OH')
var drng = sht.getDataRange();
var rng = sht.getRange(13, 2, 111, 1)
//.getRange(13, 2, drng.getLastRow()-1, drng.getLastColumn())
var rngA = rng.getValues();//Array of input values
Logger.log(rngA);
for(var i = 0; i < rngA.length; i++) {
if(rngA[i][0] === 'subtotal'){
rng.offset(0,3).setFormula('=iferror(sum(filter(Invoices!$E:$E,Invoices!$F:$F=$B14,Invoices!$A:$A=$C$2)))');
}
else{
rng.offset(0,3).setValue('Dumb');
}
}
}
When I run this, rngA does get the first column of values (which in this instance starts at B13) however, it will not input the formula in the third column of values. Instead it moves right through the first if statement and executes the else statement. The only thing I can think is there's something wrong either with my if statement or my array.
I tried setting if(rngA[i][0] === 'subtotal') to if(rngA[i][1] === 'subtotal'), but that still returned "dumb" on every line.
Any help would be appreciated so I can stop being "dumb"!
Here's the link to my sheet.
https://docs.google.com/spreadsheets/d/1cDkwWThXDTssH89gJX7W1zKzsW86oLXO-FPfAIJvc-g/edit?usp=sharing
Thanks
The problem is not in your if condition, although if you use three equal signs you are making a strict comparison, so subtotal in your case should start with capital letter.
That said, your problem is happening when you assign a value or formula to rng.offset(0,3), because the result of that expression is a range with the same size as rng but offset 3 columns to the right. You can verify this by using: Logger.log(rng.offset(0,3).getA1Notation());, thus whenever you assign a value or formula there you are assigning it to the whole offset rng. Not what you want right?
You should use offset() from a single cell in your case, not a whole range.
Your function could be simplified to something like the following:
function EquationIterationTest(){
var s = SpreadsheetApp.getActiveSpreadsheet();
var sht = s.getSheetByName('Heath, OH')
var rng = sht.getRange(13, 2, 111, 1)
for(var i = 1; i <= rng.getNumRows(); i++) {
var cell = rng.getCell(i,1);
if(cell.getValue() === 'Subtotal'){
cell.offset(0,3).setFormula(
'=iferror(sum(filter(Invoices!$E:$E,Invoices!$F:$F=$B14,Invoices!$A:$A=$C$2)))'
);
}
else{
cell.offset(0,3).setValue('Dummy');
}
}
}
So I want to make sure I put this here because while the answer above was previously correct, I figured out a much faster way to do it using arrays. This function checks against the data range for a value (in this case "Subtotal") and then appends the equation to rows that that do not contain that value. It is easy to make it compare against the value though by changing the operator from != to ==.
function NewIterationTest(){
var ss = SpreadsheetApp.getActiveSpreadsheet();
var activeSheet = ss.getActiveSheet();
var data = activeSheet.getRange(13, 2, 112, 1).getValues();
for (var i = 0; i < data.length; i++){
var rowData = data[i];
var checkData = data;
var row = checkData[i];
var colB = row[0];
if(colB != 'Subtotal'{
activeSheet.getRange(13 + i, 5).setFormula('=iferror(sum(filter(Invoices!$E:$E,Invoices!$F:$F=$B14,Invoices!$A:$A=$C$2)))');
}
}
}
However, if anyone could tell me how to also eliminate compare against whether or not a the text is bold, that would be helpful. Not sure it can be done though since it's pulling against the array.

Copy active cell to other cells containing string

In Google Sheets I'm trying to create a script that will take the value from the active cell and paste that value to any cell in Column B containing the string "HR". Any ideas?
This isn't too bad; you just have to wrap your head around a few concepts from Apps Script and Javascript to make it efficient. But first let's start with the naive approach!
function firstTry() {
var activeSheet = SpreadsheetApp.getActiveSheet(); // whatever is open
var activeCell = SpreadsheetApp.getCurrentCell(); // this is a single-cell range
var activeCellValue = activeCell.getValue(); // could be a string, number, etc
// Now let's look in column B for stuff to change
for (var i = 1; i <= activeSheet.getLastRow(); i++) {
var cell = activeSheet.getRange("B" + i);
var val = cell.getValue();
var valStr = String(val); // We could have gotten a number
if (valStr.indexOf("HR") != -1) {
cell.setValue(activeCellValue);
}
}
}
This will probably work, but isn't too efficient: each call to getValue() or setValue() takes some time. It'd be better to just get all the values at once, and then paste back a modified Column B when we're satisfied:
function improvement() {
var activeSheet = SpreadsheetApp.getActiveSheet(); // whatever is open
var activeCell = SpreadsheetApp.getCurrentCell(); // this is a single-cell range
var activeCellValue = activeCell.getValue(); // could be a string, number, etc
// Now let's look in column B for stuff to change
var rowsWithData = activeSheet.getLastRow() - 1;
var colBRange = activeSheet.getRange(1, // start on row 1
2, // start on column 2
rowsWithData, // this many rows
1); // just one column
// Let's get the data as an array of arrays. JS arrays are 0-based, btw
var colBData = colBRange.getValues();
for (var i = 0; i < colBData.length; i++) {
var val = colBData[i][0]; // row i, first column
var valStr = String(val); // We might have gotten a number
if (valStr.indexOf("HR") != -1) {
colBData[i][0] = activeCellValue; // modify copied data
}
}
// Lastly, write column B back out
colBRange.setValues(colBData);
}
You could go further with a fancy filter function instead of looping over the data explicitly, but that starts to get less clear.
Caveats as the OP points out in comments below, blindly calling setValues like this will pave over any formulas you have. This would have been no big deal, except that this includes hyperlinks. You could get really involved by calling getFormulas in parallel with getValues and then decide whether to call setValue or setFormula depending on the original contents of each cell.

How do I have Google Sheets automatically sort my rows based on the value in one of my columns?

I am working on a Google Sheets document(Link is at the end of this paragraph) which is meant to track accessories that people borrow. I have written a formula which updates the status of the entry as the following options: Good, Overdue, Returned, Returned Late, and Missing Info. My goal is to have the rows rearrange everyday to put the overdue items first, good status entries second, missing info third, and returned items last. I was able to something similar by using the built-in data filter. To do this I went to Data->Create Filter->Click on symbol on bottom right corner of status column->Sort Z-A. However this solution is manual as it isn't possible(to my knowledge) to run the filter function using the app script. Additionally, the order it sorts the rows isn't exactly what I need. I have written the following app script function to try to counter that however it doesn't seem to be working as imagined.
function swap(i, target, correctOrd){
var link = decVar();
var temp = 0;
temp = link.mainSheet.getRange(i, 1, 1, link.col).getValues();
var oneRowCopy = link.mainSheet.getRange(i,1,1, link.col);
var targetRow = link.mainSheet.getRange(target,1,1, link.col);
oneRowCopy.copyTo(targetRow);
temp.copyTo(targetRow);
}
function reArrange(){
var correctOrd = [].concat.apply([], getStatus());
var link = decVar(); var temp = 0;
for(var i = 2, j = 0; i<correctOrd.length+1; i++, j++){
if(i != correctOrd[j]){
swap(i, j, correctOrd);
correctOrd[j] = i;
}
}
}
The rest of the code can be found in the script part of the document shared.
https://docs.google.com/spreadsheets/d/15qaNHuMoVyEJmwR5pL9ruPFDeiNR60cKWLKxnfjaWko/edit?usp=sharing
Please let me know if you have any ideas on how something like this could be done or what I'm doing wrong with my code.
Thank you!
You need to define a custom comparator for the Array.prototype.sort() method. Your comparator needs to return negative for when the first object comes before the second object, 0 when the two are interchangeable, and positive when the first comes after the second object.
To avoid a lot of nasty case considerations, this can be easily done by defining a "lookup table" which returns a numeric value for a given possible input value:
// Comparison object. Objects to come first should be ranked lower.
var ordering = {
"Good": 5, // Will come last.
"Missing Info": 2,
"Overdue": 1, // Will come first.
"Returned": 3,
"Returned Late": 4,
};
var compareIndex = 1; // Column B data, for example
var sheetData = sheet.getRange(2, 1, sheet.getLastRow() - 1, sheet.getLastColumn()).getValues();
sheetData.sort(function (row1, row2) {
// If the compare value is not found in the comparison object, or is convertible to "false" (e.g. 0)
// then the "|| 400" portion makes it default to a value of 400.
var v1 = ordering[row1[compareIndex]] || 400;
var v2 = ordering[row2[compareIndex]] || 400;
return (v1 - v2);
});
sheet.getRange(2, 1, sheetData.length, sheetData[0].length).setValues(sheetData);
By changing the values in the comparison object, you can change the order in which the rows are sorted. If you wanted "Good" ratings to come first, you would want the value of "Good" to be the lowest.
See also related questions like this one.
Although inbuilt spreadsheet functions can be slow compared to Java script array functions, This custom built function can be used for your test case:
function OverdueGoodMissingReturned() {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sh = ss.getSheetByName("Main");
var lr = sh.getLastRow();
var lc = sh.getLastColumn();
var irng = sh.getRange(1,9,lr,1).getValues(); //get col I (StatusColumn)
var irngF = irng.map(flatten); //Flatten the 2D array
function flatten(e) {return e[0];}
var lr = irngF.indexOf(''); //Find actualLast Row (This is needed,because you filled all the rows with Data Validations)
var rng = sh.getRange(2,1,lr-1,lc); //getWholeRange
rng.sort(9); //Sort by the status Column 9,Default Sort is Good,Missing,Overdue,Returned
var rv = rng.getValues();
var irng = sh.getRange(1,9,lr,1).getValues();
var irngF = irng.map(flatten);
var O1 = irngF.indexOf('Overdue')+1; // Find first overdue
var O2 = irngF.lastIndexOf('Overdue')+1; //Find Last overdue
var mv = sh.getRange(O1+':'+O2); //getRange to move
sh.moveRows(mv,2); //Move Overdue to Top
}
Note: A lot of getRange calls could be cutoff ,If you don't fill the empty rows with data validations. rng.sort could also be cut short to sheet.sort()

How can I store a range of cells to an array?

If I have a list of data in cells A1:A150 (but the amount can vary), is there a way to push that into an array without looking at each cell individually to determine if it is empty? I exceed my execution time by doing this and I need a faster way to store the data and stop when it hits an empty cell.
Below is how I currently do it:
for (var i = 1; i < 500; i++) {
if(datasheet.getRange("A" + i).getValue() == ""){
break;
}
else{
addedlist_old.push(datasheet.getRange("A" + i).getValue())
}
If you're using only one column, I'd suggest:
// my2DArrayFromRng = sh.getRange("A2:A10").getValues();
var my2DArrayFromRng = [["A2"],["A3"],["A4"],["A5"],[],[],["A8"],["A9"],[]];
var a = my2DArrayFromRng.join().split(',').filter(Boolean);
The methods .join() and .split(',') together convert the 2D array to a plain array (["A2","A3","A4","A5",,,"A8","A9",]).
Then the method .filter(Boolean) strips the empty elements. The code above returns [A2, A3, A4, A5, A8, A9].
Try this:
var sheet = SpreadsheetApp.openById(SHEET_ID).getSheetByName(SHEET_NAME);
var lastRow = sheet.getLastRow();
var data = sheet.getRange(1, 1, lastRow, 1).getValues(); //getRange(starting Row, starting column, number of rows, number of columns)
for(var i=0;i<(lastRow-1);i++)
{
Logger.log(data[0][i]);
}
the variable data stores all the cells of column A.
Cell A1 is stored in data[0][0], cell A2 is stored in data[0][1], cell A3 is stored in data[0][2] and so on.
The getRange(starting Row, starting column, number of rows, number of columns) is a batch operation so it is much faster when you have a large dataset.
If you don't have empty cells in between it's actually pretty easy.
var lastRow = sheet.getLastRow();
var array = sheet.getRange('A1:A' + lastRow).getValues();
If you need to weed out empty entries after that, you can use a for statement, or to be faster, filter like an earlier answer shows.
var filteredArray = array.filter(function(n){ return n != '' });
The main difference between this answer and the one posted earlier that I mentioned is that getValues() will give you an array.
I've tested this and it works in google apps script, and it does not time out when I use the array, or even when I put in large amounts of data (I tested it with an array that has about 20-50 characters per entry and about 500 entries). Just make sure to define the var sheet or put in your own variable.
Try this:
It will allow you to select any column on the sheet.
var ui = SpreadsheetApp.getUi();
var ss = SpreadsheetApp.getActiveSpreadsheet();
function onOpen() {
ui.createMenu('Sheet Functions')
.addItem('Get values from column', 'getVals')
.addToUi();
}
function getVals() {
var sheet = ss.getActiveSheet();
var getColumnLetter = ui.prompt('Select column..' , 'Enter the letter of the target column..', ui.ButtonSet.OK_CANCEL);
if(getColumnLetter.getSelectedButton() == ui.Button.CANCEL) {
return } else {
getColumnLetter = getColumnLetter.getResponseText().toUpperCase();
}
var columnNo = getColumnLetter.charCodeAt(0) - 64;
try { var data = sheet.getRange(1, columnNo, sheet.getMaxRows()).getValues().filter(String); } catch (e) { ui.alert('Invalid input please try again.', ui.ButtonSet.OK); return;}
/*
*
* Do what ever you need to do down here
*
*/
}