Writing a googlefinance wrapper - google-apps-script

I'm working on a Google Sheet to track my stock portfolio. I use the googlefinance function to retrieve a stock price or last day change, but learned that it does not support all exchanges that I trade on.
I then thought to write a wrapper called simply finance, passing off the fetching of prices to Yahoo Finance in case the exchange isn't supported by Google. The wrapper would also give me the flexibility to make my sheet a bit more clean as well. For instance, Google and Yahoo use different indicators for stock exchanges. For instance, the Hong Kong Exchange is HKG on Google but HK on Yahoo. I would just like to type the exchange code that I use, and handle it in the wrapper. Here's an array with examples:
// exchange code that I use, that Google uses, Yahoo uses, exchange currency
[HKG, HKG, HK, HKD],
[TYO, TYO, T, JPY],
[TPX, TPE, TW, TWD],
[KRX, KRX, KS, KRW],
[FRA, FRA, F, EUR],
[NDQ, NASDAQ, null, USD],
[NSY, NYSE, null, USD]
I later stepped off the idea of using an array, but just hardcode a switch statement, but still giving the array gives some background.
Now consider the following sheet and script:
A B C
1 TYO 9984 =finance(A1, B1, "price")
2 NDQ AAPL =finance(A2, B2, "price")
3 NSY GE =finance(A3, B3, "price")
4 HKG 0865 =finance(A4, B4, "price")
function finance(exchange, ticker, type) {
if (exchange == 'TYO') { // googlefinance() doesn't support TYO
return yahoofinance(ticker + '.T', type);
}
else {
switch (exchange) {
case 'HKG': return googlefinance('HKG:' + ticker, type); break;
case 'NDQ': return googlefinance('NASDAQ:' + ticker, type); break;
case 'NSY': return googlefinance('NYSE:' + ticker, type); break;
}
}
}
function yahoofinance(ticker, type) {
return true; // implement later
}
I have 2 questions:
I was expecting column C to fill with values, but instead get googlefinance is undefined. How can I solve this?
googlefinance gets refreshed on the server each 2 mins (I believe). How can I make my own wrapper to refresh every 2 minutes (so also call yahoofinance every 2 mins) so that the cells are always updated with almost-realtime price information?

Issues
Issue 1:
The code returns undefined because you are returning something undefined.
Here:
return googlefinance('HKG:' + ticker, type)
googlefinance hasn't been defined anywhere in the script.
Your goal must be to return a string instead.
Issue 2:
Another issue, is that you are using a custom formula to return another formula and expect the latter to evaluate. You can't execute a formula as a result of another formula.
Modification 1:
The switch statement overcomplicates the code and it does not add value.
You can replace it with a simple string concatenation ("a"+"b") or more convenient with template literals:
return `=googlefinance("${exchange}:${ticker}", "${type}")`;
this will return something in this format:
=googlefinance("NDQ:AAPL", "price")
but this will be a text in your sheet, it won't work as a formula.
Modification 2:
Change your approach. Instead of using a custom formula, use a regular google apps script function. You won't be able to use it as a custom formula then, but you can execute it in multiple ways, starting with a simple manual execution. Later, search other threads to see how you can execute that from custom menus or triggers.
Solution - Regular Function approach:
function regularFinance() {
const type = "price";
const ss = SpreadsheetApp.getActive();
const sh = ss.getSheetByName('Sheet1'); // put the name of your sheet
const vals = sh.getRange('A1:B'+sh.getLastRow()).getValues();
const formulas = [];
vals.forEach(r=>{
let dt = r[0]=='TYO'? yahoofinance(r[1], type):
`=googlefinance("${r[0]}:${r[1]}", "${type}")`;
formulas.push([dt]);
})
// paste data in column C
sh.getRange(1,3,formulas.length,1).setValues(formulas);
function yahoofinance(ticker, type) {
return true; // implement later
}
}
Like I said, this function is not a custom formula. It is a regular function which needs to be executed. One way to do that is to manually execute it from the script editor:
Output:
Make sure to correct the formulas. I am not familiar with what you want to achieve, so I will leave the formulas to you.

Related

Stop custom function from auto refreshing/periodically calling external API

