Improving efficiency of adding a column to an array in a loop? - javascript

So I wrote some code that goes to a book's ID based on a "dictionary" and takes the data from all the sheets in that book after the second sheet. That data is then aggregated into a single array. My question centers around the idea of improving the for loop in the if statement. That is adding the "supplier name" to the front of the subarray that represents a row in the sheet/data. I was told that this is inefficient as the code has to go through each subarray and add a value.
Is there a more efficient way of doing this? I did it this way because I am copying the values of a range which are stored as an array of arrays. So to add data, I have to reaccess the subarrays. Is it possible to add the new data (supplier name) at the same time that the values are being copied? would this be more efficient? It was recommended to use an arrow function, however, I am not familiar with their usage.
function aggregate() {
var combinedData = []
var idArray = {
"suppliername":"id",
};
for (var supplierName in idArray){
var sBook = SpreadsheetApp.openById(idArray[supplierName]);
var sheets = sBook.getSheets();
for (var index = 2; index <sheets.length; index++){
var sheet = sheets[index];
var dataLength = sheet.getRange("E5:E").getValues().filter(String).length;
if(dataLength != 0){
var dataRange = sheet.getRange(5,2,dataLength,14);
var dataValues = dataRange.getValues();
for (row in dataValues) {
dataValues[row].unshift(supplierName);
};
combinedData = combinedData.concat(dataValues);
};
};
};
var dataLength = combinedData.length;
const dSheet = SpreadsheetApp.openById("id").getSheets()[0];
dSheet.getRange(2,1,dSheet.getMaxRows(),dSheet.getMaxColumns()).clearContent();
var dRange = dSheet.getRange(2,1,dataLength,15);
dRange.setValues(combinedData);
};

In your script, how about the following modification?
From:
for (row in dataValues) {
dataValues[row].unshift(supplierName);
};
combinedData = combinedData.concat(dataValues);
To:
combinedData = combinedData.concat(dataValues.map(e => [supplierName].concat(e)));
or
combinedData = [...combinedData, ...dataValues.map(e => [supplierName, ...e])];
References:
map()
Spread syntax

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.

Removing duplicates in Google Sheets (script) taking too long to process

I am trying to have my entire sheet deduped and the script works fine however, it takes 60+ seconds to run. Am I over complicating this and really there is a simpler code to get what I need? It just seems like a long time to process such a simple task.
My data is only between 4-12k rows.
function removeDuplicates() {
var sheet = SpreadsheetApp.getActiveSpreadsheet().getSheets()[1];
var data = sheet.getDataRange().getValues();
var newData = [];
for (var i in data) {
var row = data[i];
var duplicate = false;
for (var j in newData) {
if (row.join() == newData[j].join()) {
duplicate = true;
}
}
//If not a duplicate, put in newData array
if (!duplicate) {
newData.push(row);
}
}
//Delete the old Sheet and insert the newData array
sheet.clearContents();
sheet.getRange(1, 1, newData.length, newData[0].length).setValues(newData);
}
If you use an object, you'll dramatically lower the number of iterations.
function removeDuplicates() {
const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheets()[0];
const data = sheet.getDataRange().getValues();
let newDataObject = {};
for (let row of data) {
newDataObject[row.join()] = row;
}
const newData = Object.values(newDataObject);
// Clear the old Sheet and insert the newData array
sheet.clearContents();
sheet.getRange(1, 1, newData.length, newData[0].length).setValues(newData);
}
As other approach, how about using the method of removeDuplicates()? When your script is modified, it becomes as follows.
Modified script:
function removeDuplicates() {
SpreadsheetApp.getActiveSpreadsheet().getSheets()[1].getDataRange().removeDuplicates();
}
Reference:
removeDuplicates()
Now that we have V8 you can use the Set class. Didn't make any attempts to benchmark performance so I don't know if you'll fair any better where speed of execution is concerned, code is far more readable though. Try the following and tell me how it goes:
// V8 runtime version using Set
function removeDuplicates(sheetName) {
var sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName(sheetName);
var rows = sheet.getDataRange().getValues();
var dedupedValues = [];
var set = new Set();
rows.forEach(function(row) {
let key = row.join();
if (set.has(key)) return;
set.add(key);
dedupedValues.push(row);
});
//Delete the old Sheet and insert the dedupedValues array
sheet.clearContents();
sheet.getRange(1, 1, dedupedValues.length, dedupedValues[0].length).setValues(dedupedValues);
}
If you're not comfortable with V8 you can accomplish the same thing using #Diego's solution...but with a few tweaks as follows:
// ES5 version using object keys
function removeDuplicates(sheetName) {
var sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName(sheetName);
var rows = sheet.getDataRange().getValues();
var dedupedValues = [];
var keys = {};
rows.forEach(function(row) {
var key = row.join();
if (key in keys) return;
keys[key] = true;
dedupedValues.push(row);
});
//Delete the old Sheet and insert the dedupedValues array
sheet.clearContents();
sheet.getRange(1, 1, dedupedValues.length, dedupedValues[0].length).setValues(dedupedValues);
}
DISCLAIMER: I don’t know the Google Sheets API.
I suggested a few improvements and added the comments in the code.
A big thing in performance is caching. So don’t do things twice if not necessary (or DRY = Don’t repeat yourself!).
If you forget your wallet and have to go upstairs again, it doubles the time taking you to leave the front door. Same with code.
If you are generally interested in performance boosts (and everyone should be) I suggest you to take a look at How to write Performant JavaScript- Mark Nadal
function removeDuplicates() {
var sheet = SpreadsheetApp.getActiveSpreadsheet().getSheets()[1];
var data = sheet.getDataRange().getValues();
var newData = [];
var newDataJoinedCache = {};
// help the loop to not have to read the same values over and over again
// if you prepare as much data as possible for the loop it will thankfully speed up
for (var i = 0, len = data.length; i < len; i++) {
// data won’t change in the loop, so we can cache it here once(!)
// as you can see, I added a semicolon to prevent unwanted results
// imagine a row with colums [ "a", "bc"] and another one with [ "ab", "c"]
// just joined without separator they will both be the same "abc", which is wrong
var joinedRow = data[i].join(";");
// no need as we will know it with one simple comparison
// var duplicate = false;
// Just make one simple comparison
// instead of over and over joining the arrays in newData just cache them
if (!newDataJoinedCache[joinedRow]) {
newData.push(data[i]);
// we push the joined string as a cache
newDataJoinedCache[joinedRow] = true;
}
}
//Delete the old Sheet and insert the newData array
sheet.clearContents();
sheet.getRange(1, 1, newData.length, newData[0].length).setValues(newData);
}
Let me know how it works out and how much time this will save in comparison.

