Apps Script - For loop is slow. How to make it faster? - javascript

My spreadsheet has a column (A) with over 1000 rows of values like 10.99€, 25.99 € and so on. for optimizing purposes, I am looping through this column and removing the "EUR" mark and replacing "." with ",". While the code works, my problem is that it takes super long to execute and for thousands of products it sometimes time outs. I know I am probably not following the best practices, but this was the best solution I could come up with because of my limited JavaScript skills. Any help?
function myFunction() {
var ss = SpreadsheetApp.getActive();
var sheet = ss.getSheetByName('Table');
var lastRow = sheet.getRange(1,1).getDataRegion(SpreadsheetApp.Dimension.ROWS).getLastRow();
for (var i = 1; i < lastRow +1; i++) {
var price = sheet.getRange(i,1).getValue();
var removeCur = price.toString().replace(" EUR","").replace(".",",");
sheet.getRange(i,1).setValue(removeCur);
}
}

It's a classic question. Classic answer -- you need to replace cell.getValue() with range.getValues(). To get this way 2D-array. Process the array with a loop (or map, etc). And then set all values of the array at once back on sheet with range.setValues()
https://developers.google.com/apps-script/guides/support/best-practices?hl=en
For this case it could be something like this:
function main() {
var ss = SpreadsheetApp.getActive();
var sheet = ss.getSheetByName('Table');
var range = sheet.getDataRange();
var data = range.getValues(); // get a 2d array
// process the array (make changes in first column)
const changes = x => x.toString().replace(" EUR","").replace(".",",");
data = data.map(x => [changes(x[0])].concat(x.slice(1,)));
range.setValues(data); // set the 2d array back to the sheet
}
Just in case here is the same code with loop for:
function main() {
var ss = SpreadsheetApp.getActive();
var sheet = ss.getSheetByName('Table');
var range = sheet.getDataRange();
var data = range.getValues();
for (var i=0; i<data.length; i++) {
data[i][0] = data[i][0].toString().replace(" EUR","").replace(".",",")
}
range.setValues(data);
}
Probably the loop for looks cleaner in this case than map.
And if you sure that all changes will be in column A you can make the script even faster if you change third line in the function this way:
var range = sheet.getRange("A1:A" + sheet.getLastRow());
It will narrow the range to one column.

Well, there's something you can do to improve your code, can't guarantee it will help you to make it faster, but we'll see.
Here's the updated version
function myFunction() {
var ss = SpreadsheetApp.getActive();
var sheet = ss.getSheetByName('Table');
var lastRow = sheet.getRange(1,1).getDataRegion(SpreadsheetApp.Dimension.ROWS).getLastRow() + 1;
var price;
var removeCur;
for (var i = 1; i < lastRow; i++) {
price = sheet.getRange(i,1).getValue();
removeCur = price.toString().replace(" EUR","").replace(".",",");
sheet.getRange(i,1).setValue(removeCur);
}
}
What I did:
Line 5: I removed the +1 in the loop and added on lastRow directly. If you have 1000 rows, you'll save 1000 assignments
Line 6-7: removed declarations in the loop. If you have 1000 rows, you'll save 2000 re-declarations (not sure if it does, but it's best practise anyway)
You could use regex for the replace, so you do it only once, but I think it's slower, so I kept the 2 replaces there

Related

Google Apps Script: Loop through a list

I have a deleteEachRow function that loops through a sheet and delete Rows that have a particular Column Value.
This works fine and was hoping to modify it in such a way that it loops through a multile sheets in the work-book and also delete rows based on multiple Column Values.
The deleteRow() script
//GLOBALS
var SS = SpreadsheetApp.openById("sheetID");
var SHEET = SS.getSheetByName("Sheet1");
var RANGE = SHEET.getDataRange();
var DELETE_VAL = "abc";
var COL_TO_SEARCH = 4; // The column to search for the DELETE_VAL (Zero is first)
function deleteEachRow(){
var rangeVals = RANGE.getValues();
//Reverse the 'for' loop.
for(var i = rangeVals.length-1; i >= 0; i--){
if(rangeVals[i][COL_TO_SEARCH] === DELETE_VAL){
SHEET.deleteRow(i+1);
};
};
};
What I have tried..
var SHEET = SS.getSheetByName(["Sheet1", "Sheet2"]);
var DELETE_VAL = ["abc","DEF"];
function deleteEachRow(){
var rangeVals = RANGE.getValues();
//Reverse the 'for' loop.
for(var i = rangeVals.length-1; i >= 0; i--){
for(var i=0; size = DELETE_VAL.length; i < size; i++){
if(rangeVals[i][COL_TO_SEARCH] === DELETE_VAL[i]){
for(var i=0; size = SHEET.length; i < size; i++){
SHEET[i].deleteRow(i+1);
};
};
};
};
};
Which completes executing from my logs, but does not actually work. I may have murdered some logic here, please pardon me, I am new to .gs/.js.
Thanks for your anticipated response.
Issue : You're passing array to getSheetByName, whereas as per documentation it accepts String only. i.e. Name of the single sheet you want to fetch.
https://developers.google.com/apps-script/reference/spreadsheet/spreadsheet#getsheetbynamename
So you can modify your function to take sheet name as input and then delete rows in that sheet. Then call your function with desired sheet names. Something like this:
var spreadSheet = SpreadsheetApp.openById("sheetID");
var DELETE_VAL = "abc";
var COL_TO_SEARCH = 4; // The column to search for the DELETE_VAL (Zero is first)
function deleteEachRow(sheetName){
var SHEET = spreadSheet.getSheetByName(sheetName);
var RANGE = SHEET.getDataRange();
var rangeVals = RANGE.getValues();
// existing logic
};
// Invoke deleteEachRow() for each sheet you want to delete the rows
["Sheet1", "Sheet2"].forEach((sheetName) => deleteEachRow(sheetName));
Umair is right, there was a simply error in the first line. But I'd want to add that the sheet.deleteRow(row) is not the best practice in case if there are many rows to delete. This command is quite time consuming.
If you have more than dozen rows to delete it's better to grab all data from a sheet (or range) var data = range.getValues(), clear the sheet (or the range), to process the array inside the script and refill the sheet back with new data new_range.setValues(array). It will work much faster.

