date-fns-tz icon indicating copy to clipboard operation
date-fns-tz copied to clipboard

Unreliable conversions with zonedTimeToUtc

Open Ketec opened this issue 3 years ago • 5 comments

zonedTimeToUtc

Sun Jul 09 2022 21:00:00 GMT+0000 <- input

Sat Jul 09 2022 14:00:00 GMT+0000 <- output

10 'Australia/Melbourne' getTimezoneOffset 

That is a difference of 7 hours.

Where do these wide swings come from?

Ketec avatar Jul 20 '22 10:07 Ketec

Agree. Also for me, JEST fails at the second line, a proof for this issue:

expect(getTimezoneOffset('Asia/Jerusalem', new Date('1995-12-17T03:23:00Z'))).toBe(2 * 60 * 60 * 1000);
expect(zonedTimeToUtc(new Date('1995-12-17T03:23:00Z'), 'Asia/Jerusalem')).not.toEqual(new Date('1995-12-17T03:23:00Z'));

Tal500 avatar Jul 27 '22 17:07 Tal500

As a temporary fix, here is my implementation to these function, using the offset function of this library:

import { getTimezoneOffset } from 'date-fns-tz';

export function utcToZonedTime(date: Date | number, timeZone: string) {
	const offset = getTimezoneOffset(timeZone, date);

	return new Date(date.valueOf() + offset);
}

export function zonedTimeToUtc(date: Date | number, timeZone: string) {
	const offset = getTimezoneOffset(timeZone, date);

	return new Date(date.valueOf() - offset);
}

It passes the following Jest test:

test('check the alternative self implemented `utcToZonedTime` and `zonedTimeToUtc`', () => {
	expect(getTimezoneOffset('Asia/Jerusalem', new Date('1995-12-17T03:23:00Z'))).toBe(2 * 60 * 60 * 1000);
	expect(utcToZonedTime(new Date('2022-07-27T18:11:40Z'), 'Asia/Jerusalem')).toEqual(new Date('2022-07-27T21:11:40Z'));
	expect(zonedTimeToUtc(new Date('2022-07-27T21:11:40Z'), 'Asia/Jerusalem')).toEqual(new Date('2022-07-27T18:11:40Z'));
});

Tal500 avatar Jul 27 '22 18:07 Tal500

Yeah, I'm having odd behaviour where this behaviours perfectly in Jest, but as soon as I run it in the the browser it just turns things into UTC time.

eg code:


    const date = {
        day: 2, 
        monthIndex: 7, 
        fullYear: 2022
    }; 
    const tz = "Australia/Melbourne"; 
    
    const utcDate = new Date(Date.UTC(date.fullYear, date.monthIndex, date.day, 0,0)); 
    const tzDate = zonedTimeToUtc(utcDate, tz);
    const rString = tzDate.toISOString(); 
    console.log({date, tz, rString}); 

Jest output:

    {
      date: { fullYear: 2022, monthIndex: 7, day: 2 },
      tz: 'Australia/Melbourne',
      rString: '2022-08-01T14:00:00.000Z'
    }

Browser output: (Locale is Melbourne)

{
    "date": {
        "day": 2,
        "monthIndex": 7,
        "fullYear": 2022
    },
    "tz": "Australia/Melbourne",
    "rString": "2022-08-02T00:00:00.000Z"
}

dwjohnston avatar Aug 02 '22 03:08 dwjohnston

Seeing same, @Tal500 workaround fixed it 🙏🏻

davetapley avatar Sep 16 '22 00:09 davetapley

Remember that with this library you should never be looking at the UTC time stored within a date. The idea is to have the "local" values of the date be "wrong" in the sense that it shows the time in the target time zone instead of the system local time zone. To accomplish that, the internal UTC time of the date is changed.

I didn't follow everything happening here with a quick read, but one problem that caught my eye is this:

const utcDate = new Date(Date.UTC(date.fullYear, date.monthIndex, date.day, 0,0)); 
const tzDate = zonedTimeToUtc(utcDate, tz);

This is just wrong. You should be using const zonedDate = utcToZonedTime(utcDate, tz). If you do that and then format the output you should see whatever UTC time you've put in printed in the local time of tz.

The only time you'd really use zonedTimeToUtc would be if a user entered a date which represents a time in a time zone other than their system time zone. Then you need this function to convert that date to the proper UTC time of the user's intended date. But in the example above you have a proper UTC time to start with, so there is no need to ever convert it unless it will be shown in the UI.

marnusw avatar Sep 16 '22 08:09 marnusw