elm-community / elm-time

A pure Elm date and time library.

Home Page:http://package.elm-lang.org/packages/elm-community/elm-time/latest

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

RFC - Proposal of different primitives

Dremora opened this issue · comments

Having been struggling with numerous date libraries in JavaScript, I decided to think of what the best API in Elm for a date library would look like. You can find some ideas here. The main goal is to minimize the amount of mistakes developers make around timezones by separating concepts of a “moment in time” and time with timezone. Being a little bit more explicit can go a long way in solving this problem.

Seeing that your library is very new and has somewhat similar concepts, I decided first to try and see what can be improved here, instead of coming up with my own implementation.

The way I think about it, moment in time refers to a particular absolute point in time — but most of the user-friendly details we can learn about it (such as day and time, as well as manipulating, formatting it and parsing a string into it) only come into play when you put it in a context of a timezone (and potentially a calendar, e.g. non-Gregorian one — but let's leave them out).

One could say that a moment in time does in fact have date and time — it's described by UTC. But this is what I think would be nice to be explicit about: by treating UTC as just another timezone we can make users think of the nature of the date they are working with. Is this a UTC date? Do we need to work with browser's timezone? Or do we need to use timezone defined somewhere in the user's settings we need to go and fetch? Those were the problem I had to solve when I was going through countless dates we were parsing, displaying and manipulating in our application.

Note that TimezoneDate = (Moment, Timezone). The reason they are separate in my gist is that while in most cases you require both moment (which I call there Timestamp) and timezone as function arguments, the timezone is almost never returned — it's used just as a parameter to transform the moment. One would read add : Timezone -> Interval -> Timestamp -> Timestamp as “Add this interval to this moment by treating it (just for the purpose of this operation) in this timezone” (argument order is different for piping reasons); the interval (say, 5 days) only makes sense in the context of a timezone, even it it's just UTC.

The presence of no-timezone-to-timezone conversion (e.g. zonedDateTime function) always makes me think: does this mean we are treating the date passed as if it was in the specified timezone, or we treat it in UTC (or any other original timezone, in case of moment.js) and simply change its timezone field? I was actually confused as I was typing this. I still am. I'm pretty sure I'm not alone.

In the current implementation, you have most operations defined on a timezone-less DateTime. While operations such as addHours can be performed on a bare date if it's treated as UTC, it's nevertheless very useful (I'd argue it's more useful) to perform those operations on a date with the potentially non-UTC timezone. Duplicating the API is probably not the best idea, this is where this separation helps as well.

Because ZonedDateTime internally stores local time rather than UTC time, it's incapable of correctly storing time during the daylight saving switch.

Would my approach fit into this library? Should I try it out in a fork or a separate codebase? Let me know what you think!

While your API is a little different, your proposed design, as I understand it, is very similar to the way this library currently works.

The way I think about it, moment in time refers to a particular absolute point in time

That is what DateTime is. It is an absolute point in UTC whose days are identified using the Gregorian calendar. Personally, I don't see any value in supporting any of the other calendar representations but I don't think there's anything in the design to prevent that.

One could say that a moment in time does in fact have date and time — it's described by UTC. But this is what I think would be nice to be explicit about: by treating UTC as just another timezone we can make users think of the nature of the date they are working with.

UTC is just another timezone, it just happens to have no effect on DateTimes since they represent the same things.

> import Time.ZonedDateTime exposing (..) 
> import Time.DateTime as DT
> import Time.TimeZones exposing (..)
> DT.dateTime { zero | year = 2016, month = 10, day = 8, hour = 11, minute = 30 } \
|   |> fromDateTime (utc ()) \
|   |> toISO8601
"2016-10-08T11:30:00+00:00" : String

Is this a UTC date? Do we need to work with browser's timezone? Or do we need to use timezone defined somewhere in the user's settings we need to go and fetch? Those were the problem I had to solve when I was going through countless dates we were parsing, displaying and manipulating in our application.

You should never have to ask that first question. This is why the library imposes the constraints that it does on ZonedDateTime: the majority of the time you should work with DateTime values since they are absolute and you should only ever use ZonedDateTimes to either:

a) render a date and time in the user's time zone
b) convert a date and time value in the user's time zone to UTC (i.e. DateTime)

The presence of no-timezone-to-timezone conversion (e.g. zonedDateTime function) always makes me think: does this mean we are treating the date passed as if it was in the specified timezone, or we treat it in UTC (or any other original timezone, in case of moment.js) and simply change its timezone field? I was actually confused as I was typing this. I still am. I'm pretty sure I'm not alone.

zonedDateTime constructs a date and time value as if it were in the specified timezone. The presence of zonedDateTime is necessary if you want to be able to answer questions such as "when is 8PM in the user's time zone?". For example:

