How can I optimize my code using batch operations? - google-apps-script

I want to perform a basic multiplication on a range of values in my spreadsheet, and then divide those values by a range of values from a column, note that my range is 8 columns long and my division range is one column long.
I have this code:
function multiply() {
var doc = SpreadsheetApp.openById(SCRIPT_PROP.getProperty("1iXQxyL3URe1X1FgbZ76mEFAxLnxegyDzXOMF6WQ5Yqs"));
var sheet = doc.getSheetByName("json");
var sheet2 = doc.getSheetByName("tabla de frecuencias");
var sheet3 = doc.getSheetByName("Template");
var range = sheet2.getDataRange();
var numRows = range.getNumRows();
var numCols = range.getNumColumns()-1; //This will get the division values which are located in the last column
var targ = sheet2.getLastColumn();
for (var i = 2; i <= numRows; i++) {
for (var j = 2; j <= numCols; j++) {
var A = range.getCell(i,j).getValue();
var value = A;
for (var v = 1; v <= numRows; v++) {
var T = range.getCell(v,targ).getValue();
range.getCell(i,j).setValue(value*100/T);
}
}
}
}
It's very slow, it reads and writes on each cell from a sheet where I have numeric values ready to be multiplied by 100 and divided by a value located in a single column, this value is different for each row.
My script gets the job done extremly slowly, batch operations appear promising, if that's not the best solution, I will accept any other alternate solution regardless of the question title.

I think this is what your looking for.
function multiply()
{
var ss=SpreadsheetApp.getActive();
var sh=ss.getSheetByName("Sheet60");
var rg=sh.getDataRange();
var dataA=rg.getValues();
for(var i=1;i<dataA.length;i++)
{
for(var j=1;j<dataA[0].length-1;j++)
{
var value=dataA[i][j];
for (var v=1;v<dataA.length;v++)
{
var T=dataA[v][dataA[0].length-1];//the value in the last column on this row
Logger.log('v=%s dataA[%s][%s]=%s',v,i,j,dataA[i][j]);
dataA[i][j]=value * 100 / T;//This seems wrong because it puts a different value into dataA[i][j] which doesn't change inside this inner loop and so only the last value remains in dataA[i][j]
}
}
}
rg.setValues(dataA);
}
Try to minimize the use of getValue and replace with one getValues to get all values in two dimensional array. Then setValues all at one time at the end of the loop.
Okay made another change. Getting more reasonable results.
I'm beginning to think that you may not actually want the v loop at all. Take a look at this one and look at the Logger. We just write the data once.
function percentages()
{
var ss=SpreadsheetApp.getActive();
var sh=ss.getSheetByName("Sheet60");
var rg=sh.getDataRange();
var dataA=rg.getValues();
for(var i=1;i<dataA.length;i++)
{
for(var j=1;j<dataA[0].length-1;j++)
{
var value=dataA[i][j];
var T=dataA[i][dataA[0].length-1];
dataA[i][j]=value * 100 / T;
Logger.log('dataA[%s][%s]=%s',i,j,dataA[i][j]);
}
}
rg.setValues(dataA);
}
This is the output for this version.

Related

How to store data in Array using For loop in Google apps script - pass array by value

