Google spreadsheet custom function: How to get a continuously updated valued? - google-apps-script

I wrote a custom google app script function in a script associated with my google doc spreadsheet. The function calls a third party service to get data. I can put the function in a cell:
=myfunction("something")
and it returns the correct value from the service. However, how can I keep this value updated so that it's showing the latest data from the service?
Update
For example:
=temperature("90120")
For getting the current temperature in a given zip code. Also my sheet may have dozens or hundreds of these so I'd prefer something that is performant and maintainable. It doesn't truly need to be continuous, polling once a minute or ideally more frequently could work. I'm wondering if there's some way from the script to set a timer to run to update a range of cells?

Not sure why you need dozens or hundreds.
1. Is the spreadsheet used by another process?
2. Is the spreadsheet visually reviewed by actual users?
If #1, you could replace the spreadsheet with a custom API via the content service to return JSON results for all temperatures.
If #2, you may hit limits or performance issues with so many functions firing so often. Why should fire the functions if no one is viewing the results. Alternatively, you could make it an on-demand with a custom menu option.

I have a similar problem.
This is how I am doing it atm, but its not the best solution. I am looking for a better one.
If any value at sheet Prices and column D changes.
Meaning if any cell value changes in the whole column it updates the custom function value.
//Search Price sheet with the given name. Return price. dummy param updates google ss once the "Prices" sheet values changed.
function searchPrice(price,dummy)
{
var SPREADSHEET_NAME = "Prices";
var SEARCH_COL_IDX = 2;
var RETURN_COL_IDX = 3;
var values = SpreadsheetApp.getActiveSpreadsheet().getSheetByName(SPREADSHEET_NAME).getDataRange().getValues();
for (var i = 0; i < values.length; i++)
{
var row = values[i];
if (row[SEARCH_COL_IDX] == price)
{
return row[RETURN_COL_IDX];
}
}
}
This is how you call it =searchPrice(B8,Prices!D:D)
Just give your custom function a dummy param. It doesn't do anything in the custom function.

Related

Modify Tinyurl Google Script Function for Sheets

I've been using this function to shorten links (Get range, if length over 200, copy and paste over new tinyurl link in same cell). But I wanna modify it to take active cell or active range instead of input range from UI prompt response. In this case I probably wouldn't need the if(x.length > 200) condition either. I've tried to research for some solutions that I could implement but its too complex for my beginner skills and understanding of original code to modify. Is there an easy fix I could do to it?
function tinyUrl() {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var ui = SpreadsheetApp.getUi();
var final = [];
var response = ui.prompt('Enter range:');
if(response.getSelectedButton() == ui.Button.Ok) {
try{
var values = [].concat.apply([], ss.getRange(response.getResponseText()).getValues()).filter(String);
SpreadsheetApp.getActiveSpreadsheet().toast('Processing range: '+ss.getRange(response.getResponseText()).getA1Notation(),'Task Started');
values.forEach(x => {
if(x.length > 200) {
final.push(["IMPORTDATA(concatenate(\"http:// tiny url.com/api-create.php?url="+x+"\"),\"\")"]);
}else{
final.push(["=HYPERLINK(\""+x+"\")"]);
}
})
}catch(e){
SpreadsheetApp.getUi().alert(response.getResponseText()+' is not valid range!');
}
}else{
return;
}
var r = response.getResponseText().split(":");
if(r[0].length=1 &&r[1].length == 1){
ss.getRange(r[0]+"1:"+r[0]+final.lenght).setFormulas(final);
}else{
ss.getRange(r[0]+":"+r[0].slice(0,1)+final.length).setFormulas(final);
}
}
The exact solution depends on your actual application. I think your question is how to detect where to apply your custom function. But let's take a step back.
There are two ways to interact with existing data in sheet and a custom function via AppsScript.
One is that you can directly call your custom function with ranges in the sheet. If the input range is a single cell, the data is read directly. If the input range spans more than a single cell, the data is read as nested lists: a list of columns which are lists of rows. For example, A1:B2 will be read as [[A1, B1], [A2, B2]].
So, for example, you can always call your custom function tinyurl() wherever you input url in your sheet and let your custom function decide when to shorten it. Since your custom function is called in-place, there is no detection issue; UI prompt is not required.
The downside is that if you call your custom function in too many places, there will be delay in getting results. And I don't think the results are always stored with the sheet. (ie. when you open the sheet again, all cells with tinyurl() may refresh and cause delay.)
Second method is via the Range Class which is what you are using. Instead of using UI prompt though, you can add onEdit() trigger to your custom function and let your function either check for currently selected cell via SpreadsheetApp.getActive().getActiveRange() or scan for urls over the sheet where you intend for user inputs to be stored.
The downside of using onEdit() trigger is the UI lag. Relying on actively selected range can also be problematic. Yet if you scan over the whole sheet, well, you have to do that. At the end though, you get to store the resultant URLs permanently with the sheet. So after the initial lag, you won't have delays in the future.
In this route, you may find getLastRow(), getLastColumn() in Sheet and getNextDataCell() in Range convenient. If you choose to process the whole sheet, you may instead use the onOpen() trigger.
I would prefer to store all URLs in one place and use the 1st option. Thus, the custom function is only called once and that's useful for minimizing delay. Other parts of the sheet can reference cells in that centralized range.
The exact solution depends on your actual application.

