Google Apps Script - Improve Script to Execute Faster - javascript

I have a sheet with over 3000 points (lat, long). I am running the following code to calculate distance of each point from 9 reference points and writing the minimum distance next to each point in the sheet. The code runs fine but is slow and times out before running for all the points. How can I make the following code more efficient - so that it reads, computes and writes back the data faster?
/*
******************************
Reference Points
Point_A 41.166866 -76.151926
Point_B 41.087500 -76.204694
Point_C 40.540960 -75.704900
Point_D 40.401080 -75.589600
Point_E 40.326130 -75.642500
Point_F 40.167480 -75.921500
Point_G 40.093370 -76.084700
Point_H 39.974450 -76.063000
Point_I 39.722350 -76.156000
********************************
*/
var SpreadsheetID = "ABC";
var SheetName = "XYZ";
function DistanceCalculator() {
var ss = SpreadsheetApp.openById(SpreadsheetID)
var sheet = ss.getSheetByName(SheetName);
//Select the column
var columnToCheck = sheet.getRange("A:A").getValues();
// Get the last row in the column
var lastRow = getLastRowSpecial(columnToCheck);
// Ref point latitude
var Latitude_Point_A = 41.166866;
var Latitude_Point_B = 41.087500;
var Latitude_Point_C = 40.540960;
var Latitude_Point_D = 40.401080;
var Latitude_Point_E = 40.326130;
var Latitude_Point_F = 40.167480;
var Latitude_Point_G = 40.093370;
var Latitude_Point_H = 39.974450;
var Latitude_Point_I = 39.722350;
// Ref point longitude
var Longitude_Point_A = -76.151926;
var Longitude_Point_B = -76.204694;
var Longitude_Point_C = -75.704900;
var Longitude_Point_D = -75.589600;
var Longitude_Point_E = -75.642500;
var Longitude_Point_F = -75.921500;
var Longitude_Point_G = -76.084700;
var Longitude_Point_H = -76.084700;
var Longitude_Point_I = -76.156000;
for( var i=1; i<=lastRow;i++){
//Reading Lat_Long from the sheet
var Lat_Check = sheet.getRange(i,2).getValue();
var Long_Check = sheet.getRange(i,3).getValue();
//Calculating distance between each point and reference points
var distance01 = calculateDistance(Latitude_Point_A,Longitude_Point_A,Lat_Check,Long_Check);
var distance02 = calculateDistance(Latitude_Point_B,Longitude_Point_B,Lat_Check,Long_Check);
var distance03 = calculateDistance(Latitude_Point_C,Longitude_Point_C,Lat_Check,Long_Check);
var distance04 = calculateDistance(Latitude_Point_D,Longitude_Point_D,Lat_Check,Long_Check);
var distance05 = calculateDistance(Latitude_Point_E,Longitude_Point_E,Lat_Check,Long_Check);
var distance06 = calculateDistance(Latitude_Point_F,Longitude_Point_F,Lat_Check,Long_Check);
var distance07 = calculateDistance(Latitude_Point_G,Longitude_Point_G,Lat_Check,Long_Check);
var distance08 = calculateDistance(Latitude_Point_H,Longitude_Point_H,Lat_Check,Long_Check);
var distance09 = calculateDistance(Latitude_Point_I,Longitude_Point_I,Lat_Check,Long_Check);
//measuring minimum distance
var minimumDistance = Math.round(Math.min(distance01,distance02,distance03,distance04,distance05,distance06,distance07,distance08,distance09))
sheet.getRange(i,4).setValue(minimumDistance);
}
}
function calculateDistance(Ref_Lat, Ref_Long, Looped_Lat, Looped_Long){
var p = 0.017453292519943295; // Math.PI / 180
var c = Math.cos;
var a = 0.5 - c((Looped_Lat - Ref_Lat) * p)/2 +
c(Ref_Lat * p) * c(Looped_Lat * p) *
(1 - c((Looped_Long - Ref_Long) * p))/2;
var result = 7926 * Math.asin(Math.sqrt(a)); // 2 * R; R = 3963 miles
return result
};
function getLastRowSpecial(range){
var rowNum = 0;
var blank = false;
for(var row = 0; row < range.length; row++){
if(range[row][0] === "" && !blank){
rowNum = row;
blank = true;
}else if(range[row][0] !== ""){
blank = false;
};
};
return rowNum;
};
Sample Spreadsheet Screenshot

