dayjs icon indicating copy to clipboard operation
dayjs copied to clipboard

Old dates have weird minute offsets in utc

Open irgipaulius opened this issue 3 years ago • 14 comments

Describe the bug Take this as an example:

// loop same day every 11 years for the past 200 years
for (let i = 2022; i > 1800; i -= 11) {
  console.log(dayjs(`12/31/${i}`).utc(true).toDate());
}

Expected behavior

2022-12-31T00:00:00.000Z
2011-12-31T00:00:00.000Z
2000-12-31T00:00:00.000Z
1989-12-31T00:00:00.000Z
1978-12-31T00:00:00.000Z
1967-12-31T00:00:00.000Z
1956-12-31T00:00:00.000Z
1945-12-31T00:00:00.000Z
1934-12-31T00:00:00.000Z
1923-12-31T00:00:00.000Z
1912-12-31T00:00:00.000Z
1901-12-31T00:00:00.000Z
1890-12-31T00:00:00.000Z
1879-12-31T00:00:00.000Z
1868-12-31T00:00:00.000Z
1857-12-31T00:00:00.000Z
1846-12-31T00:00:00.000Z
1835-12-31T00:00:00.000Z
1824-12-31T00:00:00.000Z
1813-12-31T00:00:00.000Z
1802-12-31T00:00:00.000Z

Actual behavior

2022-12-31T00:00:00.000Z
2011-12-31T00:00:00.000Z
2000-12-31T00:00:00.000Z
1989-12-31T00:00:00.000Z
1978-12-31T00:00:00.000Z
1967-12-31T00:00:00.000Z
1956-12-31T00:00:00.000Z
1945-12-31T00:00:00.000Z
1934-12-31T00:00:00.000Z
1923-12-31T00:00:00.000Z
1912-12-31T00:06:00.000Z   /// what???
1901-12-31T00:06:00.000Z
1890-12-31T00:06:00.000Z
1879-12-31T00:03:44.000Z   /// huh??!
1868-12-31T00:03:44.000Z
1857-12-31T00:03:44.000Z
1846-12-31T00:03:44.000Z
1835-12-31T00:03:44.000Z
1824-12-31T00:03:44.000Z
1813-12-31T00:03:44.000Z
1802-12-31T00:03:44.000Z   /// it goes on like this until 100 AC

Information

  • Day.js Version ^1.11.4
  • OS: MacOs Big Sur 11.6
  • NodeJS
  • Time zone: [e.g. GMT+03:00 DST]

irgipaulius avatar Jul 21 '22 12:07 irgipaulius

Hi, what is your timezone? I can't reproduce the issue with Europe/Moscow timezone

UPD: reproduced with this demo

Here is a demo with moment.js and same results.

Bykiev avatar Jul 21 '22 17:07 Bykiev

My timezone is Europe/Vilnius.

This is curious. running with moment ^2.29.3 I get a different result. I don't know how to make such cool demos, so I'll just put it here:

code:

const moment = require("moment");
const dayjs = require("dayjs");
const utc = require("dayjs/plugin/utc");
dayjs.extend(utc);

const f = "YYYY-MM-DD HH:mm:ss Z";

// loop same day every 11 years for the past 200 years
for (let i = 2022; i > 1800; i -= 11) {
  const dayjsDate = dayjs(`12/31/${i}`).utc(true).format(f);
  const momentDate = moment.utc(`12/31/${i}`).format(f).toString();
  console.log(`${dayjsDate} === ${momentDate}`);
}

result:

