3

a date input where the user enters 2019-12-22 gives these values:

  • input.value: "2019-12-22"
  • input.valueAsNumber: 1576972800000
  • input.valueAsDate: "Sat Dec 21 2019 16:00:00 GMT-0800 (Pacific Standard Time)"
    • this resulting date object just seems wrong
    • when the browser returns a value, it treats the user input as universal time
    • so the date object's utc representation is the same as what the input displays to the user
    • input.valueAsDate.getUTCDate() returns 22, which is what the user entered
    • input.valueAsDate.getDate() returns 21, NOT what the user entered
    • thus we conclude the date input displays and accepts utc time, not local times

we want the resulting date.toString() to show the same result as the original user input in the date-input

how can we allow users to interact with local times, but then obtain a correct date object in our scripts?

ChaseMoskal
  • 7,151
  • 5
  • 37
  • 50
  • I think you are confusing the internal date state and the representation that is returned by its `toString()` method. Just use `input.valueAsDate.toLocaleDateString()` and it should match what the user entered. – str Dec 22 '19 at 08:26
  • hey thanks for stopping by! unfortunately: no, negative: it is not true. just verified in experiment, when user enters `2019-12-22`, your suggestion of `input.valueAsDate.toLocaleDateString()` actually returns `"21/12/2019"` -- which does not match what the user entered -- the assumption that the date input accepts local time and then returns you a date object whose local output matches, is false -- i know, it's bewildering, and the only way around it is to compensate for the time offset manually – ChaseMoskal Dec 22 '19 at 08:32
  • Strangely, this works fine in Firefox but not in Chrome (which I assume you are using). – str Dec 22 '19 at 09:04
  • @str -- no man, i just reproduced this in firefox just now: `input.valueAsDate.toLocaleDateString()` returns `"12/21/2019"` -- could there be some browser configuration that's different between us? surely it's the difference in time-zone between us, what's your timezone? perhaps your timezone offset just isn't enough to show a discrepancy in the day -- for this reason, it would have probably been much wiser for me to use a time-input for this whole example, as the issue would reproduce for anybody not in greenwich :) – ChaseMoskal Dec 22 '19 at 20:44
  • I set my computer's timezone to yours (PST) to verify the behaviour of Firefox and Chrome. They did not return the same thing for me. – str Dec 22 '19 at 20:55
  • @str -- that's actually.. really really scary! i mean, what does that even mean? does that mean my app will break for you in one browser, but not the other? but it works in both browsers for me!? ahhh!! *(runs out of the building screaming)* – ChaseMoskal Dec 22 '19 at 20:56

2 Answers2

3

This issue is caused by a decision by the TC-39 to treat date–only ISO 8601 format timestamps as UTC, when it would have been more logical to be consistent with ISO 8601 and treated them as local. See Why does Date.parse give incorrect results?

The simple solution is to manually parse the string, do not use the built–in parser, as at least one current implementation until recently parsed YYYY-MM-DD as local. Also, do not use the current timezone offset to adjust the time value as that doesn't allow for historic changes in offsets or possible daylight saving changes.

// Parse timestamp in YYYY-MM-DD format as local
function parseISOLocal(s) {
  let [y, m, d] = s.split(/\D/);
  return new Date(y, --m, d);
}

// Format date as YYYY-MM-DD local
function formatISOLocal(d) {
  let z = n => (n<10?'0':'') + n;
  return d.getFullYear() + '-' + z(d.getMonth()+1) + '-' + z(d.getDate());
}

let s = '2019-12-22';
let d = parseISOLocal(s);
console.log( d.toString());
console.log( formatISOLocal(d));

Edit

Where input type date is supported and YYYY-MM-DD is parsed per ECMA-262 as UTC, you can use valueAsDate and UTC methods. However, not all browsers support input type date and not all parsers will parse that format as UTC.

It's much more reliable to not rely on input type date and to manually parse the value, checking format and validity. This is one reason why date widgets and libraries are commonly used instead of built–in Date functionality.

let inp = document.getElementById('dob');
let dobObj = inp.valueAsDate;
let dobStr = inp.value;

console.log('Value as date: ' + dobObj);   // Safari: null
console.log('Value as string: ' + dobStr); // 2018-06-15
<input id="dob" type="date" value="2018-06-15">
RobG
  • 142,382
  • 31
  • 172
  • 209
  • 1
    thank you very much for your answer! i must ask, instead of string-parsing the `input.value`, wouldn't it be better to use the utc components of the input's date object? eg, `input.valueAsDate` and `.getUTCFullYear()`, `.getUTCMonth()`, and `.getUTCDate()`? – ChaseMoskal Dec 22 '19 at 20:54
  • 1
    @ChaseMoskal—that requires use of the built–in parser and the vagaries that entails. – RobG Dec 23 '19 at 00:55
  • It seems that `valueAsDate` parses as UTC, so it's the same as `new Date(inp.value)` (where supported) - yes? So the original approach of parsing manually is still the better option. – Matt Johnson-Pint Dec 23 '19 at 17:33
-1

Since the date/time input elements are accepting user input as UTC time, but we want to accept local time, we must manually compensate by the amount of local timezone offset that is configured on the user's computer

So we accept the input value as a number, but then offset it by the amount of timezone offset before making a Date object with it

// ascertain the timezone offset
const timeOffset = (new Date()).getTimezoneOffset() * 60 * 1000

// compensate for the weirdness
const milliseconds = input.valueAsNumber + timeOffset

// make a real date object
const date = new Date(milliseconds)

the resulting date object, when displayed to the user via toString() or toLocaleDateString(), will be the same as the time that the user had originally input.

jonrsharpe
  • 115,751
  • 26
  • 228
  • 437
ChaseMoskal
  • 7,151
  • 5
  • 37
  • 50
  • 2
    With that code you effectively change the date to a different moment in time. It seems you care about the date representation only, but here you actually change the internal state which is most probably not what you want. – str Dec 22 '19 at 08:27
  • because the input elements are accepting user input as UTC time instead of local time, we have to offset the user input by the amount of their timezone in order to achieve a date object for which `toLocaleDateString()` matches the user's local time (and what they originally entered as input) -- i know it's counter-intuitive and bewildering, it's this unexpected behavior that's caused me to investigate so much -- it seems to be really stupid that there isn't a `valueAsLocalDate`, such that inputs could be used either as UTC or Local inputs.. without the luxury, we must compensate ourselves – ChaseMoskal Dec 22 '19 at 08:40
  • 2
    This is not a good approach as it will apply the current timezone offset instead of the offset for the actual date so does not allow for different offsets based on historic or daylight saving changes. Also, it relies on the built–in parser, which is notoriously fickle and at least one current browser parses YYYY-MM-DD as local, not UTC. – RobG Dec 22 '19 at 09:32
  • @jonrsharpe -- how come you deleted my answer's little heading? why can we make headings if we can't use them? i thought it was a good summary of the technique the answer is detailing -- isn't that useful? when is it appropriate to use the heading markdown feature? – ChaseMoskal Dec 22 '19 at 20:58