I believe your goal is as follows.
You want to reduce the process cost of your script.
In your script, getValue and setValue are used in a loop. I thought that this might be the reason for your issue. In this case, at first, the values are retrieved from the sheet and the values are processed, then, the values are put to the sheet. This flow is suitable for reducing the process cost. I think that "Best Practice" will be useful for understanding your issue. Ref
Also, I thought that when the process cost of your script is reduced, a little ingenuity might be required to be used for modifying your script. So I propose one of several modified scripts as a reference. Please modify your script as follows.
From:
for( var i=0; i<lastRow;i++){
//Reading Lat_Long from the sheet
var Lat_Check = sheet.getRange(i,2).getValue();
var Long_Check = sheet.getRange(i,3).getValue();
//Calculating distance between each point and reference points
var distance01 = calculateDistance(Latitude_Point_A,Longitude_Point_A,Lat_Check,Long_Check);
var distance02 = calculateDistance(Latitude_Point_B,Longitude_Point_B,Lat_Check,Long_Check);
var distance03 = calculateDistance(Latitude_Point_C,Longitude_Point_C,Lat_Check,Long_Check);
var distance04 = calculateDistance(Latitude_Point_D,Longitude_Point_D,Lat_Check,Long_Check);
var distance05 = calculateDistance(Latitude_Point_E,Longitude_Point_E,Lat_Check,Long_Check);
var distance06 = calculateDistance(Latitude_Point_F,Longitude_Point_F,Lat_Check,Long_Check);
var distance07 = calculateDistance(Latitude_Point_G,Longitude_Point_G,Lat_Check,Long_Check);
var distance08 = calculateDistance(Latitude_Point_H,Longitude_Point_H,Lat_Check,Long_Check);
var distance09 = calculateDistance(Latitude_Point_I,Longitude_Point_I,Lat_Check,Long_Check);
//measuring minimum distance
var minimumDistance = Math.round(Math.min(distance01,distance02,distance03,distance04,distance05,distance06,distance07,distance08,distance09))
sheet.getRange(i,4).setValue(minimumDistance);
}
To:
var values = sheet.getRange("B1:C" + lastRow).getValues();
var ar = [[Latitude_Point_A, Longitude_Point_A], [Latitude_Point_B, Longitude_Point_B], [Latitude_Point_C, Longitude_Point_C], [Latitude_Point_D, Longitude_Point_D], [Latitude_Point_E, Longitude_Point_E], [Latitude_Point_F, Longitude_Point_F], [Latitude_Point_G, Longitude_Point_G], [Latitude_Point_H, Longitude_Point_H], [Latitude_Point_I, Longitude_Point_I]];
var res = values.map(([b, c]) => [Math.round(Math.min(...ar.map(e => calculateDistance(...e, b, c))))]);
sheet.getRange(1, 4, res.length).setValues(res);
In this modification, from your script, it supposes that the 1st row is not the header row. Please be careful about this.
In this modification, the values are retrieved from the sheet by getValues, and then, the retrieved values are calculated and put to the sheet by setValues.
Note:
When I saw your script, I thought that an error occurs at var Lat_Check = sheet.getRange(i,2).getValue(); because the 1st value of i is 0. And in the function calculateDistance, Ref_Lat, Ref_Long are not used. So I'm worried that your showing script might be different from the script that you have the issue with. So, when you test the above modification, when an error occurs, can you confirm whether your showing script is the correct script for your situation. And, please provide the sample Spreadsheet for replicating the issue as the image. By this, I would like to confirm it.
References:
map()
getValues()
setValues(values)

Related

Google scripts: How to put a range in setValue

