Google docs ImportXML called from script - google-apps-script

I am using ImportXML in a google docs sheet to aqcuire data from the sistrix api. It works fine but I encountered the limitation of 50 ImportXML commands in one sheet. So I used a script that writes the ImportXML command to a cell (temporary) formula and takes back the resulting value of the cell and copies it to the destination cell. So you can do as much ImportXML queries as you need, as they only appear in one temporary cell in the sheet.
The problem here is, that the ImportXML query SOMETIMES takes very long or returns with N/A.
Is it possible that my script sometimes doesnt wait for the ImportXML query to return and so the result is corrupted? I am currently doing it in this way:
function GetFormulaData(formula, sheet, row, col)
{
// write the formula (ImportXML(...)) to the specified cell
sheet.getRange(row, col).setFormula(formula);
// return the value of this cell resulting from the formula
return sheet.getRange(row, col).getValue();
}
So this can obviously only work if the formula (the ImportXML query) is done and has written the return value into the cell, so I can read afterwards.
Does anybody have experience or alternatives with calling ImportXML from a script?

I have solved this now in a different way. It is more common to use UrlFetchapp() within google doc scripts than ImportXML. But you have to gain the xml data yourself from the http response.
I do it in this way now:
var response = UrlFetchApp.fetch(apiString).getContentText();
var xmlContent = Xml.parse(response, true);
var answer = xmlContent.response.answer;
// get what you need from the XML answer
if (answer != null)
{
var element = answer.getElement('foo');
if (element != null)
{
var attrib = element.getAttribute('bar');
if (attrib != null)
value = attrib.getValue(); // the value you want
}
}

Related

Converting Google Sheets IF formula to Apps script

As a test, I have entered the following formula in cell K2 of my spreadsheet: =IF($M2=today(),"Today"). This acheives the desired effect and I would like this to be applied to all the rows below (with m3 referring to k3 , m4 to k4 etc.) , HOWEVER, this sheet is updated via Google Form so I cannot leave a formula in these cells as it will be overwritten.
Therefore I need to write and run the formula in apps script but, whilst I have enough knowledge of script language to do write basic If functions, this one is beyond my skills.
I have referred to this: How to get range and then set value in Google Apps Script and tried to adapt it to my purposes but to no avail.
Could someone please enlighten me?
Try this script:
function isMToday() {
sheet = SpreadsheetApp.getActiveSheet();
lastRow = sheet.getLastRow();
// get M2:M range
mRange = sheet.getRange(2, 13, lastRow - 1, 1);
// get display values instead to avoid timezone issues
mValues = mRange.getDisplayValues();
today = new Date();
// check every mValue, if today, return today, else false
output = mValues.map(mValue => {
mValueDate = new Date(mValue);
if (mValueDate.getDate() == today.getDate() &&
mValueDate.getMonth() == today.getMonth() &&
mValueDate.getFullYear() == today.getFullYear())
return ["Today"];
else
return [false];
});
// write output to K2:K
mRange.offset(0, -2).setValues(output);
}
After execution:
You do not need AppScript to do this even though it is on a Form Responses tab.
Instead of this formula in cell K2 (as you described):
=IF($M2=today(),"Today")
Use this formula in cell K1, and delete all the other formulas in column K:
=ARRAYFORMULA(IF(ROW(M:M)=1,"Today?",IF(M:M=TODAY(),"Today")))

App Script Conditional Formatting to apply on sheet by name

I have been trying to make a Google App Script code which highlight the cell if it has specific text like "L".
I have made a below code but its not working and when i run this no error appears. i do not know what is the problem.
Can you have a look at it, please that why its not working.
function formatting() {
var sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('Dec');
var range = sheet.getRange("C:AG");
if (range == 'L') {
ss.range.setBackgroundColor('#ea9999');
}
}
Issues with the code:
Three things to mention:
range is a range object, not a string. In the if condition you are comparing an object of type range with an object of type string. You need to use getValue to get the values of the range object and then compare that with the string L.
This code will take a lot of time to complete because you have a large range of cells you want to check but also you are iteratively using GAS API methods. As explained in Best Practices it is way more efficient to use batch operations like getValues,
getBackgrounds and setBackgrounds.
Another improvement you can make is to use getLastRow to restrict the row limit of your range since you are looking for non-empty values. There is no reason for checking empty cells after the last row with content.
Google Apps Script Solution:
function formatting() {
const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('Dec');
const range = sheet.getRange("C1:AG"+sheet.getLastRow());
const values = range.getValues();
const bcolors = range.getBackgrounds();
const new_bcolors = values.map((r,i)=>r.map((c,j)=>c=='L'?'#ea9999':bcolors[i][j]))
range.setBackgrounds(new_bcolors)
}
Google Sheets Solution:
Another idea would be to just create a conditional formatting in Google Sheets:
and specify a custom color with your hex code:
JavaScript References:
map
ternary operator

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

Google sheet not updating custom function return value

