date-fns-tz
date-fns-tz copied to clipboard
date-fns-tz v2 proposal
These are the proposed functions for date-fns-tz
version 2. Comments are welcome.
parseUTC
Parse a date string representing UTC time to a Date
instance. This is a reexport of date-fns/parseJSON
with a more semantically relevant name.
localToZonedTime
Transform a Date
instance in the local system time zone, i.e. when it is passed to format
it would show the local time, to a zoned Date
instance of the target time zone, i.e. when passed to format
it would show the equivalent time in that time zone.
This is achieved by modifying the underlying timestamp of the Date
. In other words, since a JS Date
can only ever be in the system time zone, the way to fake another time zone is to change the timestamp.
Matching the convention set by date-fns
this function does not accept string arguments. A date string should be parsed with parseUTC
, parseISO
or parse
first.
zonedToLocalTime
Transform a Date
instance representing the time in some time zone other than the local system time zone to a Date
instance with the equivalent value in the local system time zone, i.e. formatting the result or calling .toString()
will show the equivalent local time.
This is achieved by modifying the underlying timestamp of the Date
. It is the inverse of localToZonedTime
, so in this case it is assumed the input is faking another time zone, and the timestamp is changed to be correct in the current local time zone.
Matching the convention set by date-fns
this function does not accept string arguments. A date string should be parsed with parse
first.
utcToZonedTime
This is an alias of localToZonedTime
which can be used when it makes better semantic sense, such as when a UTC date received from an API is being parsed for display in a targed time zone. Since a UTC time will usually be provided as a string value, this function accepts UTC date strings that can be parsed by the parseUTC
function and does so internally.
zonedTimeToUTC
This is an alias of zonedToLocalTime
which can be used when it makes better semantic sense, such as when the intent is to save the UTC time of a date to an API. The resulting Date
which formats correctly in the system time zone has the desired internal UTC time, and thus the actual UTC value can be obtained from zonedTimeToUTC(...).toISOString()
or .getTime()
.
format
Full time zone formatting support, without any modification to the date instance. (No changes from format
in v1, except that it will no longer accept string inputs.)
formatAsZonedTime
A combination of utcToZonedTime
(or localToZonedTime
) and format
. In other words the date will be transformed to the target time zone specified in the options prior to formatting. Since utcToZonedTime
is used internally the date argument can also be a string that can be parsed by parseUTC
.
parseISO
As in date-fns
the previous implementation of toDate
in date-fns-tz@1
has been renamed to parseISO
. It extends the date-fns
version of this function with better time zone support. It is a rather large function, though, and should only be used when dates are not in ISO format or date strings represent a time zone other than UTC time. When this is not the case parseUTC
should be used instead.
@dcousens, @kroleg
since a JS Date can only ever be in the system time zone
I think that JS Date actually always UTC (because it only stores timestamps inside) and it appears as it is in system time zone because when you call Date.toString()
or any other date formatting function JS adds system timezone offset and formats timezoned date. Instance of Date represents a point in time and should be treated as such i.e. there should be no need to add offsets only for formatting purpose.
So from this point of view i think that date-fns-tz needs to export only 2 methods: parseFromTimezone(datetime: string, timeZone: string)
and formatAsZonedTime(datetime: Date, { timeZone, ...otherDateFnsOptions)
.
I think reasoning about local
or UTC
is confusing. It is a Date
, with it's semantics.
How about the following?
shiftDateTo(date, timeZone)
Shift date
such that date.valueOf
is changed by the target time zone's relative offset to the local time.
For example, if in Australia (AEDT)
const fiveAm = new Date('2019-12-29 05:00') // 5am in local time
const result = shiftDateTo(fiveAm, 'Asia/Tokyo')
// result is 3am in local time (Australia), 5am in Tokyo
unshiftDateFrom(date, timeZone)
Unshift date
such that date.valueOf
changes to "return" to local time a date that is wrong or was missing timezone information. For example, the input was kept in a timezone ambiguous format like 2019-12-29 05:00
that is was local to Asia/Tokyo
, and you need the time in actual local time.
const fiveAm = new Date('2019-12-29 05:00') // 5am in local time
const result = unshiftDateFrom(fiveAm, 'Asia/Tokyo')
// result is 7am in local time (Australia), 5am in Tokyo
That way, the following identity should hold
const a = new Date('2019-12-29 05:00') // 5am in local time
const b = shiftDateTo(a, 'Asia/Tokyo') // 3am in local time
const c = unshiftDateFrom(b, 'Asia/Tokyo') // 5am in local time
edit: renamed again
@dcousens why would you shift date in the first place? Isn't it easier to just pass a time zone whe you parse string date like parseFromTimeZone('2019-12-29 05:00', 'Asia/Tokyo')
?
@kroleg I'll try and outline a few scenarios.
Scenario 1
Imagine we have a server in Asia/Tokyo
, and a user in Australia/Sydney
.
I want to see if their last post was after 11am user time.
const lastPostedAt = new Date(user.posts[0].createdAt)
const local11am = new Date()
local11am.setHours(11)
local11am.setMinutes(0)
local11am.setSeconds(0)
local11am.setMilliseconds(0)
const users11am = shiftDateTo(local11am, user.timezone)
if (lastPostedAt > users11am) {
// the last post was after 11am user-time on this calendar day
}
Scenario 2
For a second scenario, I have a friend in San Francisco that has provided me with a date and time string 2019-12-30 14:00
for a web meeting; and I know their timezone is US/Pacific
.
I need to know what the local time for that web meeting would be so I can attend.
const provided = new Date(`2019-12-30 14:00`)
const local = unshiftDateFrom(provided, `US/Pacific`)
// local is 9am on Tuesday 31st October (local time)
Arguably, yes, the second scenario could use parseFromTimeZone
, however, there are complicated instances that I unfortunately need/want to use the Date
instead.
@dcousens for scenario 1 there is a much easier way to get local time:
const local11am = parseFromTimeZone('11:00', user.timezone);
And then you compare Date instances lastPostedAt > users11am
Edit: As i mentioned above Date
is just a timestamp. There are only 2 Date use cases: 1) you need to parse date (from string or timestamp). 2) You need to display date in some time zone
@kroleg 11:00
is an Invalid Date
.
I don't always use today's calendar date.
I don't want to transform a Date
instance to a string
accounting for local time myself. I am using this library for that.
@dcousens you gave me 2 problems and i told you how you can solve them with only 2 functions. Do you have another problems/secenarios you forgot to mention?
11:00 is an Invalid Date.
parseFromTimezone
fucntion may assume "today" if date is missing (not sure if it does right now)
I don't always use today's calendar date.
then pass full datetime like parseFromTimezone('2019-10-11 11:10')
You actually haven't provided 2 solutions, you've merely pointed at a string-parsing equivalent of shiftDateTo
and claimed it can resolve both problem(s).
Please post a working solution for when you have a Date
object that has a valueOf
that is in some other's local time, not UTC - see Scenario 2, but imagine all you are given is the provided
date object, no strings.
Thank you both for the discussion so far.
I believe it makes sense to have functions for shifting Date instances as well as functions with string input/outputs, i.e. parse and format. Firstly, the former is a requirement to implement the latter and as such might as well be exposed as low level functions for use when it makes sense. Aside from the examples given by @dcousens (which might be implementable starting/ending with strings, but depending on preference in a less elegant way), a further issue would be use alongside other libraries. Most notably date picker elements often take Date instances as input/output, so it must be possible to work with Dates.
I like the idea of not having aliased functions but rather two shifting functions that don't reference local/UTC if that is confusing (and I agree it can be). The suggestions are good, and I'll give the names a bit more thought.
Other than those I'm starting to like the idea of then overriding the format, parse, parseISO, and parseJSON as parseUTC
functions from date-fns
to add built in time zone support, i.e. they will shift the dates and format/parse in one step. Then these could take the time zone as an explicit parameter rather than on the options, which will also make them better suited for convenient functional programming variants.
Most notably date picker elements often take Date instances as input/output, so it must be possible to work with Dates.
I somehow failed to convey that this is exactly the scenario as to why strings were not suitable :+1: - 3rd party libraries.
formatAsZonedTime
is very welcome, because currently it looks like it must be done as below:
format(
utcToZonedTime(dateStringInUtc, TIME_ZONE),
DATE_FORMAT,
{timeZone: TIME_ZONE},
);
vs
moment.tz(dateStringInUtc, TIME_ZONE).format(DATE_FORMAT)
The time zone must be specified twice, which is overly verbose.
Correct me if I'm wrong.
I'd love date-fns-tz
to be just a wrapper around format, parse, parseISO, parseJSON
(and perhaps a few other, like startOfDay
, etc.). I would probably not rename parseJSON
to parseUTC
. I agree that the name is awkward but it might be confusing for date-fns-tz
and date-fns
to use different nomenclature.
I'd like to add my two cents about the shifting functions. Even though the shifting logic is necessary to use Intl
for formatting/parsing it still seems confusing to me to expose the shifting functions as a part of the official API. My main issue with them is that they basically return sort of a "broken" Date
object that by itself doesn't really contain all the data necessary to understand what time it is representing.
JavaScript's Date
, as @kroleg mentioned, is internally always UTC. That makes it unambiguous what point in time it represents. Unfortunately, Date
does not allow storing any time zone information, so shifting the time in it puts it in a weird state when we no longer can be sure what point in time it represents without the context it was created in (and that context can be easily lost in bigger code bases).
I do understand @dcousens's argument that these functions could be useful for integrating with other libraries. In my opinion, it would be less confusing if they were clearly marked as a more of a plumbing API rather than the primary API of date-fns-tz
. Perhaps they could be exported as date-fns/compat
or similar with docs stating their purpose (and the problem with the Date objects they returned that I mentioned above).
An alternative approach would be to provide a wrapper class around Date
that contains the time zone, but that would go against date-fns
philosophy.
Thanks for your thoughts @jgonera. The point about renaming parseJSON
is well taken.
I don't think I'll put the shifting functions in a separate date-fns-tz/compat
, but it does make sense to treat them more like plumbing functions than the primary API. I think the key is to never shift a date and then pass it around in a large code base. Shifting should happen when dates go into or come out of the UI, and that's about it. This can be reflected in the documentation of those functions.
I'm just starting to use date-fns[-tz] and I love all of it :heart: except utcToZonedTime
/ zonedTimeToUtc
scare me :worried:
Normally I think of Date
as a moment in time, with accessor methods that just happen to compute calendar day/hour/... components in local TZ.
But when shifting, I'd be also passing around "naive" Date
that no longer represent a moment in time at all, but rather a struct of calendar day/hour/... components. It feels dangerous to mix both semantic types represented by same class.
I agree there are low-level use cases for the 2nd "calendar" meaning, I just wish the API was shaped differently so code using the 1st vs 2nd meaning look different...
Actually, in a sense the base format
from date-fns
is already interpretting its Date
parameter in the "calendar components" sense. That's subjective — it also fits the "moment+local interpretation" sense — but if you think of the time-zone format patterns it supports (XX..X
, xx..x
, zz..z
, OO..O
) those are weird because they don't represent anything about the passed-in Date! They always represent the system TZ.
And the extended format
from date-fns-tz collapses the ambiguity towards "calendar" meaning by supporting a timeZone
param that overrides the timezone, but does NOT adjust the time:
> require('date-fns').format(new Date(), 'HH:mm XXX')
'18:46 +03:00'
> require('date-fns').format(new Date(), 'HH:mm XXX', {timeZone: 'Asia/Singapore'}) // just ignored
'18:46 +03:00'
> require('date-fns-tz').format(new Date(), 'HH:mm XXX')
'18:46 +03:00'
> require('date-fns-tz').format(new Date(), 'HH:mm XXX', {timeZone: 'Asia/Singapore'})
'18:46 +08:00'
> require('date-fns-tz').format(new Date(), 'HH:mm XXX', {timeZone: 'UTC'})
'18:46 Z'
Thinking/reading more about it, I see many functions in date-fns
treat dates in "calendar" sense, eg. startOfDay
(https://github.com/date-fns/date-fns/issues/669).
These don't make sense as "function of a moment" because they're timezone-dependent, but they do mostly make sense as "function of calendaric date&time". So they can be mostly used via shifting, except that gets problematic for those moments when some date/time just didn't exist in some timezone (or exists twice), due to DST / one-off political jumps :-(
Agree @cben a date is a moment in time that accounts for DST as well.
moment can handle ISO8601 timestamps with offset such as 2013-02-01T12:52:34+09:00
I would expect a date library to format that timestamp into a specific timezone that was passed in as a 2nd parameter.
How would one go about parsing a date string with a known format zoned to a known TZ? Please correct me if I'm wrong but, as I understand it, there's not built-in way (neither in the current version or the proposed v2 API) to do this.
Here's how you would accomplish this in other popular date handling libraries:
// dayjs using `timezone` plugin
dayjs.tz("11-20-2020", "America/Los_Angeles").format();
// '2020-11-20T00:00:00-08:00'
// moment using `moment-timezone`
moment.tz('11-20-2020', 'MM-DD-YYYY', 'America/Los_Angeles').format();
// '2020-11-20T00:00:00-08:00'
DateTime.fromFormat('11-20-2020', 'MM-dd-yyyy', { zone: 'America/Los_Angeles' }).toISO();
// '2020-11-20T00:00:00-08:00'
IMHO, this is a fairly basic use case scenario when it comes to timezones and date-fns
should provide a way of allowing for this.
@nfantone the functionality you are after is possible by using the parse
function from date-fns
:
import {parse} from 'date-fns'
import {format, zonedTimeToUtc} from 'date-fns-tz'
const dateForLA = parse('11-20-2020', 'MM-dd-yyyy', new Date()) // new Date is a required arg but ignored, see date-fns#2849
// We know this date represents the time in LA, so it is passed to format as is,
// with the `timeZone` needed to get the correct value for the xxx token
const output = format(dateForLA, 'yyyy-MM-dd\'T\'HH:mm:ssxxx', { timeZone: 'America/Los_Angeles' })
console.log(output) // 2020-11-20T00:00:00-08:00
// If you want to send the proper UTC time to a server or save to a database, use
const date = zonedTimeToUtc(parsed, 'America/Los_Angeles')
console.log(date.toISOString()) // 2020-11-20T08:00:00.000Z
The question is whether there is a way to make this more intuitive.
Edit:
After thinking about it this is probably the proper way of doing it in v2:
import {parse, formatAsZonedTime} from 'date-fns-tz'
// or parseAsZonedTime?
const date = parse('11-20-2020', 'America/Los_Angeles', 'MM-dd-yyyy')
console.log(date.toISOString()) // 2020-11-20T08:00:00.000Z
const output = formatAsZonedTime(date, 'America/Los_Angeles', 'yyyy-MM-dd\'T\'HH:mm:ssxxx')
console.log(output) // 2020-11-20T00:00:00-08:00
@marnusw Hi! Thanks for your answer.
May be so - honestly, it's been over a year since I posted the above. Regardless, the fact that you even had to think about how to go about parsing a zoned data kinda goes in the same line as my original point.
@nfantone O I see. When I looked at it I thought it was this past October.
Thank you for your input, though, it has made me think and contributes toward the final API.
Not sure if it's possible now, get GMT version of timezone offset: smth('Africa/Casablanca') -> GMT+01:00
.
I believe it's OOOO
in format, but I have no idea how to get it with just string timezone, an example would be also great.
@vladshcherbin use format
with any Date and time zone on the options and just pass OOOO
as the format string.
@marnusw the thing is, I only have a timezone, just Africa/Casablanca
.
Maybe there could be a simple helper for such things, e.g. formatTimezone('Africa/Casablanca', 'OOOO')
-> GMT+01:00
What do you think?
Africa/Casablanca is a timezone.
+01:00 is an offset.
Timezones change their offsets over time, regularly due to daylight-savings, and irregularly due to political decisions... Here is an example of winter vs summer difference:
> require('date-fns-tz').format(new Date(2021, 12), 'OOOO', {timeZone: 'Asia/Jerusalem'})
'GMT+02:00'
> require('date-fns-tz').format(new Date(2021, 06), 'OOOO', {timeZone: 'Asia/Jerusalem'})
'GMT+03:00'
So a simple mapping TZ->offset is ill-defined. You can pass new Date()
for present offset, but beware that writing code that treats such offset as the offset for a particular TZ risks being buggy if it later applies it to any other timestamp.