Weird inconsistencies in writing a date into Google sheets - google-apps-script

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.

Related

Google Script Forcing Date Format [duplicate]

I'm trying to get from a time formatted Cell (hh:mm:ss) the hour value, the values can be bigger 24:00:00 for example 20000:00:00 should give 20000:
Table:
if your read the Value of E1:
var total = sheet.getRange("E1").getValue();
Logger.log(total);
The result is:
Sat Apr 12 07:09:21 GMT+00:09 1902
Now I've tried to convert it to a Date object and get the Unix time stamp of it:
var date = new Date(total);
var milsec = date.getTime();
Logger.log(Utilities.formatString("%11.6f",milsec));
var hours = milsec / 1000 / 60 / 60;
Logger.log(hours)
1374127872020.000000
381702.1866722222
The question is how to get the correct value of 20000 ?
Expanding on what Serge did, I wrote some functions that should be a bit easier to read and take into account timezone differences between the spreadsheet and the script.
function getValueAsSeconds(range) {
var value = range.getValue();
// Get the date value in the spreadsheet's timezone.
var spreadsheetTimezone = range.getSheet().getParent().getSpreadsheetTimeZone();
var dateString = Utilities.formatDate(value, spreadsheetTimezone,
'EEE, d MMM yyyy HH:mm:ss');
var date = new Date(dateString);
// Initialize the date of the epoch.
var epoch = new Date('Dec 30, 1899 00:00:00');
// Calculate the number of milliseconds between the epoch and the value.
var diff = date.getTime() - epoch.getTime();
// Convert the milliseconds to seconds and return.
return Math.round(diff / 1000);
}
function getValueAsMinutes(range) {
return getValueAsSeconds(range) / 60;
}
function getValueAsHours(range) {
return getValueAsMinutes(range) / 60;
}
You can use these functions like so:
var range = SpreadsheetApp.getActiveSheet().getRange('A1');
Logger.log(getValueAsHours(range));
Needless to say, this is a lot of work to get the number of hours from a range. Please star Issue 402 which is a feature request to have the ability to get the literal string value from a cell.
There are two new functions getDisplayValue() and getDisplayValues() that returns the datetime or anything exactly the way it looks to you on a Spreadsheet. Check out the documentation here
The value you see (Sat Apr 12 07:09:21 GMT+00:09 1902) is the equivalent date in Javascript standard time that is 20000 hours later than ref date.
you should simply remove the spreadsheet reference value from your result to get what you want.
This code does the trick :
function getHours(){
var sh = SpreadsheetApp.getActiveSpreadsheet();
var cellValue = sh.getRange('E1').getValue();
var eqDate = new Date(cellValue);// this is the date object corresponding to your cell value in JS standard
Logger.log('Cell Date in JS format '+eqDate)
Logger.log('ref date in JS '+new Date(0,0,0,0,0,0));
var testOnZero = eqDate.getTime();Logger.log('Use this with a cell value = 0 to check the value to use in the next line of code '+testOnZero);
var hours = (eqDate.getTime()+ 2.2091616E12 )/3600000 ; // getTime retrieves the value in milliseconds, 2.2091616E12 is the difference between javascript ref and spreadsheet ref.
Logger.log('Value in hours with offset correction : '+hours); // show result in hours (obtained by dividing by 3600000)
}
note : this code gets only hours , if your going to have minutes and/or seconds then it should be developped to handle that too... let us know if you need it.
EDIT : a word of explanation...
Spreadsheets use a reference date of 12/30/1899 while Javascript is using 01/01/1970, that means there is a difference of 25568 days between both references. All this assuming we use the same time zone in both systems. When we convert a date value in a spreadsheet to a javascript date object the GAS engine automatically adds the difference to keep consistency between dates.
In this case we don't want to know the real date of something but rather an absolute hours value, ie a "duration", so we need to remove the 25568 day offset. This is done using the getTime() method that returns milliseconds counted from the JS reference date, the only thing we have to know is the value in milliseconds of the spreadsheet reference date and substract this value from the actual date object. Then a bit of maths to get hours instead of milliseconds and we're done.
I know this seems a bit complicated and I'm not sure my attempt to explain will really clarify the question but it's always worth trying isn't it ?
Anyway the result is what we needed as long as (as stated in the comments) one adjust the offset value according to the time zone settings of the spreadsheet. It would of course be possible to let the script handle that automatically but it would have make the script more complex, not sure it's really necessary.
For simple spreadsheets you may be able to change your spreadsheet timezone to GMT without daylight saving and use this short conversion function:
function durationToSeconds(value) {
var timezoneName = SpreadsheetApp.getActive().getSpreadsheetTimeZone();
if (timezoneName != "Etc/GMT") {
throw new Error("Timezone must be GMT to handle time durations, found " + timezoneName);
}
return (Number(value) + 2209161600000) / 1000;
}
Eric Koleda's answer is in many ways more general. I wrote this while trying to understand how it handles the corner cases with the spreadsheet timezone, browser timezone and the timezone changes in 1900 in Alaska and Stockholm.
Make a cell somewhere with a duration value of "00:00:00". This cell will be used as a reference. Could be a hidden cell, or a cell in a different sheet with config values. E.g. as below:
then write a function with two parameters - 1) value you want to process, and 2) reference value of "00:00:00". E.g.:
function gethours(val, ref) {
let dv = new Date(val)
let dr = new Date(ref)
return (dv.getTime() - dr.getTime())/(1000*60*60)
}
Since whatever Sheets are doing with the Duration type is exactly the same for both, we can now convert them to Dates and subtract, which gives correct value. In the code example above I used .getTime() which gives number of milliseconds since Jan 1, 1970, ... .
If we tried to compute what is exactly happening to the value, and make corrections, code gets too complicated.
One caveat: if the number of hours is very large say 200,000:00:00 there is substantial fractional value showing up since days/years are not exactly 24hrs/365days (? speculating here). Specifically, 200000:00:00 gives 200,000.16 as a result.

