luxon icon indicating copy to clipboard operation
luxon copied to clipboard

Suggestions for v2.0

Open GillesDebunne opened this issue 4 years ago • 12 comments

These are ideas for potential breaking changes that could be introduced in v2.0, all open for discussion.

  1. Settings.setDefaultZone instead of Settings.defaultZone setter since input type differs from getter
  2. IANAZone constructor made private, use create() instead to benefit from the cache
  3. Rename Zone.universal to isUniversal
  4. New DateTime.now() method equivalent to local with no parameter
  5. Rename options.setZone to keepZone in DateTime constructors' options
  6. Add a new DateTime.plus/minus(12, 'hours') method
  7. Deprecate plus/minus(ms), i.e. a number parameter which has no obvious meaning when reading the code
  8. In DateTime, resolvedLocaleOpts, toLocaleString and toLocaleParts all accept an options parameter which is a union of LocaleOptions (i.e. { locale, outputCalendar, numberingSystem } already used in reconfigure and toFormat) and Intl.DateTimeFormatOptions. These could be split into two options parameters instead, each with a different role
  9. DateTime::resolvedLocaleOpts returns an { locale, outputCalendar, numberingSystem } object, whereas Intl.ResolvedDateTimeFormatOptions returns { locale, calendar, numberingSystem (and more) }. outputCalendar could be renamed to calendar instead to be more consistent.
  10. Rename DateTime::resolvedLocaleOpts to resolvedLocaleOptions
  11. DateTime.toSeconds returns an integer value (see #565), so that it can be used in Unix timestamps creation, and can otherwise be replaced by toMillis() / 1000

GillesDebunne avatar May 30 '20 21:05 GillesDebunne

Thoughts:

  1. Settings.setDefaultZone instead of Settings.defaultZone setter since input type differs from getter - agreed
  2. IANAZone constructor made private, use create() instead to benefit from the cache - does this break any testing scenarios?
  3. Rename Zone.universal to isUniversal - agree
  4. New DateTime.now() method equivalent to local with no parameter - yes, I've found myself wanting this too
  5. Rename options.setZone to keepZone in DateTime constructors' options - I keep going back and forth on this one. Sure, we can change it
  6. Add a new DateTime.plus/minus(12, 'hours') method - this one I don't like. I don't think it's harder to say {hours: 12} than "hours", 12
  7. Deprecate plus/minus(ms), i.e. a number parameter which has no obvious meaning when reading the code -- agree
  8. In DateTime, resolvedLocaleOpts, toLocaleString and toLocaleParts all accept an options parameter which is a union of LocaleOptions (i.e. { locale, outputCalendar, numberingSystem } already used in reconfigure and toFormat) and Intl.DateTimeFormatOptions. These could be split into two options parameters instead, each with a different role - agree. This is parallel to the change we made to fromObject where the units and the configure options are separate args
  9. DateTime::resolvedLocaleOpts returns an { locale, outputCalendar, numberingSystem } object, whereas Intl.ResolvedDateTimeFormatOptions returns { locale, calendar, numberingSystem (and more) }. outputCalendar could be renamed to calendar instead to be more consistent. -- I'm not sure about this. Using outputCalendar was meant to clarify that we aren't actually operating in that calendar (e.g. plus/minus won't change the months in that calendaring system) just outputting strings in it. I worry if we just make it calendar no one will look twice before assuming it understands other calendars more deeply than it does.
  10. Rename DateTime::resolvedLocaleOpts to resolvedLocaleOptions -- agree
  11. DateTime.toSeconds returns an integer value (see #565), so that it can be used in Unix timestamps creation, and can otherwise be replaced by toMillis() / 1000 -- if we're going to round it (or floor it, i guess), we shouldn't call it toSeconds, because that's just inaccurate. Maybe toUnix?

icambron avatar Jun 02 '20 06:06 icambron

Thoughts:

2 I though this came from the 2.0 branch. I cannot remember why I did it in my branch, although it seems reasonable. I think all I did was to update the tests (24 occurrences) to use create instead of new

5 I agree the name should be more explicit. The impact of an undefined zone is also not obvious. The examples should show the results of these different options

6 Since I believe 90% of the case apply a single unit, this version seems more readable as an English sentence. At least that's what I wrote first before looking at the documentation.

9 makes sense

11 toUnix feels too specific, and the timestamp precision seems to depend on the file system. What about asSeconds, which indicates a conversion? toSeconds could be deprecated to reduce the footprint.

GillesDebunne avatar Jun 03 '20 08:06 GillesDebunne

2 - ok with me then

6 - thinking back to when I first decided this. One big reason I didn't do this is that it encourages chaining the operations like dt.plus(11, 'hours').plus(12, 'days') etc. The problem with that is that the order matters. That was a common support issue in Moment and I was hoping to avoid it by simply discouraging it through API design. You can of course still chain like that, but it's sort of obvious that you don't have to. Then Luxon can use the ordering of the operations that matches people's expectations (highest-order-to-lowest-order)

11 - what about just being even more explicit: toSecondsFloored?

icambron avatar Jun 03 '20 17:06 icambron

  1. Fair point, let's keep the current api

for 11. I think i still prefer asSeconds, but no hard feelings. An other option is to deprecate toSeconds since it seems misleading, and let people use toMillis instead (or even the implicit valueOf())

gdebunne avatar Jun 03 '20 19:06 gdebunne

Notes:

7. affects DateTime and Duration plus/minus as well as Interval.after/before/splitBy.

8. I actually chose to remove the Locale options altogether. It makes toLocaleString simpler and closer to the Intl specs, which seems good for such a central method. Plus overriding the locale is probably rare, and can easily be done separately if needed: dt.toLocaleString({ locale: 'en-gb' }) -> dt.setLocale('en-gb').toLocaleString();)

For completeness in the v2.0 changelog,

  • DateTime.reconfigure() now requires a parameter.
  • Deprecated the round parameter in Duration#toFormat.

Only 5. and 11. left, which may require some thought. I like asSeconds for 11.

GillesDebunne avatar Aug 21 '20 23:08 GillesDebunne

Regarding 5., options.setZone is used in 5 DateTime constructors: fromISO, fromHTTP, fromFormat, fromSQL and fromRFC2822 which can all parse an optional time zone / offset.

Looking at it today, it took me a while to understand the real meaning of this option. The time zone in the string (if present) is taken into account to compute the proper associated timestamp. However, the created DateTime object is defined in the zone parameter time zone instead, unless setZone is set to true, in which case the one defined in the parsed string (if any) is used instead.

This is a complex behavior, that could be detailed in the documentation. The parameter could be renamed keepZone or preserveZone.

Just like 8. above, which mixed two functionalities in its options (define the output format, but also alter the locale), it seems easier to separate concerns, by removing this parameter altogether. That way, these 5 methods would have the same options parameter as all the other constructors (local, fromJSDate, fromObject...), which is more consistent.

When the parsed string provides a time zone, it should simply be respected. Otherwise, the zone parameter will be used as is done today and in the other constructors.

This is equivalent to what setZone: true would do today. To reproduce today's setZone: false behavior, one would have to use fromX(string).setZone(...).

The documentation could then become: @param {string|Zone} [options.zone='default'] - use this zone if none is specified in the input string. Apply toDefaultZone() or setZone() on the created DateTime to enforce its time zone if wanted.

Two cons of this approach:

  • This is a serious breaking change
  • If today's default behavior is what 90% of users want, requiring one more function call may feel verbose.

Pros:

  • More consistent, easier to understand

GillesDebunne avatar Aug 22 '20 21:08 GillesDebunne

Just found this issue, and thought I'd share my thoughts 🙂

I generally agree with previous decisions, except for the following:


  1. New DateTime.now() method equivalent to local with no parameter

This seems like a bad idea. It's not at all obvious which zone the instance will be created with, where .local() and .utc() are nicely explicit about it. Adding a .now() method will just result in more bugs, as developers will inevitably forget to think about the zone when creating the instance - also, since it does exactly the same as .local() with no parameter, it would just pollute the API with a redundant method. Please don't do this.


  1. Rename options.setZone to keepZone in DateTime constructors' options

Agreed, the name setZone is confusing - keepZone is the name I'd expect for this option.

That said, I'd be much happier if this was just true by default - when an instance is created from a date string that specifies an offset, it is super confusing that the instance doesn't have that same offset. Basically, I'd expect this to be true:

const isoDate = "2020-01-01T00:00+05:00";
DateTime.fromISO(isoDate).toISO() === isoDate; // currently false, which is super confusing

We literally set this option to true everywhere we parse dates, but it's a hassle to write, easy to forget, and a major source of bugs. It's a breaking change for sure, but in my opinion, it would be a really, really good one.


  1. Add a new DateTime.plus/minus(12, 'hours') method

This one seems like a bad idea. As mentioned in previous comments, the order of operations matter, and this would likely encourage incorrect use. Also, introducing multiple ways to do the same thing, just creates unnecessary complexity and confusion. Please keep the API clean and simple - it's enough to have one way to do something :)


  1. DateTime.toSeconds returns an integer value ...

This one I have mixed feelings about. Getting the Unix time is one use case for this method, but it may not be the only one - and for other use cases, the loss of precision could be a surprise. This certainly needs to be clearly documented.

That said, I think I'm in favor of changing .toSeconds() so it floors the value, for the following reasons:

  1. Most of the .toSomething() methods are already lossy - some loose the zone information, and some, like .toRelativeCalendar(), loose pretty much everything. Therefore, I actually think it would be reasonable to assume something might get lost when using those methods.

  2. We also have the .toMillis() method, and the existence of that kinda indicates that .toSeconds() might do more than just divide the milliseconds by 1000 - because why would you need a function just for that? I've actually caught myself assuming .toSeconds() would return a floored value, for that exact reason.

  3. I really don't see a need, or use case, for a method to get the exact value in seconds - getting the Unix time seem to be the primary use case for this method, and if the exact value is for some reason needed, we can always get that by just dividing the milliseconds by 1000.

Regarding the proposed .asSeconds() method, I think that's a really bad idea, for the following reasons:

  1. We already have a .as("seconds") method on Duration, and that returns the value, without loss of precision - so it would be super confusing if .asSeconds() on DateTime floors the value.

  2. Having both .toSeconds() and .asSeconds() would be super confusing, as the names are way too similar.

thomas-darling avatar Oct 05 '20 21:10 thomas-darling

Hmm, on a second thought...

  1. DateTime.toSeconds returns an integer value ...

If the only change we make, is to floor the number of seconds returned by .toSeconds(), then that would actually introduce an inconsistency, similar to the one we currently have with setZone in .fromISO():

const seconds = 1577818800.5;
DateTime.fromSeconds(seconds).toSeconds() === seconds; // currently true, which is good

// but, if we change it to floor the value, this would be false, and that would be super confusing

Therefore, if we were to make this change, then .fromSeconds(...) should also be changed, such that it only accept whole numbers - otherwise the loss of precision during a roundtrip could introduce subtle bugs, that would be very hard to find.

Now, if you do need to parse fractional seconds, you can of course always do this:

const date = DateTime.fromMillis(seconds * 1000)

Ignore everything below this line - it is wrong, and only left here for context

But, here's where it gets really fun - because Duration also has a .fromSeconds(...) method - and there, fractional seconds probably should be supported, as it has nothing to do with Unix time. So making those change in DateTime would introduce an inconsistency.

Therefore, I'm now in favor of keeping the current behavior of .fromSeconds(...) and .toSeconds().

As for how to get the floored value...

I believe what is really being asked for here, is in fact a way to get what is commonly known as "Unix time". Therefore, I'm in favor of simply adding a .toUnix() method.

This does raise a couple of interesting question though:

  1. If we have a .toUnix() method, should we also have a .fromUnix(...) method, that only accepts whole numbers? I'd argue we should.

  2. If we have that, do we then need the .fromSeconds(...) and .toSeconds(...) methods at all? I'd argue we don't, although they do no real harm either...

So, to summarize my thoughts on this point, I'd suggest:

  • Add a toUnix() method that returns the floored number of seconds.

  • Add a fromUnix(...) method that only accepts a whole number of seconds.

  • Consider removing .fromSeconds(...) and .toSeconds(), as they are now largely redundant. If needed, use .fromMillis(seconds * 1000) and .toMillis() / 1000 instead.

thomas-darling avatar Oct 05 '20 23:10 thomas-darling

Sorry, looks like I managed to confused myself in the previous comment 😄

The Duration class does not have a .fromSeconds(...) method, so there's actually no consistency issue with that.

So, to summarize my thoughts on this, I suggest the following:

  • Change the .toSeconds() method so it returns the floored number of seconds. If fractional seconds are needed, use .toMillis() / 1000 instead.

  • Change the .fromSeconds(...) method so it only accepts a whole number of seconds, and throws an error otherwise. If fractional seconds are needed, use .fromMillis(seconds * 1000) instead.

Additionally:

  • Consider renaming .fromSeconds() and .toSeconds() to .fromUnix(...) and .toUnix() However, given that there are no consistency issue after all, I'm now leaning towards not doing that.

  • Add a .toMillis() method to the Duration class. This just because I realized it doesn't have that, despite having a .fromMillis(...) method...

thomas-darling avatar Oct 06 '20 02:10 thomas-darling

DateTime.toSeconds returns an integer value (see #565), so that it can be used in Unix timestamps creation, and can otherwise be replaced by toMillis() / 1000

To make sure this doesn't get missed, I commented some thoughts about this on the original thread: https://github.com/moment/luxon/issues/565#issuecomment-707645187

joepie91 avatar Oct 13 '20 10:10 joepie91

I have just talked with @joepie91 in the IRC about it. I’m not as seasoned as most of you guys, but I think the best solution is to rename the .toSeconds() and .fromSeconds() to .toUnix() and .fromUnix() we don’t have a .toMinutes() or anything similar. In the documentation .toSeconds() is listed under Unix Timestamps.

With renaming it it’s better defined what it does and if somebody wants the seconds without the ms they can still do .toFormat('X'), which is meant for more special cases. Right now .toSeconds() will lead to more confusion for users with practically no benefit. People that want a unix timestamp will look for a unix function and most people that want seconds, will think that gives them seconds, but it actually doesn’t.

JWPapi avatar Oct 13 '20 10:10 JWPapi

I've opened a PR to add another function for whole number unix timestamps, it uses the same Math.floor method as Moment.

#1114

jpike88 avatar Jan 07 '22 06:01 jpike88