Get date ISO string without conversion to UTC timezone
Asked Answered
C

8

23

Is there a way in JavaScript to obtain an ISO string of a new Date object:

  • while maintaining the original local timezone (i.e. not converting to UTC)
  • with time component set to midnight
  • without resorting to manually rebuilding a new Date via individual date parts and reformatting the result?

I've been trying this

var date = new Date();
date.setHours(0, 0, 0, 0);
document.write(date.toISOString());

and I am getting this

2017-04-20T04:00:00.000Z

I want to get this

2017-04-20T00:00:00.000

(The lack of the trailing Z would indicate local time)

Essentially, I want a local timezone string, but formatted in the the "ISO" way. Relying on the "toLocale..." formatter method of JS Date provides inconsistent results depending on the locale settings.

Is there a built-in function or way as I've been trying to do to get the desired output (without rebuilding a date object with the date parts)?

Coattail answered 20/4, 2017 at 19:56 Comment(2)
This question is unclear, because your title asks for the ISO string "without time", but your question body asks for the ISO string with the hours, minutes, seconds, and milliseconds set to zero. Further, you don't specify what you want to happen with respect to timezone offsets. For that, are you wanting the current date, as it is in UTC, or are you wanting the current date, as it is in the local timezone? While I'd assume you wanted the UTC date, due to using toISOString(), you accepted an answer that adjusts the date for the local timezone, which makes timezone handling unclear.Laure
What you really want is the Date portion of the string formatted in ISO style, and the time set to midnight, without the adjustment to the UTC timezone (which can produce a different date altogether depending on how far away you are from UTC and midnight). I have an edit to the question that, when re-opened, can also provide a correct answer to this question.Subglacial
F
12

Just use setUTCHours instead of setHours and compensate for timezone:

    var date = new Date();
    var timezoneOffset = date.getMinutes() + date.getTimezoneOffset();
    var timestamp = date.getTime() + timezoneOffset * 1000;
    var correctDate = new Date(timestamp);
    
    correctDate.setUTCHours(0, 0, 0, 0);
    document.write(correctDate.toISOString())

setHours will set time in your local timezone, but when you display it, it will show the time in UTC. If you just set it as UTC from the beginning, you'll get the result you want.

EDIT:

Just be aware that if you are ahead of UTC, your date will be stored as a UTC date from the previous day, so running setUTCHours will not work as intended, changing your date to midnight of the previous day. Therefore, you first need to add the timezone offset to the date.

Fogel answered 20/4, 2017 at 20:1 Comment(4)
Hmm. I think this only works if you are at or behind UTC. I'm at +1030, and when I do this before 10:30 AM, it still gives me the previous day.Botanist
You are indeed correct. It is necessary to check whether changing the time will result in a date change (which can happen if you are ahead of UTC)Fogel
I edited my code so it should work for any timezone. Further corrections are welcome, as I have only tested this by simulating a +10 timezone! :)Fogel
This answer has the right idea, but technically incorrect implementation. It will not work for many timezone + time combinations (both in negative and positive offset zones). The main issue is that it's actually doing the conversion to UTC twice (instead of undoing the timezone offset), but this is not observed in many instances because it's not working in the correct units (minutes -- which means the timezoneOffset needs to be multipled by 60, then 1000) in the first place. All of this is corrected in the answer I provided with proof as to why we are doing the operations we are doing.Subglacial
H
24

var isoDate = new Date().toISOString().substring(0,10);
console.log(isoDate);
Hearst answered 31/5, 2019 at 0:30 Comment(3)
According to MDN (developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/…) this solution will not return expected results for years outside 0000-9999 range.Aroma
This solution does not correct for timezone issues and produces unexpected results. When your user bug report is "why is the output wrong by a day?", this was why.Weathersby
@Weathersby is correct, a correct implementation (and explanation as to why this answer is wrong) is provided in my answerSubglacial
C
15

If you want your code to be logically persistent, a substring based on hard coded indexes is never safe:

var iso = date.toISOString();
iso = iso.substring(0, iso.indexOf('T'));
Cigar answered 6/11, 2020 at 18:49 Comment(2)
Beware of future Y10K bugsGynaeco
This will return the incorrect day if system is not set to UTC (will return yesterday if system set to positive offset timezone and tomorrow if negative offset timezone) and the current system time is within UTC midnight - timezone offset. See my answer for a correct answer.Subglacial
F
12