Pasting multiple rows of data into single cell using JavaScript

So I have combinations of names to tasks in a table where several different task are associated with the same name. But I need to put the task into one cell next to the associated name. Using JavaScript. Heres what I got;
function Unique(){
var ss = SpreadsheetApp.openById("ID");
var dataRaw = ss.getSheetByName("Sheet1");
var destination = ss.getSheetByName("Sheet2");
var names2 = dataRaw.getRange(2,10,dataRaw.getLastRow(),1).getValues();
var names1 = names2.flat(1);
var names = names1;
//var names = ["name1","name1","name2", "name3", "name3"];
var uniqueNames = []; //empty array
var count = 0;
var found = false;
for (i = 0; i < names.length; i++){
for(y =0; y < uniqueNames.length; y++){
if(names[i] == uniqueNames[y]){
found = true;
}
}
count++;
if(count == 1 && found == false){
uniqueNames.push(names[i]);
}
count = 0;
found = false;
}
/* can I use this??? maybe it's not needed
var uniqueNames2 = uniqueNames.map(function(obj) {
return Object.keys(obj).sort().map(function(key) {
return obj[key];
});
});
*/
var dest = destination.getRange(1,2,uniqueNames.length,uniqueNames[0].length);
dest.setValue(uniqueNames); //maybe this is not needed
console.log(uniqueNames[0].length);
}
My approach is to;
take in names and output the unique names so there is no doubles
once i have unique names use some type of for() loop or map() function to find tasks and pair with names? maybe im wrong?
and then setValues() to the range that I need.
The problems that I'm running into are that My Unique() function needs a regular array not array of arrays, which i fix using
array.flat(1)
but then to paste the values javaScript needs the array or arrays to be just an array which I COULD fix with
Object.keys(obj).sort().map(function(key)
in the commented out section? to turn an array of arrays back into an array... but then my "width" is not consistent for my array, columns, and I get the error that my range is not the same number of columns as my data. I feel that this is fairly simple and I am grossly over complicating things. Any help would be great thank you. My google sheet below https://docs.google.com/spreadsheets/d/1rbz52kkzhVAGX21MUVoexzPUvWxjk-RCw-5PrRLoBBc/edit?usp=sharing
I believe your goal as follows.
You want to achieve the following conversion.
From
Task Names
Task 1 name one
Task 2 name one
Task 3 name one
Task 4 name one
Task 5 name one
Task 1 name two
Task 2 name two
Task 3 name two
Task 1 name three
Task 2 name three
Task 3 name three
To
task names
Task 1
Task 2
Task 3
Task 4
Task 5 Name one
Task 1
Task 2
Task 3 name two
Task 1
Task 2
Task 3 name three
For this, how about this answer?
Modification points:
In your script, for example, about var names2 = dataRaw.getRange(2,10,dataRaw.getLastRow(),1).getValues();, I thought that you might misunderstand the row and column for getRange. And, in this case, only one row Names of column "B" on "Sheet1" is retrieved. The row of Task is not retrieved in your script. And also, from dest.setValue(uniqueNames);, you might misuderstood setValue and setValues.
When above points are reflected to your script, it becomes as follows.
Modified script:
In this modification, at name2, the values from the cells "B2:B12" are retrieved, and the unique values are retrieved using your script. Then, the values from the cells "A2:B12" are retrieved, and the values for putting to Spreadsheet are created using the created unique values. Then, the created values are put to the Spreadsheet.
Modified script:
function Unique_org2(){
var ss = SpreadsheetApp.openById("ID");
var dataRaw = ss.getSheetByName("Sheet1");
var destination = ss.getSheetByName("Sheet2");
var names2 = dataRaw.getRange(2,2,dataRaw.getLastRow()-1,1).getValues(); // <--- Modified
var names1 = names2.flat(1);
var names = names1;
var uniqueNames = [];
var count = 0;
var found = false;
for (i = 0; i < names.length; i++){
for(y =0; y < uniqueNames.length; y++){
if(names[i] == uniqueNames[y]){
found = true;
}
}
count++;
if(count == 1 && found == false){
uniqueNames.push(names[i]);
}
count = 0;
found = false;
}
// --- I added below script.
var values = dataRaw.getRange(2, 1, dataRaw.getLastRow() - 1, 2).getValues(); // Added
var uniqueNames = uniqueNames.reduce((ar, e) => {
var temp = "";
values.forEach(([a, b]) => {
if (e == b) temp += a + "\n";
});
ar.push([temp.trim(), e]);
return ar;
}, []);
// ---
var dest = destination.getRange(2,1,uniqueNames.length,uniqueNames[0].length); // <--- Modified
dest.setValues(uniqueNames); // <--- Modified
}
Other pattern:
In this pattern, in order to achieve your goal, I would like to propose the other sample script of following flow. This flow might be able to reduce the process cost from above modified script.
Retrieve values from the cells "A2:B12" of "Sheet1".
Create an object from the retrieved values.
Convert the object to an array for putting to Spreadsheet.
Put the values to Spreadsheet to the destination sheet.
Sample script:
function Unique(){
var ss = SpreadsheetApp.openById("ID");
var dataRaw = ss.getSheetByName("Sheet1");
var destination = ss.getSheetByName("Sheet2");
// 1. Retrieve values from the cells "A2:B12" of "Sheet1".
const values = dataRaw.getRange(2, 1, dataRaw.getLastRow() - 1, 2).getValues();
// 2. Create an object from the retrieved values.
const obj = values.reduce((o, [a, b]) => Object.assign(o, {[b]: (o[b] ? o[b] + a : a) + "\n"}), {});
// 3. Convert the object to an array for putting to Spreadsheet.
const res = Object.entries(obj).map(([k, v]) => [v.trim(), k]);
// 4. Put the values to Spreadsheet to the destination sheet.
destination.getRange(2, 1, res.length, res[0].length).setValues(res);
}
References:
getRange(row, column, numRows, numColumns)
setValue(value)
setValues(values)

How to push array position in the array faster

I have a google spreadsheet data in form of array. Now I want to push the array position I used for loop which is working fine on small data but when the data length is increased it result in delay.
Is there a faster way to push the array position in the array.
Here is the code which I am currently using:-
var ss = SpreadsheetApp.openById('19zxxxxxxxxxxxxxxxxxxxxxxxxOI');
var sheet1 = ss.getSheetByName('Sheet2');
var data = sheet1.getRange("A:H").getValues();
var email = Session.getActiveUser().getEmail();
for(var i = 0; i < data.length; i++)
{
data.unshift(i+1);
} // this for loop takes too much time.
data = data.filter(function(item){return item[7] == email});
var x = data.map(function(val){
return val.slice(0, -7);
})
Logger.log(x)
return x;
}
I believe your goal as follows.
If data.unshift(i+1) is data[i].unshift(i+1) as TheMaster's comment, you want to retrieve the values of the column "A" when the value of column "G" is the same with email. At that time, you want to add the row number to the 1st index of the row value.
From your script, I understood like this.
You want to reduce the process cost of this situation.
For this problem, how about this solution?
Pattern 1:
In this pattern, your script is modified. In this case, the result values are retrieve by one loop.
Sample script:
function myFunction() {
var ss = SpreadsheetApp.openById('19zxxxxxxxxxxxxxxxxxxxxxxxxOI');
var sheet1 = ss.getSheetByName('Sheet2');
var data = sheet1.getRange("A1:H" + sheet1.getLastRow()).getValues(); // Modified
var email = Session.getActiveUser().getEmail();
const res = data.reduce((ar, [a,,,,,,g], i) => { // Modified
if (g == email) ar.push([i + 1, a]);
return ar;
}, []);
Logger.log(res)
return res;
}
Pattern 2:
In this pattern, as other method, TextFinder and Sheets API are used. In this case, the size of base data by searching email with TextFinder can be reduced. And each values are retrieved by one API call using Sheets API.
Sample script:
Before you use this script, please enable Sheets API at Advanced Google services.
function myFunction() {
const spreadsheetId = '19zxxxxxxxxxxxxxxxxxxxxxxxxOI';
const ss = SpreadsheetApp.openById(spreadsheetId);
const sheet = ss.getSheetByName('Sheet2');
const email = Session.getActiveUser().getEmail();
// 1. Retrieve the ranges of rows by searching "email" at the column "G".
const ranges = sheet.getRange("G1:G" + sheet.getLastRow()).createTextFinder(email).findAll();
// 2. Create an object for using with Sheets API.
const reqs = ranges.reduce((o, e) => {
const row = e.getRow();
o.rows.push(row);
o.ranges.push("A" + row);
return o;
}, {rows: [], ranges: []});
// 3. Retrieve values and add the row number.
const res = Sheets.Spreadsheets.Values.batchGet(spreadsheetId, {ranges: reqs.ranges})
.valueRanges
.map((e, i) => ([reqs.rows[i], e.values[0][0]]));
Logger.log(res)
return res;
}
If email is included other string, please use matchEntireCell(true) to TextFinder.
References:
reduce()
Advanced Google services
Method: spreadsheets.values.batchGet

