moment / luxon

⏱ A library for working with dates and times in JS
https://moment.github.io/luxon
MIT License
15.35k stars 730 forks source link

Can Luxon retrieve a list of all valid timezone names? #353

Closed Lx closed 5 years ago

Lx commented 5 years ago

I'm trying to implement functionality in my app that allows the user to enter a local time, and then assign a custom timezone identifier for a different timezone (e.g. Asia/Ho_Chi_Minh).

I'm doing this so that due dates/times can be entered relative to a remote location without the user having to manually calculate what the local time of their current location would be at that instant (and maybe get it wrong due to DST changes, etc.).

Luxon lets me retrieve the timezone identifier for the current location:

DateTime.local().zoneName

but I'm having a hard time establishing whether or not Luxon will allow me to retrieve an entire list of all valid identifiers. There's no obvious API call in the docs for this, and I'm mindful that as Luxon calls on the browser, it most likely doesn't maintain its own list internally.

Is there a means of achieving this through Luxon? If not, are there any alternative recommendations for this need?

icambron commented 5 years ago

No, unfortunately, there's no way to do that until the Intl library is able to supply that. I've asked for it to be added, but as far as I know it's not even in the works.

Lx commented 5 years ago

Appreciate the confirmation. Do you have a recommended alternative for this in mind?

As far as I’m aware, Moment Timezone is the most accessible alternative that definitely offers this ability, but I’m certainly open to other libraries if you have suggestions.

icambron commented 5 years ago

@Lx yeah, Moment Timezone is probably your best bet.

zdraganov commented 5 years ago

I'm managed to get the list of supported timezones (and later cache it) using tzdata library and the following code snippet:

const {DateTime} = require('luxon')
const {zones} = require('tzdata')

const luxonValidTimezones = Object.entries(zones)
  .filter(([zoneName, v]) => Array.isArray(v))
  .map(([zoneName, v]) => zoneName)
  .filter(tz => DateTime.local().setZone(tz).isValid)
vvo commented 4 years ago

For anyone reaching this issue, I too needed to get a list of time zones. Even better I wanted to have a simplified list of time zones just like Google Calendar does. When grouping them is safe, let's group them (safe = same country, same DST rules). And I wanted this list of time zone to always be "up to date".

Since I could not find a good list that would also be maintained, I created an npm package for it: https://github.com/vvo/tzdb/. The list of time zones is automatically updated when there are changes (like new time zones).

You can then use luxon if you need to manipulate dates with time zones.

Good luck!

brunolm commented 4 years ago

Is there a way to get the abbreviation with the name?

e.g.

PST America/Los_Angeles
vvo commented 4 years ago

Is there a way to get the abbreviation with the name?

Yes, see https://moment.github.io/luxon/docs/manual/formatting#table-of-tokens. The formatting token is "ZZZZ" (abbreviated named offset), good luck!

justingolden21 commented 3 years ago

Coming in from 2021 here to mention that getting a list of timezones would be useful, so we don't need to copy from an external source, that source then needs to be updated and in sync with luxon, or use another external library which also needs to be in sync with luxon. Luxon already has the information (clearly, since it uses it) so not only should be be easy to add, but much lwoer file size than adding redundant data. Just my 2 cents, love luxon : )

brunolm commented 3 years ago

I agree with @justingolden21 .

And to clarify how I solved the issue, well, I did exactly what Justin said, I have a redundant map that I have to sync.

peetjvv commented 3 years ago

I'm managed to get the list of supported timezones (and later cache it) using tzdata library and the following code snippet:

const {DateTime} = require('luxon')
const {zones} = require('tzdata')

const luxonValidTimezones = Object.entries(zones)
  .filter(([zoneName, v]) => Array.isArray(v))
  .map(([zoneName, v]) => zoneName)
  .filter(tz => DateTime.local().setZone(tz).isValid)

Note that .filter(([zoneName, v]) => Array.isArray(v)) seems to remove some legitimate timezones in Africa, e.g. Africa/Addis_Ababa.

I found this method more foolproof. Also, some of the timezone names from tzdata seems to be duplicated - hence the Set to remove the duplicates. The sort is optional - all depends on whether you need them alphabetised for your use-case.

const {DateTime} = require('luxon')
const {zones} = require('tzdata')

const luxonValidTimezones = [
  ...new Set<string>(
    Object.keys(tzdata.zones).filter(
      tz => tz.includes('/') && DateTime.local().setZone(tz).isValid
    )
  ),
].sort((a, b) => (a < b ? -1 : 1));
sfertman commented 2 years ago

Is there a way to get the abbreviation with the name?

Yes, see https://moment.github.io/luxon/docs/manual/formatting#table-of-tokens. The formatting token is "ZZZZ" (abbreviated named offset), good luck!

Link changed to: https://moment.github.io/luxon/#/formatting?id=table-of-tokens

RocksSavage commented 2 years ago

Hello from the future! It's 2022 now, and @icambron's wish has finally come true.

I thank this post: https://stackoverflow.com/questions/38399465/how-to-get-list-of-all-timezones-in-javascript