Just use setUTCHours instead of setHours and compensate for timezone:

    var date = new Date();
    var timezoneOffset = date.getMinutes() + date.getTimezoneOffset();
    var timestamp = date.getTime() + timezoneOffset * 1000;
    var correctDate = new Date(timestamp);
    
    correctDate.setUTCHours(0, 0, 0, 0);
    document.write(correctDate.toISOString())

setHours will set time in your local timezone, but when you display it, it will show the time in UTC. If you just set it as UTC from the beginning, you'll get the result you want.

EDIT:

Just be aware that if you are ahead of UTC, your date will be stored as a UTC date from the previous day, so running setUTCHours will not work as intended, changing your date to midnight of the previous day. Therefore, you first need to add the timezone offset to the date.

Fogel answered 20/4, 2017 at 20:1 Comment(4)
Hmm. I think this only works if you are at or behind UTC. I'm at +1030, and when I do this before 10:30 AM, it still gives me the previous day.Botanist
You are indeed correct. It is necessary to check whether changing the time will result in a date change (which can happen if you are ahead of UTC)Fogel
I edited my code so it should work for any timezone. Further corrections are welcome, as I have only tested this by simulating a +10 timezone! :)Fogel
This answer has the right idea, but technically incorrect implementation. It will not work for many timezone + time combinations (both in negative and positive offset zones). The main issue is that it's actually doing the conversion to UTC twice (instead of undoing the timezone offset), but this is not observed in many instances because it's not working in the correct units (minutes -- which means the timezoneOffset needs to be multipled by 60, then 1000) in the first place. All of this is corrected in the answer I provided with proof as to why we are doing the operations we are doing.Subglacial
Z
7

One liner, without third party lib:

const date = new Date().toLocaleString('sv').split(' ')[0]; // Ex: '2023-01-17'

This is taking advantage of Sweeden's date format

EDIT: Old answer below, before Jon's remark about taking timezone into account

const date = new Date().toISOString().split('T')[0]; // Ex: '2023-01-17'
Zipah answered 17/1, 2023 at 15:18 Comment(2)
This will return the incorrect day if system is not set to UTC (will return yesterday if system set to positive offset timezone and tomorrow if negative offset timezone) and the current system time is within UTC midnight - timezone offset. See my answer for a correct answer without third part libSubglacial
The problem with locale strings, is they are subject to change in the future (and have in the past, as this answer shows: https://mcmap.net/q/422118/-get-iso-8601-using-intl-datetimeformat)Subglacial
S
3

The problem with most of the answers here, including Diego's answer, is that it will run into day boundary conversions on certain timezone + time combinations. i.e. you want to get today's date at midnight, but because the system is set to a negative offset timezone, it will will get tomorrow's date (or vice versa -- will get yesterday's date if your system is set to a postive offset timezone).

Pedro Castilho's answer has the right idea, but it's actually not correct for all timezones + time combinations, and it's only by happenstance for the combinations where it is correct.

Before we get to the correct implementation of Pedro's answer, let's cover our assumptions and prove our assertions:

  • First off, it's important to remember that timezone offsets are always added to UTC time in order to arrive at the local time. Think about this with a simple example: When it's 12pm in London, it's 1pm in Paris. The timezone offset for Paris is +1, so in formula this is expressed as:

    Local Time = UTC Time + Offset
    UTC Time = Local Time - Offset
    
  • Secondly, when we call JS new Date().toISOString(), JS gets the current system time, the current system timezone setting (can be confirmed by checking Intl.DateTimeFormat().resolvedOptions().timeZone property in JS console), and uses that info to convert to UTC time (because all JS objects are stored as UTC), and does not convert the result back to local time. So in our example, if we're on a system set to exactly June 1 at midnight in Paris timezone, calling new Date().toISOString() will return 2023-05-31T23:00:00.000Z

  • Thirdly, what the questioner wants is the formatting of the JS Date.toISOString(), but without conversion to UTC timezone -- i.e. 2023-06-01T00:00:00.000. Since all JS date objects are stored in UTC natively, the most straightforward way to do this, is to simply allow the conversion by JS to UTC to take place, then undo it.

    • In order to undo the conversion, we must start from a consistent base -- i.e. will not change depending on system timezone and/or locale settings. The reliable choice here is Date.getTime. When we call new Date().getTime(), it will return the number of seconds since the epoch, and those seconds are just a flat integer -- with no reference to timezone. The timezone is implicitly assumed to be UTC, and all of JS methods operate off this assumption. Since we want to undo the UTC conversion that has already happened when JS created the Date object, we need to use the formula for local time, but taking its inverse i.e. we need to subtract back the offset that was previously added. So we obtain the following formulas:

      Local Time = UTC Time - Offset 
      UTC Time = Local Time + Offset
      