Google Apps Script - Usage of "indexOf" method

first: I really tried hard to get along, but I am more a supporter than a programmer.
I put some Text in Google Calc and wanted to check the amount of the occurances of "Mueller, Klaus" (It appears 5 times within the data range). The sheet contains 941 rows and 1 Column ("A").
Here is my code to find out:
function countKlaus() {
// Aktives Spreadsheet auswählen
var ss = SpreadsheetApp.getActiveSpreadsheet();
// Aktives Tabellenblatt auswählen
var sheet = ss.getSheetByName("Tabellenblatt1");
var start = 1;
var end = sheet.getLastRow();
var data = sheet.getRange(start,1,end,1).getValues();
var curRow = start;
var cntKlaus = 0;
for( x in data )
{
var value = daten[x];
//ui.alert(value);
if(value.indexOf("Mueller, Klaus")> -1){
cntKlaus = cntKlaus + 1;
}
}
ui.alert(cntKlaus);
}
The result message is "0" but should be "5".
Issues:
You are very close to the solution, except for these two issues:
daten[x] should be replaced by data[x].
ui.alert(cntKlaus) should be replaced by SpreadsheetApp.getUi().alert(cntKlaus).
Solution (optimized by me) - Recommended:
function countKlaus() {
const ss = SpreadsheetApp.getActiveSpreadsheet();
const sheet = ss.getSheetByName("Tabellenblatt1");
const cntKlaus = sheet
.getRange('A1:A' + sheet.getLastRow())
.getValues()
.flat()
.filter(r=>r.includes("Mueller, Klaus"))
.length;
SpreadsheetApp.getUi().alert(cntKlaus);
}
You can leave out this term + sheet.getLastRow() since we are filtering on a non-blank value. But I think it will be faster to have less data to use filter on in the first place.
References:
flat : convert the 2D array to 1D array.
filter : filter only on "Mueller, Klaus".
Array.prototype.length: get the length of the filtered data
which is the desired result.
includes: check if Mueller, Klaus is included in the text.
Bonus info
Just for your information, my solution can be rewritten in one line of code if that's important to you:
SpreadsheetApp.getUi().alert(SpreadsheetApp.getActiveSpreadsheet().getSheetByName("Tabellenblatt1").getRange('A1:A').getValues().flat().filter(r=>r.includes("Mueller, Klaus")).length);

javascript if function not working properly for index match (apps script)

I'm trying to check whether there are duplicate phone numbers in a column and to then return the first id number associated with the phone match in google apps script.
I created an if statement, according to every reference I've checked I've coded it right. The script runs but nothing populates??
I defined the ranges for the count that has the phone number and id number associated with it. I ensured its in the correct array my using map and used a basic if else statement.
Everything seems to run but the statement doesn't throw out the id numbers. In fact nothing happens.
Please help...
function myFunction() {
var sss = SpreadsheetApp.openById('spreadsheetid'); // sss = source spreadsheet
var ss = sss.getSheetByName('spreadsheetname'); // ss = source sheet
var lr = ss.getLastRow();
for (var i=3;i<lr;i++) {
var phone = ss.getRange(i,4,lr).getValues();
var id = ss.getRange(i,1,lr).getValues();
var phonearray = phone.map(function(r){return r[0]});
if(phonearray == phone[i]){
//emailarray.indexOf(email[i]) &&
ss.getRange(i,30,lr).setValue(id[i]);
} else
ss.getRange(i,30,lr).setValue("");
}
}
There are some things you need to consider
A valuerange is a 2-dimensional array, thus to access the value of an individual cell you need to define phone[i][0] instead of phone[i]
The method getRange requires the syntax getRange(startRow, startColumn, numRows) rather than getRange(startRow, startColumn, lastRow). Keep this in mind when you retrieve your range of interest.
Searching for duplicates will require in your case two (nested) for loops.
Please find below a sample of how you can modify your code to achieve the desired functionality:
function myFunction() {
var sss = SpreadsheetApp.openById('spreadsheetid'); // sss = source spreadsheet
var ss = sss.getSheetByName('spreadsheetname'); // ss = source sheet
var lr = ss.getLastRow();
var phones = ss.getRange(3,4,lr-3+1).getValues();
var ids = ss.getRange(3,1,lr-3+1).getValues();
var phonearray = phones.map(function(r){return r[0]});
for (var i=0;i<=(lr-3);i++) {
var val = phones[i][0];
var duplicates = [];
for(var j = 0; j < phonearray.length; j++){
if (phonearray[j] == val){
duplicates.push(j);
}
}
if(duplicates.length>1){
Logger.log("duplicate");
//emailarray.indexOf(email[i]) &&
ss.getRange((i+3),30).setValue(ids[i]);
} else{
Logger.log("no duplicate");
ss.getRange((i+3),30).setValue("");
}
}
}

