Using Built-in function inside Custom Function - google-apps-script

In google sheets, I'd like my custom function to use one of the built-in functions. Specifically, this function would take a string as a parameter, comb through another sheet to locate that value, and then return a link to that cell's address.
function href(findMe) {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var hrefSheet = ss.getSheetByName("otherSheet");
hrefSheet.activate();
var rows = hrefSheet.getDataRange();
var numRows = rows.getNumRows();
var finds = rows.getValues()[1][0];
var hrefCellRow;
for(var i=0; i<numRows; i++){
if(finds[i][0] == findMe){
hrefCellRow = i+1;
break;
}
}
return address(hrefCellRow, 1); //address() is a function that is built in to sheets
}
So if I have a value "XYZ" in the "otherSheet" sheet, when I type
=href("XYZ")
I'd like it to try to find the value XYZ and return an address to the active cell. Extra kudos if it returns an actual link that when clicked, goes to that cell.

I didn't think of this before. I could just use the address function outside of the script and have the custom function embedded in the built-in function (instead of the reverse, which I tried to do). it's not as pretty, but it would be simply
=address(href(findMe),1)
Still haven't found a way to link to another cell, without perhaps creating a function that would reset the active cell?

Related

Absolute reference with UDF & filtering data in google sheets script

Hi I am begineer to scripts, here is the code and googlesheet for reference
What i am getting
what i want to achieve
/**
*#param quest1 Question of the note
*#param quest1 Answer of the note
*#customfunction*/
function DMNOTE(quest1,ans1,quest2,ans2,quest3,ans3,) {
var ss = SpreadsheetApp.getActiveSpreadsheet().getActiveSheet();
var result = quest1+'-'+ans1+','+quest2+'-'+ans2+','+quest3+'-'+ans3;
return result;
}
I want to achieve absolute reference for "quest" parameter and i want it to loop for rest of the coulmns till column where i enter the function.Also under "Formula Required" column i have put formla for reference thats how i want my UDF to work.
Follwing up i need to filter "Non-solicit agreement" and keep only "No" under it and Copy & Paste all colums highlited in blue to update tab.
function toFilter (){
// filter and retain "no" in non-solicit agreement
var ss = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("Worksheet");
ss.getRange(1,1,ss.getLastRow(),ss.getLastColumn());
var createfilter = SpreadsheetApp.newFilterCriteria().setHiddenValues("Yes").build();
ss.getFilter().setColumnFilterCriteria(8, createfilter);
}
Hope i make sense. Any help is appriciated
To work with any number of arguments, you can have a function like below in your script
function DMNOTE(...arg){
let result = ''
for(let i=0;i<arg.length;i++){
result += `${arg[i]}-${arg[i+1]},`
i++;
}
return result.substring(0, result.length - 1);
}
And then form your spreadsheet you can call as =DMNOTE(A$1,A2,B$1,B2,C$1,C2) or =DMNOTE(A$1,A2,B$1,B2,C$1,C2,D$1,D2) The function will process all the arguments being passed and returns the result.
How to filter "non-solicit agreement" and take only data which has "no"
The code you provided comes already very close to what you desire, you just need to make the following adjustments:
setHiddenValues() expect an array, no a string, so change "Yes" to ["Yes"]
getFilter() only works to modify a filter that is present in the sheet already, in case there is none, it is better to delete the old filter and create a new one:
function toFilter(){
var ss = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("Worksheet");
ss.getRange(1,1,ss.getLastRow(),ss.getLastColumn());
var createfilter = SpreadsheetApp.newFilterCriteria().setHiddenValues(["Yes"]).build();
if(ss.getFilter() != null) {
ss.getFilter().remove();
}
ss.getDataRange().createFilter().setColumnFilterCriteria(8, createfilter);
}
How to Copy & Paste all columns highlited in blue to update tab
Build a function that
calls toFilter()
checks for all rows either they are hidden
copies the non- hidden rows into an array
pastes this array to the sheet Update
function copyIfNotHidden(){
toFilter();
var spreadsheet = SpreadsheetApp.getActiveSpreadsheet();
var sheet1 = spreadsheet.getSheetByName("Worksheet");
var sheet2 = spreadsheet.getSheetByName("Update");
var data = sheet1.getDataRange().getValues();
var array = [];
for (var i = 0; i < data.length; i++){
if(sheet1.isRowHiddenByFilter(i+1)==false){
array.push(data[i]);
}
}
sheet2.getRange(sheet2.getLastRow()+1, 1, array.length, array[0].length).setValues(array);
}