When we put all the above together, we can come up with a very general-purpose method that converts a JS Date Object to an "ISO string" but formatted in the local timezone:

// jsDateToISOLocalStr
// Converts a JS date to a "ISO-style" string (i.e. not converting to UTC Zero Offset Time)
// i.e. Suppose it is May 2, 2023 @ 4:36 AM, and we are on a system with a +10 timezone (around Australia)
// The difference between 'toISOString' builtin and jsDateToISOLocalStr is simply as thus:
// new Date().toISOString() --> '2023-05-01T18:36:31.866Z'
// jsDateToISOLocalStr()    --> '2023-05-02T04:36:31.866'
// All JS Date objects are stored as UTC, and so if a local timezone is displayed by JS, we must "convert back"
// params:
//   * d (optional) -- anything that can be parsed by JS new Date constructor
// returns -- One of the below:
//   * if d can be parsed by JS Date: an ISO-style string representation of d according to local (machine) time
//   * if d is not truthy or not passed in: an ISO-style string representation of 'right now' according to local time
//   * if d cannot be parsed by JS date (that is, when new Date(d) returns "Invalid Date"): null
export const jsDateToISOLocalStr = (d) => {
    let retVal = null;
    // We necessarily do some string parsing because JS Date doesn't parse dates consistently
    // e.g. -- new Date("2023-05-01") returns midnight at UTC,
    //         new Date("2023-05-01 ") (or any non-strict variant) returns midnight in the local timezone
    // We will force to the local time unless any time component is already in the string
    // If the total string length is less than 11, then there isn't any time component.
    const parsedInput = (typeof d === "string" || d instanceof String) && d.length < 11 ? `${d} ` : d;
    // We must not pass a date argument if we want today's date. "new Date(null)" returns the epoch (Jan 1 1970)!
    const utcDate = d ? new Date(parsedInput) : new Date();
    if (String(utcDate).toLowerCase() !== "invalid date") {
        // Conversion to UTC already took place -- now we must *undo* that
        const localTimestamp = utcDate.getTime() - utcDate.getTimezoneOffset() * 60 * 1000;
        const localDate = new Date(localTimestamp);
        // Remove the final trailing 'Z' to indicate local time
        retVal = localDate.toISOString().slice(0, -1);
    }
    return retVal;
};

Now we can finally get the desired output:

const isoLocalDateTime = jsDateToISOLocalStr();
const isoLocalDate = isoLocalDateTime.split("T")[0];

// To write the local date set to midnight time
document.write(`${isoLocalDate}T00:00:00.000`);
// Optionally, to write the local date set to the current time
document.write(isoLocalDateTime);
// Optionally, to just write the local date with no time component
document.write(isoLocalDate);
Subglacial answered 8/5, 2023 at 19:18 Comment(0)
T
2

If you can live with depending on the great momentjs.com library, it will be as easy as this:

moment().format('YYYY-MM-DD');

or

moment().toISOString();
Target answered 16/8, 2019 at 18:56 Comment(1)
I'm not criticizing the answer, but it's now worth noting that MomentJS has declared itself +deprecated+ done, and in legacy mode, in favour of alternatives that make the date objects immutable. momentjs.com/docs/#/-project-statusBreton
F
1

with date-fns format

import {format} from "date-fns-tz"    
format(new Date(), 'yyyy-MM-dd')
Filamentous answered 31/3, 2022 at 22:58 Comment(0)
D
1

To get local datetime in ISO format, ignoring local timezone offset, use next short function:

function dateToLocalISO(date) {
    return new Date(date.getTime() - date.getTimezoneOffset() * 60 * 1000).toISOString().slice(0, -1); // Remove the last 'Z' indicating local time
}

console.log('Now = ' + dateToLocalISO(new Date()));
console.log('Today = ' + dateToLocalISO(new Date(new Date().setHours(0, 0, 0, 0))));
Dampier answered 15/10, 2023 at 10:23 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.