Implementing vlookup and match in function - google-apps-script

I'm trying to create a function in Sheets that combines a "Vlookup" and "Match" combination that I use frequently.
I want to use my function, "Rates" to accept 1 argument and return a combination of Vlookup and Match, that always uses the same values.
Vlookup(argument, DEFINED RANGE (always stays the same defined range), match(A1 (always cell A1), DIFFERENT DEFINED RANGE, 0), FALSE)
I have tried creating a script, but have no experience coding, and I receive an error that "vlookup is not defined"
function ratesearch(service) {
return vlookup(service, Rates, Match($A$1,RatesIndex,0),FALSE);
}
Actual results: #ERROR!
ReferenceError: "vlookup" is not defined. (line 2).

function findRate() {
var accountName = SpreadsheetApp.getActiveSpreadsheet().getActiveSheet().getRange(1,1).getValue(); //determine the account name to use in the horizontal search
var rateTab = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('Rates'); //hold the name of the rate tab for further dissection
var rateNumColumns =rateTab.getLastColumn(); //count the number of columns on the rate tab so we can later create an array
var rateNumRows = rateTab.getLastRow(); //count the number of rows on the rate tab so we can create an array
var rateSheet = rateTab.getRange(1,1,rateNumRows,rateNumColumns).getValues(); //create an array based on the number of rows & columns on the rate tab
var currentRow = SpreadsheetApp.getActiveSpreadsheet().getActiveSheet().getActiveCell().getRow(); //gets the current row so we can get the name of the rate to search
var rateToSearch = SpreadsheetApp.getActiveSpreadsheet().getActiveSheet().getRange(currentRow,1).getValue(); //gets the name of the rate to search on the rates tab
for(rr=0;rr<rateSheet.length;++rr){
if (rateSheet[rr][0]==rateToSearch){break} ;// if we find the name of the
}
for(cc=0;cc<rateNumColumns;++cc){
if (rateSheet[0][cc]==accountName){break};
}
var rate = rateSheet[rr][cc] ; //the value of the rate as specified by rate name and account name
return rate;
}

Optimization points for Alex's answer:
Never forget to declare variables with var, const or let (rr and cc). If you omit the keyword, the variables will be global and cause you a lot of trouble (as they will not reset after the loop finishes). The best way is to use block-scoped let.
Following #1, do not rely on out-of-scope variables (rateSheet[rr][cc]).
You do not need to call SpreadsheetApp.getActiveSpreadsheet() multiple times - that's what variables are for. Call once, then reuse.
getRange(1,1,<last row>, <last col>) is equivalent to a single getDataRange call.
use find or findIndex method to avoid verbose loops.
With the points applied, you get a clean and optimized function to use:
const findRate = () => {
const ss = SpreadsheetApp.getActiveSpreadsheet();
const accountName = ss.getActiveSheet().getRange(1, 1).getValue();
const rateTab = ss.getSheetByName("Rates");
const rates = rateTab.getDataRange().getValues();
const currentRow = ss.getActiveSheet().getActiveCell().getRow();
var rateToSearch = ss.getActiveSheet().getRange(currentRow, 1).getValue();
const rr = rates.findIndex((rate) => rate === rateToSearch);
const [firstRates] = rates;
const cc = firstRates.findIndex((rate) => rate === accountName);
return rates[rr][cc];
};
Note that the "vlookup" is not defined error indicates there is no vlookup variable / function declaration in scope. Which obviously is the case as there is no built-in Google Apps Script vlookup function.

