Open pascuflow opened 1 month ago
Hi, I am thinking about the same thing. We need something like toZonedTime from date-fns-tz, so I was trying to simulate it with TZDate. But after playing around with it, I found out some behaviours that do not seem intuitive to me when creating instances, so I wanted to share. For reference, I am working in Pacific Time zone (GMT-7) and creating dates in zone Europe/Prague (GMT+2). I am located in Prague but for testing purposes for our project, I switch to Pacific time using the Windows Date & time settings.
// this results in Sun Oct 20 2024 03:00:00 GMT+0200
new TZDate(2024, 9, 20, 3, 'Europe/Prague').toString()
// this is Sun Oct 20 2024 12:00:00 GMT+0200
new TZDate('2024-10-20 03:00:00', 'Europe/Prague').toString()
// and this is again Sun Oct 20 2024 03:00:00 GMT+0200
parse('2024-10-20 03:00:00', 'yyyy-MM-dd HH:mm:ss', TZDate.tz('Europe/Prague')).toString()
What I find mainly interesting is that the first two examples return different dates actually. The first one takes the number arguments and sets the month/day/hours in the target timezone. While the second one takes the string date and converts it from my local timezone to the target timezone, therefore adding 9 hours. The last example seems the most obvious in that the string is parsed using the reference date, which is in GMT+2.
So my question is, is this the intended behaviour? In our app, we would benefit from both approaches - converting a date to target timezone and creating a date in the target timezone. We would like to get rid of date-fns-tz, but there is no straightforward alternative in @date-fns/tz. What would be the correct approach to this? Should I perhaps create a separate issue for this?
Thanks :)
I created my own simplified toZonedTime
function using tzOffset
from @date-fns/tz
.
import { tzOffset } from '@date-fns/tz'
/**
* Returns a date instance with values representing the local time in the time zone specified of the UTC time from the date provided.
* In other words, when the new date is formatted it will show the equivalent hours in the target time zone regardless of the current system time zone.
* Taken from date-fns-tz https://github.com/marnusw/date-fns-tz/blob/00b48b2cd4505a69203aac5734773114bce13204/src/toZonedTime/index.ts
*
* Why this function is needed:
* - `date-fns-tz` v3.x is not compatible with `date-fns` v4.x yet, see: https://github.com/marnusw/date-fns-tz/issues/300
* - `date-fns` now provides its own `@date-fns/tz` package
* - `@date-fns/tz` doesn't have an equivalent of the `toZonedTime()` function, see: https://github.com/date-fns/tz/issues/9
* - Unfortunately `toZonedTime()` is essential in our business logic
*
* @param date — the date with the relevant UTC time
* @param timeZone — Time zone name (IANA or UTC offset)
*/
export function toZonedTime(date: Date | string | number, timeZone: string): Date {
const _date: Date = typeof date === 'string' || typeof date === 'number' ? new Date(date) : date
// The tzOffset function allows to get the time zone UTC offset in minutes from the given time zone and a date
const offsetMilliseconds = tzOffset(timeZone, _date) * 60 * 1000
const d = new Date(_date.getTime() + offsetMilliseconds)
const resultDate = new Date(0)
resultDate.setFullYear(d.getUTCFullYear(), d.getUTCMonth(), d.getUTCDate())
resultDate.setHours(d.getUTCHours(), d.getUTCMinutes(), d.getUTCSeconds(), d.getUTCMilliseconds())
return resultDate
}
const today = toZonedTime(new Date(), 'Europe/Moscow')
Hi, I am thinking about the same thing. We need something like toZonedTime from date-fns-tz, so I was trying to simulate it with TZDate. But after playing around with it, I found out some behaviours that do not seem intuitive to me when creating instances, so I wanted to share. For reference, I am working in Pacific Time zone (GMT-7) and creating dates in zone Europe/Prague (GMT+2). I am located in Prague but for testing purposes for our project, I switch to Pacific time using the Windows Date & time settings.
// this results in Sun Oct 20 2024 03:00:00 GMT+0200 new TZDate(2024, 9, 20, 3, 'Europe/Prague').toString() // this is Sun Oct 20 2024 12:00:00 GMT+0200 new TZDate('2024-10-20 03:00:00', 'Europe/Prague').toString() // and this is again Sun Oct 20 2024 03:00:00 GMT+0200 parse('2024-10-20 03:00:00', 'yyyy-MM-dd HH:mm:ss', TZDate.tz('Europe/Prague')).toString()
What I find mainly interesting is that the first two examples return different dates actually. The first one takes the number arguments and sets the month/day/hours in the target timezone. While the second one takes the string date and converts it from my local timezone to the target timezone, therefore adding 9 hours. The last example seems the most obvious in that the string is parsed using the reference date, which is in GMT+2.
So my question is, is this the intended behaviour? In our app, we would benefit from both approaches - converting a date to target timezone and creating a date in the target timezone. We would like to get rid of date-fns-tz, but there is no straightforward alternative in @date-fns/tz. What would be the correct approach to this? Should I perhaps create a separate issue for this?
Thanks :)
Might be related: #10
Might be related: #6
Yes, the transpose function does the job, thanks, I completely missed that. So I assume that the different behaviour for different constructors is meant to be this way? It is slightly confusing that the resulting dates have different underlying timestamp, but I guess it can be somehow justified :).
I just searched for all import { toZonedTime } from 'date-fns-tz' in my IDE and replaced it with import { TZDate } from "@date-fns/tz";
Then i searched for "toZonedTime" and replaced it with "new TZDate"
Everything seem to work as expected
That should work in most cases perhaps, but for example we get date string from backend that is without time zone and we need to interpret it as if it was in a particular time zone, no matter where the user is located. So we actually need the underlying milliseconds to change and for that the transpose function is great.
new TZDate('2024-10-20 03:00:00', 'Europe/Prague')
I think the underlying problem is that TZDate wants to be a thin wrapper around Date, and so that date/time string ends up getting parsed by the Date constructor, and then the resulting absolute time will be a result of the local system timezone. This is unfortunate because clearly you don't want anything to do with the local system timezone in that code.
tl;dr - use parseISO
or parse
Since date-fns 4.1.0 now includes first-class time zone support, would like to stop using date-fns-tz but having trouble getting Date from TZDate in a simple way.
For example, how would one migrate toZonedTime?