I am using Google Apps Script and a custom function to call an external API to verify phone numbers.
Below is the code for my function.
/**
* This CUSTOM FUNCTION uses the numVerify API to validate
* a phone number based on the input from JotForm and a
* country code which is derived from the JotForm country
*
* Numverify website: https://numverify.com/dashboard (account via LastPass)
* Numverify docs: https://numverify.com/documentation
*/
function PHONE_CHECK(number, country){
if(country == "")
return [["", "country_not_set"]]
// check the API result has already been retrieved
var range = SpreadsheetApp.getActiveSheet().getActiveRange()
var apires = range.offset(0, 1).getValue()
if(apires.length > 0)
return range.offset(0, 0, 1, 2).getValues()
var url = 'http://apilayer.net/api/validate'
+ '?access_key=' + NUMVERIFY_KEY
+ '&number=' + encodeURIComponent(number)
+ '&country_code=' + encodeURIComponent(country)
+ '&format=1';
var response = UrlFetchApp.fetch(url, {'muteHttpExceptions': true});
var json = response.getContentText();
var data = JSON.parse(json);
if(data.valid !== undefined){
if(data.valid){
return [[data.international_format, "OK"]]
}else{
return [["", "invalid_number"]] // overflows data to the next column (API Error) while keeping the phone field clear for import into TL
}
}else if(data.success !== undefined){
if(data.error.type.length > 0){
return [[number, data.error.type]]
}else{
return [[number, "no_error_type"]]
}
}else{
return [[number, "unexpected_error"]] // this generally shouldn't happen...
}
}
Given this formula, which takes a phone number and country code, it will then check the phone number against the numverify API and return the result in the cell and overflow to the cell to the right of it. The overflow is used to indicate whether the API was called successfully and to check if the result was already retrieved.
Example:
=PHONE_CHECK("+32123456789", "BE")
Note that the first cell is empty because the API returns an 'invalid phone number' code. Because of privacy, I won't put any real phone numbers here. In case I would've used a real phone number, the first cell would contain the phone number formatted in the international number format.
Since I'm using the free plan, I don't want to rerun the function every time if I already know what the result is, as I don't want to run up against the rate limit. Unfortunately, this doesn't seem to work and periodically (it looks like once every day), it will refresh the results for each row in the sheet.
So two questions:
Is something wrong with my logic in checking the API result and then just exiting the function? (see below for the code)
If the logic is right, why does Google Sheets seem to periodically ignore (or refresh?) the values in that second column and call the external API anyhow?
var range = SpreadsheetApp.getActiveSheet().getActiveRange() // get the cell from which the function is called
var apires = range.offset(0, 1).getValue() // get the values directly to the right of the cell
if(apires.length > 0) // check if there's anything there...
return range.offset(0, 0, 1, 2).getValues() // return an array that basically just resets the same values, effectively stopping the script from running
Your Aim:
You want a custom function, AKA a formula to only run once, or as many times as is necessary to produce a certain result.
You want the same formula to write a value to the another cell, for example the adjacent cell, that will tell the formula in future, if it should be run again or not.
Short Answer:
I'm afraid that values that are evaluated from custom functions AKA formulas are transient, and what you want to accomplish is not possible with them.
Explanation:
You can run a quick test with this custom function:
function arrayTest() {
return [[1, 2, 3, 4 ,5]]
}
If you put this in a cell as below:
You will see that if you delete the formula in the original cell, the overflow values also dissapear.
Therefore something like the following code will almost always produce the same value:
function checkTest() {
var cell = SpreadsheetApp.getActiveRange()
var status = cell.offset(0, 1).getValue();
if (status != "") {
return "already executed" // in your case without calling API
} else {
return [["OK","executed"]] // in your case making API call - will happen ~90% of the time.
}
}
// OUTPUT [["OK","executed"]]
Here I am inserting a row and deleting it to force re-calculation of the formulas.
The first thing that Sheets does before re-calculating a formula is that it clears the previous values populated by formula. Since the conditional statment depends on the value of its previous execution, it will always evaluate to the same result. In your case, it will almost always make the API call.
Confusingly, this is not 100% reliable! You will find that sometimes, it will work as you intend. Though in my tests, this only happened around 1 times out of 10, and most often when the formulas updated when saving changes to the script editor.
Ideally, though not possible, you would want to be able to write something like this:
function checkTest() {
var cell = SpreadsheetApp.getActiveRange();
var cellValue = cell.getValue();
var adjacentCell = cell.offset(0, 1);
var status = adjacentCell.getValue();
if (status == "") {
cell.setValue(cellValue)
adjacentCell.setValue("executed")
}
}
Which would clear the formula once it has run, alas, setValue() is disabled for formulas! If you wanted to use setValue() you would need to run your script from a menu, trigger or the script editor. In which case it would no longer make sense as a formula.z
References
https://developers.google.com/apps-script/guides/sheets/functions

Implementing vlookup and match in function

