Time Arithmetic Calculation Error - google-apps-script

I have the following code on a gsheet -
var ss = SpreadsheetApp.getActiveSpreadsheet().getActiveSheet();
var task = ss.getRange(1,2).getValue(); //ss.getRange("A2").getValue();
var date = new Date();
function onEdit(e) {
if (e.range.getA1Notation() == 'B2') {
if (/^\w+$/.test(e.value)) {
// console.log(e.value);
eval(e.value)();
e.range.clear();
}
}
}
function Start() {
var last = ss.getLastRow();
ss.getRange(last+1, 1).setValue(task);
ss.getRange(last+1, 2).setValue(date);
}
function End() {
var last = ss.getLastRow();
ss.getRange(last, 3).setValue(date);
var endTime = ss.getRange(last, 3).getValue();
var startTime = ss.getRange(last, 2).getValue();
ss.getRange(last, 4).setValue(endTime-startTime);
}
Whenever the cell in B2 is edited, it runs one of the validated names of the functions - either Start or End.
If 'start', it puts the value of cell b1 (validated list of task names) in first column, and current time in MM/DD/YYYY HH:MM:SS format.
If 'end', its the current time in MM/DD/YYYY HH:MM:SS format in the third column, and attempts to put the difference between the "start" time and "end" time in the fourth column, or a calculation.
Here's the error (or my lack of understanding how gsheets works)
The code produces the following:
Task| Start | End | Duration
DOS | 8/2/2017 16:44:28 | 8/2/2017 16:44:31 | 2,418.00
Question - what is the 2,418? The total duration should be 2 seconds, or 00:00:02. Given the above code, is it a code issue or a format of the cell issue?
when I put in the cell d2 = c2-b2, it works fine as long as column is formatted as a duration. But I'd rather not do it, as the inserting or start/end times is dependent upon the last row - so if I copy/paste the formula down to the bottom on the gsheet the data will not be continuous.

JavaScript timestamps are in milliseconds. Divide by 1000 to get seconds.
Also, using eval like you do seems a pretty bad idea. I would use a switch statement there, and display an error for the user if the entered function is not one of options. As is, your code silently fails if they enter "stat" instead of "start".

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.

How to create Work Shifts clock AppScript

I am learning right now scripts functionally in Google Sheet, however, can't twist my head around constructing a very simple App script.
I have the following table (Snoopi Tab)
https://docs.google.com/spreadsheets/d/1l6nYBAqB1GWoMkIOwlykhiuMpaXdWHTo7UhgZdq6hT8/edit?usp=sharing
I want it to do this simple action:
EXAMPLE: If today is not Sunday or Saturday and the date is 14.2.14 and cell BF5 is
---> go down 3 rows and paste current time "Clocking in" working-shift
When button "IN" is clicked:
If (TODAYDATE = Value in cell in row 5) & (row 3 ==!"S") both true
Set current time in (same column just row 8)
Same with "OUT" button, but this I'll try to figure by myself.
The other answer is acceptable, but is very resource intensive and have a lot of loops to do resulting to very slow execution time especially when it gets later on the year since it will loop all those dates.
Also, the run you did on the other answer did finish successfully but didn't write anything due to it missing the actual date value. This might have been caused by a timezone issue, or by only modifying the actual date while getting the raw time of the cell value.
A better alternative would be to make use of the 4th row where it contains x value when the date is equal to the current date. By using that, you wouldn't need to loop thus resulting in faster execution time and wouldn't need to convert time thus making it safer. As long as row 4 is populated on all columns (which your problem is), there should be no issue of using this script.
Script:
function WorkClock() {
var currentDate = new Date();
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheet = SpreadsheetApp.getActiveSheet();
// you only need 3rd and 4th row of data
var data = sheet.getRange("E3:NE4").getValues();
// 4th row contains 'x' when today matches the column, find that index
var indexToday = data[1].indexOf('x');
// if that column's row 3 is not 'S'
if(data[0][indexToday] != 'S')
// write the time on row 8
sheet.getRange(8, indexToday + 5).setValue(Utilities.formatDate(currentDate, ss.getSpreadsheetTimeZone(), 'HH:mm'));
}
Output:
Note:
Timezone used is based on the spreadsheet's timezone which is GMT-8. Wherever the user is, it will use GMT-8, not its local time which should be helpful in some cases.
Performance difference between this and looping all dates would be vast if we are now dealing with the later months of the year (e.g. November, December)
For the OUT button, create another function by duplicating the current function. Then replace where you write the time. Instead of row 8 (Start), write it in row 10 (Finish).
function myFunction() {
var actualDate = new Date(new Date().setHours(0, 0, 0, 0)).getTime();
var sheet = SpreadsheetApp.getActiveSheet();
var data = sheet.getRange("E3:NE6").getValues();
for(var i = 0; i < data[0].length; i++) {
if (data[2][i].getTime() === actualDate) {
if (data[0][i] !== "S") {
sheet.getRange(8, (5+i)).setValue(new Date().getHours() + ":" + new Date().getMinutes());
}
break;
}
}
}

