I'd like to call this custom formula in Google sheets +100 times. Can I add an array to this script?
function GOOGLEMAPS(start_adress,end_adress) {
var mapObj = Maps.newDirectionFinder();
mapObj.setOrigin(start_adress);
mapObj.setDestination(end_adress);
var directions = mapObj.getDirections();
var meters = directions["routes"][0]["legs"][0]["distance"]["value"];
var distance = meters/1000;
return distance;
}
You might add an array but this could easily lead to exceed the quota of the number calls being done in a short period of time, also it could exceed the maximum execution time for a custom function (30 seconds) so the advise is to not do that when using the Map Service.
Anyway, you could send an array to a custom function by useing a A1:B2 style reference justlimit the number of distances to be calculated in order to prevent the errors mentioned above.
function GOOGLEMAPS(arr) {
var output = [];
for(var i = 0; i < arr.length; i++){
var start_address = arr[i][0];
var end_adress = arr[i][1];
var mapObj = Maps.newDirectionFinder();
mapObj.setOrigin(start_adress);
mapObj.setDestination(end_adress);
var directions = mapObj.getDirections();
var meters = directions["routes"][0]["legs"][0]["distance"]["value"];
var distance = meters/1000;
output.push([distance]);
}
return output;
}
Resource
https://developers.google.com/apps-script/guides/sheets/functions#guidelines_for_custom_functions
Related
Errors When Calculating Distance Between Two Addresses
Google Sheets JSON Error: "Service invoked too many times for one day:"
Other related
Google Apps Script - How to get driving distance from Maps for two points in spreadsheet
Related
I have built a simple custom function in Apps Script using URLFetchApp to get the follower count for TikTok accounts.
function tiktok_fans() {
var raw_data = new RegExp(/("followerCount":)([0-9]+)/g);
var handle = '#charlidamelio';
var web_content = UrlFetchApp.fetch('https://www.tiktok.com/'+ handle + '?lang=en').getContentText();
var match_text = raw_data.exec(web_content);
var result = (match_text[2]);
Logger.log(result)
return result
}
The Log comes back with the correct number for followers.
However, when I change the code to;
function tiktok_fans(handle) {
var raw_data = new RegExp(/("followerCount":)([0-9]+)/g);
//var handle = '#charlidamelio';
var web_content = UrlFetchApp.fetch('https://www.tiktok.com/'+ handle + '?lang=en').getContentText();
var match_text = raw_data.exec(web_content);
var result = (match_text[2]);
Logger.log(result)
return result
}
and use it in a spreadsheet for example =tiktok_fans(A1), where A1 has #charlidamelio I get an #ERROR response in the cell
TypeError: Cannot read property '2' of null (line 6).
Why does it work in the logs but not in the spreadsheet?
--additional info--
Still getting the same error after testing #Tanaike answer below, "TypeError: Cannot read property '2' of null (line 6)."
Have mapped out manually to see the error, each time the below runs, a different log returns "null". I believe this is to do with the ContentText size/in the cache. I have tried utilising Utilities.sleep() in between functions with no luck, I still get null's.
code
var raw_data = new RegExp(/("followerCount":)([0-9]+)/g);
//tiktok urls
var qld = UrlFetchApp.fetch('https://www.tiktok.com/#thisisqueensland?lang=en').getContentText();
var nsw = UrlFetchApp.fetch('https://www.tiktok.com/#visitnsw?lang=en').getContentText();
var syd = UrlFetchApp.fetch('https://www.tiktok.com/#sydney?lang=en').getContentText();
var tas = UrlFetchApp.fetch('https://www.tiktok.com/#tasmania?lang=en').getContentText();
var nt = UrlFetchApp.fetch('https://www.tiktok.com/#ntaustralia?lang=en').getContentText();
var nz = UrlFetchApp.fetch('https://www.tiktok.com/#purenz?lang=en').getContentText();
var aus = UrlFetchApp.fetch('https://www.tiktok.com/#australia?lang=en').getContentText();
var vic = UrlFetchApp.fetch('https://www.tiktok.com/#visitmelbourne?lang=en').getContentText();
//find folowers with regex
var match_qld = raw_data.exec(qld);
var match_nsw = raw_data.exec(nsw);
var match_syd = raw_data.exec(syd);
var match_tas = raw_data.exec(tas);
var match_nt = raw_data.exec(nt);
var match_nz = raw_data.exec(nz);
var match_aus = raw_data.exec(aus);
var match_vic = raw_data.exec(vic);
Logger.log(match_qld);
Logger.log(match_nsw);
Logger.log(match_syd);
Logger.log(match_tas);
Logger.log(match_nt);
Logger.log(match_nz);
Logger.log(match_aus);
Logger.log(match_vic);
Issue:
From your situation, I remembered that the request of UrlFetchApp with the custom function is different from the request of UrlFetchApp with the script editor. So I thought that the reason for your issue might be related to this thread. https://stackoverflow.com/a/63024816 In your situation, your situation seems to be the opposite of this thread. But, it is considered that this issue is due to the specification of the site.
In order to check this difference, I checked the file size of the retrieved HTML data.
The file size of HTML data retrieved by UrlFetchApp executing with the script editor is 518k bytes.
The file size of HTML data retrieved by UrlFetchApp executing with the custom function is 9k bytes.
It seems that the request of UrlFetchApp executing with the custom function is the same as that of UrlFetchApp executing withWeb Apps. The data of 9k bytes are retrieved by using this.
From the above result, it is found that the retrieved HTML is different between the script editor and the custom function. Namely, the HTML data retrieved by the custom function doesn't include the regex of ("followerCount":)([0-9]+). By this, such an error occurs. I thought that this might be the reason for your issue.
Workaround:
When I tested your situation with Web Apps and triggers, the same issue occurs. By this, in the current stage, I thought that the method for automatically executing the script might not be able to be used. So, as a workaround, how about using a button and the custom menu? When the script is run by the button and the custom menu, the script works. It seems that this method is the same as that of the script editor.
The sample script is as follows.
Sample script:
Before you run the script, please set range. For example, please assign this function to a button on Spreadsheet. When you click the button, the script is run. In this sample, it supposes that the values like #charlidamelio are put to the column "A".
function sample() {
var range = "A2:A10"; // Please set the range of "handle".
var raw_data = new RegExp(/("followerCount":)([0-9]+)/g);
var sheet = SpreadsheetApp.getActiveSheet();
var r = sheet.getRange(range);
var values = r.getValues();
var res = values.map(([handle]) => {
if (handle != "") {
var web_content = UrlFetchApp.fetch('https://www.tiktok.com/'+ handle + '?lang=en').getContentText();
var match_text = raw_data.exec(web_content);
return [match_text[2]];
}
return [""];
});
r.offset(0, 1).setValues(res);
}
When this script is run, the values are retrieved from the URL and put to the column "B".
Note:
This is a simple script. So please modify it for your actual situation.
Reference:
Related thread.
UrlFetchApp request fails in Menu Functions but not in Custom Functions (connecting to external REST API)
Added:
About the following additional question,
whilst this works for 1 TikTok handle, when trying to run a list of multiple it fails each time, with the error TypeError: Cannot read property '2' of null. After doing some investigating and manually mapping out 8 handles, I can see that each time it runs, it returns "null" for one or more of the web_content variables. Is there a way to slow the script down/run each UrlFetchApp one at a time to ensure each returns content?
i've tried this and still getting an error. Have tried up to 10000ms. I've added some more detail to the original question, hope this makes sense as to the error. It is always in a different log that I get nulls, hence why I think it's a timing or cache issue.
In this case, how about the following sample script?
Sample script:
In this sample script, when the value cannot be retrieved from the URL, the value is tried to retrieve again as the retry. This sample script uses the 2 times as the retry. So when the value cannot be retrieved by 2 retries, the empty value is returned.
function sample() {
var range = "A2:A10"; // Please set the range of "handle".
var raw_data = new RegExp(/("followerCount":)([0-9]+)/g);
var sheet = SpreadsheetApp.getActiveSheet();
var r = sheet.getRange(range);
var values = r.getValues();
var res = values.map(([handle]) => {
if (handle != "") {
var web_content = UrlFetchApp.fetch('https://www.tiktok.com/'+ handle + '?lang=en').getContentText();
var match_text = raw_data.exec(web_content);
if (!match_text || match_text.length != 3) {
var retry = 2; // Number of retry.
for (var i = 0; i < retry; i++) {
Utilities.sleep(3000);
web_content = UrlFetchApp.fetch('https://www.tiktok.com/'+ handle + '?lang=en').getContentText();
match_text = raw_data.exec(web_content);
if (match_text || match_text.length == 3) break;
}
}
return [match_text && match_text.length == 3 ? match_text[2] : ""];
}
return [""];
});
r.offset(0, 1).setValues(res);
}
Please adjust the value of retry and Utilities.sleep(3000).
This works for me as a Custom Function:
function MYFUNK(n=2) {
const url = 'my website url'
const re = new RegExp(`<p id="un${n}.*\/p>`,'g')
const r = UrlFetchApp.fetch(url).getContentText();
const v = r.match(re);
Logger.log(v);
return v;
}
I used my own website and I have several paragraphs with ids from un1 to un7 and I'm taking the value of A1 for the only parameter. It returns the correct string each time I change it.
For these inputs:
Origin
Destination
Arrival Time
I want two Google Sheets formulas that result in showing:
the distance in miles
travel times (driving, with traffic).
It'd be simple formulas that reference cells in the sheet that goes something like
=TravelTime(Origin,Destination,Arrive)
I've set up a Google Directions API account and pieced together this so far, but I have no idea how to get the URL to work and how to get the formula to return the outputs I want.
function TravelTime(Origin,Destination,Arrive) {
var Origin
var Destination
var Arrive
var apiUrl = 'https://maps.googleapis.com/maps/api/directions/json?origin='&Origin&'&destination='&Destination&'&key=MYKEY';
}
It seems you are trying to call an external API.
Try doing something like this:
function TravelTime(Origin,Destination,Arrive) {
var apiUrl = 'https://maps.googleapis.com/maps/api/directions/json?origin='&Origin&'&destination='&Destination&'&key=MYKEY';
var response = UrlFetchApp.fetch(apiUrl);
var json = response.getContentText();
var data = JSON.parse(json); //your final object, get the data you want here then return it.
}
Hope it helps!
I am trying to write a Google Apps script to modify calendar events so I have modified an example to list events first. When I try debugging this it reports an error that "Calendar is not defined" on the line "events = Calendar.Events.list(calendarId, options);"
I have enabled the advanced calendar API, and am basing my script on one from the Google documentation, so I assume that one worked. Is there anything else I need to do to access the relevant objects and methods?
/*
Adapted from code in https://developers.google.com/apps-script/advanced/calendar
*/
function syncColourCode() {
var calendarId = CalendarApp.getDefaultCalendar();
var properties = PropertiesService.getUserProperties();
var fullSync = properties.getProperty('fullSync'); // sync status is stored in user properties
var options = {
maxResults: 100
};
var syncToken = properties.getProperty('syncToken'); // pointer token from last sync also stored in user properties
if (syncToken && !fullSync) { // if there is a sync token from last time and sync status has not been set to full sync
options.syncToken = syncToken; // adds the current sync token to the list of sync options
} else {
// Sync events from today onwards.
options.timeMin = new Date().toISOString(); //change to new Date().toISOString() from getRelativeDate(-1, 0).toISOString()
}
// Retrieve events one page at a time.
var events;
var pageToken;
do {
try {
options.pageToken = pageToken;
events = Calendar.Events.list(calendarId, options);
} catch (e) {
Not a google-apps expert, but from reviewing the code, I see a possible problem. At no point do I see your code checking to see if getDefaultCalendar() actually returned a valid calendar ID. Later your code uses that ID under the assumption that it is good. Have you checked the value of calendarId that is returned?
Sometimes you have to read a little deeper into the message, but I always try to start with trusting the error return. In this case "Calendar is not defined" makes me question the value of calendarId.
It seem that Google made some change so that there is no Calendar reference from the AppScript API.
Anyway to get the event you may use this API:
CalendarApp.getEvents(startTime, endTime)
https://developers.google.com/apps-script/reference/calendar/calendar-app#geteventsstarttime-endtime
Below are my example function running within google sheet.
function listEventsWithinTwoMonth(){
var calendar = CalendarApp.getDefaultCalendar();
var spreadsheet = SpreadsheetApp.getActiveSheet();
var now = new Date();
var twoMonthFromNow = new Date(now.getTime() + (24 * 60 * 60 * 30 * 4 * 1000));
var events = calendar.getEvents(now, twoMonthFromNow);
if (events.length > 0) {
// Header Rows
spreadsheet.appendRow(["#่","id","StartTime","EndTime","Title","Description"]);
for (i = 0; i < events.length; i++) {
var event = events[i];
Logger.log("" + (i+1) + event.getId() +" "+ event.getStartTime()+" "+event.getEndTime()+" "+event.getTitle()+" "+event.getDescription())
spreadsheet.appendRow([(i+1),event.getId(),event.getStartTime(),event.getEndTime(),event.getTitle(),event.getDescription()]);
}
} else {
Logger.log('No upcoming events found.');
}
}
Hope this help.
CalendarApp.getDefaultCalendar() retrieves an object of the class Calendar that has multiple properties, among others an Id.
To retrieve the calendar Id, you need to define
var calendarId = CalendarApp.getDefaultCalendar().getId();
I'm a novice and I'm trying to get a Google Apps Script to spit out the country related to a set of latitude and longitude co-ordinates, by hitting the Google Maps Geocoder API and doing a reverse geocode.
So far my function is poor and looks like this:
function reverseGeocode() {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheet = ss.getSheetByName("Output");
var range = sheet.getRange("A1");
sheet.setActiveRange(range);
// Gets the address of a point in London.
var response = Maps.newGeocoder().reverseGeocode(51.5096, -0.15537);
for (var i = 0; i < response.results.length; i++) {
var result = response.results[i];
range.setValue(result.formatted_address);
}
};
At the moment it just spits out the longer formatted address to cell A1. Any idea what I'd have to replace result.formatted_address with to get it to spit out the country?
I think there's an array in the result for address_components.types which has a part called country... but since I'm a novice I don't know how to reference it!
Here's a function to extract an address component like "country" given the address_components you mention.
function extractAddressComponent(componentList, componentName) {
for(var i=0; i<componentList.length; i++) {
if(componentList[i].types.indexOf(componentName)!=-1) {
return componentList[i].long_name;
}
}
}
Inserting in your sample I use:
var country = extractAddressComponent( result.address_components, "country");
I'm working on an apps script to periodically check for modified items on a web service. Because API calls were taking too long, I've been trying to cache some of the data periodically in ScriptDb. However, trying to update data using scriptDb.saveBatch always results in the following error:
Service invoked too many times in a short time: scriptdb rateMax. Try Utilities.sleep(1000) between calls.
My script is querying ScriptDb and returning a result set of ~7600 records, modifying those records, and then saving everything back in a batch. I can't think of any way, given the tools Google makes available, to reduce the number of database calls I make. Is this really too much for ScriptDb to handle, or is there some way to improve on my code?
function getRootFolders() {
var updateTimestamp = new Date().valueOf();
var results = GetModifiedFolders(ROOT_FOLDER); //Returns results from an API call
var data = results.data; //The actual data from the API, as an array
var len = data.length;
if (len > 0) {
//Get a collection of dbMaps from ScriptDb
var maps = {}; //Store as an object for easy updating
var getMaps = db.query({'type': 'baseFolder'}).limit(50000); //Returns 7621 items
while (getMaps.hasNext()) {
var map = getMaps.next();
maps[map.boxId] = map;
}
//Iterate through the results
for (i = 0; i < len; i++) {
var item = data[i];
var map = maps[item.boxId]; //Try to retrive an existing dbMap
if (map) { //If it exists, update the existing dbMap
map.modified = item.modified;
map.updateTimestamp = updateTimestamp;
}
else { //Otherwise, insert the result into the collection of dbMaps
item.type = 'baseFolder';
item.updateTimestamp = updateTimestamp;
maps[item.boxId] = item;
}
}
//Convert the object back to an array, and use that to save to ScriptDb
var toSave = [];
for (var prop in dbMaps) {
toSave.push(dbMaps[prop]);
}
var mutations = db.saveBatch(toSave, false); //FAIL with scriptdb rateMax
if (db.allOk(mutations)) {
( . . . )
}
}
}
EDIT:
I've made a few changes in an effort to stop this from happening, but to no avail. I'm sleeping for several minutes before calling saveBatch, and then I'm saving in multiple, smaller batches, sleeping in between each one.
At this point, I can't imagine why I'm still getting this rateMax error. Is there something wrong with my code that I'm missing, or is this a bug in apps script? I assume it's my fault, but I can't see it.
Here's what I've added:
//Retrieving data from the API takes ~1 minute
//Sleep for a while to avoid rateMax error
var waitUntil = updateTimestamp + 240000; //Wait until there's only 1 minute left in the 5 minute quota
var msToWait = waitUntil - (now.valueOf());
Utilities.sleep(msToWait); //Sleep for ~3 minutes
//Save in batches
var batchSize = 250;
var batch = [];
var i = 0;
for (var prop in maps) {
batch.push(maps[prop]);
i++;
//When the batch reaches full size, save it
if (i % batchSize == 0 || i == len) {
Utilities.sleep(1000);
var mutations = db.saveBatch(batch, false);
if (!db.allOk(mutations)) {
return false;
}
batch = [];
}
}
Split the batch in smaller parts.
Wont affect the code because batch is not atomic anyways.