I'm trying to create a function in Sheets that combines a "Vlookup" and "Match" combination that I use frequently.
I want to use my function, "Rates" to accept 1 argument and return a combination of Vlookup and Match, that always uses the same values.
Vlookup(argument, DEFINED RANGE (always stays the same defined range), match(A1 (always cell A1), DIFFERENT DEFINED RANGE, 0), FALSE)
I have tried creating a script, but have no experience coding, and I receive an error that "vlookup is not defined"
function ratesearch(service) {
return vlookup(service, Rates, Match($A$1,RatesIndex,0),FALSE);
}
Actual results: #ERROR!
ReferenceError: "vlookup" is not defined. (line 2).
function findRate() {
var accountName = SpreadsheetApp.getActiveSpreadsheet().getActiveSheet().getRange(1,1).getValue(); //determine the account name to use in the horizontal search
var rateTab = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('Rates'); //hold the name of the rate tab for further dissection
var rateNumColumns =rateTab.getLastColumn(); //count the number of columns on the rate tab so we can later create an array
var rateNumRows = rateTab.getLastRow(); //count the number of rows on the rate tab so we can create an array
var rateSheet = rateTab.getRange(1,1,rateNumRows,rateNumColumns).getValues(); //create an array based on the number of rows & columns on the rate tab
var currentRow = SpreadsheetApp.getActiveSpreadsheet().getActiveSheet().getActiveCell().getRow(); //gets the current row so we can get the name of the rate to search
var rateToSearch = SpreadsheetApp.getActiveSpreadsheet().getActiveSheet().getRange(currentRow,1).getValue(); //gets the name of the rate to search on the rates tab
for(rr=0;rr<rateSheet.length;++rr){
if (rateSheet[rr][0]==rateToSearch){break} ;// if we find the name of the
}
for(cc=0;cc<rateNumColumns;++cc){
if (rateSheet[0][cc]==accountName){break};
}
var rate = rateSheet[rr][cc] ; //the value of the rate as specified by rate name and account name
return rate;
}
Optimization points for Alex's answer:
Never forget to declare variables with var, const or let (rr and cc). If you omit the keyword, the variables will be global and cause you a lot of trouble (as they will not reset after the loop finishes). The best way is to use block-scoped let.
Following #1, do not rely on out-of-scope variables (rateSheet[rr][cc]).
You do not need to call SpreadsheetApp.getActiveSpreadsheet() multiple times - that's what variables are for. Call once, then reuse.
getRange(1,1,<last row>, <last col>) is equivalent to a single getDataRange call.
use find or findIndex method to avoid verbose loops.
With the points applied, you get a clean and optimized function to use:
const findRate = () => {
const ss = SpreadsheetApp.getActiveSpreadsheet();
const accountName = ss.getActiveSheet().getRange(1, 1).getValue();
const rateTab = ss.getSheetByName("Rates");
const rates = rateTab.getDataRange().getValues();
const currentRow = ss.getActiveSheet().getActiveCell().getRow();
var rateToSearch = ss.getActiveSheet().getRange(currentRow, 1).getValue();
const rr = rates.findIndex((rate) => rate === rateToSearch);
const [firstRates] = rates;
const cc = firstRates.findIndex((rate) => rate === accountName);
return rates[rr][cc];
};
Note that the "vlookup" is not defined error indicates there is no vlookup variable / function declaration in scope. Which obviously is the case as there is no built-in Google Apps Script vlookup function.
You can't access random ranges from a custom function so you would have to provide the data to the function, some of the other solutions here that use get active spreadsheet won't work as a custom function which I am guessing is what the OP is looking for, here is an example of a script that does that but word of warning before you go down this road, custom functions are much slower than the built in functions so doing this will be much slower than vlookup and match, if you only have a few functions like this in the sheet you will be fine, but if you build large tables with dozens of rows that use custom functions it will slow down you spreadsheet substantially.
// Combines VLOOKUP and MATCH into a single function
// equivalent to VLOOKUP(rowValue, tableData, MATCH(columnName, tableHeader))
// but in this version tableData includes tableHeader
function findInTable(tableData, columnName, rowValue) {
if (rowValue === "") {
return "";
}
if (tableData.length == 0) {
return "Empty Table";
}
const header = tableData[0];
const index = header.indexOf(columnName);
if (index == -1) {
return `Can't find columnName: ${columnName}`;
}
const row = tableData.find(row => row[0] == rowValue);
if (row === undefined) {
return `Can't find row for rowValue: ${rowValue}`;
}
return row[index];
}
Another optimization I suggest you do is use named ranges, it allows you to transform something like:
=VLOOKUP(C5, 'Other Sheet'!A2:G782, MATCH("Column Name", 'Other Sheet'!A1:G1))
into a more readable and easier to look at:
=VLOOKUP(C5, someTableData, MATCH("Column Name", someTableHeader))
for the custom function form this will look like:
=findInTable(A1:G782, "Column Name", C5)
Note that I shorted the argument list by merging the data and header, this makes some assumptions about the table structure, e.g. that there is a one header line and that the lookup value is in the first column but it makes it even shorter and easier to read.
But as mention before this comes at the cost of being slower.
I ended up giving up on using this for my needs due to how slow it is and how much faster VLOOKUP and MATCH are since they are built in functions.
vlookup is not something you can use in a function in a script, it is a spreadsheet formula.
Google Scripts use JavaScript, so you'll need to write your code in JS then output it to a relevant cell.
If you could share your sheet/script we could help figure it out with you.

