Vlookup + indexOf to find values in a CSV via Google App Script without using loop - google-apps-script

The main idea is not to need looping to generate a VLOOKUP because it generates a huge slowdown when the amount of data is very large.
To VLOOKUP on data directly in the sheet I do as follows:
function myFunction() {
var s = SpreadsheetApp.getActiveSpreadsheet().getActiveSheet();
var searchValue = s.getRange("Test!A1").getValue();
var data = SpreadsheetApp.openById("XXXXXXXXXXXX").getSheetByName("Test 2");
var dataValues = data.getRange("A1:A").getValues();
var dataList = dataValues.join("ღ").split("ღ");
var index = dataList.indexOf(searchValue);
if (index === -1) {
s.getRange("Test!B1").setValue('off');
} else {
var row = index + 1;
var foundValue = data.getRange("D"+row).getValue();
s.getRange("Test!B1").setValue(foundValue);
}
}
But there is a big problem in this method, because when many different accounts try to access this sheet at the same time, the error type error: could not connect sheet xxxxx appears or causes huge delay sometimes.
So what was the solution I found? Publish spreadsheet pages as CSV so they can be used and this error doesn't happen when many accounts call the same spreadsheet.
Currently, as I haven't found a way to use indexOf using the first column when I import the CSV with several columns of data, I had to create a spreadsheet page only with the copy data of column A, and then I got to the final result of VLOOKUP like this:
(the value in var searchValue in this example case will be two)
function myFunction() {
var s = SpreadsheetApp.getActiveSpreadsheet().getActiveSheet();
var url_columnA = 'AAAAA';
var url_vlookup = 'BBBBB';
var dataSearch = Utilities.parseCsv(UrlFetchApp.fetch(url_columnA));
var dataList = dataSearch.join("ღ").split("ღ");
var searchValue = s.getRange("Test!A1").getValue();
var index = dataList.indexOf(searchValue);
if (index === -1) {
s.getRange("Test!B1").setValue('off');
} else {
var row = index;
var dataVlookup = Utilities.parseCsv(UrlFetchApp.fetch(url_vlookup));
var foundValue = dataVlookup[row][3];
s.getRange("Test!B1").setValue(foundValue);
}
}
Return example:
other number
var url_vlookup:
Col A
Col B
Col C
Col D
home
1
a
win
away
2
b
loose
one
3
c
number
two
4
d
other number
three
5
e
number again?
var url_columnA:
Col A
home
away
one
two
three
Is there any way to handle var url_vlookup data for search the value in column A so that it's not necessary to use this page var url_columnA separated or is the only way to do it without looping?

The first column can easily be separated after parsing using Array.map:
const dataVlookup = Utilities.parseCsv(UrlFetchApp.fetch(url_vlookup));
const url_columnA = dataVlookup.map(row => row[0])

Related

Google Script - Delete a row based on a blank value in a column

newish to Google Apps Script and typically tend to fall through it when I'm writing a script.
I have written the below script but it is aggressively inefficient.
The app is run against over 2k rows of data in 5 columns trying to remove any rows where the cell contains a blank value.
This probably takes the code longer than it takes me manually so trying to find a way to make this more efficient.
Thanks in advance.
function process() {
var app = SpreadsheetApp;
var ss = app.getActiveSpreadsheet();
var mProSheet = ss.getSheetByName("MCC-Processed");
//Remove rows where column E is blank
var mProRange = mProSheet.getDataRange();
var mProRangVal = mProRange.getValues();
var deleteVal = '' //Delete value is blank
var deleteColumIdentifier = 4 //column E is 4th column (A=0)
for(var i = mccProRangVal.length-1; i >= 0; i--){
if(mccProRangVal[i][deleteColumIdentifier] === deleteVal){
mProSheet.deleteRow(i+1);
}
}
}
Script:
function process2() {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var mProSheet = ss.getSheetByName("MCC-Processed");
var mProRangVal = mProSheet.getDataRange().getValues();
var deleteVal = ''
var valToDelete = mProRangVal.filter(function (item) { return item[4] === deleteVal; });
var newDataVal = mProRangVal.filter(x => !valToDelete.includes(x));
mProSheet.getDataRange().clear();
var row = newDataVal.length
var col = newDataVal[0].length
mProSheet.getRange(1, 1, row, col).setValues(newDataVal)
}
In the code you have provided, given there's a huge amount of data (2k rows), it is slow because of the deleteRow() function with the for loop which iterates per row and checking on column E if it's blank. Here is an alternate solution. From your mProRangval, which is your whole data, you can use the filter function to determine those with blank rows. Store this in another array. You can then compare the two arrays to get the difference.
I have tested 2k rows with 800 blanks the reduced the time is from 3 mins down to 2 secs using this function.
Result 1:
(Using for loop)
Result 2:
(using filter)
References:
How to get the difference between two arrays in JavaScript?
https://www.youtube.com/watch?v=PT_TDhMhWsE