How do I make less calls to sheet.getRange and reduce the execution time in a double loop

I want to reduce the calls to sheet.getRange in the following function, because the execution times out.
I've tried re-arranging the loops and parsing the data as a double array, however the size of my spreadsheet changes every day and I need to be able to reference the columns by name only.
function runDuplicateRemover() {
var sheet= SpreadsheetApp.getActive().getSheetByName('Sheet 1');
var rangeData = sheet.getDataRange();
var lastRow = rangeData.getLastRow();
var Cdata = sheet.getDataRange().getValues();
// here I am accessing the column which is used to find duplicates
var colCRM = Cdata[0].indexOf("CRM ID")+1;
var arrayOfDuplicates = [];
for(i=1; i<lastRow; i++){
var cellToCompare = sheet.getRange(i+1,colCRM);
// I am just changing all the colors to see the execution
cellToCompare.setBackground("#88b4fc");
var crmToCompare =cellToCompare.getValue();
//checks to see that this value is not already contained in the rows to delete
if (!cellToCompare.isBlank() && (arrayOfDuplicates.indexOf(i)+1)==0 ){
arrayOfDuplicates.push(i);
cellToCompare.setBackground("#f9d9f9");
for (j = i+1; j<lastRow; j++) {
var cellCurrent = sheet.getRange(j+1,colCRM);
cellCurrent.setBackground("#f2fc88");
var crmCurrent = cellCurrent.getValue();
if (crmToCompare == crmCurrent) {
arrayOfDuplicates.push(j);
cellCurrent.setBackground("#fc92f1");
}
}
//pops last value since that's the only one I want to keep
sheet.getRange(arrayOfDuplicates.pop()+1,colCRM).setBackground("#dbf7d4");
}
}
for (t = arrayOfDuplicates.length-1; t>=0; t--) {
sheet.deleteRow(arrayOfDuplicates[t]+1);
}
}
I'd like to reduce the calls to the sheet.getRange, however I don't know how to delete the rows and then return the data back to the sheet without messing all the column order up.

Simple Google Script not working

I have decided to use Google Scripts when I can when using spreadsheets, just to start increasing knowledge. BUT I am untrained and got stuck quite quickly.
I saw some code on youtube for a different script but I thought I extracted what I needed and could get this to work...
function y_add_up_function() {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheet = ss.getSheets()[0];
var range = sheet.getRange["I2:J15"];
var data = range.getValues();
var sum = 0;
for (var i = 0; i < 14; ++i)
{
var row = data[i];
if(row[1] == "y")
{
sum = sum + row[0];
}
}
return sum;
}
This is my spreadsheet. I am trying to just simply get the SS to add up where I have ticked y next to a price, but I am getting an error that reads:
TypeError: Cannot call method "getValues" of undefined. (Line 5)
Please teach me what I am doing wrong ^_^!!!
Cheers!
The error means that you are trying to call a method on an undefined object. If you look at your code, you have range.getValues(). This means that range is undefined. Therefore you need to look at the assignment of range: var range = sheet.getRange["I2:J15"]. And here is the problem, getRange is a method and need to be called with parenthesis, that is getRange("I2:J15").
Here is the working code:
function y_add_up_function() {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheet = ss.getSheets()[0];
var range = sheet.getRange("I2:J15"); // <-- fix here
var data = range.getValues();
var sum = 0;
for (var i = 0; i < 14; ++i)
{
var row = data[i];
if(row[1] == "y")
{
sum = sum + parseFloat(row[0].substring(2)); // <-- fix to get floats from "£ <number>"
}
}
return sum;
}
Also note that since you have pound signs in your cost column, you need to strip the signs to get a number. Here I have used .substring(2), but this is not very fail safe. If you e.g. forget to put "£ " in a cell the number will be read incorrectly. I recommend putting the pound sign in the column header or use a more sophisticated method to remove it when parsing the numbers.
You can for example use row[0].replace(/^[0-9.]/g, ''). This will replace all characters that are not digits nor decimal points. This has no error-checking, but I think it is enough in a personal spreadsheet.

Categories

Resources