Google app script for loop is extremely slow - google-apps-script

I have a pivot table set up in a google sheet file where I've labeled the sheet pivot table 1. I want to get each value of the first column of the pivot, duplicate each values 12 times in an array and paste these values in the 3rd column of sheet 5. However, it seems extremely slow to do with the script never really completing (just takes 10+ minutes before I've cancelled it).
The pivot has approximately 3000 lines, which would result in a 3000 * 12 = 36000 array.
Any thoughts on how I can optimize this?
function test2() {
// activating current spreadsheet for use
var spreadsheet = SpreadsheetApp.getActive();
//empty array
var array_dept = []
// returns (integer #) the last row of the pivot table 1 sheet that has content
var last_row = spreadsheet.getSheetByName("Pivot Table 1").getLastRow();
// Get value in pivot table 1 from range of row 1 (dept name), column 1, all the way to last row
// Then paste it in sheet5 from row 1, column 3, all the way to the last row defined above
for (var i = 1; i < last_row; i++ )
{
//get value and then paste it in a destination
var value_dept = spreadsheet.getSheetByName("Pivot Table 1").getRange(i,1).getValue();
array_dept.fill(value_dept, -12 + (12*i) , 12*i)
}
destination_dept = spreadsheet.getSheetByName("Sheet5").getRange(1,3, last_row);
destination_dept.setValues(array_dept);
}

You don't need use a loop if you know the first row and the last row on the source column. You can just define the range:
var pivotRange = pivot.getRange(1,1,last_row)
var targetRange = target.getRange(1,3,last_row)
doc ref; this is just one of five methods to define a range.
In the OP script, there would be 3000xgetRange + 3000xgetValue. In this answer there are: 2xgetRange and 1 x getValue. This should account for a substantial amount of script processing. Of course, we know nothing of the rest of the spreadsheet (its size, formula, functions, triggers, etc). But all other things being equal, this should improve the performance of the script.
function test2() {
// activating current spreadsheet for use
var spreadsheet = SpreadsheetApp.getActiveSpreadsheet()
var pivotSheetName = "Pivot Table 1"
var pivot = spreadsheet.getSheetByName(pivotSheetName)
//temporary array
var array_dept = []
// returns (integer #) the last row of the pivot table 1 sheet that has content
var last_row = pivot.getLastRow();
//Logger.log("DEBUG: last row in the pivot table:"+last_row)
var pivotRange = pivot.getRange(1,1,last_row)
// Logger.log("DEBUG: the range for the pivot range = "+pivotRange.getA1Notation())
var pivotData = pivotRange.getValues()
//Then paste it in sheet5 from row 1, column 3, all the way to the last row defined above
var targetSheetName = "Sheet5"
var target = spreadsheet.getSheetByName(targetSheetName)
var targetRange = target.getRange(1,3,last_row)
// Logger.log("DEBUG: the range for the target range = "+targetRange.getA1Notation())
targetRange.setValues(pivotData)
Logger.log("Done")
}

Related

Unable to complete data move within apps script

Ive been messing with this google apps script for far too long and need some help.
I have a table on a sheet called options that starts on col A line 31 and is 3 col wide.
Col a is all checkboxes. I was able to write a script that checks to see which checkboxes are checked.
For each checked box it copies that rows data in b:c into an array.
Then opens an existing tab called Worksheet and is supposed to paste them in the first empty cell it finds in column b.
function createNamedRanges() {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheet = ss.getSheetByName("Worksheet");
var range = sheet.getRange("B2:C");
var namedRange = ss.setNamedRange("outputRange", range);}
function processSelectedRows() {
var sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("Prompt Options");
var data = sheet.getDataRange().getValues();
var checkedRows = [];
for (var i = 30; i < data.length; i++) {
var row = data[i];
var checkbox = sheet.getRange(i + 1, 1).getValue() == true;
if (checkbox){
checkedRows.push([row[1], row[2]]);
} }
var worksheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("Worksheet");
var pasteRange = SpreadsheetApp.getActiveSpreadsheet().getRangeByName("outputRange");
pasteRange.offset(worksheet.getLastRow(), 0).setValues(checkedRows);
}
The first row on the worksheet tab are headers. The first array to copy over is 11 rows. When I ran the script. I got an error that sat there was only 1 row in the range and I had 11 rows of data. Ok, I figured I neeeded to name a range. This table will be a different size every time. So I named this range outoutRange and no matter what size i make it I get error messages.
This is my latest error message and it is hitting the very last line of code
Exception: The number of rows in the data does not match the number of rows in the range. The data has 11 but the range has 1007.
You assistance is appreciated
Modification points:
If your Worksheet is the default grid like 1000 rows and 26 columns, I think that pasteRange is all rows like 1000. I thought that this might be the reason for your current issue.
In order to retrieve the last row of the columns "B" and "C" of "Worksheet" sheet, how about the following modification?
From:
pasteRange.offset(worksheet.getLastRow(), 0).setValues(checkedRows);
To:
var lastRow = pasteRange.getLastRow() - pasteRange.getDisplayValues().reverse().findIndex(([b, c]) => b && c);
worksheet.getRange(lastRow + 1, 2, checkedRows.length, checkedRows[0].length).setValues(checkedRows);
By this modification, the values of checkedRows is put to the next row of the last row of columns "B" and "C" of "Worksheet" sheet.