I have used the example code from this link, to make the code below.
https://yagisanatode.com/2017/12/13/google-apps-script-iterating-through-ranges-in-sheets-the-right-and-wrong-way/
Most of the code is working as expected, except for the last row.
In Column E, I want to place the custom function =apiav() with the data from cell A.
However the code is returning =apiav(Range) in the Google sheet cells. (to be clear it should be something like =apiav(a1))
I tried everything i could think of and of course googled for hours, but i am really lost and can't find the right solution for this.
function energy(){
var sector = "Energy";
var ss=SpreadsheetApp.getActiveSpreadsheet();
var sheet = ss.getActiveSheet();
var rangeData = sheet.getDataRange();
var lastColumn = 2;
var lastRow = 999 ;
var sheet = ss.getSheets()[0];
var searchRange = sheet.getRange(2,2, lastRow-1 ,1 );
var ouputrange = sheet.getRange(2,4, lastRow-1 ,1 );
//clear range first
ouputrange.clear("D:D");
ouputrange.clear("E:E");
/*
GOOD - Create a client-side array of the relevant data
*/
// Get array of values in the search Range
var rangeValues = searchRange.getValues();
// Loop through array and if condition met, add relevant
// background color.
for ( i = 0; i < lastColumn - 1; i++){
for ( j = 0 ; j < lastRow - 1; j++){
if(rangeValues[j][i] === sector){
sheet.getRange(j+2,i+4).setValue("yes");
var formularange = sheet.getRange (j+2,i+1);
sheet.getRange(j+2,i+5).setValue('=apiav(' + formularange + ')');
}
};
};
};
Replace:
var formularange = sheet.getRange(j+2,i+1);
with:
var formularange = sheet.getRange(j+2,i+1).getA1Notation();
So you will be able to pass the cell reference instead of the range object.

Trying to paste Values from formula Google App Script

This is just a snippet of my code from Google App Script which iterates through each row in columns 1, 2, 3. If an edit is made in column 3, an incremental ID will be generated and a concatenation of the same row and different columns will also be generated - in this case Column D, E, and F. I am struggling with figuring out a way to change the formulas into values. What am I missing here?
// Location format = [sheet, ID Column, ID Column Row Start, Edit Column]
var locations = [
["Consolidated Media Plan",1,9,3]
];
function onEdit(e){
// Set a comment on the edited cell to indicate when it was changed.
//Entry data
var range = e.range;
var col = range.getColumn();
var row = range.getRow();
// Location Data
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheet = ss.getActiveSheet();
function getNewID(){
function IDrange(){
var dataRange = sheet.getDataRange();
var lastRow = dataRange.getLastRow();
return sheet.getRange(IDrowStart,IDcol,lastRow-IDrowStart).getValues();
};
//Get largest Value in range
function getLastID(range){
var sorted = range.sort();
var lastIDval = sorted[sorted.length-1][0];
return lastIDval;
};
//Stores leading letters and zeroes and trailing letters
function getLettersNzeroes(id){
//Get any letters or zeroes.
var re = new RegExp("^([a-zA-Z0])$");
var letterZero = [];
for(char = 0; char < id.length; char++){
if(re.test(id[char])){
letterZero.push([char,id[char]]);// [[position, letter or zero]]
};
};
// Categorize letters and zeroes into start and end blocks
var startLetterZero = "",
endLetter = "",
len = letterZero.length - 1;
for(j = 0; j < letterZero.length; j++){
if(letterZero[j][0] === j){
startLetterZero += letterZero[j][1];
}else if(letterZero[j][1] !== "0" && letterZero[len][0] - (len - j) == letterZero[j][0]){
endLetter += letterZero[j][1];
};
};
var startNend = {"start":startLetterZero,"end":endLetter};
return startNend;
};
//Gets last id number. Adds 1 an checks to set if its new length is greater than the lastNumber.
function getNewNumber(id){
var removeZero = false;
var lastNum = parseInt(id.replace(/\D/g,''),10);//Remove letters
var newNum = (lastNum+1).toString();
if(lastNum.toString().length !== newNum.length){
var removeZero = true;
};
var newNumSet = {"num":newNum, "removeZero": removeZero};
return newNumSet
};
var lastID = getLastID(IDrange());
var lettersNzeroes = getLettersNzeroes(lastID);
var newNumber = getNewNumber(lastID);
//If the number is 9,99,999,9999 etc we need to remove a zero if it exists.
if(newNumber.removeZero === true && lettersNzeroes.start.indexOf("0") !== -1.0){
lettersNzeroes.start = lettersNzeroes.start.slice(0,-1);
};
//Rejoin everything together
var newID = lettersNzeroes.start +
newNumber.num +
lettersNzeroes.end;
return newID;
};
for(i = 0; i < locations.length; i++){
var sheetID = locations[i][0],
IDcol = locations[i][1],
IDrowStart = locations[i][2],
EditCol = locations[i][3];
var offset = IDcol - EditCol;
var cell = sheet.getActiveCell();
if(sheetID === sheet.getName()){
if(EditCol === col){
//ID Already Exists the editing cell isn't blank.
if(cell.offset(0,offset).isBlank() && cell.isBlank() === false){
var newID = getNewID();
cell.offset(0,offset).setValue(newID);
cell.offset(0,-1).setFormulaR1C1('=concatenate(R[0]C[-1],"_",INDEX(Glossary!K:K,MATCH(R[0]C[2],Glossary!J:J,0)))');
};
};
};
};
};
EDIT:
This is my full code, I have been unsuccessful with trying to retrieve just the values of the formula within the same (i.e, If C9 gets edited, a formula with the values specific to the 9th row should be populated)
Also, I've tried to add an index/match formula to the concatenation formula at the bottom of the code - it works as expected on the google sheets, but when I run it with the script it pastes the correct formula but it returns a #NAME? error message. However, when I copy and paste the exact same formula in the cell, it works perfectly, any idea what could be causing this error?
This works for me. I know it's not exactly the same thing but I didn't have access to getNewId()
function onEdit(e) {
var sh=e.range.getSheet();
if(sh.getName()!='Sheet1')return;
//e.source.toast('flag1');
if(e.range.columnStart==3 && e.range.offset(0,1).isBlank() && e.value) {
//e.source.toast('flag2');
e.range.offset(0,1).setValue(e.value);
e.range.offset(0,2).setFormulaR1C1('=concatenate(R[0]C[-1],"_",R[0]C[-2],"_",R[0]C[-3],"_",R[0]C[-4])');
}
}