Google sparesheet formula/function to get random values without recalculating(randbetween)

I am trying to achive the following. In google sparesheet I have one sheet with values "AllValues", in another sheet "Randomvalues" I would like to get random values from sheet "AllValues".
I have tried two options, first I tried randbetween formula:
=INDEX(AllValues!A4:A103,RANDBETWEEN(1,COUNTA(AllValues!A4:A103)),1)
It is working, but it refresh/recalculate new values all the time column is changed. Googeled a lot and seems that there is not much to do to freeze already calculated results.
Next I tried function:
function random() {
var sss = SpreadsheetApp.getActiveSpreadsheet();
var ss = sss.getSheetByName('Values'); //the sheet that has the data
var range = ss.getRange(1,1,ss.getLastRow(), 4); //the range you need: 4 columns on all row which are available
var data = range.getValues();
for(var i = 0; i < data.length; i++)
{
var j = Math.floor(Math.random()*(data[i].length)); //method of randomization
var element = data[i][j]; // The element which is randomizely choose
ss.getRange(i+1, 6).setValue(element);
}
}
But this function is not working for me, google sparesheet gives error on line 11, that setVaue is not allowed.
Line 11: ss.getRange(i+1, 6).setValue(element);
Googled this one too, there are lot of suggestion, but I am not very familiar with functions, I did not managed to get it working.
Hope that someone can help me out.
Using a formula assumes repeated calculations usually. You cannot prevent them and only can try to return old values instead. This task is not trivial, since any formula cannot refer to the same cell where the result is to be returned (a circular reference occurs). Do not use formulas for single time calculation.
On the other hand, using a script function makes it possible to generate required data directly and only once or on demand. I think, the function below will help you to understand all the neccesary steps for sample source and target ranges.
function random() {
var source = "AllValues!A4:A103",
target = "RandomValues!F2:F22";
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sourceValues = ss.getRange(source).getValues(),
targetRange = ss.getRange(target),
targetValues = [];
while (targetValues.length < targetRange.getHeight()) {
var randomIndex = Math.floor(Math.random() * sourceValues.length);
targetValues.push(sourceValues[randomIndex]);
}
targetRange.setValues(targetValues);
}
You can run it manually or choose a proper trigger.
There are multiple ways of achieving this goal.
Custom Menu
As mentioned by #Tanaike, you can avoid the recalculation and the formula dependency by using a Custom Menu:
// #OnlyCurrentDoc
// Create a function that binds the "simple trigger" for the open event:
function onOpen(e) {
// Add a menu to the UI with the function we want to be able to invoke.
const ui = SpreadsheetApp.getUi();
ui.createMenu("Randomizer")
.addItem("Sample from 'AllValues' sheet", "sampleAllValues")
.addToUi();
}
You then need a function definition matching this name sampleAllValues, and when the user selects the associated menu option, it will be invoked with the permissions of the clicking user (the user will be prompted first to provide consent for access per the script's OAuth scopes).
function sampleAllValues() {
const wb = SpreadsheetApp.getActive();
const destination = wb.getSheetByName("RandomValues");
const source = wb.getSheetByName("AllValues");
if (!source || !destination)
throw new Error("Missing required sheets 'RandomValues' and 'AllValues'");
// Create a flat array of all non-empty values in all rows and columns of the source sheet.
const data = source.getDataRange().getValues().reduce(function (compiled, row) {
var vals = row.filter(function (val) { return val !== ""; });
if (vals.length)
Array.prototype.push.apply(compiled, vals);
return compiled;
}, []);
// Sample the smaller of 50 elements or 10% of the data, without replacement.
const sample = [];
var sampleSize = Math.min(50, Math.floor(data.length * .1));
while (sampleSize-- > 0)
{
var choice = Math.floor(Math.random() * data.length);
Array.prototype.push.apply(sample, data.splice(choice, 1));
}
// If we have any samples collected, write them to the destination sheet.
if (sample.length)
{
destination.getDataRange().clearContent();
// Write a 2D column array.
destination.getRange(1, 1, sample.length, 1)
.setValues(sample.map(function (element) { return [ element ]; }));
// Write a 2D row array
// destination.getRange(1, 1, 1, sample.length)
// .setValues( [sample] );
}
}
Custom Function
If you still wanted to use a custom function from the RandomValues sheet, e.g.
RandomValues!A1: =sampleAllValues(50, AllValues!A1:A)
then you would need to return sample instead of write to a specific sheet. Note that custom functions are treated deterministically--they are computed at the time of entry and then only recalculated when the values of their arguments change. Custom functions run with very limited scope, so be sure to review their restrictions.
The above usage hints that you might find it useful to allow passing in the number of desired samples, and the values to sample from:
function sampleAllValues(sampleSize, value2Darray) {
const data = value2Darray.reduce(function (compiled, row) {
/* as above */
}, []);
/* sample as above */
return sample; // Must be 2D row or 2D column array, or a single primitive e.g. `1`
}
No matter which route you take, be sure to review your script's error logging by viewing your script's Stackdriver logs. (View -> Stackdriver Logging)
References:
Sheet#getRange
Custom functions
Custom menus
Array#reduce
Array#map
Array#splice
.push.apply

Cannot find method getRange(number,(class),number)

I have read all answers about this error, but none of them work for me. So maybe someone has another idea.
I'm trying to pass parameter to function getRange but when I'm trying to invoke this function it shows me error
Cannot find method getRange(number,(class),number)
Here is my code:
function conditionalCheck(color,rangeCondition, rangeSum, criteria){
var condition = SpreadsheetApp.getActiveSheet().getRange(2,rangeCondition,15).getValues();
var val = SpreadsheetApp.getActiveSpreadsheet().getActiveSheet().getRange(2,rangeSum,15).getValues();
var bg = SpreadsheetApp.getActiveSpreadsheet().getActiveSheet().getRange(2,rangeSum,15).getBackgrounds();
var sum = 0;
for(var i=0;i<val.length;i++){
if(condition[i][0] == criteria && bg[i][0] != color){
sum += val[i][0];
}
}
return sum;
}
And I pass a custom function like:
conditionalCheck("#ffff00",1,3,A3)
This is how the sheet looks like:
I understand that JS trying to guess the type of parameters, that is why it thinks that ex. "rangeCondition" is a class, but I don't know how to give him a Number type.
Funny thing is, that this function works when I open the spreadsheet, but when I'm trying to invoke this function while I'm working it shows me this error. So to update sheet I have to reopen the whole spreadsheet.
I was able to reproduce the error by executing the function directly from the editor.
This behavior makes sense, considering custom function needs to receive a valid parameter. At runtime, your 'rangeSum' parameter is set to 'undefined', which invalidates the getRange() method as it requires a number.
It's actually quite strange that you got your 'for' loop working. In most cases, using the the '+' operator on array values obtained from the sheet will concatenate them into a string, so you'll get '5413' instead of 13 (5 + 4 + 1 + 3)
function calculateSum(column) {
column = column||1; //set the default column value to 1
var sheet = SpreadsheetApp.getActiveSheet();
var range = sheet.getRange(1, column, sheet.getLastRow());
var values = range.getValues();
var sum = 0;
for (var i=0; i < values.length; i++) {
sum += parseInt(values[i]);
}
return sum;
}
Finally, this approach is not very efficient in terms of performance. Once you get the instance of the object, you should use that instance instead of attacking the server with multiple calls like you do in the first 3 lines of your code. For example
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheet = ss.getActiveSheet();
var range = sheet.getRange(1, 1, sheet.getLastRow(), 1);
If you plan on using this custom function in multiple places within the sheet, consider replacing it with the script that processes the entire range - this way you will only perform a single read and write operation to update all values. Otherwise, you may end up filling up your service call quotas very quickly
UPDATE
Instead of creating custom function, create the function that takes the entire range and processes all data in one call. You can test the 'calculateSum()' function above with predefined column number, e.g.
var column = 1 // the column where you get the values from
Upon getting the 'sum' value, write it into the target range
var targetRange = sheet.getRange("B1"); // the cell that you write the sum to
targetRange.setValue(sum);
Finally, you can make that function execute after you open or edit the sheet by appending the following code
function onOpen() {
var ui = SpreadsheetApp.getUi();
var menu = ui.createMenu('Menu')
.addItem('Calculate sum', 'calculateSum')
.addToUi();
calculateSum();
}
function onEdit() {
calculateSum();
}
Calling it from the custom menu would be simple - you just need to create the handler and pass function name as parameter.
I found a solution for a problem with reloading custom function after changing data.
The solution is described here:
Refresh data retrieved by a custom function in google spreadsheet
It showed that we cannot refresh custom function if we don't change inputted parameters because it is cached. So if we change inputted data, a function will reload. We can force a function if we put there timestamp

How to wait for an HTTPrequest response

I have a function that request for an URL and if i use that function with a return in it the entire html code will be in the cell with the function name request "=testFormula()".
Why if i use the formula function it will work but if i use the event one it will not work ?
function onEdit(event){
testCom();
}
// This one will trigger automaticaly when you edit any cell in the sheet
function testCom(){
var doc = SpreadsheetApp.getActiveSpreadsheet();
doc.getRange('a1').setValue("Before fetching");
var response = UrlFetchApp.fetch("http://www.google.com/");
doc.getRange('a1').setValue(response);
}
// Use in a cell "=testFormula()" to run this function
function testFormula(){
var response = UrlFetchApp.fetch("http://www.google.com/");
return response;
}
Thanks in advance
According to the documentation http://goo.gl/khdkL the fetch call is sequential, therefore the next line is not executed until the remote URL has been read
If you look at the example from the documentation, they do exactly what you are tyring to do, show the response in several cells:
var result = UrlFetchApp.fetch("http://api.twitter.com/1/statuses/user_timeline.json",
options);
var o = Utilities.jsonParse(result.getContentText());
var doc = SpreadsheetApp.getActiveSpreadsheet();
var cell = doc.getRange('a1');
var index = 0;
for (var i in o) {
var row = o[i];
var col = 0;
for (var j in row) {
if (fields[j]) {
cell.offset(index, col).setValue(row[j]);
col++;
}
}
index++;
}
The onEdit() is a simple trigger and there are restrictions on what you can do inside simple triggers.
To overcome your problem, do the following
a. Rename your function to something else, say onEdit1()
b. In the script editor, click Resources --> Current project's triggers and add a new on edit trigger pointing to this function.
Authorize your script if needed and you should be good to go

Google Apps Script add 1 to the value of a cell in a spreadsheet on button click

I am trying to set up a sign up app where it counts the number of yes, maybe and no responses. Can someone show me how to add 1 to the value of a cell within a spreadsheet from an app? Below is the function that I am using for the Yes button on my app, I have added the click handler to the app.
function yesButton(e){
var app = UiApp.getActiveApplication();
var ss = SpreadsheetApp.openById('spreadsheet ID').getSheets()[0];
var yesCount = ss.getRange("A2");
var yesAdd = yesCount.getValues();
var yesTotal = (yesAdd.value) + 1;
yesTotal.setValue(yesAdd);
return app;
}
When you call getValues(), you obtain a 2D array. Which is referenced like this. yesAdd[row#][col#]. Instead, you should use the function getValue() which returns just a single value (not in an array).
Just change the one line to
var yesAdd = yesCount.getValue();
Then write back to the cell that you read it from.
yesCount.setValue(yesAdd+1);
yesCount still refers to the cell that you'd like to edit, so we set the value on that cell.
Just used this and it worked perfectly, wanted a simple plus minus script to use with some invisible buttons and it works.
I just split mine into plus and minus for current sheet..
function plus() {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var yesCount = ss.getRange("B7");
var yesAdd = yesCount.getValue();
yesCount.setValue(yesAdd+1);
}
.
function minus() {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var yesCount = ss.getRange("B7");
var yesAdd = yesCount.getValue();
yesCount.setValue(yesAdd-1);
}