You can't access random ranges from a custom function so you would have to provide the data to the function, some of the other solutions here that use get active spreadsheet won't work as a custom function which I am guessing is what the OP is looking for, here is an example of a script that does that but word of warning before you go down this road, custom functions are much slower than the built in functions so doing this will be much slower than vlookup and match, if you only have a few functions like this in the sheet you will be fine, but if you build large tables with dozens of rows that use custom functions it will slow down you spreadsheet substantially.
// Combines VLOOKUP and MATCH into a single function
// equivalent to VLOOKUP(rowValue, tableData, MATCH(columnName, tableHeader))
// but in this version tableData includes tableHeader
function findInTable(tableData, columnName, rowValue) {
if (rowValue === "") {
return "";
}
if (tableData.length == 0) {
return "Empty Table";
}
const header = tableData[0];
const index = header.indexOf(columnName);
if (index == -1) {
return `Can't find columnName: ${columnName}`;
}
const row = tableData.find(row => row[0] == rowValue);
if (row === undefined) {
return `Can't find row for rowValue: ${rowValue}`;
}
return row[index];
}
Another optimization I suggest you do is use named ranges, it allows you to transform something like:
=VLOOKUP(C5, 'Other Sheet'!A2:G782, MATCH("Column Name", 'Other Sheet'!A1:G1))
into a more readable and easier to look at:
=VLOOKUP(C5, someTableData, MATCH("Column Name", someTableHeader))
for the custom function form this will look like:
=findInTable(A1:G782, "Column Name", C5)
Note that I shorted the argument list by merging the data and header, this makes some assumptions about the table structure, e.g. that there is a one header line and that the lookup value is in the first column but it makes it even shorter and easier to read.
But as mention before this comes at the cost of being slower.
I ended up giving up on using this for my needs due to how slow it is and how much faster VLOOKUP and MATCH are since they are built in functions.

vlookup is not something you can use in a function in a script, it is a spreadsheet formula.
Google Scripts use JavaScript, so you'll need to write your code in JS then output it to a relevant cell.
If you could share your sheet/script we could help figure it out with you.

Related

Sheets Filter Script across multiple tabs displaying blank rows on master sheet, and also causing other scripts to stop functioning

