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

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)
};

Related

Writing a googlefinance wrapper

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.

How can I pass an app script function value (a string) to a google sheet cell as an importXML function directly?

I have many urls I want to pull some info from with importxml on a google sheet.
I'm trying to get review scores of monitors from a website.
I want to create a function so that when I give it a cell as reference (that contains a product's url) it will create a string for an importxml function (with a fixed and proper xpath) and pass it to the cell it is called. (or an adjacent one)
function puan(x) {
var cellFunction = '=IMPORTXML("' + x + '";"//div[#id=\'puan\']//#data-percent")';
return cellFunction;
}
I tried something like this but it didn't work. It just returns the value like a string.
The string looks ok and it returns the value I want if it is directly passed to the cell.
Then I tried this to select the active cell and pass the value there but it didn't work.
I think I'm using it wrong. In the documentation of app script it says custom funcions can only change the value of the cell it is called (or adjacent ones)
SpreadsheetApp.getActiveSpreadsheet().getActiveSheet().getActiveRange().setValue(cellFunction);
When I add this to my function and delete "return cellFunction;" it returns "0"
I think I'm using it wrong.
Can you guide me because I couldn't find a solution?
I'm also open to suggestions for better ways of extracting same info.
You should be using setFormula() for this purpose: https://developers.google.com/apps-script/reference/spreadsheet/range#setformulaformula
If you still can't get it to work, share a copy of your sheet and I will try to help further.
You have two options:
Either return cellFunction to the main function by calling puan(x) inside the main function
Sample:
function myFunction() {
var URL = "Your URL"
SpreadsheetApp.getActiveSpreadsheet().getActiveSheet().getActiveRange().setValue(puan(URL));
}
function puan(x) {
var cellFunction = '=IMPORTXML("' + x + '";"//div[#id=\'puan\']//#data-percent")';
return cellFunction;
}
Or set the value into the spreadsheet inside puan(x)
Sample:
function myFunction() {
var URL = "Your URL"
puan(URL);
}
function puan(x) {
var cellFunction = '=IMPORTXML("' + x + '";"//div[#id=\'puan\']//#data-percent")';
SpreadsheetApp.getActiveSpreadsheet().getActiveSheet().getActiveRange().setValue(cellFunction);
}
Note:
Using setFormula() instead of setValue() is in your case possible, but optional.

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.

Google sheets custom function built-in function

I have following custom function in google sheets, I tried to call a built-in function "TEXT" in my custom function but it is not successful. The Google sheets will prompt "unknown" function "TEXT". Is there a solution for this?
function NextMonth(StockTradeDate) {
var DeltaDate;
if (**TEXT**(StockTradeDate,"mmm") = "JAN" ) {
DeltaDate = 30;
}
return DATEVALUE(StockTradeDate) + 31;
}
Google Apps Script has the Utilities library which includes the formatDate method
Utilities.formatDate(date, timeZone, format)
For details see https://developers.google.com/apps-script/reference/utilities/utilities#formatdatedate-timezone-format
It's worth to say that in Google Sheets it's no possible call a built-in function within the script. If a service like Utilities doesn't include the functions that you are looking for, then the alternative is to build your own version, get it borrowed from a library, a open source project or any other place.
I made an attempt to use the spreadsheet function library from Ethercalc and shared about this on my answer to Is there a way to evaluate a formula that is stored in a cell?
Try using javascript date methods (as below) to drive the date and conditionals you need. Could not locate documentation that supports the Sheets built-in function calls from within an apps script function. Javascript is supported.
function NEXTMONTH(StockTradeDate) {
var DeltaDate
if (StockTradeDate.getMonth() === 0 ) { //Jan:0 Feb:1 ... Dec:11 these will need more conditionals.
DeltaDate = 30;
}
var date2 = new Date(StockTradeDate.valueOf()+ DeltaDate*24*60*60*1000)
// valueOf() is millisec time since 1/1/1970
return date2
}
If you need more info regarding the date methods and implementation, w3schools has an efficient reference.

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';
};