2022-12-31 00:00:00 +00:00 === 2022-12-31 00:00:00 +00:00
2011-12-31 00:00:00 +00:00 === 2011-12-31 00:00:00 +00:00
2000-12-31 00:00:00 +00:00 === 2000-12-31 00:00:00 +00:00
1989-12-31 00:00:00 +00:00 === 1989-12-31 00:00:00 +00:00
1978-12-31 00:00:00 +00:00 === 1978-12-31 00:00:00 +00:00
1967-12-31 00:00:00 +00:00 === 1967-12-31 00:00:00 +00:00
1956-12-31 00:00:00 +00:00 === 1956-12-31 00:00:00 +00:00
1945-12-31 00:00:00 +00:00 === 1945-12-31 00:00:00 +00:00
1934-12-31 00:00:00 +00:00 === 1934-12-31 00:00:00 +00:00
1923-12-31 00:00:00 +00:00 === 1923-12-31 00:00:00 +00:00
1912-12-31 00:06:00 +00:00 === 1912-12-31 00:00:00 +00:00
1901-12-31 00:06:00 +00:00 === 1901-12-31 00:00:00 +00:00
1890-12-31 00:06:00 +00:00 === 1890-12-31 00:00:00 +00:00
1879-12-31 00:03:44 +00:00 === 1879-12-31 00:00:00 +00:00
1868-12-31 00:03:44 +00:00 === 1868-12-31 00:00:00 +00:00
1857-12-31 00:03:44 +00:00 === 1857-12-31 00:00:00 +00:00
1846-12-31 00:03:44 +00:00 === 1846-12-31 00:00:00 +00:00
1835-12-31 00:03:44 +00:00 === 1835-12-31 00:00:00 +00:00
1824-12-31 00:03:44 +00:00 === 1824-12-31 00:00:00 +00:00
1813-12-31 00:03:44 +00:00 === 1813-12-31 00:00:00 +00:00
1802-12-31 00:03:44 +00:00 === 1802-12-31 00:00:00 +00:00

the results differ very much depending on the location of the machine. Shouldn't utc be idempotent?

Edit: formatted the output

irgipaulius avatar Jul 21 '22 18:07 irgipaulius

Indeed, depends on localtion, but I'm getting the same result for both libs, again small demo and my results:

2022-12-31 00:00:00 +00:00 === 2022-12-31 00:00:00 +00:00
2011-12-31 00:00:00 +00:00 === 2011-12-31 00:00:00 +00:00
2000-12-31 00:00:00 +00:00 === 2000-12-31 00:00:00 +00:00
1989-12-31 00:00:00 +00:00 === 1989-12-31 00:00:00 +00:00
1978-12-31 00:00:00 +00:00 === 1978-12-31 00:00:00 +00:00
1967-12-31 00:00:00 +00:00 === 1967-12-31 00:00:00 +00:00
1956-12-31 00:00:00 +00:00 === 1956-12-31 00:00:00 +00:00
1945-12-31 00:00:00 +00:00 === 1945-12-31 00:00:00 +00:00
1934-12-31 00:00:00 +00:00 === 1934-12-31 00:00:00 +00:00
1923-12-31 00:00:00 +00:00 === 1923-12-31 00:00:00 +00:00
1912-12-30 23:59:43 +00:00 === 1912-12-30 23:59:43 +00:00
1901-12-30 23:59:43 +00:00 === 1901-12-30 23:59:43 +00:00
1890-12-30 23:59:43 +00:00 === 1890-12-30 23:59:43 +00:00
1879-12-30 23:59:43 +00:00 === 1879-12-30 23:59:43 +00:00
1868-12-30 23:59:43 +00:00 === 1868-12-30 23:59:43 +00:00
1857-12-30 23:59:43 +00:00 === 1857-12-30 23:59:43 +00:00
1846-12-30 23:59:43 +00:00 === 1846-12-30 23:59:43 +00:00
1835-12-30 23:59:43 +00:00 === 1835-12-30 23:59:43 +00:00
1824-12-30 23:59:43 +00:00 === 1824-12-30 23:59:43 +00:00
1813-12-30 23:59:43 +00:00 === 1813-12-30 23:59:43 +00:00
1802-12-30 23:59:43 +00:00 === 1802-12-30 23:59:43 +00:00

Bykiev avatar Jul 21 '22 18:07 Bykiev

Seems to be a bug with utc function. With utcOffset the result is correct: dayjs(12/31/${i}).utcOffset(0,true).format("YYYY-MM-DD HH:mm:ss:SSS Z")

Bykiev avatar Jul 21 '22 19:07 Bykiev

This is not a bug, but the correct implementation of time zones. So for example for CET the definition of the time zone ("UTC offset") changed several times over the years, as can be seen in the timeanddate web page).

The time zone definitions can be found on the iana Time Zone Database. This database is implemented for example in the Internationalization API of javascript, the basis for time zones in dayjs.

BePo65 avatar Jul 24 '22 04:07 BePo65