How should I add the rows (without the headers) after the last row with data (without overwriting anything in the "Master_db")

The code below is overwriting on the existing data.
#OMila helped me with the original code, I could not articulate exactly what I needed hence starting a new question.
function Dom() {
var origin_sheet = SpreadsheetApp.getActive().getSheetByName('Dom_Sum');
var firstRow = 1;
var firstCol = 1;
var numRows = origin_sheet.getLastRow();
var numCols = 22;
var origin_values = origin_sheet.getRange(firstRow, firstCol, numRows, numCols).getValues();
var dest_values = [];
for(var i = 0; i < origin_values.length; i++) {
if(origin_values[i][0] != '') {
dest_values.push(origin_values[i]);
}
}
var dest_id = "1ZGq7L7bvF1INuDgZxhHnVsihkYkYubmncSAE5uC-Pq4";
var dest_sheet = SpreadsheetApp.openById(dest_id).getSheetByName("Master_Db");
var numRowsDest = dest_values.length;
var dest_range = dest_sheet.getRange(1, 1, numRowsDest, 22);
dest_range.setValues(dest_values);
}
I would like to add the data created in the "Dom_Sum" worksheet below the last row of data in the other workbook with the worksheet name "Master_Db"
#OMila I'm really grateful to you, and if you like we could offer you a consultation fee for future projects. (boseav#gmail.com)
Instead of writing your value into the range dest_sheet.getRange(1, 1, numRowsDest, numCols)
retrieve the last row of your destination sheet and write starting with the next row
var destLastRow=dest_sheet.getLastRow();
var dest_range = dest_sheet.getRange(destLastRow+1, 1, numRowsDest, numCols);
dest_range.setValues(dest_values);

GetValue/SetValue Command + Do While Loop in Google Sheet File?

I have written a 'for loop' (see below) to get values from an order entry sheet under a tab called 'POTemplate' in the Google Sheets file I am working with. It seems to get, and set, variables just fine until it gets to the skuNO variable. It returns a blank cell value into the target Sheet entitled POHistory.
function Submit() {
var app = SpreadsheetApp;
var activeSheet =
app.getActiveSpreadsheet().getSheetByName("POTemplate");
for(var i = 24; i<= 34; i++) {
var poNO = activeSheet.getRange("h2").getValue();
var poDate = activeSheet.getRange("h3").getValue();
var skuNo = activeSheet.getRange(i, 3).getValue();
var skuDesc = activeSheet.getRange(i, 4).getValue();
var qty = activeSheet.getRange(i, 5).getValue();
var uom = activeSheet.getRange(i, 6).getValue();
var utCost = activeSheet.getRange(i, 7).getValue();
var extCost = activeSheet.getRange(i, 8).getValue();
var targetSheet =
app.getActiveSpreadsheet().getSheetByName("POHistory");}
targetSheet.getRange('a2').setValue(poNO);
targetSheet.getRange('b2').setValue(poDate);
targetSheet.getRange('c2').setValue(skuNo);
targetSheet.getRange('d2').setValue(skuDesc);
targetSheet.getRange('e2').setValue(qty);
targetSheet.getRange('f2').setValue(uom);
targetSheet.getRange('g2').setValue(utCost);
targetSheet.getRange('h2').setValue(extCost);
}
If you are looking to simply copy the cells over, copyTo may be useful. If you want to add additional information as well, then appendRow will be helpful. If you have many cells to operate on, the batch methods getValues() and setValues() will be very helpful.
var numCols = 8 - 3 + 1;
var numRows = 34 - 24 + 1;
var vals = activeSheet.getRange(24, 3, numRows, numCols).getValues();
/*
* vals = [ [skuNo1, skuDesc1, qty1, uom1, utCost1, extCost1],
* [skuNo2, ...] ]
*
* Add code that does stuff with / to vals, like using
* Array.unshift() to insert a value at the start of a given row,
* or constructing a new array based on vals but with different element orders,
* some omitted elements, new elements, etc.
*/
var targetColStart = 1; // Column A
var targetRowStart = targetSheet.getLastRow() + 1;
// Select a range having the same size as the `vals` array of arrays.
// (Or whichever array of arrays is to be batch-printed.)
targetSheet.getRange(targetRowStart, targetColStart, vals.length, vals[0].length)
.setValues(vals);