How to disable editing cells after a particular time in Google Spreadsheet?

Use Case - I have shared a Google Spreadsheet amongst a dozen friends and we are entering our prediction for matches. The catch is to enter it before the game starts.
Using Spreadsheet as everyone can see everyone's prediction.
Problem - Is there an AddOn or any feature which allows to disable editing a few cells after a particular time? Say post midnight A[7]-M[7] cells cannot be edited.
You can set a script to run at a specific time:
In the script editor, create a function:
function protectRangeAtMidnight() {
//My code will go here
}
In the script editor, click on the RESOURCES menu, and choose, CURRENT PROJECTS TRIGGERS.
Add a trigger for a specific date and time.
The problem is, what is the code going to do? If you protect the range, but the people you are sharing the spreadsheet with have edit authority, they can just unprotected the range. If you change their permissions to VIEW only, then you'd have to change it back at some point for the next game. That would work as long as there can be a time period where no one else can edit the sheet.
You can remove a user from the editor list:
Remove Editor
function protectRangeAtMidnight() {
SpreadsheetApp.openById('The SS ID').removeEditor(emailAddress);
}
You can also set file sharing permissions through DriveApp:
setSharing(accessType, permissionType)
I am actually doing exactly the same thing and came across the same problem. The solution I came up with does not lock the cells but uses data validation. Some of the solutions suggested online did not seem to take into account that you need to lock a row of results which have a date associated with it.
This is the layout I am using for my predictions:
Google sheet cropped image example
The cells in blue then have the following data validation (criteria is custom formula, reject input):
=if(isnumber(C1),and(now()<$A1,C1>=0,C1-int(C1)=0))
It checks that what is entered in C1 is a number. If it is, it then checks the following:
If the current date and time is before 'kick off'.
If the number is greater than or equal to zero.
That it is a whole number.
If so, it allows the cell to be changed. If the match has kicked-off, the cell cannot be altered and a red triangle will appear in the cell (because the data validation will be violated as now() will be after the date in question) but it stops the cells from being changed once the game has kicked off.
If you couple the above with locking the entire sheet (apart from the blue cells) it should allow users to make predictions prior to kick-off.
If it is necessary for cells to be altered after the game has begun you can modify the date in column A to then make the update before changing the date back.
Hope this helps!
What works is to:
Lock the cells or columns at a specific time, then remove the protection at another time: using a daily trigger (or even manually)
Modifications needed before running the functions:
Sheets names (In my example it's Sheet1, Sheet2)
Range to protect (in my example it's A:D) (editors won't be able to edit the specified range)
function Lock() {
var tabs = ['Sheet1', 'Sheet2'];
var ss = SpreadsheetApp.getActiveSpreadsheet();
for (var i = 0; i < tabs.length; i++) {
var spreadsheet = ss.getSheetByName(tabs[i]);
var protection = spreadsheet.getRange('A:D').protect();
protection.setDescription('Protected')
protection.removeEditors(protection.getEditors());
}
};
And to remove the protection you'll use the following script: Editors get "back" their permission to edit the specified range:
function Unlock() {
var ss = SpreadsheetApp.getActive();
var protections = ss.getProtections(SpreadsheetApp.ProtectionType.RANGE);
for (var i = 0; i < protections.length; i++) {
if (protections[i].getDescription() == 'Protected') {
protections[i].remove();
}
}
};

Google script FIlter issue

I am programming a help desk system using google script, forms and spreadsheet.
To filter the queries the submissions are placed into different sheets depending on category, this is done through the FILTER function. however every time a new submission is made the filter function does not update, (it uses the CONTINUE function to cover the other cells)
instead the cell with the FILTER function must be selected and crtl+shift+E must be entered
is there a way around this?
I have tried two methods
the first was looking to have a function to enter the shortcut, but is this possible?
the second is auto entering the continue function everytime a new submission is made, I have this working however google sheets does not recognise the named range, (the continue function has the set up CONTINUE(original cell, rows away, columns away) its the original cell that it does not identify, instead I must manually select the cell and re-write the exact same cell reference.
Thank you for your help, if you need to see my code please ask :)
This is the code for the second option where I try to enter the function manually to the cells.
var ss = SpreadsheetApp.getActiveSpreadsheet();
var numEntry = ss.getSheetByName('Home').getRange("B8").getValue() + 2;
var cat = ss.getSheetByName('Software problem').getRange(numEntry, 4, 1, 9);
cat.getCell(1, 1).setValue('=CONTINUE(D2, '+(numEntry-1)+', 1)');
Your option 1: Have a script enter keystrokes automatically? Not supported in apps-script.
Your Option 2: It shouldn't be necessary to programmatically insert CONTINUE, as the required CONTINUEs for your FILTER should be automatic, when rows in your filter range match the expressed criteria. Something else is wrong, so don't get caught up with this red herring.
You mention "google sheets does not recognise the named range" - I'd like to know what you mean by that, because I suspect this is where your solution will be. You can use named ranges within FILTER statements. You can also use open-ended ranges, like FormInput!A1:X or FormInput!E1:E.
If you're trying to manipulate named ranges using scripts, then you may have run into a known issue, "removeNamedRange() only removes named ranges that were created via Apps Script". (To get around that, manually delete the named range, then create it only from script.)
Here's a function I use to create a named range for all data on a sheet. You could adapt this to your situation. (I use this with QUERY functions instead of FILTER, you might want to consider that as an alternative.)
function setNamedRangeFromSheet(sheetName) {
// Cannot remove a named range that was added via UI - http://code.google.com/p/google-apps-script-issues/issues/detail?id=1041
var ss = SpreadsheetApp.getActiveSpreadsheet();
try { ss.removeNamedRange(sheetName) } catch (error) {};
var sheet = ss.getSheetByName(sheetName);
var range = sheet.getDataRange();
ss.setNamedRange(sheetName,range);
}
Using FILTER, you need to match the length of your sourceArray (which can be a named range) and any criteria arrays you use. To programmatically create a named range for a single-column criteria within your sourceArray, and of the same length, use getNumRows() on the sourceArray range.
Now, within your submission handling function, triggered on form submit, you'd have something like this. (I assume your trouble reports are coming into a single sheet, "FormInput" - adjust as necessary.)
...
var ss = SpreadsheetApp.getActiveSpreadsheet();
try { ss.removeNamedRange("FormInput") } catch (error) {};
var sheet = ss.getSheetByName("FormInput");
var inputRange = sheet.getDataRange();
ss.setNamedRange("FormInput",inputRange);
try { ss.removeNamedRange("Criteria") } catch (error) {};
var criteriaCol = 4; // Another guess, that Column E contains our criteria
var criteriaRange = sheet.getRange(0,criteriaCol,inputRange.getNumRows(),1);
ss.setNamedRange("Criteria",criteriaRange);
...
And with that in place, the content of A1 on your "Software problem" sheet just needs to contain the following. (Assuming that you're looking for "Bug"s.):
=FILTER(FormInput,Criteria="Bug")
I mentioned open-ended ranges earlier. If you aren't doing enough manipulation of data to justify named ranges, you could set up your filter like this, and not have to change it as new input came in:
=FILTER(FormInput!A1:X,FormInput!E1:E="Bug")

google script reject spreadsheet submit

I have a function in a spreadsheet based script that is triggered when a submission is made with the spreadsheet form :
function onEntry(e){
Logger.log(e);
MailApp.sendEmail("scriptadmin#uniben.edu", "New Mail Request", "Someone submited data");
}
How can I reject the entry, say if it's a duplicate entry ?
Using the documentation on events you will have to choose what data you want check (user name, specific field...) and compare that to data already in the spreadsheet.
You should do these iterations on an array level since it will be far more efficient and fast, you can get data in an array using something like
var data = SpreadsheetApp.openById(key).getDataRange().getValues();
You could also use javascript function like indexOf() that will return -1 if no match if found or item position in the array if a match is found.
Actually there are many ways to do that but your question is too vague to know what will be the best...
EDIT : following your comment, I'd suggest you let the duplicate form data come into the sheet and then use a script to remove duplicates. You could run this script on a on form submit trigger or on a timer to let it run daily or hourly, and send the email only if the last entry was a new one (no duplicates found)... depending on your use case.
There is a script in the gallery that does the job pretty well, it was written by Romain Vialard, a GAS TC that has contributed a lot. (the link above goes to the script description but you can get it also in the public gallery, just search for 'remove duplicates' you'll see that other scripts do that, all the scripts in the gallery have been checked by the GAS team)
4 months late, but better late than never. I believe this function does almost what was originally requested. i.e. "How do I prevent the entry from entering the spreadsheet if I decide that it's a duplicate." It is not precisely what was requested, but very close.
This code checks one column against that same column in another sheet, for all rows in that sheet. Lets say you have a list of companies or clients on a sheet. That list includes name, phone, address, etc. etc. Lets say you want to check against the phone number - if the phone number you are currently entering is already on your client sheet, then don't allow entry - or more precisely clear it out immediately upon entering it.
I'm sure the more experienced members will be able to point out flaws, but it works for me.
I believe it will also work for the case where a phone number in the middle of the sheet is changed - so it's not just last line that gets checked, it's the line that gets edited that gets checked - I've not tested this particlar scenario. Also, I made some changes to variable names to protect the innocent...hopefully I didn't mess anything up while doing that.
I call this function from within another function that is triggered by onEdit. Theoretically it should be able to be installed as an onEdit trigger itself. I hope someone finds it useful.
function checkNewEntryForDuplicate(e) {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var entrySheet = SpreadsheetApp.getActiveSheet();
var clientSheet = ss.getSheetByName("Clients");
var r = entrySheet.getActiveCell();
var lastCol = entrySheet.getLastColumn();
// If this had any consistency, we'd be able to get the row from entrySheet the same
// as we get column. But there is no getRow() method at the sheet level.
var rowNum = r.getRow();
var clientData=clientSheet.getDataRange().getValues();
var phoneColumnOffset=getPhoneColumnOffset(); // You'll need to get the offset elsewhere. I have a function that does that.
var columnNum=e.range.getColumn(); // column that is currently being edited
if (columnNum != phoneColumnOffset+1) // no point in doing anything else if it's not the column we're interested in.
return 0;
var entryRow=entrySheet.getRange(rowNum, 1, 1, lastCol);
var phoneNum = e.range.getValue();
// iterate over each row in the clientData 2-dimensional array.
for(i in clientData){
var row = clientData[i];
var duplicate = false;
// For each row this conditional statement will find duplicates
if(row[phoneColumnOffset] == phoneNum){
duplicate = true;
var msg="Duplicate Detected. Please do not enter. Deleting it..."
Browser.msgBox(msg);
entryRow.clearContent();
entryRow.clearComment();
return duplicate;
}
}
return duplicate;
}
I am doing the same things but having no scripts at all and just by spreadsheet functions. That kind of things are just like SQL for me and very interest to do.
For your question, this link will help: http://www.labnol.org/software/find-remove-duplicate-records-google-docs/5169/

GAS passing variables to time based triggers

I have a Google spreadsheet with an onEdit() trigger to create a second time based trigger.
Simply: when the status column is edited to 'Approved' a trigger is created to send a feedback email on a supplied project completion date.
var oneTimeOnly = ScriptApp.newTrigger("emailFeedback").timeBased().at(endDate).create();
I wish to pass a variable to the second trigger. I could create Project Property or add a column in the spreadsheet. However it would be simpler to pass the variable when creating the trigger.
When I insert any additional characters inside the newTrigger quotes this causes the entire contents of the function to be stored in the trigger (which subsequently fails).
var oneTimeOnly = ScriptApp.newTrigger("emailFeedback<strong>(regEmail)</strong>").timeBased().at(endDate).create();
.
Is there a way to store a variable inside the trigger?
Using ScriptDB and new Function(), I was able to create a method for creating dynamic trigger functions.
The gist of the solution is to store the code you want to trigger is the db with the parameters you want to pass:
"myFunction('Hello world')"
Then, when the script starts, as a global variable, you attach newly created functions from your ScriptDB. (I've done this dynamically in my link below.)
globalFunctions.callThisOne = new Function("e", "myFunction("Hello world"));
Finally, when you create your trigger, you created it using the globally accessible function as such:
ScriptApp.newTrigger("globalFunctions.callThisOne").timeBased().everyDay(1).create();
I have written up a short post about this and posted the source. Hopefully it's useful.
You can read more about it here: http://goo.gl/wbUqH6
Or see the code here: http://goo.gl/zjUiYe
Sorry, there is not a way to do this.
As I understand correctly, the question was how to pass data to a time triggered function in a google script project. Eoin described a situation, but you may facing to many.
A classic situation when your script proccesses a complex spreadsheet that may run for long minutes. As you may probably know each script has about 6 minutes runtime limit. This situation you should break your script smaller logical partitions and at the end of each one can create a new trigger for the next part. Okay, but the next part must know about some data of the current running script's variables. Because no way to pass these data via newTrigger() you can create a snapshot and put into the script property context in a serialized way.
An easy way to do by ScriptProperties.setProperty().
Use ScriptProperties.setProperty() to store serialized parameters that can be accessed by trigger method.
#user2166613 is right, but a bit short. Here is how to do it.
I show an example that uses an after() trigger. This is a really interesting use case, as it allows to decouple time consuming tasks from e.g. web app calls, so the calls return control immediately and the processing is done in the background.
In my example I adapt the column widths of a sheet in a function that this run delayed.
// call this function to set a time based trigger and transfer parameters
function setTimeTrigger_AdaptColumnWidths() {
var ssId = SpreadsheetApp.getActiveSpreadsheet().getId();
var wsId = SpreadsheetApp.getActiveSpreadsheet().getActiveSheet().getSheetId();
var scriptProperties = PropertiesService.getScriptProperties();
scriptProperties.setProperty('spreadsheetId', ssId);
scriptProperties.setProperty('worksheetId', wsId);
// Delay 10 secs, but real execution time may vary up to 15 min!
ScriptApp.newTrigger('adaptColumnWidths').timeBased().after(10000).create();
}
// this function is called by the trigger
function adaptColumnWidths() {
var scriptProperties = PropertiesService.getScriptProperties();
ssId = scriptProperties.getProperty('spreadsheetId');
wsId = scriptProperties.getProperty('worksheetId');
// getSheetById() is a custom function – see below – not yet in Spreadsheet Class!
sheet = getSheetById(SpreadsheetApp.openById(ssId), wsId);
// now do what we want to do in the timeBased trigger
for (var i = 1; i <= sheet.getLastColumn(); i++){
sheet.autoResizeColumn(i);
}
}
// -----
// custom function – hopefully this will become a method of Spreadsheet Class any time soon
function getSheetById(ss, wsId) {
var sheets = ss.getSheets();
for (var i=0; i<sheets.length; i++) {
if (sheets[i].getSheetId() == wsId) return sheets[i];
}
}
Please be aware that your are storing in a dataspace here that is common to all your function calls. So multiple calls in a short time can overwrite each others parameters.
In my use case this is not a problem as the spreadsheet and the worksheet will not change. But do NOT try to transfer data that can change from call to call.
The real moment of execution can vary up to 15 minutes (no matter what exact time you ask for), so there is plenty of room for multiple function calls to interfere with each other!!!