How to avoid recalculation of rand() function, when using getValue/activate? - google-apps-script

I have a google spreadsheet and a randomly selected row is always shown in row 3:
A3:
=round(rand() * 808 + 1)
B3:
=VLOOKUP(A3;A8:B815;2)
When i click on a button, i want the formula to reload and also jump to that row, so i made a macro:
function Refresh() {
var spreadsheet = SpreadsheetApp.getActive();
spreadsheet.getRange('A1').setValue("");
SpreadsheetApp.flush();
spreadsheet.getRange("A7").offset(spreadsheet.getRange('A3').getValue(),0).activate()
};
The reason, that i didnt put the calculation of random into the macro is, that i also want this random row in spreadsheets where macros are disabled (mobile + users who dont know).
When i run the macro, a new value is calculated in A3 (from flush) and then the corresponding row is activated. The issue is, that with the last row of the macro the value of A3 is recalculated because something changed and then the number does not match up anymore with the activated row.
Is there a way to block calculations? Or to scroll to the row without activating recalculations?

Tl;Dr: It's not possible to avoid the recalculation of RAND() or any other Google Sheets volatile function (NOW(), TODAY(), RANDBETWEEN()).
RAND() is a volatile function and this kind of function are recalculated every time that the spreadsheet is opened, an edit is made and optionally every certain time set in the spreadsheet settings. Also it's recalculated on the Google servers when the spreadsheet is get / opened in Google Apps Script.
You might try to use an on edit trigger instead of a "macro". Simple on edit triggers doesn't require authorization to run, installable triggers require authorization of the user who is installing the trigger, so they will be triggered by any edit made by any user but not when a macro or script are executed.
Below is an example of a simple on edit trigger. It sets the value of Sheet1!A1 to a random number.
function onEdit(e){
const value = Math.random();
const spreadsheet = SpreadsheetApp.getActiveSpreadsheet();
spreadsheet.getRange('Sheet1!A1').setValue(value);
}
Related
Does Google Sheets have "Volatile" Functions like Excel?

In addition to Ruben's answer, I want to add
Some macros like onEdit do work on mobile apps
Nevertheless, activate() doesn't work on mobile apps.
During testing, I can also see that the random numbers shown in mobile apps are completely different from what's shown on desktop for the same cell with the same formula. This number is also completely different from what is logged in apps script. This shows they're not synced properly.

Related

Run formula calculation in Google Sheets without to open file

I have a scheduled addon, which writes data periodically into a Google Sheet, without to open file. I have a formula, which should make some calculations on written data and write calculated values into the sheet, so then Data Studio with this sheet as data source updates the visualization.
What is the way to periodically run a formula calculation and write calculated values without manual steps like opening file?
I'm pretty new to this topic: my search approaches to find something like convert formula to app script or schedule formula execution brought me not to any fruitful idea.
PS_ the formula, which should do the calculation is:
=IFERROR(((VLOOKUP(A2,'[1]SV'!$A:$B,2.0))/B2)+(IF(B2=1,"33,9",IF(B2=2,"16,28",IF(B2=3,"10,36",IF(B2=4,"7",IF(B2=5,"5,64")))))+IF(B2=6,"4,13",IF(B2=7,"3,27",IF(B2=8,"2,61",IF(B2=9,"2,18",IF(B2=10,"1,82")))))+IF(B2=11,"1,77",IF(B2=12,"1,81",IF(B2=13,"1,85",IF(B2=14,"1,9",IF(B2=15,"2,04")))))+IF(B2=16,"1,68",IF(B2=17,"1,61",IF(B2=18,"1,65",IF(B2=19,"1,62",IF(B2=20,"1,59","0")))))),0)
If i would know, how to convert the formula to app script, i would manage the rest - i'm familiar with running scripts with time based trigger. Or, maybe, there is a method to run formula on the same scheduled way, like scripts...?
*Disclaimer: I have not tried this and it might not work, if it doesn't, comment and I will try to provide another solution
OK here's what i would try:
In google scripts there is a trigger function, manly used for testing. what it does is it runs your script and sends you an email if there is a error, and hypothetically, you could use this to run you script periodically.
A couple of other things I may try if this doesn't work, leave a comment if you would like me to go in depth about these:
depending on how you want this to work, you could set up a web app to do this, and instead of opining google sheets open the web app with a timer built into the script, that would trigger as long as you had the web page opened.
It might also be possible to run the script automatically with a JavaScript function, requires more research.
If Google Data Studio is not showing you the correct value then you could use a time-driven trigger to save the formula results in another cell. Example:
function respondToTimeDrivenTrigger(e){
var spreadsheet = SpreadsheetApp.getActiveSpreadsheet();
/** The range holding the formula */
var range = spreadsheet.getRange('Sheet1!A1');
/** The value returned by the formula */
var value = range.getValue();
/** Write the value to the cell to the right or the source cell */
range.offset(0,1).setValue(value);
}
Resources
https://developers.google.com/apps-script/guides/sheets
https://developers.google.com/apps-script/guides/triggers/installable