How to appendRow (or write data more generally) to Google Sheets from a custom function

I've written a custom function [=ROUTEPLAN(origin,destination,mode,departuretime)] in the Google Sheets script editor. The function assigns a unique ID to the request, calls the Google Maps Directions API, passes as params the arguments as listed in the function, parses the JSON and extracts the duration, end latitude and end longitude for each step of the journey, and then appends a row for each step, with the request ID for the whole journey, the sequential step number, the duration, end latitude and end longitude:
function ROUTEPLAN() {
//Call the google route planner api
//(variables for api declared here but removed for brevity)
var routeResponse = UrlFetchApp.fetch("https://maps.googleapis.com/maps/api/directions/json?origin=" + origin
+ "&destination=" + destination
+ "&mode=" + mode +
"&region=uk&departure-time=" + departuretime
+ "&key=MYAPIKEY")
//Assign a unique ID to this request
var requestID = Date.now() + Math.random();
//Parse JSON from routeResponse
var json = routeResponse.getContentText();
var data = JSON.parse(json);
//Insert the RequestID, step number, duration, end Latitude and end Longitude for each step of the journey into the RouteDetails sheet
var steps = data["routes"][0]["legs"][0]["steps"];
for (i = 0; i < steps.length; i++) {
var stepID = i + 1;
var duration = steps[i]["duration"]["value"];
var endLat = steps[i]["end_location"]["lat"];
var endLng = steps[i]["end_location"]["lng"];
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheet = ss.getSheetByName("RouteDetails")
sheet.appendRow([requestID,stepID,duration,endLat,endLng]);
}
}
Or at least that's what I want it to do. It worked fine until I tinkered with it, and now I'm getting an ERROR when I call the function in the spreadsheet, telling me I don't have permission to call appendRow. I know why this is happening (although I don't understand why it wasn't happening before), but I cannot work out what I'm supposed to do about it.
If appendRow exists, there must be some circumstance in which it can be used to write data the sheet, but I can't figure out the circumstances in which permission to write to the sheet would be granted.
The purpose of the sheet is to provide data to a chatbot (the chatbot app has read & write permissions to the sheet). I'm not intending to provide access beyond that (i.e. i'm not intending to publish this for wider use). I've tried going down the installable trigger route, but despite following all the instructions that made absolutely no difference to the outcome. From the limited understanding I gained from reading about API Executables, that doesn't seem to be an option either.
Can anyone tell me how to solve this? Thank you :-)
A custom function can not modify the structure of the spreadsheet, so calling appendRow() is not allowed as stated in the documentation:
A custom function cannot affect cells other than those it returns a value to. In other words, a custom function cannot edit arbitrary cells, only the cells it is called from and their adjacent cells. To edit arbitrary cells, use a custom menu to run a function instead
If you want to return multiple rows from your function, it needs to return a two dimensional array. Note however that custom functions have the same limitation as native functions of not being able to overwrite content i.e. if you try to return two rows but the row below is already filled the function will error out.

Using built-in spreadsheet functions in a script