How to choose a destination spreadsheet of copied data based on spreadsheet name

So I have two tables. Table 1 is a large table with 7 columns. In this table, C2:C has unique ID numbers of people. Table 2 is a smaller table with the names of all people in K3:K and the unique ID on J3:J. Table 2 only has 2 columns, the IDs and the Name. So, All IDs in C2:C exists in K3:K. All the names in K3:K are also the names of sheets in the same worksheet. So what I'm trying to do is:
Loop through the IDs in table 1 (large table) with the IDs in table 2 (small table). If the IDs are the same, then I will copy the whole row in table 1 into my destination sheet. To choose my destination sheet, I check the cell adjacent to the identified ID in table 2 and choose the sheet whose name is the same.
I hope i explained that decently. My problem it's copying things in every 10 intervals. So it copies the 10th row, then the 20th, then the 30th... and I'm confused how to even approach figuring out where I went wrong because I don't understand why it's in 10 interverals and why it's choosing the sheets that its choosing.
If it helps, link to the sheet is: https://docs.google.com/spreadsheets/d/1522hM3mO9AdaTpiS2oI1IK3wwwFXbOz1qHxcMrRzASY/edit?usp=sharing
function copystuff() {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheets = SpreadsheetApp.getActiveSpreadsheet().getSheets();
var key = SpreadsheetApp.getActiveSheet().getName();
var currentS = ss.getSheetByName(key);
var sheetNameArray = [];
sheetNameArray = sheets.map(function(sheet){ // puts sheet names into an array
return [sheet.getName()];
});
var name_master = currentS.getRange("K3:K").getValues().flat().filter(r=>r!=''); //key name
var enno_master = currentS.getRange("J3:J").getValues().flat().filter(r=>r!=''); //key ID
var enno_all = currentS.getRange("C2:C").getValues().flat().filter(r=>r!=''); // number of big table
for (x = 0; x< enno_master.length; x++){ //to loop through the key number
for (y = 0; y < enno_all.length; y++){ // to loop through the numbers of big table
if(enno_master[x]==enno_all[y]){ // if ID in column C = name in column J
for(z = 0; z < sheetNameArray.length; z++){ //looping through my sheets
if(name_master[x] == sheetNameArray[[z]]){ //if name in column K, which is of the same row as the key number
var copyrange = currentS.getRange("A"+y+2+":G"+y+2).getValues(); //y is the row in table 1 where the IDs are the same.
var destfile = ss.getSheetByName(sheetNameArray[[z]]); //copying to sheet Z.
destfile.getRange(destfile.getLastRow()+1,1,1,7).setValues(copyrange); //paste
}
}
}}}}
The thing that makes it easy is turning your small table into an object so that when the object is given the number in column 3 of the big table it returns the actual sheet object. The array pA in object sht provides you with the ability to check using indexOf() if the Sheet actually exists in the object without having to use hasOwnProperty().
function copyStuff() {
const ss = SpreadsheetApp.getActive();
const sh = ss.getSheetByName('Sheet1');
const objvs = sh.getRange(3,10,getColumnHeight(10,sh,ss)-2,2).getValues();
const vs = sh.getRange(2,1,sh.getLastRow() - 1,7).getValues();
let sht = {pA:[]};
objvs.forEach(r => {sht[r[0]]=ss.getSheetByName(r[1]); sht.pA.push(r[0])});//This creates an object which correlates enNo to a sheet making it possible to greatly simplify your code.
vs.forEach(r => {
if(~sht.pA.indexOf(r[2])) {//this determines if the sheet is defined in sht object if it is then you can do a copy because the sheet exists
let s = sht[r[2]];
if(s) {
s.appendRow(r);//this appends the entire row to the next empty row at the bottom of data. You could also use copyTo or setValues(). This is a simple approach.
}
}
});
}
Helper function: this function is used to the the height of column j. It's sort of the getLastRow() but only for a column:
function getColumnHeight(col, sh, ss) {
var ss = ss || SpreadsheetApp.getActive();
var sh = sh || ss.getActiveSheet();
var col = col || sh.getActiveCell().getColumn();
var rcA = [];
if (sh.getLastRow()){ rcA = sh.getRange(1, col, sh.getLastRow(), 1).getValues().flat().reverse(); }
let s = 0;
for (let i = 0; i < rcA.length; i++) {
if (rcA[i].toString().length == 0) {
s++;
} else {
break;
}
}
return rcA.length - s;
}
~ Bitwise Not
The easiest way to find information in the documentation is to use the index in it. Especially since they just changed it all and many of us no longer know where everything is.
SUGGESTION:
Since you want to check the large table A:G & then copy each table data that matches every user ID on the second table J:K and move those matched rows to the designated sheet named after the IDs' user names, you can refer to this sample implementation below:
function copystuff() {
var ss = SpreadsheetApp.getActiveSheet();
var main =SpreadsheetApp.getActiveSpreadsheet();
var sheets = SpreadsheetApp.getActiveSpreadsheet().getSheets();
var sheetNameArray = sheets.map(function(sheet){
return [sheet.getName()]});
var lastRowK = ss.getRange(ss.getLastRow(),11).getNextDataCell(SpreadsheetApp.Direction.UP).getRow(); //Get the last row of column K on any worksheet
var user = main.getSheetByName(sheetNameArray[0]).getRange("J3:K"+lastRowK).getValues();
var tableData = main.getSheetByName(sheetNameArray[0]).getRange("A2:G"+main.getLastRow()).getDisplayValues();
var container = [];
for(x=0; x<user.length; x++){ //Loop every user from the second table
for(y=0; y<tableData.length; y++){ //loop through the large table data
if(tableData[y][2] == user[x][0]){ //check each user's ID if it has any matches on the user ID from the large table data
container.push(tableData[y]); //any matched row data from the large table will be placed temporarily on this container
}
}
if(container.length != 0){
Logger.log("User "+user[x][1]+ " with ID *"+ user[x][0] +"* has these data:\n"+container);
main.getSheetByName(user[x][1]).getRange(1,1,container.length,7).setValues(container);
}
container = []; //clean container for the next user
}
}
This script will get all table data and place them into an array variables user & tableData where it will be checked and processed to make the script run faster instead of live processing the sheet rows.
Sample Result:
After running the script, here are some of the results:
User name RJ with ID of 42:
User name May with ID of 7:
User name Angelo with ID of 25:
Here's the Log Results for review:

google script too slow, is there a way to speed it up?

I have a script that has been a labour of love, its taken me a while to figure out, but now that I have it working, when using it on a test sheet with a small amount of data it works perfectly without issues.
The issue comes when using it on a live sheet with over 40,000 rows of data, the script simply can not finish within the 30 minutes allowed.
As I am pretty new to script I wonder if I have somehow made my script needlessly long.
I have pasted my script below, but let me explain what I am trying to achieve.
each customer would have their own sales report, and because of privacy they need to be on separate sheets.
So I have my MasterData sheet and another sheet I named URL's
within URL's I have a table that contains the customer name and the ID of their spreadsheet.
What my script achieves is to Filter my MasterData by customer name, copy all the data that is visible and then paste it in the customer's individual spreadsheet taking the ID from the URL's sheet.
It has taken me quite a while to get to this point so I am hoping there is a way I can speed this up.
function get_data(){
var s1 = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("MasterSheet");
var s3 = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("URL's")
var range = s1.getRange(1, 1, s1.getLastRow(), s1.getLastColumn())
var filter = range.getFilter() || range.createFilter();
var foo_index = 2; // column A
var data = []
for (var j = 1;j<s3.getLastRow(); j++){
var filterName = s3.getRange(j+1, 1).getValue()
var criteria = SpreadsheetApp.newFilterCriteria().whenTextEqualTo(filterName).build();
var values = s1.getFilter().setColumnFilterCriteria(1, criteria)
var id = s3.getRange(j+1,2).getValue()
var s2 = SpreadsheetApp.openById(id).getSheetByName("MasterSheet")
var clear = s2.getRange(2,1,s2.getLastRow(),s2.getLastColumn()).clear({contentsOnly: true})
var data = []
for (var i = 1; i < s1.getLastRow(); i++){
if(!s1.isRowHiddenByFilter(i+1)) {
var row_data = s1.getRange(i+1, 1, 1, s1.getLastColumn()).getValues()
data.push(row_data[0])
}
}
var paste = s2.getRange(s2.getLastRow()+1,1,data.length,25).setValues(data)
}
}
I had to make some guesses about header names but if you'll go through this code as well as I tried to go through your code then I think you will come up with a faster solution.
function get_data() {
const ss = SpreadsheetApp.getActive();
const s1 = ss.getSheetByName("MasterSheet");
const s3 = ss.getSheetByName("URL's");
const [mhA, ...mdata]=s1.getDataRange().getValues();
const [chA, ...cdata]=s3.getDataRange().getValues();
let midx = {};//index fo mastersheet by column name
let cidx = {};//index to url's sheet by column name
mhA.forEach((h, i) => { midx[h] == i });
chA.forEach((h, i) => { cidx[h] == i });
cdata.forEach((cr,i)=>{
let data = [];
let cname=cr[cidx['CustomerName']];//I dont really know what you have in s3 for headers so I guessed
let cssid=cr[cidx['ID']];
mdata.forEach((mr,j)=>{
if(mr[midx['CustomerName']]=cname) {//if customer name from s3 matches customer from s1 the save that row in data
data.push(mr);
}
});
let css=SpreadsheetApp.openById(cssid);
let csh=css.getSheetByName("MasterSheet");
csh.getRange(csh.getLastRow()+1,1,data.length,data[0].length).setValues(data);// put all rows into customer mastersheet
});
}