Using Row() within a formula with appendRow

So I'm pulling some data from GMail and adding a new row to a sheet that has a specific format. Name, Address, etc etc
On Column "P" I want to replicate the below:
=IF(NOT(ISBLANK($J3985)),"Replied", IF((TODAY()>=$O3985),"Late", "OK"))
However, I want to replace 3985 with Row(), for the row number that I'm appending, while I'm appending it. I've tried playing with: ADDRESS(row(),10) but this returns a string value that I can't seem to re-insert into a formula in a manner that works.
What I'm passing through in appendRow now:
var replied = "";
var later = x // a Date that's today + 6 weeks
var checkResult = `=IF(NOT(ISBLANK(` + replied + `)), "Replied", IF((TODAY()>=` + later + `), "Late", "OK"))`;
I want it so that I can populate the "responded" cell at a later point in the sheet and for this to still work. Would be keen to hear your suggestions for the same.
If you use appendRow:
=IF(NOT(ISBLANK(INDIRECT("RC[-6]",FALSE))),"Replied", IF((TODAY()>=INDIRECT("RC[-1]",FALSE)),"Late", "OK"))
If you use setFormulaR1C1:
Method A
Putting the row number directly with template literal
Method B
You could use setFormulaR1C1(formula)
'=IF(NOT(ISBLANK(RC[-6])), "Replied", IF((TODAY()>=RC[-1]), "Late", "OK"))';

API indexedDB- Showing date in format DD.MM.YYYY

I have an html with a form where I colect data type: text, radio and date.
When I pick up the date, I can see the format is dd.mm.yyyy.
Then I went to see the way the date is save and in the BD I see:
Object
date
:
"2017-09-12"
Now my question is, is there a way to show in the html the date in the format dd-mm-YYYY or to save the date in dd-mm-yyyy instead.
Thank you so much in advance
Marcela
You can store date objects in indexedDB. A date object is not concerned with how it is displayed, and does not provide or store properties related to a date's format.
If you are viewing the set of objects in an object store using a browser's devtools panel, date values are displayed according to some built-in setting in devtools. The way dates are displayed in a devtools view of an object store does not reflect how dates are actually stored. Dates are actually stored as simple numbers. There is a big difference between how a value is stored and how it is displayed.
Therefore, so long as you property store dates as dates (not as strings that look like dates), then you can choose how to display a date after retrieving it from indexedDB. You make this decision outside of using indexedDB, because this is not a concern of indexedDB. indexedDB is only concerned with storing values.
So, when writing code:
1) When saving an object with a date property to indexedDB, ensure the date property is of type date.
2) When getting an object from indexedDB, get its date property, and then convert the date property into a string in a format that you prefer.
Here is some really ugly pseudo-code to help you:
// This function accepts a date value as input, and then returns a
// string representing the formatted date
function formatDate(dateValue) {
// Get the parts of the date
var dayOfMonthNumber = dateValue.getDate();
// The +1 is because January is 0, but we want it to be 1
var monthNumber = dateValue.getMonth() + 1;
var yearNumber = dateValue.getYear();
// Compose a string of the parts
var dateString = '';
dateString += dayOfMonthNumber;
dateString += '-';
dateString += monthNumber;
dateString += '-';
dateString += yearNumber;
return dateString;
}
myObject.myDateProperty = new Date('1.2.3');
saveObjectInIndexedDB(myObject);
var myObject = getObjectFromIndexedDB();
var myDatePropertyAsADate = myObject.myDateProperty;
var myFormattedDateString = formatDate(myDatePropertyAsDate);
var myElement = document.getElementById('myElement');
myElement.textContent = myFormattedDateString;