I am very new to Google Apps Script (as well as JavaScript, for that matter), but I have been trying to tinker with it for fun.
I have tried writing a script to fetch API price data in Google Sheets, but am finding that the returned value is not updating when re-evaluating the script in the same cell.
Below is a script to fetch bitcoin price data from Coinbase's API. The script parses the JSON response of the request, as is described here.
function getBTCPrice() {
var url = "https://api.coinbase.com/v2/prices/BTC-USD/spot";
var response = UrlFetchApp.fetch(url);
var jsonSpotPrice = response.getContentText();
var parseSpotPrice = JSON.parse(jsonSpotPrice);
var price = "$" + parseSpotPrice.data.amount;
return price
}
Now, if I type =getBTCPrice() in some cell, and then re-evaluate a few moments later, I get the same price; however, if I evaluate the script in a different cell, I get a different result.
I've read some stuff about Google caching values in cells, so that perhaps the script isn't evaluated because the value of the cell has not changed. Is this the case here? If so, is there a workaround?
Any help is greatly appreciated!
I finally figured it out! Instead of trying to call the custom function from an actual sheet cell (which apparently stores cached values), the trick is to call the function within a script.
Using my above script:
function getBTCPrice(url) {
var response = UrlFetchApp.fetch(url);
var jsonSpotPrice = response.getContentText();
var parseSpotPrice = JSON.parse(jsonSpotPrice);
var price = "$" + parseSpotPrice.data.amount;
return price;
}
You can then call this function from another script. Specifically, I was looking to assign the updated price to a cell. Below is an example, which assigns the price to the active spreadsheet, in cell A1:
function updatePrice(){
var a = getBTCPrice("https://api.coinbase.com/v2/prices/BTC-USD/spot");
SpreadsheetApp.getActiveSpreadsheet().getRange('A1').setValue(a);
}
You can then proceed to set an appropriate time trigger. And that's all there is to it!
Have a look at this answer on Refresh data retrieved by a custom function in google spreadsheet.
As the answerer says, the trick is to
My solution was to add another parameter to my script, which I don't even use. Now, when you call the function with a parameter that is different than previous calls, it will have to rerun the script because the result for these parameters will not be in the cache.
Vik
In addition of Vikramaditya Gaonkar answer, you can use a installable trigger to get a refresh result each minute.
function getBTCPrice(input) {
url = "https://api.coinbase.com/v2/prices/BTC-USD/spot";
response = UrlFetchApp.fetch(url);
var jsonSpotPrice = response.getContentText();
var parseSpotPrice = JSON.parse(jsonSpotPrice);
var price = "$" + parseSpotPrice.data.amount;
return price
}
function up(){
SpreadsheetApp.getActiveSheet().getRange('A1').setValue(Math.random());
}
The parameter of getBTCPrice function is, in my case, cell A1 which is randomize each minute. For this, I create a installable trigger on up function
function up, time-driven, minute timer, every minute
I was also trying to make my custom function update, after searching I came up with the following function:
function updateFormulas() {
range = SpreadsheetApp.getActiveSpreadsheet().getDataRange();
formulas = range.getFormulas();
range.clear();
SpreadsheetApp.flush();
range.setValues(formulas);
}
The function above update all formulas of the spreadsheet. In my experience to make a custom function update I had to change its value, so I get all the data of the sheet, then I get the formulas and store them into a variable, then I clear their values and apply this change with "flush", finally I update the values I have just cleared with the formulas I have stored.
I created this function and in my case I have set the trigger for 1 minute to execute it, every minute all functions of the table are updated.
I hope this helps you.

Set formula for adjacent cell if text is present

I'm working with a Google Sheets form which also accepts answers via text message. I'm trying to work out a method using Google Apps Scripts to split the body of the text message using a comma as a delimiter.
The problem I'm running into is overwriting information submitted by the form and not by text message.
My current script is:
function splitCells() {
var sheet = SpreadsheetApp.getActiveSpreadsheet().getSheets()[0];
var colC = sheet.getRange("C2:C").getValues();
var colD = sheet.getRange("D2:D").getFormulas();
//Logger.log(colC);
for(var i in colC){
if(typeof(colC[i][0]) =='string'){
colD = '=if(istext(C2:C),split(C2:C,",",true))';
} else {
colD = 'D2:D';
}
}
sheet.getRange("D2:D").setFormula(colD);
}
The function is working correctly, splitting the contents of column C (the SMS body) into D, E, and F as expected. But, it's overwriting data in column D because the else condition isn't being met (colC is blank in those places).
How do I get the script to move over blank cells without replacing the contents of the cell?
It's sort of confusing to explain, so here's a sample document you can check out. A custom menu should install when you open it and you can run the script from there (or from the editor).
Thanks for the help.
There are a few simple mistakes to start.
A spreadsheet cell can contain a value or a formula, not both.
If you use setFormula/s(), any value in a cell will be replaced by the result of the formula, even if the formula is blank.
Since you want to have a mix of values and formulas, you should set formulas only in the specific cells that match the criteria:
// If we received a SMS response, set a formula to parse it
sheet.getRange(2+i,4).setValue('=if(istext(C2:C),split(C2:C,",",true),"")')
The criteria test isn't sufficient. A blank cell is still of type string, but it's a blank string. So this evaluates true for both form entries and SMS entries:
if(typeof(colC[i][0]) =='string'){ ...
A more effective test checks for a non-blank response:
if(colC[i][0] != ''){ ...
An even better one would ensure that the value in column C meets the required format requirements.
You are looping over an array using the for .. in loop, which is meant for going over object properties. This works, but the loop value i will be a string, which can cause problems when doing math. Better to get in the habit of looping over the numeric index. (See.)
The full-column range expression C2:C is elegant, however you end up with an array that contains all rows in the spreadsheet, more than a thousand in your example. Since we're going to loop over all rows, it's best to limit that range:
var colC = sheet.getRange(2, 3, sheet.getLastRow()).getValues(); // C2:C, only non-blank rows
Adjusting for those problems, we have:
function splitCells2() {
var sheet = SpreadsheetApp.getActiveSpreadsheet().getSheets()[0];
var colC = sheet.getRange(2, 3, sheet.getLastRow()).getValues(); // C2:C, only non-blank rows
//Logger.log(colC);
for(var i=0; i< colC.length; i++){
if(colC[i][0] != ''){
// If we received a SMS response, set a formula to parse it
sheet.getRange(2+i,4).setValue('=if(istext(C2:C),split(C2:C,",",true),"")')
}
}
}