@BePo65 interesting. This causes a few issues for my needs though.

  • utc gives different results on machines in different timezones. I have no way of testing/verifying that to be absolutely sure.
  • my application potentially works with old dates. I don't want to have to round dates every time.

are there any other ways to set utc mode without this "feature"?

currently I use moment and it works as I expect it to.

irgipaulius avatar Jul 24 '22 13:07 irgipaulius

This is not a bug, but the correct implementation of time zones. So for example for CET the definition of the time zone ("UTC offset") changed several times over the years, as can be seen in the timeanddate web page).

The time zone definitions can be found on the iana Time Zone Database. This database is implemented for example in the Internationalization API of javascript, the basis for time zones in dayjs.

Hi, sorry, I'm far from timezones, but why does .utcOffset(0,true) has another results?

Bykiev avatar Jul 24 '22 18:07 Bykiev

why does .utcOffset(0,true) has another results

Using utcOffset with 1 parameter, sets the utcOffset of a given dayjs object.

Using utcOffset with 2 parameters, sets the utcOffset of a given dayjs object while keeping the time value of that object. And therefore using .utcOffset(0, true) gets other values (this fragment avoids the critical branch in the code).

Regarding that point, the documentation is not really complete (at least I didn't find it in the documentation, only in the source code). @Bykiev : perhaps you want to make a PR for the documentation on the corresponding project?

Back to time zones / utcOffset:
sorry that it took me so long to understand the issue. If I get it right, your point is not about correct definition of timezones. The point is that .utcOffset(true) does not keep the local time. And that is really an error in dayjs.

I'm working on a pr for this issue; give me a few days.

BePo65 avatar Jul 25 '22 08:07 BePo65

in the meantime: how about using

dayjs.utc(`12/31/${i}`).format(f)

instead of

dayjs(`12/31/${i}`).utc(true).format(f)

In a quick test, this solves the point on my machine (I believe 😄 ).

Nevertheless I will try to fix the utcOffset issue, as I am working on the dayjs 2.0 utc plugin and this could / should be fixed there too.

BePo65 avatar Jul 25 '22 08:07 BePo65

why does .utcOffset(0,true) has another results

Using utcOffset with 1 parameter, sets the utcOffset of a given dayjs object.

Using utcOffset with 2 parameters, sets the utcOffset of a given dayjs object while keeping the time value of that object. And therefore using .utcOffset(0, true) gets other values (this fragment avoids the critical branch in the code).

Regarding that point, the documentation is not really complete (at least I didn't find it in the documentation, only in the source code). @Bykiev : perhaps you want to make a PR for the documentation on the corresponding project?

Back to time zones / utcOffset: sorry that it took me so long to understand the issue. If I get it right, your point is not about correct definition of timezones. The point is that .utcOffset(true) does not keep the local time. And that is really an error in dayjs.

I'm working on a pr for this issue; give me a few days.

Thank you for your investigation, indeed, utcOffset(true) doesn't preserve time, but it's undocumented feature and should be avoided. I can't understand, why does time converted from 1912-12-30 to 1912-12-30 23:59:43 +00:00 while using utc(true) and converted to 1912-12-31 00:00:0 +00:00 while using utcOffset(0,true)? My timezone is Europe/Moscow

in console:

$y: 1912…}
$L: "en"
$u: true
$d: Tue Dec 31 1912 02:30:00 GMT+0230 (Москва, стандартное время)
$x: Object
$y: 1912
$M: 11
$D: 30
$W: 1
$H: 23
$m: 59
$s: 43
$ms: 0

Bykiev avatar Jul 25 '22 09:07 Bykiev

in the meantime: how about using

dayjs.utc(`12/31/${i}`).format(f)

instead of

dayjs(`12/31/${i}`).utc(true).format(f)

In a quick test, this solves the point on my machine (I believe 😄 ).

Nevertheless I will try to fix the utcOffset issue, as I am working on the dayjs 2.0 utc plugin and this could / should be fixed there too.

With such format it returns the same 1912-12-30 23:59:43 +00:00, but if the input string format is RFC2822/ISO (${i}-12-31) it'll return 1912-12-31 00:00:0 +00:00.

Bykiev avatar Jul 25 '22 09:07 Bykiev

I did some more research and it seems the issue is not related to dayjs, here is small demo with date:

Ouput:

Sat Dec 31 2022 03:00:00 GMT+0300 (Москва, стандартное время)
Sat Dec 31 2011 04:00:00 GMT+0400 (Москва, стандартное время)
Sun Dec 31 2000 03:00:00 GMT+0300 (Москва, стандартное время)
Sun Dec 31 1989 03:00:00 GMT+0300 (Москва, стандартное время)
Sun Dec 31 1978 03:00:00 GMT+0300 (Москва, стандартное время)
Sun Dec 31 1967 03:00:00 GMT+0300 (Москва, стандартное время)
Mon Dec 31 1956 03:00:00 GMT+0300 (Москва, стандартное время)
Mon Dec 31 1945 03:00:00 GMT+0300 (Москва, стандартное время)
Mon Dec 31 1934 03:00:00 GMT+0300 (Москва, стандартное время)
Mon Dec 31 1923 02:00:00 GMT+0200 (Москва, стандартное время)
Tue Dec 31 1912 02:30:17 GMT+0230 (Москва, стандартное время)
Tue Dec 31 1901 02:30:17 GMT+0230 (Москва, стандартное время)
Wed Dec 31 1890 02:30:17 GMT+0230 (Москва, стандартное время)
Wed Dec 31 1879 02:30:17 GMT+0230 (Москва, стандартное время)
Thu Dec 31 1868 02:30:17 GMT+0230 (Москва, стандартное время)
Thu Dec 31 1857 02:30:17 GMT+0230 (Москва, стандартное время)
Thu Dec 31 1846 02:30:17 GMT+0230 (Москва, стандартное время)
Thu Dec 31 1835 02:30:17 GMT+0230 (Москва, стандартное время)
Fri Dec 31 1824 02:30:17 GMT+0230 (Москва, стандартное время)
Fri Dec 31 1813 02:30:17 GMT+0230 (Москва, стандартное время)
Fri Dec 31 1802 02:30:17 GMT+0230 (Москва, стандартное время)

Still can't understand how did it working as UTC offset in 1912 is +02:30... Where 17 seconds are from?

@BePo65, thank you for your links above, all is correct, the timezone shift in 1912 was +02:30:17

My misunderstood between utc(true) and utcOffset(0, true) is utc(true) converting time from timezone to UTC, but utcOffset(0, true) just preserve specified time with specified time shift.

Bykiev avatar Jul 25 '22 10:07 Bykiev

One of the problems of offset is the handling of seconds (see issue #1905).

BePo65 avatar Jul 25 '22 11:07 BePo65

After more tests and investigation here my current interpretation of the original problem of this issue:

The code from @irgipaulius compares dayjs'12/31/2000').utc(true).format(f)
with moment.utc('12/31/2000').format(f) and gets different results for the 2 values; which is ok, because

  • .utc(true) takes the original date (2000-12-31T00:00:00.000) and converts it to an utc date keeping the local time resulting in 2000-12-31T00:00:00.000.
  • .utc('12/31/2000') simply parses the string and creates an utc date from it (2000-12-31T00:00:00.000)..

Both results look the same at the first view, but things get complicated, if the timezone offset contains seconds. Using utc(dateString) simply ignores the offset, as it parses directly to an utc date, while date(datestring).utc(true) subtracts the utcOffset (which includes the seconds) from the date (which does not contain the seconds). As a result the time part gets "wrong". And that's true for moment and dayjs.

I created an example for 'Europe/Kiev":

element value comment
dateString '1879-12-31'
utcOffset 122 to be exactly: "02:02:04"
date.utc().format(f) "1879-12-30 21:57:56 +00:00" .utc() just ignores the time
date.utc(true).format(f) "1879-12-30 23:59:56 +00:00" here are the 4s from the exact utcOffset
dayjs.utc(dateString).format(f) "1879-12-31 00:00:00 +00:00" parses the string directly to utc

So if you would use the same function for moment and dayjs, the results would be s´the same in both cases - even moment(string).utc(true) shows the 'mysterious' seconds 😄

Besides of this, there remains the problem of dayjs with seconds in the timezone offset (see also issue #1905). I created a PR (#2016) for this, hoping that it helps solve the points with historical dates.

BePo65 avatar Aug 06 '22 14:08 BePo65