Trying to combine InputBox and indexOf to find text in a single column and make active

I am trying to have the user input text like , 9188675309 in the input box and store that into searchstring variable.
Then it needs to scan the entire column of R which is formatted to look like (918) 867-5309
If the user puts in 5309 or 86753 can it also find the matches?
If any or all matches are found and then highlight the whole row,
function seekAndselect(){
var searchString = Browser.inputBox("Search For Phone Number?");
var sh = SpreadsheetApp.getActiveSpreadsheet();
var ss = sh.getActiveSheet();
var cell = ss.getActiveCell();
cell.setBackground('#ffff55');// replace by cell.setBackground(null); to reset the color when "leaving" the cell
var activeR = cell.getRow()-1;
var activeC = cell.getColumn()-1;
var data = ss.getDataRange().getValues();
var step = 0
for(var r=0;r<data.length;++r){
for(var c=0;c<data[0].length;++c){
step++
Logger.log(step+' -- '+searchString+' = '+data[r][c]);
if(data[r][c]==''||step==1){ continue };
if(searchString.toString().toLowerCase()==data[r][c].toString().toLowerCase()){
ss.setActiveSelection(r+1,c+1);
return;
}
}
}
}
Will there be more than one match? It was a bit unclear. If there could be more than one match, then loop through the R column and match() the input/values. This way also allows for partial matches.
function seekAndselect(){
var sh = SpreadsheetApp.getActiveSpreadsheet();
var ss = sh.getActiveSheet();
// User input
var searchString = Browser.inputBox("Search For Phone Number?");
// Remove non numbers
var clean_search = searchString.replace(/[^0-9]/g, "");
// Get the R column values
var r_col = ss.getRange(1, 18, ss.getLastRow()).getValues();
// Search each row in col R for a match and highlight the row
r_col.forEach(function (e, i) {
// Remove non numbers from data before matching
if (e[0].replace(/[^0-9]/g, "").match(clean_search)) {
var found_range = ss.getRange((i + 1), 1, 1, ss.getLastColumn())
// Highlights the row from col A to R
found_range.setBackground('#ffff55')
// Sets the active selection to the last matched row from col A to R
ss.setActiveSelection(found_range)
}
});
}
I wasnt sure what you wanted with the setActiveSelection(), if there are multiple matches it will only select the last one. Also, if there are a lot of matches, this could take a long time to run, possibly even time-out. If that happens, a batchUpdate() would be needed