Google script - Why copyTo in for-loop doesn't work?

I'm trying to do copy-paste (only values) in some dynamic parts of my spreadsheet in order to get it ready to be exported to excel. I'm trying to use the copyTo method using an array and a nested for-loop.
// here i define all the sheets
var activeSS, mikumimSheet,rankSheet, conveSheet, srcSheet, clkImpSheet, gaSheet, salesSheet, lndpageSheet;
activeSS = SpreadsheetApp.getActiveSpreadsheet();
mikumimSheet = activeSS.getSheetByName('עותק של מיקומים בגוגל');
rankSheet = activeSS.getSheetByName("מיקומים בגוגל");
conveSheet = activeSS.getSheetByName("המרות");
srcSheet = activeSS.getSheetByName("מקורות תנועה");
clkImpSheet = activeSS.getSheetByName("חשיפות והקלקות");
gaSheet = activeSS.getSheetByName("Google Ads");
salesSheet = activeSS.getSheetByName("מכירות באתר");
lndpageSheet = activeSS.getSheetByName("דפי נחיתה");
// here some code inject data to cells all over the spread sheet
// setHeaders();
// here i store all the cells i need to copy paste and run the copyTo method
function copyPasteReplace() {
var cells = [
[srcSheet, "A2:G2", "B53:C53"],
[salesSheet, "A1:F1"],
[clkImpSheet, "A1:E1"],
[rankSheet, "A1:E1", "C11:E11"],
[conveSheet, "A1:G1", "A30:H30", "O11:O17"],
[lndpageSheet, "A1:D1"],
[gaSheet, "A1:F1"]
];
for (var i = 0; i < cells.length; i++) {
for (var n = 1; n < cells[i].length; n++) {
cells[i][0].getRange(cells[i][n]).copyTo(cells[i][0].getRange(cells[i][n]), {
contentsOnly: true
});
}
}
}
// this is the order of execution
function onOpennn() {
setHeaders();
copyPasteReplace();
}
I expected all the values in the sheets first get injected and then pasted in value form (without formulas).
What actualy happens in that the cells just get emptied.
I found the problem with my code. It turns out, that i had to add the sheet name in the ranges array, like so:
// instead of:
var cells = [
[srcSheet, "A2:G2", "B53:C53"],
[salesSheet, "A1:F1"],
[clkImpSheet, "A1:E1"],
[rankSheet, "A1:E1", "C11:E11"],
[conveSheet, "A1:G1", "A30:H30", "O11:O17"],
[lndpageSheet, "A1:D1"],
[gaSheet, "A1:F1"]
];
// It should have been:
var cells = [
[srcSheet, 'מקורות תנועה!A2:G2', 'מקורות תנועה!B53:C53'],
[salesSheet, 'מכירות באתר!A1:F1'],
[clkImpSheet, 'חשיפות והקלקות!A1:E1'],
[rankSheet, 'מיקומים בגוגל!A1:E1', 'מיקומים בגוגל!C9:H9'],
[conveSheet, 'המרות!A1:G1', 'המרות!B24:G24', 'המרות!O11:O17'],
[lndpageSheet, 'דפי נחיתה!A1:D1'],
[gaSheet, 'Google Ads!A1:F1']
];
I don't know the reason for it, because the sheet is already defined,
but that what fixed it.

Categories

Resources