Exception: The number of columns in the data does not match the number of columns in the range. The data has 0 but the range has 1

I am very new to javascript and have searched around a ton for this and can't seem to find the issue with my code. I am attempting to simply write a code that will copy the values in a column from a pivot table sheet in Google Sheet and then paste the values in another sheet. However, before pasting the values, I want each individual value to be duplicated 12 times (for 12 months). So, assuming I have 10 unique values (A, B, C, D, E, F, G, H, I, J) that I am copying, I want to return value A 12 times in a row, then value B 12 times in a row, etc.
I run getValues, which seems to put the values in a 2 dimensional array. I've then taken this temp_array that I had created and used a for loop to duplicate each value 12 times in a new array.
However, when I setValues, I am pasting the values in my spreadsheet correctly, but I get this error message regardless (The number of columns in the data does not match the number of columns in the range. The data has 0 but the range has 1.), any ideas why?
Here is a small example of what my input could look like (1st image) and what I would want the output to look like (2nd image)
function test2() {
// activating current spreadsheet for use
var spreadsheet = SpreadsheetApp.getActive();
//empty array
var array_dept_temp = [];
// returns cell position (ex: C5) of the last row of the pivot table 1 sheet that has content in column 1
var last_row = spreadsheet.getSheetByName("Pivot Table 1").getRange("A:A").getNextDataCell(SpreadsheetApp.Direction.DOWN).getRowIndex();
//subtracting 1 from last row because we are excluding the headers. This gives us our row_length
var row_length = last_row - 1
var array_dept = [[]]
array_dept = new Array(row_length*12)
//new Array(row_length*12);
// Get value in pivot table 1 from range of row 2 (dept name, but exclude the header), column 1, all the way to last row
// Then paste it in sheet5 from row 1, column 3, all the way to the last row defined above
array_dept_temp = spreadsheet.getSheetByName("Pivot Table 1").getRange(2,1, last_row).getValues();
for (var i = 1; i < row_length; i++ )
{
//get value and then paste it in a destination
array_dept.fill(array_dept_temp[i-1], (-12 + (12*i)) , 12*i);
}
var destination_dept = spreadsheet.getSheetByName("Sheet5").getRange(2,3,row_length*12);
destination_dept.setValues(array_dept);
}
Suggestion / Alternate solution:
Try:
function test() {
  var spreadsheet = SpreadsheetApp.getActive();
  var sheet = spreadsheet.getSheetByName("Pivot Table 1");
  var array_dept_temp = sheet.getRange(2,1, sheet.getLastRow()-1).getValues();
  var array_dept = [];
  for (var i = 0; i < array_dept_temp.length; i++) {
    array_dept = [...array_dept, ...Array.apply(null, Array(12)).map(function(){return array_dept_temp[i]})]
  }
  var destination_dept = spreadsheet.getSheetByName("Sheet5").getRange(2,3,array_dept.length);
  destination_dept.setValues(array_dept);
}
Result:
Another way without using fill or from.
Also some modification, you can just use .getLastRow() function to get the last row, however take not that if there is data below it will count all the rows including the blank until the row that has data. And you may also use .length on your data to setValue.
From your showing sample input and output situations, how about the following modified script?
Modified script:
function test2_sample() {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var srcSheet = ss.getSheetByName("Pivot Table 1");
var dstSheet = ss.getSheetByName("Sheet5");
var srcValues = srcSheet.getRange("A2:A" + srcSheet.getLastRow()).getValues();
var dstValues = srcValues.flatMap(a => Array(12).fill(a));
dstSheet.getRange(2, 3, dstValues.length).setValues(dstValues);
}
When this script is run using your sample input sheet, I think that your expected output values are obtained.
Now, I thought that var dstValues = srcValues.flatMap(([a]) => Array(12).fill(a).map(e => [e])); can be modified to var dstValues = srcValues.flatMap(a => Array(12).fill(a));. This is simpler.
From your reply of Are you able to explain what this does? var dstValues = srcValues.flatMap(([a]) => Array(12).fill(a).map(e => [e]));, in this script, var dstValues = srcValues.flatMap(([a]) => Array(12).fill(a).map(e => [e])); can be also modified as follows. I thought that this might also help to understand it.
function test2_sample() {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var srcSheet = ss.getSheetByName("Pivot Table 1");
var dstSheet = ss.getSheetByName("Sheet5");
var srcValues = srcSheet.getRange("A2:A" + srcSheet.getLastRow()).getValues();
var dstValues = [];
for (var i = 0; i < srcValues.length; i++) {
dstValues = dstValues.concat(Array(12).fill(srcValues[i]));
}
dstSheet.getRange(2, 3, dstValues.length).setValues(dstValues);
}
Note:
As additional information, when your showing script is modified, how about the following modification? In your script, I thought that it is required to add the values to array_dept in the loop. And, it is required to flatten the elements in the array.
function test2() {
var spreadsheet = SpreadsheetApp.getActive();
var array_dept_temp = [];
var last_row = spreadsheet.getSheetByName("Pivot Table 1").getRange("A:A").getNextDataCell(SpreadsheetApp.Direction.DOWN).getRowIndex();
var row_length = last_row - 1
var array_dept = []
array_dept_temp = spreadsheet.getSheetByName("Pivot Table 1").getRange(2, 1, last_row).getValues();
for (var i = 0; i < row_length; i++) {
array_dept = [...array_dept, ...Array(12).fill(array_dept_temp[i])];
}
var destination_dept = spreadsheet.getSheetByName("Sheet5").getRange(2, 3, array_dept.length);
destination_dept.setValues(array_dept);
}
Reference:
flatMap()