I'm using Google App Script for the first time.
I'm using it on a Google Doc spreadsheet.
I'm trying very simple functions, just to learn the basics. For example this works:
function test_hello() {
return 'hello';
}
But I'm puzzled by this simple one :
function test_today() {
return today();
}
It makes an #ERROR! wherever I use it.
And when I put my cursor on it, it says :
error : ReferenceError: "today" is not defined.
While the today() function works when used directly in the spreadsheet.
Does this mean that in scripts, I cannot use spreadsheet built-in functions?
Is there any elegant way around this?
Some spreadsheet functions are quite useful to me (I like weekday() for example).
A non-elegant way could be to create columns to calculate intermediate values that I need, and that can be calculated with spreadsheet functions. But I'd rather avoid something this dirty and cumbersome.
Google Apps Script is a subset of JavaScript, spreadsheet functions are currently not supported.
For example, if you want to create a function that returns today's date you should write :
function test_today(){
return new Date()
}// note that this will eventually return a value in milliseconds , you'll have to set the cell format to 'date' or 'time' or both ;-)
syntax is the same as with sheet functions : =test_today() see tutorial
There are many internet ressources on javascript, one of the most useful I found is w3school
Google Apps Script still does not (1/7/20) include an API to Google Sheets native functions.
But you can set the formula (native functions) of a cell named as a named range in a spreadsheet.
Then in the GAS:
var nativeOutput = spreadsheet.getRangeByName("outputCell").getValue();
Voila! Your GAS is calling the native function in the cell.
You can send data from the GAS to the native function in the cell, by naming another cell in the sheet (or in any sheet) referred to by the formula in the other cell:
spreadsheet.getRangeByName("inputCell").setValue(inputData);
Your GAS can dynamically create these cells, rather than hardcoding them, eg:
// Create native function, its input and output cells; set input value; use native function's output value:
// Use active spreadsheet.
var spreadsheet = SpreadsheetApp.getActive();
// Name input, output cells as ranges.
spreadsheet.setNamedRange("inputCell", spreadsheet.getRange("tuples!F1"));
spreadsheet.setNamedRange("outputCell", spreadsheet.getRange("tuples!F2"));
var outputCell = spreadsheet.getRangeByName("outputCell");
var inputCell = spreadsheet.getRangeByName("inputCell");
// Set native formula that consumes input cell's value, outputting in formula's cell.
outputCell.setFormula("=WEEKNUM(inputCell)");
// Call native function by setting input cell's value for formula to consume.
// Formula sets its cell's value to formula's output value.
inputCell.setValue(15);
// Consume native function output.
var nativeOutput = outputCell.getValue();
Logger.log("nativeOutput: "+ JSON.stringify(nativeOutput)); // Logs "nativeOutput: 3"
Beware: this technique exposes the code in cells that a spreadsheet user can access/change, and other spreadsheet operations could overwrite these cells.
What the spreadsheet functions can do, Javascript can do. I just have to replace var day_num = weekday() by var day_num = new Date(date).getDay()
Here is the result :
/**
* Writes the day of the week (Monday, Tuesday, etc), based on a date
*/
function day_name(date) {
// calculate day number (between 1 and 7)
var day_num = new Date(date).getDay();
// return the corresponding day name
switch(day_num) {
case 0: return 'Sunday'; break;
case 1: return 'Monday'; break;
case 2: return 'Tuesday'; break;
case 3: return 'Wednesday'; break;
case 4: return 'Thursday'; break;
case 5: return 'Friday'; break;
case 6: return 'Saturday'; break;
}
return 'DEFECT - not a valid day number';
};

google apps script: built-in spreadsheet functions fail in script - wrong syntax?

I am learning GAS and want to use spreadsheet functions within my script. As a test I did a simple case but on save it threw "reference Error: 'Left' is not defined." I've looked through examples of code and can't see an alternate syntax.
function testLeft(){
return LEFT("abcdef",3);
}
A second simple test, same result
function testNow(){
return Now()
}
Any suggestions? My wild guess is that there is a special syntax within scripts for using a built-in spreadsheet function. Or maybe not all functions available directly in spreadsheets are available for use in GAS?
Thanks.
Unfortunately spreadsheet functions are not available in Google Apps Script. In this case you can use JavaScript's substring() method to get the portion of the string you desire.
I am from a VBA background and I have found this site
http://excelramblings.blogspot.co.uk/2012/05/google-apps-script-equivalents-for.html
provided by Bruce Mcpherson very helpful lots of ready made functions for copying and pasting into your Google App Script especially if you are converting from an Excel spreadsheet to a Google spreadsheet.
Bruces Code for LEFT:
function Left(str,optLen) {
return Mid( str, 1 , optLen);
}
And to use the above LEFT function you will also need Bruces Mid function:
function Mid (str,optStart,optLen) {
var start = IsMissing (optStart) ? 0 : optStart - 1;
var length = IsMissing (optLen) ? Len(str) - start + 1 : optLen ;
DebugAssert( str.slice, str + ' is not a valid string for Mid function');
return str.slice ( start, start + length);
This is my attempt at simulating the mid Function
function fMid(varInStr, intPlace) {
//GAS does not have a Mid Function so I have made this one for now
//varInStr is the input string and you want a character returned from it at a given position
//intPlace is the position of the character you want
//Example
//=fMid("123456789", 9) returns "9"
var P
var N
P = intPlace -1
N = intPlace
return varInStr.substring(P,N)
};