timex icon indicating copy to clipboard operation
timex copied to clipboard

RFC: Add months and years to `Duration` units

Open kenny-evitt opened this issue 6 years ago • 2 comments

Obviously this is tricky to implement (due to the unreasonable complexity of reality!):

... but it would still be nice to do, if possible, feasible, reasonable, and generally desired.

I'm willing to help, at least a little, but I'm pretty new to Elixir, completely unfamiliar with the Timex internals, but somewhat aware of the heroic effort, and some of the attendant details, involved in writing and maintaining a good date-time library like Joda-Time, Noda Time, and, AFAICT, this one!

kenny-evitt avatar Jul 10 '19 23:07 kenny-evitt

So the primary issue is that converting to months/years from a Duration lacks a critical piece of information: the origin date/time. With that piece of information we can calculate the number of months/years represented by the duration in much the same way we diff in those units.

Since a Duration doesn't carry an origin date/time and we can't infer one automatically without almost certainly producing unexpected results, it would be necessary to provide the source Date or DateTime/NaiveDateTime from which the duration can be based, as a parameter to to_{months,years}; and likewise for from_{months,years}.

I'm not sure how useful that really is though. In general, expressing a duration in terms of months or years is a casual way we talk about time, but is technically extremely vague. It makes much more sense to pre-process units like this in the context in which they are derived. To use a more concrete example, if you have some app that allows users to express something like "one month from today, remind me about X" - then it is pretty unambiguous what the intent is there, and a precise duration can be generated which is based on the knowledge that one month from say April 12th, is going to be May 12th, and that is exactly 31 days. You don't need to generate a duration based on months, rather you do something like today = Timex.today(); days = Timex.diff(today, Timex.shift(today, months: 1), :days); Duration.from_days(days).

If instead you have an app that allows something like "once a month, remind me about X", where the origin date/time is unspecified, then it is ambiguous what the interval should be, and when the reminder should occur. That has to be implied or defined by the context, i.e. "starting today" or "on the last day of each month", but once you have that context, it is no longer necessary to use months as a unit, since you have more precise options from which to build a duration with that information.

Years is of course similar, but even less likely to be useful in Duration form. In both cases, constructing a Duration of these lengths that is both long-lived and needs to represent a fixed point in time has the risk that the time zone data they are based on will change - i.e. if you build a Duration that represents the time until the next reminder a year from now in the local time zone, say America/New_York, and IANA changes the rules so that there is no longer DST in that time zone, the duration will now point to a different time than the one it is intended to point to. This is the purpose of the Interval type, and the shift API - to allow calculating these things in a zone-aware way.

TL;DR - I think what I'd like to see to really sway me on this would be to have use cases where these units make sense in the context of durations, where there aren't safer/better ways to represent the same thing. Long durations shouldn't be used for OTP things like timers, since it is inherently error-prone. For things that occur on a schedule, Interval is generally the better choice. For simply calculating a future point in time based on a unit, the shift/diff APIs are best for that. What is left after that, I'm not sure, but if yourself and others are willing to chime in, we may be able to close the gap with something that solves the problem in a sane way.

bitwalker avatar Jul 11 '19 05:07 bitwalker

TLDR – Timex is almost certainly fine as-is, especially in light of details of which I just learned about Joda-Time and how it handles its corresponding types.

@bitwalker I'm pretty convinced by what you wrote that this is probably too ambiguous to warrant implementing. My motivation for requesting it is that basically I ended up using Duration instead of the shift API for a specific calculation as doing so seemed 'easier'. I'm going to look into changing that calculation to use shift instead as I'd like to possibly be able to support months and years.

But ... there are ways to resolve the relevant ambiguities, even if only 'clumsily'. In some sense, it seems reasonable that shift_options and Duration would be equivalent. On one hand, even minutes, hours, days, and weeks are themselves not entirely unambiguous due to time standard adjustments.

Basically, my use case involves working with date-times and a user-specified 'delay' that's currently only a number of days, but we may want to add support for weeks or even months (tho the latter seems very unlikely); but almost certainly not years. But I can easily imagine similar logic that could reasonably involve years-long durations. A precision of several days in that case seems like it would be fine.

On the other hand, Joda-Time seems to be implemented similarly to Timex. Joda-Time also has a Duration type (class):

... and it, like Timex, seems to encapsulate a fixed (precise) duration, in its case "an immutable duration specifying a length of time in milliseconds". Note that many of the functions (methods) for that module (class) are named, e.g. toStandardDays.

For something equivalent to Timex's shift options, Joda-Time includes the Period type:

That type includes behavior for working with weeks, months, and years. In the docs for Duration it's noted:

A duration may be converted to a Period to obtain field values [e.g. weeks, months, or years]. This conversion will typically cause a loss of precision.

kenny-evitt avatar Jul 11 '19 15:07 kenny-evitt