Count rows by background color failed

Hey guys I am in google sheets excel and I want to count how many rows have the sheet based in background color.
I have the next code:
function countColoredCells(countRange,colorRef) {
var activeRg = SpreadsheetApp.getActiveRange();
var activeSht = SpreadsheetApp.getActiveSheet();
var activeformula = activeRg.getFormula();
var countRangeAddress = activeformula.match(/\((.*)\,/).pop().trim();
var backGrounds = activeSht.getRange(countRangeAddress).getBackgrounds();
var colorRefAddress = activeformula.match(/\,(.*)\)/).pop().trim();
var BackGround = activeSht.getRange(colorRefAddress).getBackground();
var countCells = 0;
for (var i = 0; i < backGrounds.length; i++)
for (var k = 0; k < backGrounds[i].length; k++)
if ( backGrounds[i][k] == BackGround )
countCells = countCells + 1;
return countCells;
};
function onOpen() {
var spreadsheet = SpreadsheetApp.getActiveSpreadsheet();
var entries = [{
name : "Contar",
functionName : "countColoredCells"
}];
spreadsheet.addMenu("Contar numero de casillas por color", entries);
}
But when i put the next code doesn't work? Shows error. What's happens?
=countcoloredcells(B1:B9,A1)
When you run activeformula.match(/\((.*)\,/) I think there are instances when .match() is returning null and therefore you can not call .pop().
You'll have to do something like this...
var match = activeformula.match(/\((.*)\,/);
if(match) var countRangeAddress = match.pop().trim(); //if match is not null...
But of course deal with not having that variable in the rest of your script.
The script you posted uses a unusual method of getting the range argument. It fails at regex because at your location, Sheets uses ; as delimiter rather than , making the match null. Regardless, Taking the argument directly should work. So, Try changing the original code
From
var countRangeAddress = activeformula.match(/\((.*)\,/).pop().trim();
var backGrounds = activeSht.getRange(countRangeAddress).getBackgrounds();
var colorRefAddress = activeformula.match(/\,(.*)\)/).pop().trim();
To
var countRangeAddress = countRange
var backGrounds = activeSht.getRange(countRangeAddress).getBackgrounds();
var colorRefAddress = colorRef
You could also try replacing the regex match , with ; ( like match(/\((.*)\;/).)
Usage should be like:
=countcoloredcells("B1:B9";"A1")
Use this code instead.
The arguments need to be passed as strings.
/**
* Usage countColoredCells("A1:B3";"C5")
* #param {range} countRange Range to be evaluated
* #param {colorRef} colorRef Cell with background color to be searched for in countRange
* #return {number}
* #customfunction
*/
function countColoredCells(countRange,colorRef) {
var activeRange = SpreadsheetApp.getActiveRange();
var activeSheet = activeRange.getSheet();
var formula = activeRange.getFormula();
var backgrounds = activeSheet.getRange(countRange).getBackgrounds();
var colorRefBackground = activeSheet.getRange(colorRef).getBackground();
var count = 0;
for(var i=0;i<backgrounds.length;i++)
for(var j=0;j<backgrounds[0].length;j++)
if( backgrounds[i][j] == colorRefBackground )
count=count+1;
return count;
};

Categories

Resources