Edit: answer at the bottom
Im experiencing some weird behavior I came across when using google script.
I have a 2d array and I insert data into it using for loop.
I noticed that if use
appendRow(someArray[i])
INSIDE the for loop, everything works as expected.
But If try to access data or use appendRow outside of the for loop, it always gives me the data of the last row the ran in the for loop.
so:
appendRow(someArray[1])
gives same result as
appendRow(someArray[2]),appendRow(someArray[3])
when used OUTSIDE of the for loop.
Can anyone tell me whats causes it?
It also happens when im using setValue on a 2d array, I can use it outside of the loop, or all of the rows are identical.
I have spent 2 hours on this simple little thing, and finally understand what causing the problem but I still cant figure out how to fix it.
Im attaching a simple code that explains what the problem, please focus on the second FOR loop.
function myFunctionAppendRowInside() {
var sheet = SpreadsheetApp.getActiveSpreadsheet().getActiveSheet();
var data = sheet.getDataRange().getValues();
var newRow = data[5];
var arr = new Array(100,100);
var currentId = 20000;
var productSku = data[5][2];
for (var i = 0; i < 99; i++){
arr[i] = newRow
}
for (var i = 0; i < 99; i++){
target.getRange(targetRow,1).setValue(evento[i]);
arr[i][0] = currentId + i;
arr[i][2] = productSku + i;
sheet.appendRow(arr[i]);
}
//All of them gives the same row, which is the one created in the last run of the FOR loop arr[98]
sheet.appendRow(arr[1]);
sheet.appendRow(arr[2]);
sheet.appendRow(arr[3]);
}
Please explain to me whats causes it and how to overcome it.
Edit : added my code which uses "setValues" , but still experiencing the same problem. My array is populated only with the LAST row created by the "for loop"
function myFunction() {
var sheet = SpreadsheetApp.getActiveSpreadsheet().getActiveSheet();
var activeSpreadsheet = SpreadsheetApp.getActiveSpreadsheet();
var newSheet = activeSpreadsheet.insertSheet();
newSheet.setName("newSheet");
var data = sheet.getDataRange().getValues();
//Taking the 5th row, using it as a template for rest of rows
var newRow = data[5];
//2d Array
var arr = new Array(100,100);
//data for the only 2 columns who are going to change between rows
var currentId = 20000;
var productSku = data[5][2];
for (var i = 0; i < 99; i++){
newRow[0] = currentId + i;
newRow[2] = productSku + i;
arr[i] = newRow;
}
newSheet.getRange(2, 1, arr.length, arr[0].length).setValues(arr);
}
Second Edit:
So the issue was that "getValues()" returns an array, which I needed to work with.
But Array are passed by reference and not by value, so any changes I made along the code, where changing the original array which I got from "getValues".
The solution:
iterate over the array received from "getValues", and copy the values one by one(cell by cell) to the a new array and only then manipulate it.
I also created a 2d array, which also requires running a "for loop"
Im attaching the working code which does:
1.copy row 13, which includes 51 columns from my original sheet.
2.create an empty 2d array (9999x51).
3. take row 13 and manipulate its columns based of current iteration (ie row '1' will include original data + '1'
The code :
function myFunction() {
var sheet = SpreadsheetApp.getActiveSpreadsheet().getActiveSheet();
var activeSpreadsheet = SpreadsheetApp.getActiveSpreadsheet();
var data = sheet.getDataRange().getValues();
//create Array
var arr = [];
//turn it to a 2 dimension array
for (var i=0;i<10000;i++) {
arr[i] = [];
}
//starting point for our new id which will run from 30000-39999
var currentId = 30000;
//run on our 2 dimension array and manipulate data cell by cell
for (var i=0; i <= 9999; i++){
for (var j=0; j<= data[13].length - 1; j++){
if (j == 0){
var obj = currentId + i;
arr[i][j] = obj;
}else{
if (j == 2){
arr[i][j] = data[13][j] + i;
}else{
arr[i][j] = data[13][j];
}
}
}
}
//copy to new sheet
var newSheet = activeSpreadsheet.insertSheet();
newSheet.setName("newSheet466");
newSheet.getRange(1, 1,10000, 51).setValues(arr);
}
I'm not sure what causes your problem. But instead of appending each row (which can get very slow) it is faster to get a range and set its value. For example:
sheet.getRange(1, 1, arr.length, arr[0].length).setValues(arr);
Where the two 1's are the starting position that you wish to use. For more info check out the documentation on getRange.

Google Sheets Script - Repeating Multiple Strings X Times in Multiple Rows

How can I repeat 'SAT' 10 times in a row and then 'SUN' times in a row more efficiently?
I know I could use copy/paste, click/drag, custom functions (like this) and combinations of built-in functions like TRANSPOSE(SPLIT(TRIM(REPT(). Those are great if you don't have to use them often. However, with more than just 'SAT' and 'SUN' as values, that quickly becomes cumbersome.
I asked a similar question a few days ago. Working off the solutions provided by dev1998 and Serge insas, I created a working script below.
function testAtA1() {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheet = SpreadsheetApp.setActiveSheet(ss.getSheetByName('TEST'));
// start columns and rows
var currentRow = 1;
var startColumn = 1;
var numRows = 1; // Change to match number of items listed for a given variable
var numColumns = 1;
var day1 = [
['SAT']
]
var day2 = [
['SUN']
]
Logger.log(day1);
Logger.log(day2);
var day1Length = day1.length;
var day2Length = day2.length;
Logger.log(day1Length);
Logger.log(day2Length);
var currentRow2 = currentRow + 10
// ranges where values will be placed
for (i = 0; i < 10; i++) {
var target1 = sheet.getRange(currentRow, startColumn, numRows, numColumns).setValues(day1);
currentRow = currentRow + 1; // Change to match number of items listed for a given variable
}
for (i = 0; i < 10; i++) {
var target2 = sheet.getRange(currentRow2, startColumn, numRows, numColumns).setValues(day2);
currentRow2 = currentRow2 + 1
}
}
In addition to what I have above, I also tried
var days = [['SAT'],['SUN']]
I was able to get that to alternate SAT and then SUN X times, but not get all SAT in a row and all SUN in a row. With the first snippet I provided I was able to separate them but, I'm curious to know if there is an easier/better way.
function testAtA1() {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheet = SpreadsheetApp.setActiveSheet(ss.getSheetByName('Sheet1'));
// start columns and rows
var currentRow = 1;
var startColumn = 1;
var numRows = 10; // Change to match number of items listed for a given variable
var numColumns = 1;
var days = ['SAT','SUN','MON']
for (d = 0;d < days.length ; d++) {
var data = days[d]
var target1 = sheet.getRange(currentRow, startColumn, numRows, numColumns).setValue(data);
currentRow = currentRow+10
}
}
By adding another variable d and making a another loop with d
lesser than days.length , We have a desired solution.
=ARRAYFORMULA(TRIM(TRANSPOSE(SPLIT(QUERY(REPT(D1:D7&"😋",10),,9^99),"😋"))))
D1:D7 will contain SAT through FRI
Assuming you just want to append the rows to the spreadsheet, this will do the trick:
function addText(){
var ss = SpreadsheetApp.getActive().getActiveSheet();
var satRow = [];
for (var i = 0; i < 10; i++){
satRow.push("SAT");
}
var sunRow = [];
for (i = 0; i < 10; i++){
sunRow.push("SUN");
}
ss.appendRow(satRow);
ss.appendRow(sunRow)
}
appendRow() takes a 1-dimensional array as an argument. So just build a 1D array and append it.
If you need to insert the rows at an arbitrary point, you're probably better off going with a custom function:
/**
* Fills a range specified in rows and columns with the specified value
*
* #param {number} Number of rows to fill
* #param {number} Number of columns to fill
* #param {string} input The value to iterate.
* #return An array containing the values
* #customfunction
*/
function fillRange(rows,cols,str){
var output = [[]];
for (var i = 0; i < cols; i++){
output[0].push(str);
}
for (var i = 1; i < rows; i++){
output.push(output[0]);
}
return output;
}
To use it, type in a cell =fillRange(1,2,"SAT") to fill 2 columns in one row with "SAT".
again a more efficient solution using array and bulk read/write as recommended in best practice...
function testAtA1() {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheet = ss.getSheetByName('TEST');
// start columns and rows
var currentRow = 1;
var startColumn = 1;
var numRows = 1; // Change to match number of items listed for a given variable
var numColumns = 1;
var day1 = 'SAT';
var day2 = 'SUN';
var data = sheet.getRange(1,1,sheet.getMaxRows(),sheet.getMaxColumns()).getValues();
var currentRow2 = currentRow + 10 ;// ranges where values will be placed
for (i = 0; i < 10; i++){
data[i][0] = day1;
data[i+10][0] = day2;
}
sheet.getRange(1,1,data.length,data[0].length).setValues(data);
}

Google script - Exceeded maximum execution time , help optimize

google script spreadsheet
Novice
I try to create a matrix , if the array is a small database everything works fine, of course if it exceeds 800 lines and more rests on the error "You have exceeded the maximum allowed run time ." Not effectively create a matrix :
var s = SpreadsheetApp.getActiveSheet(); //List
var toAddArray = []; //Greate Arr
for (i = 1; i <= s.getLastRow()+1; ++i){ //Start getting Value
var numbr = s.getRange(i,4); //detect range
var Valus = numbr.getValues().toString(); //get value
//filter value
var newznach = Valus.replace(/\-/g, "").replace(/[0-9][0-9][0-9][0-9][0-9][a-zA-Zа-яА-Я][a-zA-Zа-яА-Я]/g, "").replace(/[a-zA-Zа-яА-Я][a-zA-Zа-яА-Я]/g, "");
toAddArray.push([i.toFixed(0),Valus,newznach]); //add to array 0- Row numb, 1- Value, 2- "filtered" value
}
toAddArray =
{
Row, Value, NewValue - filtered
Row, Value, NewValue - filtered
Row, Value, NewValue - filtered
...
}
Can I somehow get an array of the same the other way ( faster, easier ) ?
You're doing a call to getValues every row, that eats a lot of performance.
It is better to do one big call to have all the data and then go through it sequentially.
var s = SpreadsheetApp.getActiveSheet();
var data = s.getRange(1,4, s.getLastRow()).getValues();
var toAddArray = data.map(function(row, i) {
var Valus = row[0].toString();
var newznach = Valus.
replace(/\-/g, "").
replace(/[0-9][0-9][0-9][0-9][0-9][a-zA-Zа-яА-Я][a-zA-Zа-яА-Я]/g, "").
replace(/[a-zA-Zа-яА-Я][a-zA-Zа-яА-Я]/g, "");
return [i.toFixed(0), Valus, newznach];
});
this code:
var Valus = numbr.getValues().toString();
slows you down because you read data from the sheet in a loop.
Try reading data once into array and then work with it:
var data = s.getDataRange().getValues();
And then work with data, in a loop. This sample code log each cell in active sheet:
function logEachCell() {
var s = SpreadsheetApp.getActiveSheet();
var data = s.getDataRange().getValues();
// loop each cell
var row = [];
for (var i = 0; i < data.length; i++) {
row = data[i];
for (var j = 0; j < row.length; j++) {
Logger.log(row[j])
}
}
}

Simple conditional formatting in Google Sheets Script Editor

I am trying to create some basic conditional formatting rules with Scripts instead of the conditional formatting editor in sheets, because when a new row is added or removed, it breaks the rules already set. For example, when a row is deleted, the condition may be for the range A:C but then it adds "A1:C5,A6:C898". This causes some rows to be skipped by the rules so I am hoping a script will solve this.
So I want to simply change the cell background to green if the cell text is exactly "Y". I want to change it to red if text is exactly "N". I have other rules that I want to use but I am having trouble with the basics on this.
What I have so far in my script:
function onEdit(e) {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheet = ss.getSheetByName("PRECLOSING");
var range = sheet.getRange("E:E,I:J,M:M,P:P,S:V,AB:AC,AF:AF");
range.activate();
var values = rangeY.getValues();
//for each row that data is present
for(var i = 0; i < values.length; i++) {
var cell = sheet.getRange(i + 1, 2);
if ( values == "X" )
{
cell.setBackground('black');
return;
} else {
cell.setBackground('white');
}
}
}
I made some modifications to your code.
The function getRange i don't think it can accept the ranges in that way, instead i think it can get just ranges that are continue (eg. A1:E10)
For that I created an array with the ranges you need and then I loop through them.
The function "getValues" returns a two dimensional array so you will need a second loop to get the actual value from the cell.
Then as you already have the range, you need to get the cell inside that range and not a new complete range.
function onEdit(e) {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheet = ss.getSheetByName("PRECLOSING");;
var columns = ['E:E','I:J','M:M','P:P','S:V','AB:AC','AF:AF'];
for(var col in columns){ //loop to iterate the desired ranges
var range = sheet.getRange(columns[col]);
range.activate();
var values = range.getValues();
//for each row that data is present
for(var i = 0; i < values.length; i++) { //loop to iterate the rows
for( var j = 0; j< values[i].length ; j++){ //loop to iterate the columns
var cell = range.getCell(i+1, j+1);
if ( values[i][j] == "X" )
{
cell.setBackground('black');
}
}
}
}
}
Hope this helps.

sorting columns independently at array level

I have a spreadsheet with several columns that I need to sort individually.
I wrote the script below that works but is a bit slow since it handles each column in turn with getValues() and setValues().
I'd like to find a way to do the whole sorting at an array level for more efficiency but I don't know how... any suggestion ?
Here is the relevant part of the code I use now :
...
sh3.getRange(1,1,1,newData[0].length).setFontWeight('bold');// newData is an array corresponding to the whole sheet
for(col=1;col<newData[0].length;++col){
var val = sh3.getRange(2,col,getLastRowInCol(col),1).getValues().sort();// each column have a different height
sh3.getRange(2,col,getLastRowInCol(col),1).setValues(val)
}
}
function getLastRowInCol(col){
var values = sh3.getRange(2,col,sh3.getLastRow(),1).getValues();// skip header and find last non empty row in column
for(n=0;n<values.length;++n){
if(values[n]==''){break}
}
return n
}
Note : I know there is a Library by Romain Vialard that does the job (sorting columns in 2D arrays) but I'm interrested on how to do it 'manually' for personal JS skills improvement ;-) and also I need to sort every column independently without needing to update the sheet for every column.
How about:
function sortColumns() {
var sheet = SpreadsheetApp.getActiveSheet();
var startRow = 2;
var startCol = 1;
var dataRange = sheet.getRange(startRow, startCol, sheet.getLastRow() - startRow + 1, sheet.getLastColumn() - startCol + 1);
var data = dataRange.getValues();
// transpose data so each column item will be listed in an single array
// for each column so that it can be sorted with array.sort()
var rowToCol = [];
for (var i = 0; i < data[0].length; i++) {
rowToCol.push([]);
for (var j = 0; j < data.length; j++) {
// replace empty string with undefined as undefined sorts last
rowToCol[i].push(data[j][i]==""?undefined:data[j][i]);
}
rowToCol[i].sort();
// default sort, as above, is alphabetic ascending. For other methods
// search for Javascript array sort functions
}
// transpose sorted items back to their original shape
var result = [];
for (var i = 0; i < rowToCol[0].length; i++) {
result.push([]);
for (var j = 0; j < rowToCol.length; j++) {
result[i].push(rowToCol[j][i]==undefined?"":rowToCol[j][i]);
}
}
dataRange.setValues(result);
};
function onOpen() {
var sheet = SpreadsheetApp.getActiveSpreadsheet();
var entries = [{
name : "Sort Columns",
functionName : "sortColumns"
}];
sheet.addMenu("Script Center Menu", entries);
};