Apps script, spreadsheet and setNumberFormat - google-apps-script

In a Google spreadsheet I pull some numbers from Google Analytics via Apps script and the Analytics API.
One of the numbers is the bounce rate, which is returned in the format 42.380071394743425. I want to display this with two digits after the decimal point (and I need to add a percentage sign). I would like to do this via setNumberFormat.
However a format token like "0.00", "#,##" etc result in output like "4.238.007.139.4743.425" which is not at all what I want. I somewhat suspect a part of the problem might be that my document is in German, with a comma as decimal delimiter, and the number from the API returned has a decimal point (or I might be overlooking something simple, wich is just as likely).
So, can I use setNumberFormat, and what format token do I have to use to turn "42.380071394743425" into "42,38%" ?
I am using the build-in App service. I do not have problems with other types of KPIs, just percentage values like bounceRate.
var viewId = "<myViewId>"
var options = {};
options['max-results'] = 1;
metric = "ga:bounceRate"; // actually this is passed in as a function parameter
// formatDate is a wrapper that calls Utilities.formatDate
var startDate = formatDate(pDate, 'yyyy-MM-dd');
var endDate = formatDate(pDate, 'yyyy-MM-dd');
var report = Analytics.Data.Ga.get(viewId, startDate, endDate, metric, options);
.....
token = [];
// format is passed in as a function parameter to fit the metric
switch(format) {
case("percentage"):
token.push(["0.00%"]);
break;
default:
token.push(["0.00"]); // tried different options to no avail
break;
}
sheet.getRange(<row>,<col>).setValue(report.rows).setNumberFormats(token);
As I said the code itself is working fine if the API returns unformatted numbers (so I don't think the problem is in the code), but I can't get the bounceRate to display the way I want.
Thank you for your time.

Select Format > Number > More Formats > Custom number formats... and type ##.##"%".
Or you can set the number format by GAS the same way.
var range = sheet.getActiveRange();
range.setNumberFormat('##.##"%"');
This is US locale based. You may change the format string according to your spreadsheet's locale(File > Spreadsheet settings...). As you can see in this documentation, the format is dependant on the spreadsheet's locale.

Related

Convert a string to a date format using a script or a Google Sheets built-in formula

DESCRIPTION:
I want to convert a DD/MM/YYYY HH:mm or 25/01/2022 11:00 string, in an accepted date format.
Doesn't matter which one, it just has to be recognized by Apps Script and Google Sheets and be able to work with it.
If you can provide an Apps Script's code (not a formula in Google Sheets like I attempted to do) that converts the string into a date and then set the values in another range, to work with them as dates, I would be grateful, thanks.
If it's a Google Sheet formula no problem, as long as it works.
TRIED:
After many attempts, I tried to build a custom formula putting pieces together around the web but it doesn't function
//formula is translated from italian
=ARRAYFORMULA(IF(F10:F="",,TEXT(DATE(
IF.ERROR(REGEXEXTRACT(F10:F, "/(\d+) "), YEAR(F10:F))*1,
IF.ERROR(REGEXEXTRACT(F10:F, "/(\d+)"), MONTH(F10:F))*1,
IF.ERROR(REGEXEXTRACT(F10:F, "\d+"), DAY(F10:F))*1)+
IF.ERROR(TIME.VALUE(F10:F), REGEXEXTRACT(F10:F, "\d+:\d+")+
IF(REGEXMATCH(F10:F, "PM"), 0.5, 0)), "yyyy-mm-dd hh:mm")))
It gives a #VALUE error, which says "'11:00' is a string and can't be recognized as a date" (11:00 is an example).
I've also got the Regular Expression, but I don't know if it's correct and how to use it in code:
/([\d])\w+\/([\d])\w+\/([\d])\w+\s([\d])\w+\:([\d])\w+/g
I also tried changing the time zone but it didn't work.
Keep in mind I'm using the Italian time zone, if it's possible I'd rather keep it as it is.
Table example (like I said, what's important is that dates are accepted as dates):
F: Column source strings
Q: Column desired dates recognizable as dates by Sheets
(Q because it's the real column where I want to put the formula)
F
..
Q
16/02/2023 16:00
16/02/2023 16:00:00
25/11/2022 15:00
25/11/2022 15:00:00
For #Cooper and the solution based on the script.
I've customized the script, but it doesn't recognize the split function anymore (copy and paste of your function logs what it expects in Apps Script), and doesn't get any results in overwriting the existing string dates.
let dateStringed; //source wrong dates
var i = 0;
var flatArray;
function expired() {
//bLast is the range Last Row
dateStringed = gen.getRange(10, 6, bLast, 1).getValues();
flatArray = [].concat.apply([], dateStringed);
while (i <= bLast) {
i++;
convert();
};
Logger.log(flatArray);
gen.getRange(10, 6, bLast, 1).setValues(flatArray);
};
function convert(s=flatArray[i]) { //instead of "25/01/2022 11:00"
let [d,m,y,hr,mn] = s.split(/[\/ :]/)
Logger.log('y: %s m: %s d: %s hr: %s mn: %s',y,m,d,hr,mn);
Logger.log(new Date(y,m - 1,d,hr,mn).toLocaleString());
//don't know if it's correct, but it logs the dates
//in an easier syntax
};
For #doubleunary solution:
Demo SHEET ITA
In the sheet I copied and pasted the first column of my private original sheet, the F column with the text dates, and the Q10 cell I've pasted the formula as it is
I made sure to set local to Italy but to display english name formulas.
I don't know why, here it colors green and it doesn't give me a result.
But I did a test, and set the sheet tu US time and it functions. Any idea on how to make it function in Italian version?
Demo SHEET US
Solved: I used this script
function dateCorrected(){
gen.getRange('N10:N').clearContent();
//get the formula from another code sheet:
//'=arrayformula( SE.ERRORE( 1 / VALORE(
//regexreplace( to_text(F10:F);
//"(\d+)/(\d+)/(\d+) (\d+):(\d+)"; "$3-$2-$1 $4.$5" ) ) ^ -1 ) )'
var dateCorr = codeSheet.getRange('T1').getFormula();
Logger.log(dateCorr);
gen.getRange('N10').setFormula(dateCorr);
gen.getFilter().sort(14, false);
gen.getRange('N10:N').clearContent();
gen.getRange('N10').setFormula(dateCorr);
}
And this gives me the possibility to delete rows that meet a certain date condition. Thank you all for the support.
It is usually easiest to do the text string to datetime conversion using a spreadsheet formula. You can convert text strings like 25/01/2022 11:00 to dates with this formula in cell G10:
=arrayformula( iferror( 1 / value( regexreplace( to_text(F10:F); "(\d+)/(\d+)/(\d+) (\d+):(\d+)"; "$3-$2-$1 $4.$5" ) ) ^ -1 ) )
Format the result column as Format > Number > Date time.
In the event you need to "fix" those datetime values in place, you can replace the formula results with static values with Control+C to copy and Control+Shift+V to paste values only, or do the same with a simple range.setValues(range.getValues()) script bit.
In the event you need to pass those datetime values to Apps Script, it is usually easiest to get them as Date objects rather than text strings. The Date objects will refer to the same moment in time (in UTC) as the date times in the spreadsheet (in the spreadsheet's time zone).
You should note that Apps Script is JavaScript which means that Date objects are always in the UTC timezone. If you log them or output them in some other way, they will not be shown in the Italian timezone as you expect.
There are two easy ways to present such dates in a human-readable format in the spreadsheet's timezone. The first is to directly get the data as a text string in the format that it is shown in the spreadsheet:
function test1() {
const ss = SpreadsheetApp.getActive();
const dateStrings = ss.getRange('Sheet1!G10:G')
.getDisplayValues()
.flat()
.filter(String);
console.log(dateStrings);
}
The second is to get the data as Date objects and convert them to text strings using the spreadsheet's timezone, like this:
function test2() {
const ss = SpreadsheetApp.getActive();
const timezone = ss.getSpreadsheetTimeZone();
const dates = ss.getRange('Sheet1!G10:G')
.getValues()
.flat()
.filter(String)
.map(date =>
Object.prototype.toString.call(date) === '[object Date]'
? Utilities.formatDate(date, timezone, 'dd/MM/yyyy HH:mm')
: date
);
console.log(dates);
}
Convert String to Date:
function convert(s="25/01/2022 11:00") {
let [d,m,y,hr,mn] = s.split(/[\/ :]/)
Logger.log('y: %s m: %s d: %s hr: %s mn: %s',y,m,d,hr,mn);
Logger.log(new Date(y,m - 1,d,hr,mn));
}
Execution log
10:58:11 AM Notice Execution started
10:58:12 AM Info y: 2022 m: 01 d: 25 hr: 11 mn: 00
10:58:12 AM Info Tue Jan 25 11:00:00 GMT-07:00 2022
10:58:13 AM Notice Execution completed
To convert a string to a Date object in Google Apps Script use Utilities.parseDate.
Example:
function myFunction(){
const spreadsheet = SpreadsheetApp.getActiveSpreadsheet();
const timeZone = spreadsheet.getSpreadsheetTimeZone();
const date = Utilities.parseDate('25/01/2022 11:00',timeZone, 'dd/MM/yyyy HH:mm');
return date;
}
Using the above as a custom function might not make sense for some use cases since the same result might be achieved by using built-in functions which are more efficient and less prone to have problems.
The options for using built-in functions depends on the spreadsheet settings, i.e. DATEVALUE might return different results for ambiguos dates like 25/01/2022 as for certain regions the month goes first and for others the day of the month goes first.
=DATEVALUE("25/01/2022") works correctly when the spreasheet region is set to Italy. You might have to manually set the cell formatting to date in order to make it show a date instead of the time serialized value (a number).
To convert 25/01/2022 11:00 using formulas in the above spreadsheet, use
=INDEX(SPLIT("25/01/2022 11:00";" ");1) + SUBSTITUTE(INDEX(SPLIT("25/01/2022 11:00";" ");2);":";".")
The above formula has two main parts joined by using +. The first part returns the time serialized value corresponding to the date, the second part returns the time serialized value corresponding to the time.
Array formula
=ArrayFormula(DATEVALUE(REGEXEXTRACT(F10:F;"^([ˆ\d/]+) "))+TIMEVALUE(SUBSTITUTE(REGEXEXTRACT(F10:F;" ([ˆ\d/:]+)$");":";".")))
The same concept as the previous formula, but instead of INDEX it uses REGEXEXTRACT.
Google has a Utility to do just that!
let dateTime = '2022-12-16 13:00:00';
let timeZone = 'GMT';
let convertedDateTime = Utilities.formatDate(dateTime, timeZone, 'dd/MM/yyyy HH:ss')
check out Class Utilities for more info.

Google sheet custom function returns undefined Values sometimes

I have wrote a function to return latest weekly closing price of a stock in Google Sheet Apps Script.
But when using this in Google Sheets some cells are getting undefined data.
But the same cells are getting values correclty sometimes. Cant understand whats the problem here.
Also is there any option to debug my code when I execute the function from googlesheet cell?
function getWeeklyClosing(stockName){
var date =new Date()
var endDate = Utilities.formatDate(new Date(), "GMT+1", "yyyy/MM/dd")
var startDate = Utilities.formatDate(new Date(date.getTime()-10*(24*3600*1000)), "GMT+1", "yyyy/MM/dd")
var url ='https://www.quandl.com/api/v3/datasets/BSE/BOM'+stockName+'?start_date='+startDate+'&end_date='+endDate+'&collapse=weekly&api_key=3VCT1cPxzV5J4eGFwfvz';
var options =
{
'muteHttpExceptions': true,
"contentType" : "application/x-www-form-urlencoded",
"headers":{"Accept":"application/json"}
};
var response = JSON.parse(UrlFetchApp.fetch(url, options))
var weeklyEma=response.dataset.data[0][4];
return weeklyEma;
}
This answer corresponds to question rev 3 which had as title "How to use Promise in appscript?"
The title of the question is asking about an attempted solution rather than the actual problem ( an X-Y problem). The assumption that UrlFetchAp.fetch is asynchrous is wrong ( See Is google apps script synchronous?); the actual problem is getting undefined values on certain cells.
The solution will depend on the what you want to do when the the fetch response is
causing the undefined values. One alternative is to replace the undefined values by "" (an empty string) before sending the values to the spreadsheet that will cause having an empty cell on Google Sheets.
By the other hand, it could be that the API you are querying is not returning the JSON that you think, so first you have to understand it and then set the rules about how to send the result to the spreadsheet as not always it's possible to transform a JSON into a simple table structure.
UrlFetchApp.fetch(url) returns a HTTP response object. This response include a HTTP response code that could 200 for a successful fetch but could return other codes due to multiple reasons.
Some people in the past have suggest the use an algorithm called exponential back-off, like in this case Error message: "Cannot connect to Gmail". Be careful to not exceed the 30 seconds execution time limit.
Related
RESTful HTTP response codes

Weird inconsistencies in writing a date into Google sheets

I'm seeing weird inconsistencies and discrepancies when writing constructed dates into google sheets, they do not always transform into date object once the sheet values are retrieved at a later time.
By constructed dates I mean they are created by sticking together smaller day, month and time strings into a single string "01/02/1991 00:00:00". They are then written into the sheet using .setValues(). And then after work is done, I retrieve the array using .getValues() to only find that once in a while, those dates are retrieved as string values, with the vast majority being date objects.
Originally, I had not padded out the values, but now I add (inside the string) leading 0s. This has fixed issues in some placed but not all places.
Here are some excerpts of how I create the array that gets written eventually using .setValues
var date = data_range[i][header_date].toString()
var day = parseInt(date.slice(6,8),10)
var month = parseInt(date.slice(4,6),10)
var year = parseInt(date.slice(0,4),10)
organised_data[organised_data_index].data = organised_data[organised_data_index].data.concat(data_range[i].slice(header_data, header_data+int_entries))
var datetime_array = []
for (var k = 0; k< minutes_array.length; k++){
datetime_array[k] = pad(day,2) + "/" + pad(month,2) + "/"+ year + " " + pad(parseInt(minutes_array[k]/60),2)+ ":" + pad((minutes_array[k] % 60),2) + ":00"
}
organised_data[organised_data_index].datestamp = organised_data[organised_data_index].datestamp.concat(datetime_array)
Here is what I'm seeing in the debug:
https://i.imgur.com/OTT296s.png
Here is what I'm seeing in the sheet itself:
https://i.imgur.com/hDvqGP3.png
Has anyone else faced these issues, am I doing something wrong?
So for anyone asking why I've written my dates like this, it's because the script will be passed around between countries, and I've noticed that the localisation changes and therefore using date objects inside the javascript gets really screwey. I've opted to create the string itself since the data is time zone agnostic. Could we treat that side of my problem as a constraint?
Thanks guys, this is killing me.
I don't actually see your inconsistencies I wish I could see all of your code.
Another problem with Date objects is that Passing Date() objects with google.script.run is not allowed and will cause the entire object to loose it's data. For that reason I have used a function like this to produce date strings that I can easily pass and then be used as constructors for dates on the server. On some projects I like to eliminate the timezone altogether and just assume that the time zone of the spreadsheet is the same as the WebApp user even though it often is not.
function formatDateTime(dt){
if(dt && Object.prototype.toString.call(dt) === '[object Date]'){
var M=dt.getMonth()+1;
var d=dt.getDate();
var h=dt.getHours();
var m=dt.getMinutes();
var s=dt.getSeconds();
var MM=(M<10)?String('0'+M):String(M);
var dd=(d<10)?String('0'+d):String(d);
var hh=(h<10)?String('0'+h):String(h);
var mm=(m<10)?String('0'+m):String(m);
var ss=(s<10)?String('0'+s):String(s);
var ds=dt.getFullYear() + '-' + MM + '-' + dd + 'T' + hh + ':' + mm;
return ds;
}else{
throw("Invalid Parameter Type in formatDateTime Code.gs. Parameter is not a date.");
}
}
Reference
The automatic conversion of strings to Date objects in Sheets has been a source of several bugs for me as well (not your particular bug though!).
Since you are already writing helper functions to translate between strings and Dates, I suggest you eliminate the Date objects in your Sheet entirely by using Range.setNumberFormat("#") to always treat the Dates as strings.

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.

Number object from http request returns date type or similar

I make a Facebook API call in Google scripts to get the share count for a URL. It appears that the number (e.g. 31) is being found correctly, but when I pass it to Sheets, it shows e.g. 30/01/1900 in the sheets box.
My appScript code is:
function getShareCount(url) {
var url = "https://any.org/111";
var inputurl = "https://graph.facebook.com/v2.1/?id=" + url + "&access_token=XXXXXXXX";
var response = UrlFetchApp.fetch(inputurl);
var response = JSON.parse(response.getContentText());
var response = response.share.share_count;
Utilities.sleep(500);
return response;
}
and the spreadsheet box has: "=getShareCount(B2)"
If I purposefully break the code and run the debugger in script Apps, I can see that script apps is getting a response with Number: 31. If I change to e.g. "response.id", the URL is returned into sheets as expected. The same with other parts of the object. Those are strings, and this is a number. I can't work out what sort of object sheets is receiving, nor what method I can use to simply show the number `311.
Any ideas? Thanks!
It seems that your cell has a custom format of Date. Select the cells you want to format and click Format > Number > Automatic