zonedDateTime user.timezone { zero | year = 2016, month = 10, day = 30, hour = 20 }
  |> toDateTime
  |> notify "Good evening!"

In the current implementation, you have most operations defined on a timezone-less DateTime. While operations such as addHours can be performed on a bare date if it's treated as UTC, it's nevertheless very useful (I'd argue it's more useful) to perform those operations on a date with the potentially non-UTC timezone. Duplicating the API is probably not the best idea, this is where this separation helps as well.

This is where we disagree. Assuming your input is a zoned date time, you should convert that to an absolute DateTime value, perform your operations on that and then convert it back to a ZonedDateTime if/when you need to display it. In my experience, manipulating zoned date times directly is the source of most date and time-handling bugs.

Because ZonedDateTime internally stores local time rather than UTC time, it's incapable of correctly storing time during the daylight saving switch.

While this was an issue with this library until this morning (when I fixed it), I don't think this is inherent as there really is no concept of "during" for ZonedDateTimes since they simply serve as views over DateTimes.

Would my approach fit into this library? Should I try it out in a fork or a separate codebase? Let me know what you think!

I think some of the functions in your gist would make good additions to this library and I'll happily take PRs for those, but it would take a lot of convincing to get me to budge on the API surface of ZonedDateTimes :).

While your API is a little different, your proposed design, as I understand it, is very similar to the way this library currently works.

Mostly yes, pretty much the only differences are around handling timezones.

That is what DateTime is. It is an absolute point in UTC whose days are identified using the Gregorian calendar. Personally, I don't see any value in supporting any of the other calendar representations but I don't think there's anything in the design to prevent that.

Agree, it's totally fine, I think, to represent this moment in UTC — this is just an implementation detail. As long we have a notion of an absolute moment, adding support of custom calendars should not be a problem.

You should never have to ask that first question. This is why the library imposes the constraints that it does on ZonedDateTime: the majority of the time you should work with DateTime values since they are absolute and you should only ever use ZonedDateTimes to either:

a) render a date and time in the user's time zone
b) convert a date and time value in the user's time zone to UTC (i.e. DateTime)

...

This is where we disagree. Assuming your input is a zoned date time, you should convert that to an absolute DateTime value, perform your operations on that and then convert it back to a ZonedDateTime if/when you need to display it. In my experience, manipulating zoned date times directly is the source of most date and time-handling bugs.

I agree with the fact that we should not work with timezones if we can avoid them. The API I have provided in fact doesn't use timezone if it's not required. However, I want to show that most operations in fact require a timezone or carry an implicit timezone of UTC.

Consider setDay. One of its use cases is to modify a date with the information entered by the user. From the user's perspective, they are treating the date as if it was in the local timezone. An example would be marking a movie as watched on https://trakt.tv by providing a custom date/time via datepicker. The date is shown and manipulated in local timezone. However, it's stored on the server in UTC. With the current API, it's not possible to apply such modification to a date.

All set* operations, startOf / endOf (which are not yet implemented in this library) are susceptible to this issue. get*, while can be done via a formatting function, is a lot cleaner to do if implemented on a date with the timezone. I'm still not sure whether add* / subtract* require a timezone or not, I need to think more about it.

But I do not advocate for manipulating ZonedDateTime directly. I propose to make the aforementioned functions accept Timezone as an argument. They would still also accept and return an absolute, timezoneless date. There would be no need to convert between timezones, or in fact have multiple ways to represent a date.

I think some of the functions in your gist would make good additions to this library and I'll happily take PRs for those, but it would take a lot of convincing to get me to budge on the API surface of ZonedDateTimes :).

The functions have been pretty much taken from moment.js (with an Elm touch), they have proven themselves useful in JavaScript and I'm sure will be useful in Elm as well. I didn't really hope that convincing is going to be straightforward, and I think that some code examples from my side might help here ;) But it will be a couple of weeks until I have time to dive into the code. Happy to keep this dialog going in the meanwhile!

Personally, I don't see any value in supporting any of the other calendar representations but I don't think there's anything in the design to prevent that.

There are one billion people in India and neighboring countries that use a different calendar. I'm sure some of them are using Elm as well.

That said, I'm with you. I've never used any calendar but the Gregorian. :)

@Bogdanp Your comment helped me understand what the docs could not. Thank you!
However,
@Dremora has a point, and it's exactly why I was looking at external libraries for handling time - when we want to do user's-perspective modifications to a Moment (my name for DateTime, imho superior), there can be discontinuities between dates.

I'm building a todo list app like Todoist, that needs to be able to postpone a Task to, for example, "Tomorrow". However, tomorrow in UTC may not be tomorrow in local time!

@Dremora 's solution to this (allowing to pass a zone to such modification functions) makes a lot of sense to me.

Am archiving this repository which recommends closing this issue