The Intl browser API, which I understand Luxon depends on, now provides a nice method to get a list of all valid timezone names.

let ary = Intl.supportedValuesOf('timeZone');

dblinkhorn commented 2 years ago

I wish this list had the offset amounts included in the name for each timezone!

nik32 commented 1 year ago

Intl.supportedValuesOf('timeZone')

Hello from the future! It's 2022 now, and @icambron's wish has finally come true.

I thank this post: https://stackoverflow.com/questions/38399465/how-to-get-list-of-all-timezones-in-javascript

The Intl browser API, which I understand Luxon depends on, now provides a nice method to get a list of all valid timezone names.

let ary = Intl.supportedValuesOf('timeZone');

@RocksSavage This still doesn't seem to be supported yet. Using it in NextJS with TS gives me error -

image

This is just not some TS error. TS is indeed correct as trying to print the array in NextJS throws me an error.

image
IamParadoxdotexe commented 1 year ago

This is just not some TS error. TS is indeed correct as trying to print the array in NextJS throws me an error.

I was able to get an array of timezones in NextJS with TS without any errors by just casting Intl to any:

const timezones = (Intl as any).supportedValuesOf('timeZone');

It returns an array like

(428) ['Africa/Abidjan', 'Africa/Accra', 'Africa/Addis_Ababa', 'Africa/Algiers', 'Africa/Asmera', 'Africa/Bamako', ...
IamParadoxdotexe commented 1 year ago

I wish this list had the offset amounts included in the name for each timezone!

You can calculate the offsets yourself by modifying the zone on DateTime.local, like this:

const offset = DateTime.local({ zone: 'Africa/Abidjan' }).toFormat('Z');
console.log(offset);
> +0

You can use this to create a mapping of timezone offsets, like this:

const timezones: { [zone: string]: string } = {};
for (const zone of (Intl as any).supportedValuesOf('timeZone')) {
      timezones[zone] = DateTime.local({ zone }).toFormat('Z');
}
nik32 commented 1 year ago

This is just not some TS error. TS is indeed correct as trying to print the array in NextJS throws me an error.

I was able to get an array of timezones in NextJS with TS without any errors by just casting Intl to any:

const timezones = (Intl as any).supportedValuesOf('timeZone');

It returns an array like

(428) ['Africa/Abidjan', 'Africa/Accra', 'Africa/Addis_Ababa', 'Africa/Algiers', 'Africa/Asmera', 'Africa/Bamako', ...

Hey @IamParadoxdotexe, thanks for taking time to reply!!! I did what you said, and it does stop the TS error, but it still gave me error. After experimenting a bit I found out, that the reason being - that I was calling supportedValuesOf() outside the component itself [As I did not want to call the function again and again with the re-rendering of the component]. Can you explain this behaviour of why calling it outside the component caused the error whereas calling it inside the component was fine??

IamParadoxdotexe commented 1 year ago

Can you explain this behaviour of why calling it outside the component caused the error whereas calling it inside the component was fine??

I'm not exactly sure what is causing this behavior (since logging confirms supportedValuesOf exists) but I found this workaround:

const timezones = (Intl as any).supportedValuesOf && (Intl as any).supportedValuesOf('timeZone');

For some reason, explicitly checking for supportedValuesOf before calling it gets rid of the error. And if you're not familiar, this type of statement is called a short circuit and is equivalent to:

let timezones: string[] | undefined = undefined;
if ((Intl as any).supportedValuesOf) {
    timezones = (Intl as any).supportedValuesOf('timeZone');
}
nik32 commented 1 year ago

@IamParadoxdotexe this is a bit strange to me - coz adding the condition is indeed giving me the timezone on the frontend. If the function is actually present [as evident by the timezone list I am getting, after the condition], then why give 'Intl.supportedValuesOf is not a function'??

IamParadoxdotexe commented 1 year ago

@nik32 Yah, it's definitely unexpected behavior. My best guess is that NextJS hasn't fully imported the Intl namespace on the initial hydration. But then the act of checking for the function finishes that import. Completely a hunch, but that's my best guess.

Another option of getting around this is to just call it in an onMounted useEffect, like this:

useEffect(() => {
    timezones = (Intl as any).supportedValuesOf('timeZone');
    ...
}, []);

This will only be called on the first render and will be inside the component.

nik32 commented 1 year ago

@IamParadoxdotexe Okay I think I get the problem. The main cause is SSR. The Intl.supportedValuesOf() is client specific and thus calling it on server would lead to the 'Intl.supportedValuesOf is not a function' error [that is why, if you see the heading of the NextJS stack trace - its 'Server Error'].

Now as to why calling it inside the component worked and did not give the 'Server Error'?? I feel that's because the component is not a normal component, its a special kind of HOC!!! The HOC I am using is ebay's NiceModal.create() - for easier management of modals. This HOC is giving me the modal component, in which I am calling supportedValuesOf().

Now maybe I think this NiceModal.create() is preventing the call of supportedValuesOf() on the server [NiceModal gives the modal component only when we call its NiceModal.show()], and thus the error is not occurring if we try to get timezones inside the component.