I have some trouble with a spreadsheet: as said in the title, I put in a column a formula which is calling a custom script based on the value of another cell, but when I modify this other cell, the formula does not update... This seems to work with standard formulas, but, in my cell calling my script:
If I try to add a blank in the fomula cell, the result is still not updated.
If I clear the formula cell, and re-type the formula, it's still showing the old value.
If I copy paste the formula cell in another one, the new cell is up-to-date.
Here is my script. If few words: for given 'company' parameter, it search for all rows matching this criterion and store the 3rd column cell in a variable, finally returned ( so return the last value ):
function getLastStatut(company) {
var values = SpreadsheetApp.getActiveSheet().getDataRange().getValues();
var out = "not found";
var row;
for(i in values){
row = values[i];
if(row[1]==company){
out = row[2];
}
}
return out;
}
And for example:
A1 : Date
A2 : Test
A3 : Running
A4 : =getLastStatut(B1)
So A4 display "Running", but if I change A3, it still shows "Running", whereas it should display the value of A3.
Is this a bug or is there something I'm doing wrong? Any help is welcome.
Alexis
The problem is with the caching "feature" for custom functions. I explain it in this other thread, please read.
But the bottom line here is, a custom function should not access data that is not static, all variable information should be passed as parameter. In your case, the whole data set should be a parameter of your script. Custom functions should never get any range at all.
//getting values like this is wrong (for a custom function)
var values = SpreadsheetApp.getActiveSheet().getDataRange().getValues();
Related
I have little to no coding knowledge, so apologies if the solution is too obvious!
I am trying to add a Last Modified column to a Google Sheets file. To do this, I am using an AppScript function with the following code:
function setTimestamp(x) {
if(x != ""){
return new Date();
}
}
This works fine when I use setTimestamp(x) in my file. However, I am combining this with a Zapier action that creates a new row whenever new media is added. Every time a new row is created, any existing formulas are removed.
I assume I need to use ARRAYFORMULA to apply the setTimestamp formula to newly-created rows, but it must only apply to rows that aren't blank.
I have tried the following:
={"Last Modified";ARRAYFORMULA(setTimestamp(A2:A))} -> Only worked on first row
={"Last Modified";ARRAYFORMULA(B2:B=setTimestamp(A2:A))} -> Broke the file
={"Last Modified";ARRAYFORMULA(IF(A2:A)=1,setTimestamp(A2:A),"")} -> Expected 1 argument, got 3
Is there a way I can combine the IF into the script or a better way to solve the problem?
A public version of my file is available here: https://docs.google.com/spreadsheets/d/13zkVRPr2Wh5bHjCT8cenInHnBk7qkMkuEMdwUxC_cRU/edit?usp=sharing
All data is dummy data and stock photos.
Unfortunately, arrayformula does not function as an array map function for custom functions. (Even for native functions where you may expect it to work that way, it does not always, sadly.)
To handle array range, we need the custom function to handle array range directly. That also limits the number of individual calls to custom functions, which materially saves execution time.
To handle array range, there are 2 ways. I'll comment on both.
Array range directly as input of custom function
If the input is a single cell, it is read directly
If the input range spans more than a single cell, the data is read as nested lists: a list of lists of rows.
For example, A1 will be read as the data in A1. A1:B2 will be read as [[A1, B1], [A2, B2]].
You can remember it as columns of rows.
As for the input data format, numbers are taken without the display format. Texts are taken as strings.
If output is an array range, the result will automatically expend.
Thus, in your example, in B2 you can almost do
=setTimestamp(A2:A)
where setTimestamp() has been modified to
function out = setTimestamp(arr) {
out=Array(mat.length);
for (i=0;i<mat.length;i++){
j=0
if(arr[i][j] != ""){
out[i]=new Date();
}
}
return out
}
For more details, see the official help page. (Over the years, more details have become available.)
Almost, but not quite. For your direct question, above provides the answer. However, you seem to have an implicit requirement that your custom function is executed every time a new URL is found. Be careful that what happens here is that every time Google Sheet updates cell content, a new Date() is created and outputted.
Array range read within custom function
Since you know your URLs are in A2:A, and you want the output of your custom function to be B2:B, you can read and modify those ranges directly within your custom function via the Range Class.
In this route, you may find getLastRow(), getLastColumn() in Sheet and getNextDataCell() in Range convenient.
When you need to execute your custom function, you can run it manually or add onEdit() trigger to your custom function. (But onEdit() itself can mean substantial UI lag when using the sheet. It's usually more appropriate for sheets that parse external data automatically. See other triggers in the link for motions.)
In your example, you can almost do
function setTimestamp() {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheet = ss.getSheets()[0];
var lastRow = sheet.getLastRow();
var row=1;
var cell = sheet.getRange(row,1).getValue();
while (row<=lastRow) {
if(cell.getValue() != ""){
sheet.getRange(row,2).setValue(new Date());
}
cell = sheet.getRange(row,1,lastRow).getNextDataCell(SpreadsheetApp.Direction.DOWN);
row=cell.getRow();
}
}
which will scan for all URLs in A2:A and write current time to B2:B when executed.
Again, your example implicitly points to updating only when a new URL is found. So be careful about that. Use triggers as needed.
As for the need to place formula in B1, you can (and should) reference the output of your other application in a different sheet so that you or a different application of yours can edit without conflict.
Thus, for what was asked, we have everything.
I add script using tools/script editor. I add my simple popup function :
function test(){
Browser.msgBox('This is test')
}
Then in google sheet, i add an image as button and attach the script 'test'.
It works fine, but i want to pass value from a cell. How can i do it?
For example, i modify the function as follow :
function test(val){
Browser.msgBox('This is test. Value is '+val)
}
if i want to pass value from cell B4 --> test(B4). How to do it properly ? i'm not allowed to include the parameter B4 when assigning script to image/button. It will say 'script could not be found'.
Thanks
JohnA's solution is correct, but I want to add something to it. I recommend this enhancement to avoid modifying your script every now and then when you want pass another cell as a parameter.
Approach:
Store the cell notation you want to pass on a specific cell. (e.g. "B4" in A1)
Get the value of A1 and access the value of that cell notation.
Code:
function test() {
sheet = SpreadsheetApp.getActiveSheet();
// cell holds the range you want printed
cell = sheet.getRange("A1").getValue();
// value now holds the value of the range we got from the cell
value = sheet.getRange(cell).getValue();
Browser.msgBox(value);
}
Sheet:
B4:
B5:
Note:
Everytime you change the value of the A1, it will print that cell's value.
It will not behave properly when you write an invalid range in A1. (Exception: Range not found)
Correct, you can specify a parameter if calling the custom function as a formula in the sheet, but not as attached to a script when a button is pressed. We do not even included the parentheses when we assign a script.
If the customer function always wants to use the value of B4, you can modify your script the get the value from that cell.
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheet = ss.getActiveSheet();
var val = sheet.getRange("B4");
I am trying to use conditional formatting to compare the value of one cell against another in a dynamically referenced worksheet while using a dynamic cell reference and highlight the cell whenever the values do not match.
I have created a master sheet containing a cell with the name of the worksheet I wish to reference. This cell will be updated periodically with the name of the relevant worksheet. I am able to write a formula that correctly returns the value I wish to compare, but when I enter this expression in the conditional format rule (format cells if is not equal to) I am given a red outline with the message "Invalid formula".
I have tried various combinations such as using a custom formula and using (current cell = (formula here)) and while I am not given an error, it still does not work as intended.
I am comparing the value of cell C2 in worksheet 4719 to cell C2 in worksheet 4717. A cell in a worksheet called Mastersheet contains the entry "4717" in B1, and this is the cell that will be periodically changed to something like 4717 for example.
The formula that is able to successfully return the value I am after is =indirect(Mastersheet!B1&"!"&"C2") but I was only able to accomplish this by putting the "C2" reference in quotation marks, which is not dynamic and thus would have to be manually changed to work for any of the other values in column C.
I expect cell C1 in worksheet 4719 to turn green as the value there is different from cell C1 in worksheet 4718.
I made a publicly viewable copy of my Google sheet if my explanation does not make enough sense https://docs.google.com/spreadsheets/d/1s0oaGas46akmGV7hapnDOdhndcWudyq239kmCrLnt3U/edit?usp=sharing
try it like this:
=INDIRECT(Mastersheet!$B$1&"!"&ADDRESS(ROW(C2), COLUMN(C2), 4))
this can be used in Conditional formatting, however, referenced sheet cant be fully active so:
=INDIRECT(INDIRECT("Mastersheet!B1")&"!"&ADDRESS(ROW(C2),COLUMN(C2),4))
this can be further moded so the B1 would be dynamic too:
=INDIRECT(INDIRECT("Mastersheet!"&ADDRESS(ROW(B1),COLUMN(B1)))&"!"&
ADDRESS(ROW(C2),COLUMN(C2),4))=19
What I am trying to accomplish is I would like to search for a term in one cell, if that cell has the term write text to another cell. My specific example would be I would like to search for the term 'DSF' in column 4. If I find 'DSF' it would then write 'w' in column 5 & write '1.2' in column 3. This is searched per row.
I do understand the the .setvalue will write the needed text, but I do not understand how to create a search function. Some help would be greatly appreciated.
EDIT
Here is the code I am working with at the moment. I am modifying it from something I found.
function Recalls()
{
var sh = SpreadsheetApp.getActiveSheet();
var data = sh.getDataRange().getValues(); // read all data in the sheet
for(n=0;n<data.length;++n){ // iterate row by row and examine data in column D
if(data[n][3].toString().match('dsf')=='dsf'){ data[n][4] = 'w'}{ data[n][2] = '1.2'};// if column D contains 'dsf' then set value in index [4](E)[2](C)
}
//Logger.log(data)
//sh.getRange(1,1,data.length,data[3].length).setValues(data); // write back to the sheet
}
With the Logger.log(data) not using the // It works properly but it overwrites the sheet, which will not work since I have formulas placed in a lot of the cells. Also, Maybe I did not realize this but Is there a way to do a live update, as in once I enter text into a cell it will research the sheet? Otherwise having to 'run' the macro with not save me much time in the long run.
Try this. It runs when the sheet is edited. It only captures columns C,D,&E into the array and only writes back those columns. That should solve overwriting your formulas. It looks for 'DSF' or 'dsf' in column D (or contains dsf with other text in the same cell either case). Give it a try and let me know if I didn't understand your issue.
function onEdit(){
var sh = SpreadsheetApp.getActiveSheet();
var lr = sh.getLastRow()// get the last row number with data
var data = sh.getRange(2,3,lr,3).getValues(); // get only columns C.D,& E. Starting at row 2 thur the last row
//var data = sh.getDataRange().getValues();// read all data in the sheet
for(n=0;n<data.length-1;++n){ // iterate row by row and examine data in column D
// if(data[n][0].toString().match('dsf')=='dsf'){
if(data[n][1].match(/dfs/i)){ //changed to find either upper or lower case dfs or with other text in string.
data[n][2] = 'w';
data[n][0] = '1.2'};
}
sh.getRange(2,3,data.length,data[3].length).setValues(data); // write back to the sheet only Col C,D,& E
}
I need to introduce functionality into a google spreadsheet that will allow the user to edit the result of an array formula. The reason for the requirement is that an ARRAYFORMULA sets a default value for a group of cells, but the user sometimes needs to overwite these defaults. I'd like to know if this is even remotely possible.
example:
Row(#)|Array_1 |Array_2
------------------------------------
1 |a |=arrayformula(Array_1)
2 |b |""
3 |c |""
4 |d |""
So all rows in Array_2 are populated by an array formula. However the user wants to go directly to the second cell in Array_2 and change its value. Of course, by design ARRAYFORMULA will break. Is there some way to modify ARRAYFORMULA, so that it will simply skip over the cell that the user has edited and continue on its way as if nothing has happeded?
I realize this is an old problem but I was searching for this today and made a script that works for me.
This script puts a formula in an adjacent cell when a cell is edited in the second column. This way you can just overwrite the formula if you need to input something manually and you don't need to have the formulas go into all of the rows beforehand. I had people accidentally edit the formula and mess it up most of the time when they were pre-filled, so this works better for me.
function onEdit() {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheetList = ["Sheet1","Sheet2","Sheet3"]; // list of sheets to run script on
for (i = 0; i < sheetList.length; i++) {
var sheetName = ss.getSheetByName(sheetList[i]);
// only runs if sheet from sheetList is found
if (sheetName != null) {
var aCell = sheetName.getActiveCell();
var col = aCell.getColumn();
var adjacentCell = aCell.offset(0, -1);
var formula = 'INPUT FORMULA HERE'; // put the formula you want in the adjacentCell here. Don't use it in an arrayformula
// only runs if active cell is in column 2, if the adjacentCell is empty, and if the active cell is not empty(otherwise it runs if you delete something in column 2)
if(col==2 && adjacentCell.getValue()=="" && aCell.getValue()!="") {
adjacentCell1.setValue(formula);
}
}
}
}
Will changing the value not throw out the output of the remaining formulas?
If not, you could set up 2 new tabs: one which will receive the user over-ride values, and another "reflection" tab which you populate with
IF(tabOverride!Rx:Cy, tabOverride!Rx:Cy, tabArray!Rx:Cy)
basically the new tabs are cloned layouts of your array tab, creating an override input layer, plus a presentation layer that uses the IF('override value exists', 'then show override', 'else show array out put') logic to return the desired values.
hope that makes sense!