Automatically add variables to array?

In a google script I have written something to check my monthly expenses, which are listed in a google sheet.
Based on words the script finds, every line gets a category tag. It works fine, but the number of words to search for is getting big. And the array is getting big too.
I have listed 6 pairs (words to find, tag to add) - but in real version I have as many as 35. How can I create the pairs, and load everything automatically in the array?
This is my script:
function myFunction() {
// check usual suspects
var A1 = ["CAFE", "HORECA"]
var A2 = ["ALBERT", "AH"]
var A3 = ["VOMAR","Vomar"]
var A4 = ["HEMA","HEMA"]
var A5 = ["KRUID","Drogist"]
var A6 = ["RESTA", "Horeca"]
// in Array
var expenses = [A1,A2,A3,A4,A5,A6]
var ss = SpreadsheetApp.getActiveSheet();
var data = ss.getDataRange().getValues(); // read all data in the sheet
for (i in expenses)
{for(n=0;n<data.length;++n){ // iterate row by row and examine data in column A
if(data[n][3].toString().toUpperCase().match(expenses[i][0])==expenses[i][0]){ data[n][4] = expenses[i][1]};
// if column D contains 'xyz' then set value in index [5] (is column E)
}
Logger.log(data)
ss.getRange(1,1,data.length,data[0].length).setValues(data); // write back to the sheet
}
}
I can propose you that:
function multiPass(){
var searchCriterions = [
["CAFE","HORECA" ],
["ALBERT", "AH"],
["VOMAR","Vomar"],
["HEMA","HEMA"]
];
var dico = {};
var patt = "";
for (var i in searchCriterions) {
dico[searchCriterions[i][0]] = searchCriterions[i][1];
patt += "("+searchCriterions[i][0]+")";
if((Number(i)+1)<searchCriterions.length){
patt += "|";
}
}
var re = new RegExp(patt,"");
var ss = SpreadsheetApp.getActiveSheet();
var data = ss.getDataRange().getValues(); // read all data in the sheet
Logger.log(re);
for(n=0;n<data.length;++n){ // iterate row by row and examine data in column A
// THAT'S NOT COLUMN "A", 3 --> "D"
var test = data[n][3].toString().toUpperCase().match(re);
Logger.log(test);
if(test!==null){
data[n][4] = dico[test[0]]
};
}
ss.getRange(1,1,data.length,data[0].length).setValues(data); // write back to the sheet
}
instead of using variable for your "pairs" prefer to use a big table (it's less painfull to write)
then transform your pairs in object to quickly access the second argument of the pair and create a big regexp that check at once all the keywords instead of parsing them one by one.
Now as we are using a big array as search criterions we can totally imagine that this big array is loaded instead of hard coding it. If you have a sheet where the data is you can change the code this way:
var searchCriterions = SpreadsheetApp.getActive().getRange("namedRange").getValues();