Google Spreadsheet Makro - Add one time value to another

var valueDate = Utilities.formatDate(sheet.getRange("D2").getValue(), SpreadsheetApp.getActive().getSpreadsheetTimeZone(), "dd/MM/YYYY hh:mm:ss");
var valueResetCD = Utilities.formatDate(sheet.getRange("C2").getValue(), SpreadsheetApp.getActive().getSpreadsheetTimeZone(), "hh:mm:ss");
This are my 2 lines I currently have - As the Spreadsheet timezone may be different from the makro timezone I got told to always get the timezone from the Sheet.
In those 2 values I have the right values safed (tried to print them into a field)
first is 19/09/2020 22:34:52 (a specific date) and the second is 03:00:00.000 (3 Hours)
Now what I want to do is just add those 2 times together so I get as output: 20/09/2020 01:34:52
I tried it with: sheet.getRange("Data!$A$21").setValue(valueDate + valueResetCD) but I guess it is not that easy - it just copies the 2 string values of the times into one line.
How can I add those 2 Time / Date values to eachother so I get the output above
I believe your situation and goal as follows.
In your situation, as a sample value,
valueDate is 19/09/2020 22:34:52 retrieved from sheet.getRange("D2").getValue().
valueResetCD is 03:00:00.000 retrieved from sheet.getRange("C2").getValue().
Both values are the date object.
You want to add valueResetCD to valueDate.
When the sample values are used, you want to get 20/09/2020 01:34:52 as the result value.
Modification points:
In this case, I think that valueDate is the correct date time like 19/09/2020 22:34:52 as the date object.
But, I think that valueResetCD is 1899/12/30 03:00:00. Because the Spreadsheet uses the serial number.
From this situation, it is required to convert the serial number to unix time.
And also, in this case, the time difference is required to be considered.
Both values are added by converting from the date object to the unix time. And, the result value is converted to the date object.
When above points are reflected to the Google Apps Script, it becomes as follows.
Modified script:
function myFunction() {
var sheetName = "Sheet1"; // Please set the sheet name.
// 1. Retrieve values from the cells as the date object.
var sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName(sheetName);
var valueDate = sheet.getRange("D2").getValue(); // As a sample value, it's `19/09/2020 22:34:52`.
var valueResetCD = sheet.getRange("C2").getValue(); // As a sample value, it's `03:00:00.000`.
// 2. Convert the date object to the unix time.
var valueDateUnix = valueDate.getTime();
var differenceBetweenSerialAndUnix = new Date("1970/01/01 00:00:00").getTime() - new Date("1899/12/30 00:00:00").getTime();
var timeDifference = (new Date().getTimezoneOffset()) * 60 * 1000;
var valueResetCDUnix = valueResetCD.getTime() + differenceBetweenSerialAndUnix - timeDifference;
// 3. Add both time and convert it to the date object.
var resultDateObject = new Date(valueDateUnix + valueResetCDUnix);
var resultString = Utilities.formatDate(resultDateObject, SpreadsheetApp.getActive().getSpreadsheetTimeZone(), "dd/MM/YYYY hh:mm:ss");
console.log(resultDateObject)
console.log(resultString) // You can see "20/09/2020 01:34:52" at the log.
// 4. Put the value to the cell.
sheet.getRange("A1").setValue(resultDateObject);
}
Note:
In this answer, it supposes that the values from the cells "C2" and "D2" are the date object. If those values are the string which is not the date object, above script cannot be used. So please be careful this.
References:
Date
getTime()
getTimezoneOffset()

setting the time delta format in google sheet