Apps script, spreadsheet and setNumberFormat

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.

How do I parse this back to a mm/dd/yyyy format?

I have some dates in a Google Spreadsheet that I'm bringing in to a script like this:
var JCstartDateFix = Math.floor(Date.parse(JCstartDate) / 86400000) + 25570;
var todaysDateFix = Math.floor(Date.parse(todaysDate) / 86400000) + 25570;
How do I do the opposite of this at the end of the script to change it back into a mm/dd/yyyy formatted date?
Thanks in advance for any help on this.
Here's the whole script:
function projectedDate(JCstartDate, overallPercent, pace, todaysDate, HSstartDate, DaysInHS) {
//converts dates to a number of days
var JCstartDateFix = Math.floor(Date.parse(JCstartDate) / 86400000) + 25570;
var todaysDateFix = Math.floor(Date.parse(todaysDate) / 86400000) + 25570;
//This says that there's no projected date since the student hasn't started high school yet
if(HSstartDate == ""){
return "HS not started";
}
//This calculates grad date if the student's been here more than 8 months or if their percent is over 80.
else if(DaysInHS >= 200 || overallPercent >=80){
var percentPerDay = overallPercent/(DaysInHS);
var daysLeft = (100 - overallPercent) / percentPerDay;
if((todaysDateFix + daysLeft) > (JCstartDateFix +730)){
return "You are not on track to complete.";
}
else{
return (todaysDateFix + daysLeft);
}
}
//This calculates grad date if the student's been at JC less than 8 months
else{
if(JCstartDateFix + 600 - pace > JCstartDateFix + 730){
return "You are not on track to complete.";
}
else{
return (JCstartDateFix+600-pace);
}
}
}
I work in a school where students start at different times and work at their own pace. They have a 2 year limit to finish. So this script estimates their graduation date based on when they started and how fast they're going. It uses different formulas depending on how long they've been here. I'm happy with the dates I get on my spreadsheet, but if I format them from the spreadsheet, another script doesn't correctly pick up the text strings and gives a date in 1969 instead.
I think what I need to do is change the lines that return numbers so that those numbers are formatted as dates. I just don't know how. Thanks again!
The value you get with Date.parse() is in milliseconds, you divide it by the number of milliseconds in a day so I guess you obtain the number of days since the JS reference date, rounded to the lowest integer and then add a constant value of 25570.
What is the result supposed to be ?
It seems that it should be a number of day from the ref date but that's quite far in the future !! (about 70 years) Is this right ? could you clarify ?
Anyway, what you should do is to get a value in milliseconds again and use new Date(value in mSec) to get a date object. From there Utilities.formatDate will allow you to get any display format you want.
ref : http://www.w3schools.com/jsref/jsref_obj_date.asp
https://developers.google.com/apps-script/class_utilities#formatDate
As long as the value that you're setting in the spreadsheet is a Date object in apps script, it will appear as a date. The format will be under the control of the spreadsheet, of course, but it defaults to mm/dd/yyyy.
For example, you could just change your existing code to render Date objects. Then, when you call setValue() you will write a date out to the spreadsheet.
var JCstartDateFix = new Date(Math.floor((Date.parse(JCstartDate) / 86400000) + 25570)*86400000);
var todaysDateFix = new Date(Math.floor((Date.parse(todaysDate) / 86400000) + 25570)*86400000);