Update custom function in different cells - google-apps-script

I have a spreadsheet with a custom function calling a web API like this:
function myFunction(value) {
var value = value
...
}
and I call this function in different cells in B column in this way:
=myFunction(A2)
=myFunction(A3)
=myFunction(A4)
...
so that the values change regarding the content of A column.
Now, I would like to update all these functions with a trigger, which could be every minute, or every hour, or at midnight. I used the built in trigger on Google Apps Script interface, like I did in the past with external scripts, but it doesn't work (I think because the trigger call the function without the "value" variable).
I was thinking to add an external triggered script that update the value of another cell (let's suppose "C1"), and then use the onEdit function to update the custom functions. I searched a lot about onEdit, but I really didn't understand how to make it works in my case.
Should the onEdit function recall myFunction? In what way?
Any help is appreciated, thank you

You can use this function for refreshing formulas in your column:
function updateColumnBFormulas()
{
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheet = ss.getSheetByName('test');
var range = sheet.getRange(1, 2, sheet.getLastRow(), 1);
// get all formulas
var formulas = range.getFormulas();
for (var i = 0; i < formulas.length; i++)
{
// if there's formula in a cell
if (formulas[i][0])
{
// refresh it
sheet.getRange(i+1, 2).setFormula(formulas[i][0]);
}
}
}
Then set up trigger to run it every minute (Edit - Current project's trigger):

Ok, I solved with an external script that updates a cell, and then a dummy argument in my custon function that refers to that cell (as suggested by #RobinGertenbach)
This is the external triggered script:
function update() {
var number = Math.floor(new Date().getTime()/1000);
ss.getSheetByName("Data").getRange("A1").setValue(number);
}
and my custom function now looks like:
function myFunction(value, now) {
var value = value
var now = now
...
}
and then it is called in column B in this way:
=myFunction(A2, $A$1)
=myFunction(A3, $A$1)
=myFunction(A4, $A$1)
...
The solution with a function for refreshing formulas posted above was not doing the job, I don't know why. Formulas were refreshing but the values didn't get updated.
Thank you very much for your help!

Related

Google Apps script on cell background change recalculation [duplicate]

I know this question has been asked before but the answers given are not valid for my case because it's slightly different.
I've created a formula that looks for sheets with a pattern in the name and then uses it's content to generate the output. For example
function sampleFormula(searchTerm) {
const spreadsheet = SpreadsheetApp.getActiveSpreadsheet();
const sheets = spreadsheet.getSheets()
.filter(function(sheet) {
// If sheet name starts with DATA_SHEET_...
return sheet.getSheetName().indexOf('DATA_SHEET_') === 0;
});
const result = [];
sheets.forEach(function(sheet) {
// We get all the rows in the sheet
const rows = sheet.getDataRange().getValues();
rows.forEach(function(row) => {
// If the row it's what we are looking for we pick the columns A and C
if (row[1] === searchTerm) {
result.push([ row[0], row[2] ])
}
});
});
// If we found values we return them, otherwise we return emptry string
return result.length ? result : '';
}
The thing is I need this formula to be re-calculated when a cell in a sheet with a name starting with DATA_SHEET_ changes.
I see most answers (and what I usually do) is to pass the range we want to watch as a parameter for the formula even if it's not used. But in this case it will not work because we don't know how many ranges are we watching and we don't even know the whole sheet name (it's injected by a web service using Google Spreadsheet API).
I was expecting Google Script to have something like range.watch(formula) or range.onChange(this) but I can't found anything like that.
I also tried to create a simple function that changes the value of cell B2 which every formula depends on but I need to restore it immediately so it's not considered a change (If I actually change it all formulas will break):
// This does NOT work
function forceUpdate() {
const spreadsheet = SpreadsheetApp.getActiveSpreadsheet();
const sheet = spreadsheet.getActiveSheet();
const range = sheet.getRange(1, 1);
const content = range.getValue();
range.setValue('POTATO');
range.setValue(content);
}
So I don't know what else can I do, I have like a hundred formulas on multiple sheets doing this and they are not updating when I modify the DATA_SHEET_... sheets.
To force that a custom function be recalculated we could use a "triggering argument" that it's only taks will be to trigger the custom function recalculation. This triggering argument could be a cell reference that will be updated by a simple edit trigger or we could use an edit installable trigger to update all the formulas.
Example of using a cell reference as triggering argument
=sampleFormula("searchTerm",Triggers!A1)
Example of using an edit installable trigger to update all the formulas
Let say that formulas has the following form and the cell that holds the formula is Test!A1 and Test!F5
=sampleFormula("searchTerm",0)
where 0 just will be ignored by sampleFormula but will make it to be recalculated.
Set a edit installable trigger to fire the following function
function forceRecalculation(){
updateFormula(['Test!A1','Test!F5']);
}
The function that will make the update could be something like the following:
function updateFormula(references){
var rL = SpreadsheetApp.getActive().getRangeList(references);
rL.getRanges().forEach(function(r){
var formula = r.getFormula();
var x = formula.match(/,(\d+)\)/)[1];
var y = parseInt(x)+1;
var newFormula = formula.replace(x,y.toString());
r.setFormula(newFormula);
});
}
As you can imagine the above example will be slower that using a cell reference as the triggering argument but in some scenarios could be convenient.

Google Sheet Script Triggers issues

I was trying to create a script for google sheet that counts certain colors and it seems to be working perfectly, but when I tried to make a trigger for it, this is when I start to get problems.
I created the trigger with the OnEdit() event, but everytime I edit the table i get this exception "interval not found". So, if I want it to work correcly, I have to cut and paste the formula on the table (Because the trigger isn't working).
This is my code:
function countColor(countRange) {
var sheet = SpreadsheetApp.getActiveSpreadsheet().getActiveSheet();
var range = sheet.getRange(countRange);
var rangeWidth = range.getWidth();
var bg = range.getBackgrounds();
var score = 0;
for (var i = 0; i < rangeWidth; i++)
{
if (bg[0][i] == "#38761d")
{
score += 1;
}
else if (bg[0][i] == "#ffff00")
{
score = score + 0.5;
}
else if (bg[0][i] == "#274e13")
{
score += 1.5;
}
}
return score;
};
1 - So I want to know why the trigger isn't working correcly.
2 - I would like also to know, how to do this--> when I copy and paste the formula in another cell, these correspond to the row in which I have pasted it. (just like normal tables do). For example is the range was a1:b1 in the first cell, when I copy it on the second cell it would be a2:b2
In most cases time based triggers cannot be used with functions that accept parameters. In your case the parameter countrange will probably be populated with the triggers event object which will likely result in some very weird behavior.
Functions that you write to be used in cells are called custom functions in google apps script and you cannot run custom function from a trigger.
Custom Functions

Are there any way to insert multiple input (same or different cells) in Google Spreadsheets / script?

This is my current code, which only allow one input on cell C2, I am wondering if I can just add another input, either in cell C2 or in other cell. Thank you very much for your guidance, newbie here!
function onEdit()
{
var ss=SpreadsheetApp.getActive();
var sh=ss.getActiveSheet();
if(sh.getName()=="T-D-SOC")//It will only do this on Sheet1
{
var lr=sh.getLastRow();
var name=sh.getRange('C2').getValue();
sh.hideRows(2, lr-1);
var allRanges=ss.getNamedRanges();
var rows=[];
for(var i=0;i<allRanges.length;i++)
{
if(allRanges[i].getName()==name)
{
var rgi=allRanges[i].getRange();
var toprow=rgi.getRow();
var numrows=rgi.getNumRows();
sh.showRows(toprow, numrows);
sh.showRows(2,19);
break;
}
}
}
}
If you want to write in a cell, just define the range of another cell and write in it with setValue:
sh.getRange('C3').setValue('My text');
In order to set a value you in a specific range, you will have to use the setValue method or if you have multiple values, you can also make use of the setValues method.
However, depending on what exactly you want to achieve, you may want to check the event object e since you are using an onEdit trigger.
For instance, if you want to retrieve the range in which the edit was made, you can simply use this:
let editRange = e.range.setValue("VALUE").
Reference
Apps Script Range Class - setValue();
Apps Script Range Class - setValues();
Apps Script Event Objects.

Force Google Spreadsheet formula to recalculate

I know this question has been asked before but the answers given are not valid for my case because it's slightly different.
I've created a formula that looks for sheets with a pattern in the name and then uses it's content to generate the output. For example
function sampleFormula(searchTerm) {
const spreadsheet = SpreadsheetApp.getActiveSpreadsheet();
const sheets = spreadsheet.getSheets()
.filter(function(sheet) {
// If sheet name starts with DATA_SHEET_...
return sheet.getSheetName().indexOf('DATA_SHEET_') === 0;
});
const result = [];
sheets.forEach(function(sheet) {
// We get all the rows in the sheet
const rows = sheet.getDataRange().getValues();
rows.forEach(function(row) => {
// If the row it's what we are looking for we pick the columns A and C
if (row[1] === searchTerm) {
result.push([ row[0], row[2] ])
}
});
});
// If we found values we return them, otherwise we return emptry string
return result.length ? result : '';
}
The thing is I need this formula to be re-calculated when a cell in a sheet with a name starting with DATA_SHEET_ changes.
I see most answers (and what I usually do) is to pass the range we want to watch as a parameter for the formula even if it's not used. But in this case it will not work because we don't know how many ranges are we watching and we don't even know the whole sheet name (it's injected by a web service using Google Spreadsheet API).
I was expecting Google Script to have something like range.watch(formula) or range.onChange(this) but I can't found anything like that.
I also tried to create a simple function that changes the value of cell B2 which every formula depends on but I need to restore it immediately so it's not considered a change (If I actually change it all formulas will break):
// This does NOT work
function forceUpdate() {
const spreadsheet = SpreadsheetApp.getActiveSpreadsheet();
const sheet = spreadsheet.getActiveSheet();
const range = sheet.getRange(1, 1);
const content = range.getValue();
range.setValue('POTATO');
range.setValue(content);
}
So I don't know what else can I do, I have like a hundred formulas on multiple sheets doing this and they are not updating when I modify the DATA_SHEET_... sheets.
To force that a custom function be recalculated we could use a "triggering argument" that it's only taks will be to trigger the custom function recalculation. This triggering argument could be a cell reference that will be updated by a simple edit trigger or we could use an edit installable trigger to update all the formulas.
Example of using a cell reference as triggering argument
=sampleFormula("searchTerm",Triggers!A1)
Example of using an edit installable trigger to update all the formulas
Let say that formulas has the following form and the cell that holds the formula is Test!A1 and Test!F5
=sampleFormula("searchTerm",0)
where 0 just will be ignored by sampleFormula but will make it to be recalculated.
Set a edit installable trigger to fire the following function
function forceRecalculation(){
updateFormula(['Test!A1','Test!F5']);
}
The function that will make the update could be something like the following:
function updateFormula(references){
var rL = SpreadsheetApp.getActive().getRangeList(references);
rL.getRanges().forEach(function(r){
var formula = r.getFormula();
var x = formula.match(/,(\d+)\)/)[1];
var y = parseInt(x)+1;
var newFormula = formula.replace(x,y.toString());
r.setFormula(newFormula);
});
}
As you can imagine the above example will be slower that using a cell reference as the triggering argument but in some scenarios could be convenient.

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