How to start autofill at last cell in one column to last row of adjacent column in Apps Script/Google Sheets?

So I have a table that will have data to the last row except column B. (for example, cols A, C, D etc. stop at row 40, but B stops at maybe row 25). I want to programmatically keep the series going starting at the last cell with data in B and autofill down to the last row in the spreadsheet. (Hopefully a script doing this will recognize the series and not just copy the same data to all the empty cells. When I do it manually it works.) I have something started here but I can't figure out how to call out the range of where to start the series. I get an error on line 7 "Exception: Range not found".
function fillDownFunction() {
var ss = SpreadsheetApp.getActiveSpreadsheet().getActiveSheet();
var sc = ss.getRange("B1:B").getValues();
var scr = sc.filter(String).length;
var lr = ss.getLastRow();
var fillDownRange = ss.getRange(scr,2,lr)
ss.getRange(scr).copyTo(fillDownRange);
}
In addition to the answers already provided, Apps Script already has an autoFill() method that you can use to do this. With this you just have to define a source and a destination range. This works the same as selecting the range and dragging down your mouse manually on the corner.
function fillDownFunction() {
var ss= SpreadsheetApp.getActiveSpreadsheet().getActiveSheet()
var lastsheetrow = ss.getLastRow() //last row with data
//last row in the autofill range with data
var lastrangerow = ss.getRange("B1:B").getNextDataCell(SpreadsheetApp.Direction.DOWN).getRow()
var sourcerange = ss.getRange(2, 2,lastrangerow-1) //in your screenshot, this would be B2:B6
var destinationrange = ss.getRange(2, 2, lastsheetrow-1) //This would be B2:B12
sourcerange.autoFill(destinationrange, SpreadsheetApp.AutoFillSeries.DEFAULT_SERIES)
}
Note that I skipped the header row and offset the range lengths by -1 to compensate. This is because the autofill logic uses the entire range, so it would also take into account "Item2" and repeat it every 6 rows as "Item3", "Item4" and so on. A disadvantage in this case, but may prove useful if you plan to use autofill in more complex ways in the future.
Here is one way you could have the last fill value copy down. The function grabs all of the values, maps the row that needs to fill down and then copies that last value down the rest of the sheet.
function fillDownFunction() {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var tab = ss.getActiveSheet();
var sc = tab.getDataRange().getValues();
var lastRow = sc.length;
var values = sc.map(function(r) { return r[1]; }).filter(i => i);
var lastFillRow = values.length;
var fillDownValue = values.slice(-1);
tab.getRange(lastFillRow + 1, 2, lastRow - lastFillRow).setValue([fillDownValue]);
}