I have a multifaceted question.
I'm attempting to have a script which pulls the tab names, and uses that info to pull specific cells from each one (tabs for the doc change frequently) in order to create a Master Sheet. The Master Sheet is meant to display all open action items, and filter out any closed items / blank rows. The script I have so far works, but it pulls all empty rows from each tab - which I don't want.
Here's 1 of the 2 current scripts I have for the master sheet:
function onEdit(e) {
//set variable
const masterSheet = "Open Action Items";
const mastersheetFormulaCell = "E2";
const ignoreSheets = ["Template", "Blank"];
const dataRange = "I2:K";
const checkRange = "J2:J"
//end set variables
const ss = SpreadsheetApp.getActiveSpreadsheet();
ignoreSheets.push(masterSheet);
const allSheets = ss.getSheets();
const filteredListOfSheets = allSheets.filter(s => ignoreSheets.indexOf(s.getSheetName()) == -1);
let formulaArray = filteredListOfSheets.map(s => `iferror(FILTER('${s.getSheetName()}'!${dataRange}, NOT(ISBLANK('${s.getSheetName()}'!${checkRange}))),{"","",""})`);
let formulaText = "={" + formulaArray.join(";") + "}";
ss.getSheetByName(masterSheet).getRange(mastersheetFormulaCell).setFormula(formulaText);
}
The other part of this is another script that has been running ok when it was the only script running, but has since stopped working when the other script were introduced. This script added a checkbox to column C based on criteria in column B.
Here's that script:
function onEdit(e) {
if (e.range.getColumn() == 2) {
var sheet = e.source.getActiveSheet();
if (e.value === "Tech Note" ||
e.value === "Intake Process")
sheet.getRange(e.range.getRow(),3).insertCheckboxes();
else
sheet.getRange(e.range.getRow(),3).removeCheckboxes();
}
}
Here's a sample sheet
The "Open Action Items" tab is the master sheet the script is meant to update. It should list all the open items from other tabs (explained below)
The "Copy of E3-O Case Notes" is a tab which is the basis of what every tab will eventually look like. Columns F-K of this tab pull open items from A-E. There may likely be a more efficient way to sort this whole sheet...
Any help appreciated, thank you!
I'll address the second question first, as it's a more fundamental problem.
The other part of this is another script that has been running ok when it was the only script running, but has since stopped working when the other script were introduced.
In the script project attached to your sample, you have 3 files which each define an onEdit() function. This is problematic because each time you define onEdit() you're redefining the same identifier. The project only has a single global scope, so there can only be 1 onEdit() function defined, regardless of how many files your project contains.
Essentially, this is equivalent to what you've defined in your project:
function onEdit(e) {
console.log("onEdit #1");
}
function onEdit(e) {
console.log("onEdit #2");
}
function onEdit(e) {
console.log("onEdit #3");
}
onEdit();
Running the above snippet will only execute the last definition of onEdit().
To accomplish what you're trying to do, you can instead define unique functions for all the actions you want to perform and then, in a single onEdit() definition, you can call those functions. Something like:
function editAction1(e) {
console.log("edit action #1");
}
function editAction2(e) {
console.log("edit action #2");
}
function editAction3(e) {
console.log("edit action #3");
}
function onEdit(e) {
editAction1(e);
editAction2(e);
editAction3(e);
}
onEdit();
When defining an onEdit() trigger, you really want to optimize it so that it can complete its execution as quickly as possible. From the Apps Script best practices, you want to pay particular attention to "Minimize calls to other services" and "Use batch operations".
A few specific tips for you:
Avoid repeated calls to the same Apps Script API (e.g. Sheet.getName()). Instead, run it once and store the value in local variable.
As much as possible, avoid making Apps Script API calls within loops and in callback functions passed to methods such as Array.prototype.filter() and Array.prototype.map().
When you do need to loop through data, especially when Apps Script API calls are involved, minimize the number of times you iterate through the data.
With onEdit() triggers, try to structure the logic so that you identify cases where you can exit early (similar to how you perform the column check before going ahead with manipulating checkboxes). I doubt you actually need to iterate through all of the sheets and update the "Open Action Items" formula for every single edit. If I'm interpreting the formula properly, it's something that should only be done when sheets are added or removed.
Finally, to address the blank rows in your formula output, instead of using SORT() to group the blank rows you can use QUERY() to actually filter them out.
Something like:
=QUERY({ <...array contents...> }, "select * where Col1 is not null")
Note that when using QUERY() you need to be careful that the input data is consistent in regards to type. From the documentation (emphasis mine):
In case of mixed data types in a single column, the majority data type
determines the data type of the column for query purposes. Minority
data types are considered null values.
In your sample sheet, a lot of the example data varies and doesn't match what you'd actually expect to see (e.g. "dghdgh" as a value in a column meant for dates). This is important given the warning above... when you have mixed data types for a given column (i.e. numbers and strings) whichever type is least prevalent will silently be considered null.
After taking a closer, end-to-end look at your sample, I noticed you're performing a very convoluted series of transformations (e.g. in the data sheets there's the hidden "D" column, the QUERY() columns to the right of the actual data, etc.). This all culminates in a large set of parallel QUERY() calls that you're generating via your onEdit() implementation.
This can all be made so much simpler. Here's a pass at simplifying the Apps Script code, which is dependent on also cleaning up the spreadsheet that it's attached to.
function onEdit(e) {
/*
Both onEdit actions are specific to a subset of the sheets. This
regular expression is passed to both functions to facilitate only
dealing with the desired sheets.
*/
const validSheetPattern = /^E[0-9]+/;
updateCheckboxes(e, validSheetPattern);
updateActionItems(e, validSheetPattern);
}
function updateCheckboxes(e, validSheetPattern) {
const sheet = e.range.getSheet();
// Return immediately if the checkbox manipulation is unnecessary.
if (!validSheetPattern.exec(sheet.getName())) return;
if (e.range.getColumn() != 2) return;
const needsCheckbox = ["Tech Note", "Intake Process"];
const checkboxCell = sheet.getRange(e.range.getRow(), 3);
if (needsCheckbox.includes(e.value)) {
checkboxCell.insertCheckboxes();
} else {
checkboxCell.removeCheckboxes();
}
}
function updateActionItems(e, validSheetPattern) {
const masterSheetName = "Open Action Items";
const dataLocation = "A3:E";
/*
Track the data you need for generating formauls in an array
of objects. Adding new formulas should be as simple as adding
another object here, as opposed to duplicating the logic
below with a growing set of manually indexed variable names
(e.g. cell1/cell2/cell3, range1/range2/range3, etc.).
*/
const formulas = [
{
location: "A3",
code: "Tech Note",
},
{
location: "E3",
code: "Intake Process",
},
];
const masterSheet = e.source.getSheetByName(masterSheetName);
const sheets = e.source.getSheets();
/*
Instead of building an array of QUERY() calls, build an array of data ranges that
can be used in a single QUERY() call.
*/
let dataRangeParts = [];
for (const sheet of sheets) {
// Only call getSheetName() once, instead of multiple times throughout the loop.
const name = sheet.getSheetName();
// Skip this iteration of the loop if we're not dealing with a data sheet.
if (!validSheetPattern.exec(name)) continue;
dataRangeParts.push(`'${name}'!${dataLocation}`);
}
const dataRange = dataRangeParts.join(";");
for (const formula of formulas) {
/*
And instead of doing a bunch of intermediate transformations within the sheet,
just query the data directly in this generated query.
*/
const query = `SELECT Col5,Col1,Col4 WHERE Col2='${formula.code}' AND Col3=FALSE`;
const formulaText = `IFERROR(QUERY({${dataRange}},"${query}"),{"","",""})`;
formula.cell = masterSheet.getRange(formula.location);
formula.cell.setFormula(formulaText);
}
}
Here's a modified sample spreadsheet that you can reference.
The one concession I made is that the data sheets still have a "Site Code" column, which is automatically populated via a formula. Having all the data in the range(s) you feed into QUERY() makes the overall formulas for the "Open Action Items" sheet much simpler.