I have a google sheet form response data where column B is a time duration value. But it shows time in AM or PM. I tried different app script but still unable to get the value a time Delta( difference) format
The form response data itself gives time in AM or PM. Help me in this matter.
This is my latest code.
function timeFormat() {
var cell = SpreadsheetApp.getActive().getSheetByName('Sheet4').getRange('B1:B');
cell.setNumberFormat('hh:mm');
}
Is your issue basically with the AM / PM part popping up when you click into the cells formatted as 'hh:mm'? You can fix that by applying the duration format for hours to the entire range (e.g. B2:B)
GAS:
range.setNumberFormat('[hh]:mm');
Or manually under Format -> Number -> More Formats -> Custom number format (a lot easier)
And here's how the end result looks like. Note that you get the 'AM/PM' part popping up when you click into cells formatted as 'hh:mm' but not with the ones formatted as [hh]:mm:
You could use this function as a cell function and then format the cells as a duration and you will get hours:minutes:seconds.
function days(Start,End) {
if(Start && End)
{
var second=1000;
var minute=60*second;
var hour=minute*60;
var day=hour*24;
var t1=new Date(Start).valueOf();
var t2=new Date(End).valueOf();
var d=t2-t1;
return d/day;
}
else
{
return 'Invalid Inputs';
}
}
This essentially returns the date number in the form that the spreadsheet understands as days and fraction of days.

Calculating runtime minus Timestamp

I have a form which activates a procedure via an "On form submit" trigger. At the end of this routine I want to insert the difference in time between the form's Timestamp and the current time at the end of the routine (the difference of which is only a matter of a few seconds).
I've tried many things so far, but the result I typically receive is NaN.
I thought that my best bet would be to construct the runtime elements (H,M,S) and similarly deconstruct the time elements from the entire Timestamp, and then perform a bit of math on that:
var rt_ts = Math.abs(run_time - ts_time);
(btw, I got that formula from somewhere on this site, but I'm obviously grasping at anything at this point. I just can't seem to find a thread where my particular issue is addressed)
I've always found that dealing with dates and time in Javascript is tricky business (ex: the quirk that "month" start at zero while "date" starts at 1. That's unnecessarily mind-bending).
Would anyone care to lead me out of my current "grasping" mindset and guide me towards something resembling a logical approach?
You can simply add this at the top of your onFormSubmit routine :
UserProperties.setProperty('start',new Date().getTime().toString())
and this at the end that will show you the duration in millisecs.
var duration = new Date().getTime()-Number(UserProperties.getProperty('start'))
EDIT following your comment :
the time stamp coming from an onFormSubmit event is the first element of the array returned by e.values see docs here
so I don't really understand what problem you have ??
something like this below should work
var duration = new Date().getTime() - new Date(e.values[0]).getTime();//in millisecs
the value being a string I pass it it 'new Date' to make it a date object again. You can easily check that using the logger like this :
Logger.log(new Date(e.values[0]));//
It will return a complete date value in the form Fri Mar 12 15:00:00 GMT+01:00 2013
But the values will most probably be the same as in my first suggestion since the TimeStamp is the moment when the function is triggered...
I have a function which can show the times in a ss with timestamps in column A. It will also add the time of the script itself to the first timestamp (in row 3) and show this in the Log.
Notice that the google spreadsheet timestamp has a resolution in seconds and the script timestamp in milliseconds. So if you only add, say, 300 milliseconds to a spreadsheet timestamp, it might not show any difference at all if posted back to a spreadsheet. The script below only takes about 40 milliseconds to run, so I have added a Utilities.sleep(0) where you can change the value 0 to above 1000 to show a difference.
function testTime(){
var start = new Date().getTime();
var values = SpreadsheetApp.getActiveSheet().getDataRange().getValues();
for(var i = 2; i < values.length ; i++){
Logger.log(Utilities.formatDate(new Date(values[i][0]),Session.getTimeZone(),'d MMM yy hh:mm:ss' )); // this shows the date, in my case same as the ss timestamp.
Logger.log( new Date(values[i][0]).getTime() ); // this is the date in Milliseconds after 1 Jan 1970
}
Utilities.sleep(0); //you can vary this to see the effects
var endTime = new Date();
var msCumulative = (endTime.getTime() - start);
Logger.log(msCumulative);
var msTot = (msCumulative + new Date(values[2][0]).getTime());
Logger.log('script length in milliseconds ' + msTot );
var finalTime = Utilities.formatDate(new Date(msTot), Session.getTimeZone(), 'dd/MM/yyyy hh:mm:ss');
Logger.log ( finalTime); //Note that unless you change above to Utilities.sleep(1000) or greater number , this logged date/time is going to be identical to the first timestamp since the script runs so quickly, less than 1 second.
}