IMPORTRANGE excluding all rows that come after the first empty row

I'd like to know how can I can import a range excluding all rows from the source that come after the first empty row.
For example, let's say I have a sheet with 200 rows, but the row 101 is empty, but from 102 to 200 all rows have values. I'd like to import only rows 1:100.
I need to do it independently of how many rows has the source sheet, because we regularly import data and the number of valid rows grows on every import.
it would be like this:
=ARRAYFORMULA(
IMPORTRANGE("ID_or_URL", "Sheet3!F1:F"&MIN(IF(
IMPORTRANGE("ID_or_URL", "Sheet3!F1:F")="", ROW(A:A), ))))
Adding to what's already here, if you would like to do this more generically using Apps Script, you can bind this to the sheet and run as you need.
Assuming that Column A is being used for the data in the Sheet to Copy to, you can find the first row which is blank by running
function obtainFirstBlankRow() {
var sheet = SpreadsheetApp.getActive().getSheetByName('SHEET-TO-COPY-TO');
// search for first blank row
var col = sheet.getRange('A:A');
var vals = col.getValues();
var count = 0;
while (vals[count][0] != "") {
count++;
}
return count + 1;
}
This will return a number corresponding to the first row in which the cell in the A column is blank - you can change which column it checks if you need by replacing A:A in sheet.getRange() to the range you need.
You can then use:
function myFunction() {
var startRow = 1; //assuming you want to copy from row 1
var endRow = obtainFirstBlankRow();
var startCol = 1; // assuming you want to copy starting at column A
var endCol = 26; // assuming copying values up to column Z
var startRowPaste = 1; //assuming you want to copy starting at row 1
var startColPaste = 1; // assuming you want to copy starting at column A
//example range to copy from
var rangeToCopy = SpreadsheetApp.openById('IDOfSheetToCopyFrom').getSheetByName('SheetToCopyFrom')
var rangeToCopy = rangeToCopy.getRange(startRow, startCol, endRow, endCol);
//make sure the range you're copying to is the same size
var newRange = SpreadsheetApp.openById('IDOfSheetToCopyTo').getSheetByName('SheetToCopyTo')
var newRange = newRange.getRange(startRowPaste, startColPaste, rangeToCopy.getNumRows(), rangeToCopy.getNumColumns());
newRange.setValues(rangeToCopy.getValues());
}
To copy the data to the new Range.

Output of GAS function not the individual cell value, but the full column of values

