dayjs
dayjs copied to clipboard
Timezone conversion incorrect when daylight saving in effect
Describe the bug Dayjs does not seem to convert timezone correctly when DST is in effect. It produces different results from Moment. The Moment results seem intuitively what I'd expect, so believe to be correct unless there is some subtlety I'm missing.
Expected behavior With this code:
const when = '2020-07-30T12:00:00+00:00'; // Midday GMT in summer
console.log('Moment London: ' + moment(when).tz('Europe/London').format('HH:mm Z'));
console.log('Moment New York: ' + moment(when).tz('America/New_York').format('HH:mm Z'));
console.log('Dayjs London: ' + dayjs(when).tz('Europe/London').format('HH:mm Z'));
console.log('Dayjs New York: ' + dayjs(when).tz('America/New_York').format('HH:mm Z'));
The output is:
Moment London: 13:00 +01:00
Moment New York: 08:00 -04:00
Dayjs London: 12:00 +00:00 // Expect 13:00 +01:00
Dayjs New York: 08:00 -05:00 // Expect 08:00 -04:00
Information
- Day.js: v1.9.6
- OS: Windows 10 Home
- Nodejs: v12.8.3
- Time zone: GMT+00:00
is this whats going wrong in the test suite of this library? i ran it and some tests were off by an hour.
Hi, any thoughts on this issue @iamkun?
I can confirm that tests are failing in a locale affected by daylight savings (I'm in America/Vancouver
).
I found out because tests stared breaking locally, and I made some experiments showing output different from Moment: https://runkit.com/lucasrcosta/5feca83aea7c09001b879b3a
I tried sorting it out on the code and removing the current offset from localUtcOffset
on timezone/index.js
fixed one test, but I couldn't figure out solutions for the others. It does seem related to not fixing DST.
Summary of all failing tests
FAIL test/plugin/dayOfYear.test.js
● DayOfYear set
expect(received).toBe(expected) // Object.is equality
Expected value to be:
"2015-01-04T00:00:00.000Z"
Received:
"2014-01-05T00:00:00.000Z"
31 | expect(dayjs('2015-01-01T00:00:00.000Z')
32 | .dayOfYear(4)
> 33 | .toISOString()).toBe('2015-01-04T00:00:00.000Z')
34 |
35 | expect(dayjs('2015-01-01T00:00:00.000Z')
36 | .dayOfYear(32)
at Object.<anonymous> (test/plugin/dayOfYear.test.js:33:21)
FAIL test/plugin/localizedFormat.test.js
● Should not interpolate characters inside square brackets
expect(received).toBe(expected) // Object.is equality
Expected value to be:
"1970 l 1970"
Received:
"1969 l 1969"
29 |
30 | expect(actualDate.format('[l]')).toBe('l')
> 31 | expect(actualDate.format('YYYY [l] YYYY')).toBe('1970 l 1970')
32 | expect(actualDate.format('l [l] l')).toBe('1/1/1970 l 1/1/1970')
33 | expect(actualDate.format('[L LL LLL LLLL]')).toBe(expectedDate.format('[L LL LLL LLLL]'))
34 |
at Object.<anonymous> (test/plugin/localizedFormat.test.js:31:46)
FAIL test/plugin/timezone.test.js
● Parse › parse timestamp, js Date, Day.js object
expect(received).toBe(expected) // Object.is equality
Expected value to be:
"2020-08-07T12:00:00-07:00"
Received:
"2020-08-07T12:00:00-08:00"
49 | const Timestamp = dayjs.tz(d.getTime(), VAN)
50 | const Tmoment = moment.tz(d, VAN)
> 51 | expect(TjsDate.format()).toBe(result)
52 | expect(Tdayjs.format()).toBe(result)
53 | expect(Timestamp.format()).toBe(result)
54 | expect(Tmoment.format()).toBe(result)
at Object.<anonymous> (test/plugin/timezone.test.js:51:30)
● Parse › parse and convert between timezones
expect(received).toBe(expected) // Object.is equality
Expected value to be:
"2014-06-01T09:00:00-07:00"
Received:
"2014-06-01T09:00:00-08:00"
57 | it('parse and convert between timezones', () => {
58 | const newYork = dayjs.tz('2014-06-01 12:00', NY)
> 59 | expect(newYork.tz('America/Los_Angeles').format()).toBe('2014-06-01T09:00:00-07:00')
60 | expect(newYork.tz('Europe/London').format()).toBe('2014-06-01T17:00:00+01:00')
61 | })
62 |
at Object.<anonymous> (test/plugin/timezone.test.js:59:56)
● Convert › convert to target time
expect(received).toBe(expected) // Object.is equality
Expected value to be:
"2014-06-01T05:00:00-07:00"
Received:
"2014-06-01T05:00:00-08:00"
74 | const losAngeles = dayjs('2014-06-01T12:00:00Z').tz('America/Los_Angeles')
75 | const MlosAngeles = moment('2014-06-01T12:00:00Z').tz('America/Los_Angeles')
> 76 | expect(losAngeles.format()).toBe('2014-06-01T05:00:00-07:00')
77 | expect(losAngeles.format()).toBe(MlosAngeles.format())
78 | expect(losAngeles.valueOf()).toBe(1401624000000)
79 | expect(losAngeles.valueOf()).toBe(MlosAngeles.valueOf())
at Object.<anonymous> (test/plugin/timezone.test.js:76:33)
● Convert › convert to target time
expect(received).toBe(expected) // Object.is equality
Expected value to be:
"2014-06-01T05:00:00-07:00"
Received:
"2014-06-01T05:00:00-08:00"
85 | [dayjs, moment].forEach((_) => {
86 | const losAngeles = _('2014-06-01T12:00:00Z').tz('America/Los_Angeles')
> 87 | expect(losAngeles.format()).toBe('2014-06-01T05:00:00-07:00')
88 | expect(losAngeles.valueOf()).toBe(1401624000000)
89 | })
90 | })
at forEach (test/plugin/timezone.test.js:87:35)
at Array.forEach (<anonymous>)
at Object.<anonymous> (test/plugin/timezone.test.js:85:21)
● Convert › convert from time with timezone to target time
expect(received).toBe(expected) // Object.is equality
Expected value to be:
"2014-06-01T12:00:00Z"
Received:
"2014-06-01T12:00:00-01:00"
93 | const losAngelesInUTC = dayjs('2014-06-01T05:00:00-07:00').tz('UTC')
94 | const MlosAngelesInUTC = moment('2014-06-01T05:00:00-07:00').tz('UTC')
> 95 | expect(losAngelesInUTC.format()).toBe('2014-06-01T12:00:00Z')
96 | expect(losAngelesInUTC.format()).toBe(MlosAngelesInUTC.format())
97 | })
98 |
at Object.<anonymous> (test/plugin/timezone.test.js:95:38)
● Convert › format Z
expect(received).toBe(expected) // Object.is equality
Expected value to be:
"+09:00"
Received:
"+08:00"
115 | [dayjs, moment].forEach((_) => {
116 | const t = _('2020-08-06T03:48:10.258Z').tz(TOKYO)
> 117 | expect(t.format('Z')).toBe('+09:00')
118 | })
119 | })
120 | })
at forEach (test/plugin/timezone.test.js:117:29)
at Array.forEach (<anonymous>)
at Object.<anonymous> (test/plugin/timezone.test.js:115:21)
● DST, a time that never existed Spring Forward › 2012-03-11 02:00:00
expect(received).toBe(expected) // Object.is equality
Expected value to be:
"2012-03-11T03:00:00-04:00"
Received:
"2012-03-11T04:00:00-04:00"
143 | const d = dayjs.tz(s, NY)
144 | const m = moment.tz(s, NY)
> 145 | expect(d.format()).toBe('2012-03-11T03:00:00-04:00')
146 | expect(d.format()).toBe(m.format())
147 | expect(d.valueOf()).toBe(m.valueOf())
148 | expect(d.valueOf()).toBe(1331449200000)
at Object.<anonymous> (test/plugin/timezone.test.js:145:24)
● DST, a time that never existed Spring Forward › 2012-03-11 02:59:59
expect(received).toBe(expected) // Object.is equality
Expected value to be:
"2012-03-11T03:59:59-04:00"
Received:
"2012-03-11T04:59:59-04:00"
154 | const d = dayjs.tz(s, NY)
155 | const m = moment.tz(s, NY)
> 156 | expect(d.format()).toBe('2012-03-11T03:59:59-04:00')
157 | expect(d.format()).toBe(m.format())
158 | expect(d.valueOf()).toBe(m.valueOf())
159 | expect(d.valueOf()).toBe(1331452799000)
at Object.<anonymous> (test/plugin/timezone.test.js:156:24)
● DST, a time that never existed Spring Forward › 2012-03-11 03:00:00
expect(received).toBe(expected) // Object.is equality
Expected value to be:
"2012-03-11T03:00:00-04:00"
Received:
"2012-03-11T04:00:00-04:00"
165 | const d = dayjs.tz(s, NY)
166 | const m = moment.tz(s, NY)
> 167 | expect(d.format()).toBe('2012-03-11T03:00:00-04:00')
168 | expect(d.format()).toBe(m.format())
169 | expect(d.valueOf()).toBe(m.valueOf())
170 | expect(d.valueOf()).toBe(1331449200000)
at Object.<anonymous> (test/plugin/timezone.test.js:167:24)
● DST, a time that never existed Fall Back › 2012-11-04 02:00:00
expect(received).toBe(expected) // Object.is equality
Expected value to be:
"2012-11-04T02:00:00-05:00"
Received:
"2012-11-04T01:00:00-05:00"
204 | [dayjs, moment].forEach((_) => {
205 | const d = _.tz(s, NY)
> 206 | expect(d.format()).toBe('2012-11-04T02:00:00-05:00')
207 | expect(d.utcOffset()).toBe(-300)
208 | expect(d.valueOf()).toBe(1352012400000)
209 | })
at forEach (test/plugin/timezone.test.js:206:26)
at Array.forEach (<anonymous>)
at Object.<anonymous> (test/plugin/timezone.test.js:204:21)
Test Suites: 3 failed, 63 passed, 66 total
Tests: 12 failed, 601 passed, 613 total
Snapshots: 0 total
Time: 12.936s
Ran all test suites.
This bug sounds similar to the recent comment in this one: https://github.com/iamkun/dayjs/issues/1340#issuecomment-764769936
I'm going to cross-post the bug and how it can be fixed. Let me know if that doesn't actually fix this.
Not sure your bug is similar to the OPs, unless I'm missing the connection.
But I did confirm and figure out what the problem is with your case. It's actually a pretty easy fix (I think).
The problem
I'm in CST, which has an offset of -5/-6 (depending on DST). Let's use this as an example.
Given the input you gave:
2020-03-29T00:30:00Z
The code converts to local machines timezone (at this time it was -5 for me). Then it converts to the desired timezone. These are the results:
2020-03-28T20:30:00-05:00
2020-03-29T03:30:00+02:00
Then these two dates are subtracted (disregard the offset), giving -07:00. Next, we take local machine offset (-5) and subtract from -7. -5 - (-7) = +2. Correct answer.
That's how it should work. Let's look at the code and I'll show you the bug.
proto.tz = function (timezone = defaultTimezone, keepLocalTime) { const oldOffset = this.utcOffset() const target = this.toDate().toLocaleString('en-US', { timeZone: timezone }) const diff = Math.round((this.toDate() - new Date(target)) / 1000 / 60) let ins = d(target).$set(MS, this.$ms).utcOffset(localUtcOffset - diff, true) if (keepLocalTime) { const newOffset = ins.utcOffset() ins = ins.add(oldOffset - newOffset, MIN) } ins.$x.$timezone = timezone return ins }
Source: https://github.com/iamkun/dayjs/blob/dev/src/plugin/timezone/index.js#L101
Check out that it takes
localUtcOffset
and subtracts the diff. ThatlocalUtcOffset
is obtained by doing the following. The problem is that DST is applied now so the offset for me is -06:00, giving the offset of +01:00 that we're seeing.const localUtcOffset = d().utcOffset()
Source: https://github.com/iamkun/dayjs/blob/dev/src/plugin/timezone/index.js#L39
The fix
Replace
localUtcOffset
withthis.utcOffset()
and that will return the UTC offset at that dateWhat you should do
I haven't tested this but I think it'll work.
If that works for you, I'd encourage you to submit a PR and fix this.
When was the bug introduced
Bug has existed ever since the timezone plugin was added.
https://github.com/iamkun/dayjs/pull/974/files
A fix was applied in #1352 that may fix this bug. Can someone try with the latest changes and see if the issue still persists?
Still brakes here, although a little different...
Summary of all failing tests
FAIL test/plugin/timezone.test.js (5.167s)
● Convert › convert to target time
expect(received).toBe(expected) // Object.is equality
Expected value to be:
1401624000000
Received:
1401620400000
76 | expect(losAngeles.format()).toBe('2014-06-01T05:00:00-07:00')
77 | expect(losAngeles.format()).toBe(MlosAngeles.format())
> 78 | expect(losAngeles.valueOf()).toBe(1401624000000)
79 | expect(losAngeles.valueOf()).toBe(MlosAngeles.valueOf())
80 | expect(losAngeles.utcOffset()).toBe(-420)
81 | expect(losAngeles.utcOffset()).toBe(MlosAngeles.utcOffset())
at Object.<anonymous> (test/plugin/timezone.test.js:78:34)
● Convert › convert to target time
expect(received).toBe(expected) // Object.is equality
Expected value to be:
1401624000000
Received:
1401620400000
86 | const losAngeles = _('2014-06-01T12:00:00Z').tz('America/Los_Angeles')
87 | expect(losAngeles.format()).toBe('2014-06-01T05:00:00-07:00')
> 88 | expect(losAngeles.valueOf()).toBe(1401624000000)
89 | })
90 | })
91 |
at forEach (test/plugin/timezone.test.js:88:36)
at Array.forEach (<anonymous>)
at Object.<anonymous> (test/plugin/timezone.test.js:85:21)
● DST, a time that never existed Spring Forward › 2012-03-11 02:00:00
expect(received).toBe(expected) // Object.is equality
Expected value to be:
"2012-03-11T03:00:00-04:00"
Received:
"2012-03-11T04:00:00-04:00"
145 | const d = dayjs.tz(s, NY)
146 | const m = moment.tz(s, NY)
> 147 | expect(d.format()).toBe('2012-03-11T03:00:00-04:00')
148 | expect(d.format()).toBe(m.format())
149 | expect(d.valueOf()).toBe(m.valueOf())
150 | expect(d.valueOf()).toBe(1331449200000)
at Object.<anonymous> (test/plugin/timezone.test.js:147:24)
● DST, a time that never existed Spring Forward › 2012-03-11 02:59:59
expect(received).toBe(expected) // Object.is equality
Expected value to be:
"2012-03-11T03:59:59-04:00"
Received:
"2012-03-11T04:59:59-04:00"
156 | const d = dayjs.tz(s, NY)
157 | const m = moment.tz(s, NY)
> 158 | expect(d.format()).toBe('2012-03-11T03:59:59-04:00')
159 | expect(d.format()).toBe(m.format())
160 | expect(d.valueOf()).toBe(m.valueOf())
161 | expect(d.valueOf()).toBe(1331452799000)
at Object.<anonymous> (test/plugin/timezone.test.js:158:24)
● DST, a time that never existed Spring Forward › 2012-03-11 03:00:00
expect(received).toBe(expected) // Object.is equality
Expected value to be:
"2012-03-11T03:00:00-04:00"
Received:
"2012-03-11T04:00:00-04:00"
167 | const d = dayjs.tz(s, NY)
168 | const m = moment.tz(s, NY)
> 169 | expect(d.format()).toBe('2012-03-11T03:00:00-04:00')
170 | expect(d.format()).toBe(m.format())
171 | expect(d.valueOf()).toBe(m.valueOf())
172 | expect(d.valueOf()).toBe(1331449200000)
at Object.<anonymous> (test/plugin/timezone.test.js:169:24)
● DST, a time that never existed Fall Back › 2012-11-04 02:00:00
expect(received).toBe(expected) // Object.is equality
Expected value to be:
"2012-11-04T02:00:00-05:00"
Received:
"2012-11-04T01:00:00-05:00"
206 | [dayjs, moment].forEach((_) => {
207 | const d = _.tz(s, NY)
> 208 | expect(d.format()).toBe('2012-11-04T02:00:00-05:00')
209 | expect(d.utcOffset()).toBe(-300)
210 | expect(d.valueOf()).toBe(1352012400000)
211 | })
at forEach (test/plugin/timezone.test.js:208:26)
at Array.forEach (<anonymous>)
at Object.<anonymous> (test/plugin/timezone.test.js:206:21)
FAIL test/plugin/localizedFormat.test.js
● Should not interpolate characters inside square brackets
expect(received).toBe(expected) // Object.is equality
Expected value to be:
"1970 l 1970"
Received:
"1969 l 1969"
29 |
30 | expect(actualDate.format('[l]')).toBe('l')
> 31 | expect(actualDate.format('YYYY [l] YYYY')).toBe('1970 l 1970')
32 | expect(actualDate.format('l [l] l')).toBe('1/1/1970 l 1/1/1970')
33 | expect(actualDate.format('[L LL LLL LLLL]')).toBe(expectedDate.format('[L LL LLL LLLL]'))
34 |
at Object.<anonymous> (test/plugin/localizedFormat.test.js:31:46)
FAIL test/plugin/dayOfYear.test.js
● DayOfYear set
expect(received).toBe(expected) // Object.is equality
Expected value to be:
"2015-01-04T00:00:00.000Z"
Received:
"2014-01-05T00:00:00.000Z"
31 | expect(dayjs('2015-01-01T00:00:00.000Z')
32 | .dayOfYear(4)
> 33 | .toISOString()).toBe('2015-01-04T00:00:00.000Z')
34 |
35 | expect(dayjs('2015-01-01T00:00:00.000Z')
36 | .dayOfYear(32)
at Object.<anonymous> (test/plugin/dayOfYear.test.js:33:21)
Test Suites: 3 failed, 68 passed, 71 total
Tests: 8 failed, 648 passed, 656 total
Snapshots: 0 total
Time: 12.402s
Ran all test suites.
error Command failed with exit code 1.
info Visit https://yarnpkg.com/en/docs/cli/run for documentation about this command.
dayjs on dev is 📦 v0.0.0-development took 20s 982ms
➜ git pull
Already up to date.```
I'm getting the same problem now that we're in BST, I'm in Europe/London
timezone and this is what I'm getting:
- Date Time:
2021-04-15 14:00:00
-
console.log(this.$dayjs.tz('2021-04-15 14:00:00', 'UTC').toISOString())
// gives 2021-04-15 14:00:00 -
console.log(this.$dayjs.tz('2021-04-15 14:00:00', 'Europe/London').toISOString())
// gives 2021-04-15 13:00:00
Clearly wrong, any suggestions?
Has this been fixed?. I am having the same issue with a few different time zones that apply daylight savings time, or has anyone been successful with any work around?,
It sounds like it hasn't. I thought #1352 would fix it but others have reported not. Note that these changes are not officially released. In other words, you have to use the dev
branch in order to get the latest changes.
If you're interested, try with the dev
branch and see if it's still broken
It sounds like it hasn't. I thought #1352 would fix it but others have reported not. Note that these changes are not officially released. In other words, you have to use the
dev
branch in order to get the latest changes.If you're interested, try with the
dev
branch and see if it's still broken
Thanks for the tip. I've tried to install the dev branch however it still does not fix my issue.
Seems the issue still in place.
I tried to run this script in different time zones and got quite diverged results.
console.log(dayjs("2027-10-31T00:00:00Z").tz("Europe/Berlin").format());
console.log(dayjs("2027-10-31T01:00:00Z").tz("Europe/Berlin").format()); // +1hr
ServerTime: Wed Sep 15 2021 08:14:59 GMT+0200 (Central European Summer Time)
2027-10-31T02:00:00+02:00
2027-10-31T02:00:00Z
ServerTime: Wed Sep 15 2021 07:15:53 GMT+0100 (British Summer Time)
2027-10-31T02:00:00+03:00
2027-10-31T02:00:00+01:00
ServerTime: Wed Sep 15 2021 16:16:46 GMT+1000 (Vladivostok Standard Time)
2027-10-31T02:00:00+02:00
2027-10-31T02:00:00+01:00
This issue affects also development process when remote developers run it on their own local machines across the world.
I expect to have the same result which is not related to the server's timezone.
Please correct me if I'm wrong.
Seems related to https://github.com/iamkun/dayjs/issues/1412
and also related to: https://github.com/iamkun/dayjs/issues/1408
The problem is still relevant.
The problem is still relevant.
Last November we moved from dayjs
to luxon
and never looked back. This March we now have no issues traversing the DST boundary :)
The problem is still relevant.
Last November we moved from
dayjs
toluxon
and never looked back. This March we now have no issues traversing the DST boundary :)
@CoreyKovalik we have a big project, it will be much longer to switch to luxon, due to the API differences.
a year and a half after this bug was reported and it's still no fixed? damn... should i revert to momentjs after all?
@AComasSamcla I didn't check, but looks like fixed in https://github.com/iamkun/dayjs/releases/tag/v1.11.2
fix UTC plugin .valueOf not taking DST into account (https://github.com/iamkun/dayjs/issues/1448) (27d1c50)
@Serg-Mois oh nice, i checked it and seems fixed now, so probably this issue could be closed by @iamkun too. Thanks!
The issue remains,
"dayjs": {
"version": "1.11.5",
EDIT: updated to 1.11.6, the issue remains.
dayjs.utc("2022-04-22 15:08:45").unix() // wrong, expected: 1650640125, actual: 1650636525
dayjs.utc("2022-08-22 15:08:45").unix() // wrong, expected: 1661180925, actual: 1661177325
dayjs.utc("2022-01-22 15:08:45").unix() // ok, expected: 1642864125, actual: 1642864125
Verified with unixtimestamp.com and python.
EDIT2: If the time is specified this way then the conversion is correct,
dayjs.utc("2022-04-22T15:08:45.000Z").unix() // ok, expected: 1650640125, actual: 1650640125
was there ever a solution for this?
Just noting that this is still an issue in 1.11.10
Any updates on this issue? Still facing the same in latest version
I am with the same problem.
Same here, facing issues with DST handling.
It's been almost 4 years and still no fix.. I just came across the same issue.
It's happening when subtracting 6 months, off by 1 hour
Ha, I came across the same issue and couldn't figure it out until reading this --- is this still not fixed?
I'm not sure if I got this wrong, but for whatever reason setting the 'keepLocalTime' to 'false' will correctly display the time in regions affected by DST.
I have a function like this:
function getHRT(datetime) {
return dayjs(datetime)
.tz(timezone, false) // Put the 'false' here when setting the timezone.
.format(date_format + ', ' + time_format);
}
I'm working with a VILT stack, not sure if that has anything to do with it. Can someone else please confirm?
@amir-huseinspahic I don't think it is meant for that. See when keepLocalTime was introduced: https://github.com/iamkun/dayjs/issues/1149
It seems to me that if you want to say it is 6:00 in timezone 1 and want to change it to 6:00 in timezone 2, then pass in keepLocalTime = true. Otherwise, it will change the time to match the same moment 6:00 in timezone 1.
If keepLocalTime works, that's interesting, but it may not always.
Workaround for me due to confusion on timezones is to use Date directly for this purpose (which I only need in one spot in my code) whereas I use dayjs in many areas.
const date1 = new Date('December 1, 1975 23:15:30 GMT');
const date2 = new Date('August 1, 1975 23:15:30 GMT');
const date3 = new Date('January 1, 1975 23:15:30 GMT');
console.log(date1.getTimezoneOffset());
// Expected output: your local timezone offset in minutes from GMT
// In Pacific time (your results will vary): 480.
console.log(date2.getTimezoneOffset());
// Expected output: your local timezone offset in minutes from GMT
// In Pacific time (your results will vary): 420.
console.log(date1.getTimezoneOffset() === date2.getTimezoneOffset());
// Expected output: false -- Since different offsets due to daylight savings in summer.
console.log(date1.getTimezoneOffset() === date3.getTimezoneOffset());
// Expected output: true -- Same offset in winter despite two different winter dates.
tl;dr - I really just use this only in my code:
const tzOffsetHours = new Date().getTimezoneOffset()/60;
console.log(tzOffsetHours);
// Expected output today, September 17, 2024 (still daylight savings) in the Pacific Time zone in hours: 7
You could instead pull your dayjs formatted date and put into the Date object or compare to the system time somehow or find some other way to use the IANA database if issues with dayjs until dayjs is updated. I played with Date code here: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/getTimezoneOffset
Another workaround: https://github.com/iamkun/dayjs/issues/1388#issuecomment-1436092421
If such a core bug affecting so many users hasn't been fixed in 4 years, consider dayjs
abandoned. Fairwell.