0

I wrote a basic function which was intended to grab a timestamp, for logging purposes, for when each visitor hits the page. I actually want to record the time in two ways; the local time (so I can see the distribution of visits based on the users' times) as well as the UTC time (so I can see the distribution of visits from a global, normalised perspective)

var currentTime = new Date();
var timestamp = splitTimestamp(currentTime);

function splitTimestamp(timestamp) {
    var splitTimestamp = {};
    splitTimestamp.local = new Date(timestamp.getFullYear(), timestamp.getMonth(), timestamp.getDate(), timestamp.getHours(), timestamp.getMinutes(), timestamp.getSeconds()).toISOString();
    splitTimestamp.UTC = new Date(timestamp.getUTCFullYear(), timestamp.getUTCMonth(), timestamp.getUTCDate(), timestamp.getUTCHours(), timestamp.getUTCMinutes(), timestamp.getUTCSeconds()).toISOString();
    return splitTimestamp;
}

The idea being that I can just refer to each via timestamp.local or timestamp.UTC as necessary.

I had assumed that the local time derived via new Date() would just be the local time as per the browser, i.e. as per the user's local system / OS. However, I have seen a number of records which seem to contradict that. For example, I found a record from a user in New York (UTC+4) where the time had not occurred yet (i.e. it was 11am EST / 3pm UTC when I saw the record but it was showing as 2pm EST / 6pm UTC in the log)

Now - initially, I accounted for this by saying the user may simply be using system settings that don't necessarily align with their physical location (i.e. they were in New York but, for reasons known only to themselves, they keep their system / OS on UTC time) And there's nothing I can do about that, I am comfortable with that.

But... If that were the case, then the local time and the UTC time would be the same? And... they're not. So... that can't be what's happening?

Is there something obvious I'm overlooking here?

Alan O'Brien
  • 151
  • 1
  • 13
  • Yes, the user's clock is off. The user may be in New York but have their local regional settings set to another region. Can you save the user's region in storage and calculate based on where they assigned themselves? – Ross Bush Sep 08 '21 at 15:00
  • toISOString is **always** in UTC. that z at the end of the string represents the UTC time. What exactly are you trying to do? – I wrestled a bear once. Sep 08 '21 at 15:12
  • timestamps in general are always UTC... why not just save the timestamp and then also save the UTC offset, that way you can calculate one from the other.. – I wrestled a bear once. Sep 08 '21 at 15:13
  • I'm trying to populate two date variables. One representing the local timezone. One representing the UTC timezone. I hadn't realised that toISOString was inherently UTC - but, reviewing the code, does it actually matter? I'm just using it so that I get the timestamp in a format that can be recognised by the log (SharePoint) – Alan O'Brien Sep 08 '21 at 15:19
  • @Iwrestledabearonce.—[*timestamps*](https://en.wikipedia.org/wiki/Timestamp) are not "*in general … always UTC*". A timestamp is any sequence of characters that identifies a moment in time and existed before UTC. – RobG Sep 08 '21 at 20:35
  • @RobG - That's why I said "In general" instead of always. The inclusion of a timezone in a timestamp indicates location rather than time, which is unnessecary information when it comes to representing a point in time rather than a location on earth. Time stamps are almost always, at the lowest levels, represented in UTC. – I wrestled a bear once. Sep 08 '21 at 20:52
  • @Iwrestledabearonce.—"in general" in mathematics and science means "in all cases", hence "general relativity". Colloquially it means almost always. Timestamps are not almost always UTC, they can be anything. "21 Feb 2022" is a timestamp. A timezone doesn't infer location beyond a very rough longitudinal position. IANA representative locations infer a very approximate location for offset rules only, e.g. Asia/Shanghai represents the entire region of mainland China. But a computer set to Asia/Shanghai might be anywhere in the world. – RobG Sep 09 '21 at 05:23
  • @RobG - The idea that "general" means "in all cases" is blatently false. Period. In computers all timestamps are stored numerically on a low level, eg, Unix timestamps, which are UTC. Timestamps that include location info are ALWAYS stored in memory as a UTC timestamp, and optionally, an offset. That said, I'd be willing to bet that Unix timestamps are the most widely used anyway and they're always UTC. So yeah, in general, Timestamps are UTC. Feel free to continue with the semantic gymnastics if you want, that is all I have to say on the topic. – I wrestled a bear once. Sep 09 '21 at 12:31

2 Answers2

1

If you just want a time string that is in that format, you can duplicate the date object using new Date(date) (no reason to put the year, month, date, etc), and then subtract the offset and get the ISO formatted string. You should then remove the 'z' at the end of the string to indicate that it's the local timezone rather than a UTC timezone.

var date = new Date();
var utc_time = date.toISOString();

var offset_date = new Date(date);
offset_date.setMinutes(date.getMinutes()-date.getTimezoneOffset());
var local_time = offset_date.toISOString().slice(0, -1);

console.log(utc_time);
console.log(local_time);

You may also be interested in looking at my ProtoDate library. I built it to handle stuff like this.

I wrestled a bear once.
  • 22,983
  • 19
  • 69
  • 116
  • This is excellent - thanks! I think it has massively helped me to understand my "true" problem. The log (to which the timestamps are recorded) is a SharePoint list. The fields which hold the timestamps are themselves formatted as DateTime fields. So when the JS sends the timestamp, the SP has to accept that in ISO format (i.e. with the timezone component included) It then displays that timestamp according to the settings of the person viewing the list (i.e. it converts the time to their local timezone via the UTC offset) – Alan O'Brien Sep 08 '21 at 17:01
  • Which, of course, is not what I want; I just want to see the time as per local (and there will be lots of different "locals" of course) and UTC. So - I think what I need to do, is change those fields to flat text, and pass the values as strings from the JS. I lose the ability to filter with date characteristics (i.e. before X, on or after Y etc.) but I think I can live with that. – Alan O'Brien Sep 08 '21 at 17:01
1

The timestamp produced by Date.prototype.toString has all the information you need. It has the local* date and time plus the UTC offset, the format is parsable by the built–in parser and it can be converted to an ISO 8601 compliant format (also parsable by the built–in parser) with some reformatting, e.g.

// Return UTC timestamp in ISO 8601 format
// @param {string} timestamp - in format per ECMA-262 Date.prototype.toString
// @returns {string} equivalent timestamp in ISO 8601 format UTC
// e.g. Fri Sep 10 2021 08:08:32 GMT+1000 (ChST) -> 2021-09-09T22:41:11.000Z
function toUTC(timestamp){
  return new Date(timestamp).toISOString();
}

// Return offset from ECMA-262 timestamp from Date.prototype.toString
// @param {string} timestamp - in format per ECMA-262 Date.prototype.toString 
// @return {string} offset in same format as toUTC
// e.g. Fri Sep 10 2021 08:08:32 GMT+1000 (ChST) -> +10:00
function getOffset(timestamp) {
  let offset = (timestamp.match(/[+-]\d{4}/) || [])[0];
  return offset? `${offset.slice(0,3)}:${offset.slice(-2)}` : '';
}

// Reformat ECMA-262 default timestamp as ISO 8601 UTC,
// @param {string} timestamp - in format per ECMA-262 Date.prototype.toString 
// @return {string} equivalent timestamp in ISO 8601 format with local date
//                  and time with local offset
// e.g. Fri Sep 10 2021 08:08:32 GMT+1000 (ChST) -> 2021-09-09T08:08:32.000+10:00
function toISOLocal(timestamp) {
  let months = [,'Jan','Feb','Mar','Apr','May','Jun',
                 'Jul','Aug','Sep','Oct','Nov','Dec'];
  let pad = n => ('0'+n).slice(-2);
  let [dayName, monthName, day, year, hour, min, sec, g, offset] = timestamp.split(/\W/);
  let sign = /\+/.test(timestamp)? '+' : '-';
  
  return `${year}-${pad(months.indexOf(monthName))}-${pad(day)}` +
    `T${hour}:${min}:${sec}.000${getOffset(timestamp)}`; 
}

// Examples
// In ECMAScript format, e.g. Fri Sep 10 2021 08:08:32 GMT+1000 (ChST)
let timestamp = new Date().toString();

console.log('Input : ' + timestamp +
          '\nUTC   : ' + toUTC(timestamp) +
          '\nLocal : ' + toISOLocal(timestamp) +
          '\nOffset: ' + getOffset(timestamp)
);

The only issue is that the format of the offset returned by toString (±HHmm) is different to that produced by toISOString (±HH:mm). The getOffset function returns the latter format, which is easily changed if you need the other format.

Regarding the built–in parser, it's helpful to read Why does Date.parse give incorrect results?

* Where "local" means based on the host system settings, which may or may not match the values for the geographical location of the system.

RobG
  • 142,382
  • 31
  • 172
  • 209