I'm new to Google Apps script. I created a Google Apps Script to act as a alternative for vlookup (I want static data for reasons).
I've created a function that is able to communicate between Sheet 1 "Main Sheet" (user input), and Sheet 2 "Query Sheet" (contains data results).
Intention is to compare mainSheet ColW with querySheet ColA
If mainSheet ColW matches, replace the value in mainSheet ColX with the value of querySheet ColB in the corresponding row.
Example if mainSheet Col W2 is '123' and querySheet Col A5 is '123', paste value from querySheet Col B5 into mainSheet Col X2
This is for comparing input provided by a user with data that is updated in the backend after X period of time. To reduce the need to constantly review this data, the query runs in intervals. The user sheet is populated with results.
function match(){
//Sheet with the query outputs
var querySheet = SpreadsheetApp.openById("SHEETID123456789"); // query results are dumped in this doc
var querySheetTab = querySheet.getRange("query!A1:F"); //tab with the results
//Main sheet to have query data pasted
var mainSheet = SpreadsheetApp.getActiveSpreadsheet(); //current worksheet
var mainSheetTab = mainSheet.getRange("USERSHEET!W2:X"); //Tab and range of the data. W contains a number. X is where the results of B from the querySheet go
//values to compare
var mainSheetVal = mainSheet.getSheetByName('USERSHEET').getRange('W2:W').getValues(); //values to match against query results
var mainSheetComp = mainSheet.getSheetByName('USERSHEET').getRange('W2:W'); //numbers to match against query results
var mainSheetSet = mainSheet.getSheetByName('USERSHEET').getRange('X2:X'); //Where values get pasted.
var getValQS = querySheet.getSheetByName('query').getRange('A1:B').getValues(); //query results includes both objects values
var valQS = querySheet.getSheetByName('query').getRange('A1:A'); // query results
var valSet = querySheet.getSheetByName('query').getRange('B2:B'); //desired values to paste.
var output = [];
for (var i = 0; i < mainSheetVal.length; i++) {
mainSheetSet.setValue(getValQS[i][0]);
SpreadsheetApp.flush();
Utilities.sleep(10000);
if (mainSheetComp.getValue() == '') {
output.push([getValQS[i][0]]);
}
}
mainSheet.getRange(2, 12, output.length, 2).setValues(output);
}
This sheet ends up replacing X2:X with the value of A1, then A2, and continues to cycle through even with empty cells. I haven't been able to set-up a proper compare.
You want to compare the value of column "W" of USERSHEET sheet with the value of column "A" of query sheet.
When above both values are the same, you want to put the value of column "B" of query sheet to the cell of column "X" of USERSHEET sheet.
If my understanding is correct, how about this modification? Please think of this as just one of several answers.
Modification points:
Values of USERSHEET were retrieved from "W:X" by one call.
Values of USERSHEET were modified when the value of column "A" of query is the same with that of column "W" of USERSHEET.
After the values of USERSHEET were modified, the modified values were put to USERSHEET.
Modified script:
function match(){
var querySheet = SpreadsheetApp.openById("SHEETID123456789").getSheetByName("query");
var queryValues = querySheet.getRange(1, 1, querySheet.getLastRow(), 2).getValues(); // A:B
var mainSheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("USERSHEET");
var mainRange = mainSheet.getRange(2, 23, mainSheet.getLastRow() - 1, 2); // W:X
var mainValues = mainRange.getValues();
for (var i = 0; i < mainValues.length; i++) {
for (var j = 0; j < queryValues.length; j++) {
if (mainValues[i][0] == queryValues[j][0]) {
mainValues[i][1] = queryValues[j][1];
}
}
}
mainRange.setValues(mainValues);
}
Note:
In this modification, the column "W" and "X" of USERSHEET is overwritten. If you want to changed this situation, please tell me.
If I misunderstood your question, I apologize.
Try this:
This compares Column W of main sheet to Column A of query sheet and puts value in column B of the query sheet into column X in main sheet in the same row as the match in column A. It could be a little faster by moving the setValue out of the loop but I didn't want to mess around over writing your other data. Also I have not tested this so there is always the possibility of needing corrections.
function match() {
var qss=SpreadsheetApp.openById('id');
var qsh=qss.getSheetByName('query');
var qrgA=qsh.getRange(2,1,qsh.getLastRow(),1);//colA
var qrgB=qsh.getRange(2,2,qsh.getLastRow(),1);//colB
var qvB=qrgB.getValues();
var qvA=qrgA.getValues();
var qvAf=qvA.map(function(r){return r[0]});//flatten array
var mss=SpreadsheetApp.getActive();
var msh=mss.getSheetByName('USERSHEET');
var mrg=msh.getRange(2,23,msh.getLastRow(),1);//colW
var mVa=mrg.getValues();
for(var i=0;i<mVa.length;i++) {
var index=qvAf.indexOf(mvA[i][0]);
if(index>-1) {
msh.getRange(i + 2,24).setValue(qvB[index][0]);//put matches in same row of columnX
}
}
}