Google Sheets Custom Formula Sometimes Works Sometimes Doesn't

I have a spreadsheet in which I developed a custom function called RawVal:
function RawVal(BlockName) {
try {
var rawVal = 1;
var thiSheet = SpreadsheetApp.getActiveSheet();
var BlockRow = thiSheet.getRange("C:C").getValues().map(function(row) {return row[0];}).indexOf(BlockName);
if (BlockRow > -1) {
var baseVal = thiSheet.getRange("B" + (BlockRow+1)).getValue();
var ingVal = thiSheet.getRange("D" + (BlockRow+1)).getValue();
rawVal = Math.max(baseVal, ingVal);
Logger.log(BlockName+": base="+baseVal+"; ing="+ingVal+"; max="+rawVal);
}
return rawVal;
}
catch (e) {
Logger.log('RawVal yielded an error for " + Blockname + ": ' + e);
}
}
While the function is long, the intent is to replace a moderately sized function from having to be typed in on each row such as:
=if(sumif(C:C,"Emerald Block",D:D)=0,sumif(C:C,"Emerald Block",B:B),sumif(C:C,"Emerald Block",D:D))
The problem is sometimes it works and sometimes it just doesn't. And it doesn't seem to be related to the content. A cell that worked previously may display #NUM and have the error "Result was not a number". But if I delete it and retype it (but oddly not paste the formula), most of the time it will calculate correctly. Note: it is NOT stuck at "Loading", it is actually throwing an error.
Debug logs haven't been useful - and the inconsistency is driving me crazy. What have I done wrong?
EDIT: I replaced the instances of console.log with Logger.log - the cells calculated correctly for 6 hours and now have the #NUM error again.
It seems that your custom function is used in many places (on each row of the sheet). This and the fact that they stop working after a while points to excessive computational time that Google eventually refuse to provide. Try to follow their optimization suggestion and replace multiple custom functions with one function that processes an array and returns an array. Here is how it could work:
function RawVal(array) {
var thiSheet = SpreadsheetApp.getActiveSheet();
var valuesC = thiSheet.getRange("C:C").getValues().map(function(row) {return row[0];});
var valuesBD = thiSheet.getRange("B:D").getValues();
var output = array.map(function(row) {
var rawVal = 1;
var blockRow = valuesC.indexOf(row[0]);
if (blockRow > -1) {
var baseVal = valuesBD[blockRow][0];
var ingVal = valuesBD[blockRow][2];
rawVal = Math.max(baseVal, ingVal);
}
return [rawVal];
}
return output;
}
You'd use this function as =RawVal(E2:E100), for example. The argument is passed as a double array of values, and the output is a double array too.
Also, when using ranges like "C:C", consider whether the sheet has a lot of empty rows under the data: it's not unusual to see a sheet with 20000 empty rows that pointlessly get searched over by functions like that.
Alternative: use built-in functions
It seems that your custom function is mostly a re-implementation of existing =vlookup. For example,
=arrayformula(iferror(vlookup(H:H, {C:C, B:B}, 2, false), 1))
looks up all entries in H in column C, and returns the corresponding values in column B; one formula does this for all rows (and it returns 1 when there is no match). You could have another such for column D, and then another arrayformula to take elementwise maximum of those two columns (see Take element-wise maximum of two columns with an array formula for the latter). The intermediate columns can be hidden from the view.

How to use the getRange() method with an array value as one of the parameters

I don't have much experience using Javascript but I'm developing a simple code to filter some information relevant to a professor I'm helping. I am searching the row number of a certain amount of data using a for and then I'm using an array to store all the rows that contain those words. Since I'm using Appscript, I only need to relocate a certain amount of data from the row I'm returning to a final row I've already know. My code is as follows:
if(cell === "Average")
{
index++;
initialcoords[index] = n; // n is the iteration variable in the for
}
I've tested the contents of the array and they are just fine, so I'm storing correctly the rows. The problem is that I'm using a different method to paste the data in a different sheet in Google Spreadhsheets. My code to do so is the following:
function pasteInfo()
{
var ss = SpreadsheetApp.getActiveSpreadsheet();
var source = ss.getSheetByName("Sheet 1");
var destination = ss.getSheetByName("Sheet 2");
var range = source.getRange(initialcoords[1], 1, 8, 3);
range.copyValuesToRange(destination, 4, 6, 4, 6);
}
My probelm is the getRange() since it prints an error like this:
can't find method getRange((class),number,number,number).
I believe that even if n is declared as an integer, the values that I'm returning are of a different type incompatible with the getRange() method. Could anyone help me to confirm this and to help me convert it to integer? I would really appreciate your help.
You first need to define the Sheet you want to get the data from since a Spreadsheet can have multiple Sheets.
You need to ensure you have appropriate default values defined before using the parameters, otherwise the interpreter will start making guess.
Provide defaults if parameters are empty:
function fillLine(row, column, length, bgcolor)
{
row = row || 0;
column = column || 0;
length = length || 1;
bgcolor = bgcolor || "red";
var sheet = SpreadsheetApp.getActiveSheet();
sheet.getRange(1+row, 1+column, 1, length).setBackground(bgcolor)
}
You may also try the solution offered by community: Can't get Google Scripts working

Google Script sort 2D Array by any column

I had a asked an earlier question about retrieving records from a database, here: Retrieving Records from a Google Sheet with Google Script
I'm fairly comfortable with manipulating arrays and creating my own sorting algorithms, but I want to use the existing Array.sort() method to organize the data because of its speed. I'm finding that I can easily use this to sort a 2D array by the first column of data, but I can't find the syntax to sort on a different column of data, other than the first.
The closest that I've found is this: Google Apps Script Additional Sorting Rules. However, these inputs haven't worked for me. Here is what I get for the following code, for my array, tableData:
tableData.sort([{ column: 1}]);
=>TypeError: (class)#4dde8e64 is not a function, it is object. (line 49, file "sortTablebyCol")
tableData.sort([{column: 1, ascending: true}]);
=> TypeError: (class)#4d89c26e is not a function, it is object. (line 50, file "sortTablebyCol")
What is the proper syntax for choosing which column of data to sort on?
The array.sort method can have a function argument to choose on what part you want to sort. Code goes like this :
array.sort(function(x,y){
var xp = x[3];
var yp = y[3];
// in this example I used the 4th column...
return xp == yp ? 0 : xp < yp ? -1 : 1;
});
EDIT
Following your comment, here is a small demo function that should help to understand how this works.
Instead of using short form if/else condition I used the traditional form and splitted it in 3 lines to make it easier to understand.
function demo(){
// using a full sheet as array source
var array = SpreadsheetApp.getActive().getActiveSheet().getDataRange().getValues();
Logger.log('Unsorted array = '+array);
array.sort(function(x,y){
// in this example I used the 4th column...
var compareArgumentA = x[3];
var compareArgumentB = y[3];
// eventually do something with these 2 variables, for example Number(x[0]) and Number(y[0]) would do the comparison on numeric values of first column in the array (index0)
// another example x[0].toLowerCase() and y[0].toLowerCase() would do the comparison without taking care of letterCase...
Logger.log('compareArgumentA = '+compareArgumentA+' and compareArgumentB = '+compareArgumentB);
var result = 0;// initialize return value and then do the comparison : 3 cases
if(compareArgumentA == compareArgumentB ){return result }; // if equal return 0
if(compareArgumentA < compareArgumentB ){result = -1 ; return result }; // if A<B return -1 (you can change this of course and invert the sort order)
if(compareArgumentA > compareArgumentB ){result = 1 ; return result }; // if a>B return 1
}
);
Logger.log('\n\n\nSorted array = '+array);
}
I added a couple of Logger.log to check starting, intermediate and final values. Try this in a spreadsheet.
Hoping this will help.
It seems if instead of using .getValues , you restrict to .getDataRange then perhaps your original sort code "tableData.sort([{column: 1, ascending: true}]);" can work if you avoid the square bracket.
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheet = ss.getSheets()[0];
var range = sheet.getRange("A1:C7");
// Sorts by the values in the first column (A)
range.sort(1);
// Sorts by the values in the second column (B)
range.sort(2);
// Sorts descending by column B
range.sort({column: 2, ascending: false});
I found this in Google Documentation
My suggestion is to use a library like underscore.js which has a lot of useful functions to manipulate collections, arrays, map/reduce, sorting etc... Works without a glitch with Google Apps Script. That's the first library I add to any project I start on GAP.

Locating a cell's position in google sheets using a string or an integer

This problem seems so simple but I've been searching/trying for several hours to find a solution. I basically have a large spreadsheet (3k rows of ISBN's) which each have a corresponding author, title, genre etc.
My plan was to make a function that allows me to simply produce a small UI, which takes in an Isbn and an amount, has a submit button (all done) and then finds that ISBN and increments a value in that row. The big stumbling block is being able to use the isbn which has been stored as a variable and finding the match in the table, as I simply cannot find some sort of "getValue()" function in google sheets.
I've seen several examples of people loading their entire data into an array and comparing the desired value to each individual entry, but that seems ridiculously inefficient and slow to me? Surely there must be a simple, efficient way that I'm just overlooking. All I'm after is a simple way of searching for a value, finding that value in the sheet and returning that row.
Thanks in advance!
Your assumption that there must be a more efficient way to find an item in a spreadsheet is not true. There is no "find" method in spreadsheet service but array iteration is fast and bulk data reading is also very fast so you shouldn't be annoyed with the speed parameter even for very long lists.
Here are 2 code snippets that find the row number of a string in column 1.
In the first one I transposed the array so that I could search in a single column in a simple array.
Both show the time it takes in milliseconds.
function testISBN1(){
var time = new Date().getTime();
var isbn = 'test1990'; // just as a test... create your own Ui to enter the value to search for
Logger.log(findISBN(isbn)+' in '+(new Date().getTime()-time)+' mSec');// view result... false if no occurrence (do something with that value)
}
function findISBN(isbn){
var sh = SpreadsheetApp.getActive().getSheetByName('Sheet1');// select your sheet
var data = sh.getDataRange().getValues(); // get all data at once
var isbnColumn = transpose(data)[0];// if isbn is column 1 (it became row1 because of transpose
for(var n in isbnColumn){
if(isbnColumn[n] == isbn){ n++ ; return n }; // if found, increment to get the sheet row number
}
return false;// not found
}
function transpose(a) { // classical Array transpose JS function
return Object.keys(a[0]).map(
function (c) { return a.map(function (r) { return r[c]; }); }
)
}
And the second one, without transpose takes about the same time to execute (about 230 mS in a 2000 rows sheet)
function testISBN2(){
var time = new Date().getTime();
var isbn = 'test1990'; // just as a test... create your own Ui to enter the value to search for
Logger.log(findISBN2(isbn)+' in '+(new Date().getTime()-time)+' mSec');// view result... false if no occurrence (do something with that value)
}
function findISBN2(isbn){
var sh = SpreadsheetApp.getActive().getSheetByName('Sheet1');// select your sheet
var data = sh.getDataRange().getValues(); // get all data at once
for(var n in data){
if(data[n][0] == isbn){ n++ ; return n }; // if found (in column 1), increment to get the sheet row number
}
return false;// not found
}
Below is the execution transcript that shows the time taken by every line of code. As you can see, the total execution time is mainly used to get the sheet and get its values.