How to check if user is actively editing a cell

I have a very simple script that is triggered every 15 minutes to re-alphabetize a sheet based on that day's date. It works perfectly, except for one issue. The sheet is used by about 8-10 users at any given time. If a user is actively typing in a cell when the function is triggered and the sheet is resorted, then when they finish typing the cell they have edited the wrong cell. For example, if they start editing cell D24, and then after it is resorted that row becomes D28, then when they hit enter they will overwrite the new D24.
Is there any way I could incorporate something in the code to check if there are any cells that are actively being edited (greyed out because someone is inside it)? Or some other solution that would solve this issue?
function Alphabetize() {
var spreadsheet = SpreadsheetApp.getActive()
var now = new Date();
var name = Utilities.formatDate(now,"EST","MM-dd")
var day = now.getDay()
var sheet = spreadsheet.getSheetByName(name)
var col = sheet.getLastColumn()
var row = sheet.getLastRow()
var range = sheet.getRange(3, 1, row, col)
range.sort({column: 1, ascending: true})
}
Since modals don't work in this situation (time based triggers and other users) I figured out a slightly clunky work around. I added a row at the top of the sheet in the frozen section that is bright yellow and says "Press Enter. Sheet about to re-sort." multiple times so it's visible across the entire sheet. I then hid that row and inserted this code right before it sorts:
sheet.showRows(2)
Utilities.sleep(10000)
So it shows that row, waits for 10 seconds, sorts and then hides the row again with sheet.hideRows(2). Elegant, it is not. But it works on the time trigger and every user sees it. If there's a more elegant solution, I'm all ears.
Is there any way I could incorporate something in the code to check if there are any cells that are actively being edited (greyed out because someone is inside it)?
I think that this could be possible by using a web browser extension that reads the spreadsheet DOM but it will not very dependable as the web page source of many Google apps are generate automatically and anything could change without previous notice.
By one hand, the status identified by the web browser extension should be saved on a document / script store by using the Properties Service or the Cache Service so the time-driven trigger could read the status and by the other, the time-driven trigger save a flag to warn the user using the same services so the web browser could read them.
NOTE: In order to make this work, the web browser should call functions from the same project of the functions ran by the time-driver trigger.
By using Google Apps Script you could use spreadsheet.toast(...) to show a modeles and non so intrusive warning, then use a modal dialog to interrupt the user.

In google sheets can I wrap a standard function in a custom function to control when it is run?

I originally asked this on WebApps, thinking that I was just missing a google trick. There I received the answer that it would require a custom function.
https://webapps.stackexchange.com/questions/129068/recalculate-google-sheet-on-demand
Here is my original question:
I have the following formula in a sheet:
=if(E1="HOLD",,query(Cust_Orders!B6:Z5000,"Select Y,G,I,H,K where H>0 "))
With E1 being a drop down with values HOLD and FETCH
The recalculation when I go to FETCH takes about 13 seconds.
But when it is in HOLD, the query doesn't exceute, and everything goes
blank. Not what I want.
I'm trying to avoid recalculating this query every time I make a
change in the Cust_Orders range, but keep the old values, as two
different pivot tables that are viewed by other people are dependent
on it.
Google spreadsheet recalc settings all are 'on change and foo'
Is there a way to do this?
Custom functions only update when one of their parameters changes. So if the function only depends on the HOLD/FETCH cell, but executes the formula is the query cell, I think I win.
My research:
This answer google sheets custom function built-in function is specific in telling the OP how to do what he wants in the script language. I suspect that rewriting the query in appscript would not be a net win.
This link Using Bound Google Scripts to Generate a Query Object
was proposed as a solution, but this in effect is rewriting my built-in function within the script. I want to use scripts as rarely as possible, and as generically as possible, as it makes long term maintenance and modification easier.
The query function above is an example. I am looking for a more general solution that allows me to use an arbitrary formula using the same script.
This discussion on google product forums: https://support.google.com/docs/forum/AAAABuH1jm01F-8MzzCxbY/?hl=en&gpf=%23!topic%2Fdocs%2F1F-8MzzCxbY says you can't call built-ins from scripts. But the question is almost 4 years old.
I have asked this question there too, but generally asking on Google Product Forums is a Hail Mary.
A viable solution:
An example of a script calling a built-in function.
A link to an add-on that allows recalculation of a range to be toggled on/off
A more general method than custom formulas to control recalc.
I do NOT want a script that emulates the desired built-in inside the script.
A more general method than custom formulas to control recalc.
What I'm doing on project for a client is to have the "expensive" formulas saved as variables on the script and have buttons to freeze/unfreeze the certain ranges (those that have a high impact on the recalculation time.
The "unfreeze" button adds the formulas to the spreadsheet
The "freeze" button put the formulas results over the range used by the formulas
There is a document property that stores the frozen/unfrozen spreadsheet state
A sidebar is used to show the buttons the spreadsheet status.
An example of a script calling a built-in function.
Scripts can get values, display values, formulas, and other stuff but they can not call built-in functions.
In the case of my client, they have one array formula (IMPORTRANGE, QUERY, FILTER, ARRAYFORMULA, etc) by sheet, all the formulas that are been included are on A1. The formulas are saved in an array of objects of the following form
var formulas = [
{
name:'Sheet1',
address:'A1',
formula:'=IMPORTRANGE(...)'
}
]
The address property, is included for future improvements.
The key code lines of the "freeze" function are
var dataRange = sheet.getDataRange();
dataRange.copyTo(dataRange, SpreadsheetApp.CopyPasteType.PASTE_VALUES, false);
Please note that the above lines copy-paste-as-values the whole data range.
The key code lines of the "unfreze" function are
formulas.forEach(function(item){
var sheet = spreadsheet.getSheetByName(item.name);
sheet.clear();
sheet.getRange(item.address).setFormula(item.formula);
});
Please note that the above lines clear the whole sheet.

Google script: unprotect a sheet through function when user has no right to do so

I have a spreadsheet with different sheets in Google sheet, 3 users can edit each one a sheet (protections are set, each user can edit only one sheet). They all can execute a google script function that writes what they edited in a summary sheet. I don't want anyone to be abble to edit the summary sheet, so I set myself as the only available editor.
So my problem is to authorize the 3 users, only through the google script function, to write in the summary sheet. I tried to use the following function :
var unprotected = summarySheet.getRange('G3:G10');
protection.setUnprotectedRanges([unprotected]);
but since the users are not allowed to edit the summary sheet, and since the function is run with the active user, so they can't give themselves the right to unprotect a range in the summary sheet... Do you know how to workaround this problem?
Thanks a lot!
I see two script-based choices, one easy and one quite hard, and one sheet-based choice, that is easiest:
Easy:
You run the "summarize" script instead of them or, you set the summarize script run on a trigger out of your account. Then you actually leave protections alone. You could set the summarize script to run on open with error catching if the user doesn't have the necessary authority to unprotect the summary sheet and/or write to the summary sheet.
Hard:
When they run the "summarize" script it calls a published standalone script that has been given the authorization to make the necessary protection changes. I'll be honest, I wouldn't be able to code this but have seen/heard of similar implementations.
Easiest:
Finally, I want to make sure you've considered having the summary sheet itself contain the necessary formulas, parsing, etc. to summarize data from the other sheets without any need of scripts for this aspect of the sheet. The sheet could call custom functions as needed if the parsing or other summarization functionality is beyond built-in functions' capabilities. The sheet could stay fully protected and update itself in real time as users enter data (no need for users to trigger the summary creation, unless spreadsheet settings have auto-recalculate turned off).
Edited to add: put in A1 of Summary sheet something like:
=summarize()
And have that custom function return a 2-dimensional array of the summarized data.

script to force republish of google spreadsheet

I have created a form that pushes data to a Google Spreadsheet. The data is latitude, longitude, location, and other identifying data. The spreadsheet is then published as a .CSV file and imported into ARC GIS to be displayed on an interactive map. It works exactly as I wanted and I set it to republish after each change.
The problem is that when the spreadsheet has rows appended by the script, it is not seeing it as a change and republishing. In order to get the updated data imported to the map, I need to go in and manually republish. Is there anyway through the Google Apps Script that I could make a few lines of code to force a republish? I could then add that to the "on form submit" script I have or another time based one that already runs at 3 am everyday.
I have looked through the Google Apps Script documents and not found anything. When searching for help on the web, the overwhelming majority of responses are for how to publish your script as a template for other.
My testing sheet was republished after the following function was executed by either a menu entry or a time-based trigger.
function ChangeIt() {
var sheet = SpreadsheetApp.getActiveSpreadsheet()
var t = new Date()
var x = 'upd: ' + t
var range = sheet.getRange('a3')
range.setValue(x)
}
If I were in your shoes, I'd add an extra column to the end of the sheet with some benign constant data that a script can change without affecting the systems consuming the data. If an extra column isn't an option, try modifying my sample to read in a current value, change it, and immediately change it back.
Also, I'd see if the spreadsheet onEdit() trigger fires when the form submit adds a new row. If so, tie your GAS function to it to force the republish. If not, setup a timed trigger to execute the GAS function.
A quick workaround for this issue that doesn't require scripting is to simply make an array copy of the data.
For example, I made a new tab and in A1 put this: =ArrayFormula('Form Responses 1'!A1:Z1000)
While the main Form responses tab will insert rows and not play nice with formulas this new tab stay nice and constant and updates automatically when new data is added.