No longer an official way to store zoneless times in 0.19?


#1

TL;DR: Apps like mine need to store both universally unique moments in time, and times that vary with location just like ‘human time’ does. But unless I’m missing something, Elm 0.19’s time paradigm alienates the latter use case - and deprecates the best library for the job.

Greetings, Elm Discourse! I just signed up and this is my first post, but I’ve been reading these threads all day and there is a lot of cool stuff going on. (I wish there wasn’t such an unusually short 10-day cutoff, there’s a lot of “old” topics I’d want to give input on, and many threads were prematurely killed by it before anything could be said. I hope that doesn’t happen to this one.)

I’m upgrading my project to Elm 0.19, and I’m perfectly excited to do so. I also like the simplicity of the new Time API, and I started to switch to it from the old elm-community/elm-time because I want long-term support and it seemed well thought out:

human time should basically never be stored in your Model or database! It is only for display!

I was fully on board until I started switching my project over - and started to remember why I had chosen the pleasant and comprehensive elm-time library in the first place. The documentation cited above succinctly boils down time to Human, Posix, and Zones, which is fine, but I’m starting to see that the “never store Human Time” bit, which clearly drove the design of the new library, seems to have a huge oversight: There are applications whose purpose is precisely that: to store human time.

Edit for clarity: By “human time” I don’t necessarily mean the formatted final product, but time in “human parts”: the Date, plus what’s on the local clocks. Time.Posix values can’t be used for this, because (in a sense) they already have a time zone (!) built in, UTC (which is shift-able, but you can’t store a time without one).

Take my phone’s Alarm Clock for example: I have an Alarm set for 8:00 am. Perhaps I was in LA at the time I made that alarm. I fly to New York for a week. During that week, my alarm still works as expected, waking me up at 8:00 am.

An Elm implementation, however, naively using the new Time API the way it was designed (for global ‘moments’ in time such as server requests or phone calls) would look at that global moment and use the device’s local information (now set to NY time) to decide that I actually want to be woken up before the crack of dawn at 5:00 am - causing my device’s prompt defenestration.

Yikes. This seems to be a hole in the use cases covered by the library, because there is simply no way to say “8am everywhere” instead of an unchanging offset from the epoch. I’m looking for ideas to work around this.

[Edit for clarity: the Alarm Clock is an oversimplified example - it could be done without Dates at all, storing only the Time of Day. The actual code I need help with is below! ]

I realized the elm-time library I was using handles this quite nicely: DateTimes are stored internally as a Date cleverly paired with a simple integer Offset for the particular time within said date. In fact this works especially well since my Todo-list application (the current project) will also handle the case where a Date is assigned to a task with no particular time of day (a common desire), in which case that offset can just be omitted. Example code:

type TaskMoment
    = Unset
    | LocalDate Date
    | LocalMoment DateTime
    | UniversalMoment Time.Posix

Sadly, that library is now officially deprecated (time-deprecated), with the new packages supposed to be a “better way to model time”, yet, no official way of storing human time from what I can tell. Would love to hear this isn’t the case!


#2

Did you mean 8:00 instead of 9:00 there.


#3

Given your alarm clock example, I think you would simply store the representation in your program as type alias AlarmTime = { hour : Int, minute : Int }, (or possibly make it a custom type if you want to validate the hour and minute are in the appropriate range). Then when you actually need to interface with a system that needs to know the timestamp of the next alarm, you can convert it to a Posix (which would require timezone information to to correctly). Interestingly, elm/time doesn’t currently have a function to create a Posix value in that way, but it looks like there is one in justinmimbs/time-extra.


#5

TL;DR :see_no_evil:

Human time doesn’t solve your problem. There are different kinds of time. Understand what kind of time you’re dealing with and either find a third-party library that represents it or model it yourself.

What is time? :thinking:

Time is tricky to work with because we use the same English word “time” to refer to several different quantities. Some examples might be:

  • An exact moment in time (e.g. when a lunar eclipse occurred), often called a DateTime
  • A moment on a generic day (e.g. 8:00) often called a TimeOfDay
  • An amount of elapsed time, not anchored to particular moment (e.g. generically speaking of “2 hours” as a quantity) often called a Duration.

Humans have developed many ways to display these values and they vary from region to region on our planet. When working with time, it is important to choose a single underlying representation and only store that value. Then you can convert to whatever appropriate human format you want at render-time. This is what the guideline from elm/time is saying.

Alarm Clocks :alarm_clock:

In your example of the alarm clock, the quantity you want to represent is a time of day. This is a different quantity than the datetimes expressed by the Time.Posix type. You’ll need to either find a library that provides a time of day representation or write your own.

The classic way to model various kinds of time is to store a single number that is the amount of time that has elapsed since a particular zero point (called the epoch). For example:

  • date times are usually stored as a single number of milliseconds since Jan 1, 1970 (Unix epoch).
  • the PostgreSQL database stores it’s time of day values as microseconds since midnight
  • the western calendar counts the number of years since the beginning of the “common era” (CE).

You can choose the size of the value being counted (often called the resolution) based on how much precision you need. For an alarm clock, a resolution in seconds or minutes instead of microseconds might be fine.

Note that a time of day value is not a “human time”. The time of day value “57, 600 seconds since midnight” might be represented as “4:00 PM” or “16:00”, or something else entirely. Those are the “human time”.

I’ve written more in depth about modeling time-of-day for the mentor notes of exercism.io’s Ruby Clock exercise.

Recurring events :calendar:

The elm/time docs touch on these ideas when discussing recurring events. Note that this example does not use a Time.Posix to represent the time of day the event occurs. Instead, it uses an Int that’s implied to be the number of hours since midnight. If you wanted to, you could improve on that by introducing a custom type that represents time of day values.

It’s worth taking a closer look at the ideas in that section of the docs. I think it answers a lot of the questions you’re asking :slight_smile:

Custom type :hammer_and_wrench:

So what would a custom type for time of day look like? Here’s one possibility, using a resolution of minutes.

type TimeOfDay
  = TimeOfDay Int

minsPerDay : Int
minsPerDay =
  60 * 24

fromMinutes : Int -> TimeOfDay
fromMinutes mins =
  mins
    |> modBy minsPerDay -- ensure this value is less than 24 hours
    |> TimeOfDay

it would probably be opaque and only expose the fromMinutes constructor.

Making impossible states impossible :x:

As humans, we’ve subdivided time into many different sizes to make it easier for us to comprehend. Times of day like 4h 06m 13s are much easier for our brains to understand than 14,773s. However just because the human version is sub-divided doesn’t mean you should also subdivide your your computer representation. Avoid representing times like:

type TimeOfDay = { hour : Int, minute : Int, second : Int }

This allows representing invalid times like { hour = 12, minute = 75, second = 220 }. While this can be mitigated by using an opaque type, you will constantly find yourself needing to carry seconds into minutes and minutes into hours in the internal implementation when you go over 59.

When representing a time with an epoch + counter, you’re usually best off using a single counter whose resolution is the smallest unit you care about. You can break down this single counter however you want when rendering a human representation.


#6

Yes I did! Oops. Fixed!


#7

That’s a good solution for the alarm clock example - good point. However, it doesn’t work for the real world app I am working on, which is why I put some code in there… It’s just a todo list where people should be able to set a task such as “eat breakfast” for 9am wherever they are, even if they switch time zones. Hey, that’s probably a better example.

And yes, I’m currently using time-extra, thanks!


#8

Thanks for your colorful answer, joelq! I’ll try to do my due diligence in responding to all the work you put into it!

As you’ll read below, I guess a better word for it is “Localized time”, or every bit of a time except for the zone (without going all the way to Posix time, which effectively has a baked-in zone of UTC). With that in mind, I’m not sure that statement stands!

Yes, those were the conclusions I came to when I decided to make this post. It’s a matter of what comes next - hopefully that’s made clear below.

I see DateTime as a combination of a Date and a Time (like in the elm-time library, where it’s called DateTime). The “Time” being the kind you refer to in the second bullet point. As for an exact moment in time, I refer to it in my app as “Moment”, actually, and it’s just a type alias for Time.Posix (we can just use this name as well).

Don’t worry, I understand that (as I hoped to make clear in my post). In fact the API’s overhaul caused me to do hours of extensive research into the fascinating topic. That said, I have to disagree with the general nature of the statement “When working with time, it is important to choose a single underlying representation and only store that value”, specifically because it really refers to “when working with moments in time”, rather than any work that somehow involves time. An app that teaches children to read clocks, for example, could certainly said to be “working with time”, yet it may very well have not a single Posix value in the code. That’s kind of the distinction I was trying to make in my post.

Mhm, that’s essentially a summary of my post. But it wasn’t a problem before, because I had the elm-community/elm-time package, which does what you describe (provides a time-of-day in the form of an integer offset from midnight) as well as provide everything that Posix currently does. Though using a different paradigm for storage, the new API could be viewed as a subset of what this library offered. And yet, the latter is being discontinued, seemingly on the assumption that the former solves all the same problems in a better way.

Yup!

Right. Perhaps a different term is in order, then, because of course by Human Time I meant both the already-localized time as well as the various ways to represent it in text. If I restrict “human time” to only mean the latter, as you suggest, I’d need a name for the former. perhaps “Localized time” is a good candidate? Either way, I’m aware that the outputting-as-a-human-readable-string step is still needed, even after the time has been localized. For now I just need to store it first. :sweat_smile:

Already done :slightly_smiling_face: Again, I researched everything the internet of Elm had to offer on Time before writing this post. Don’t get the wrong idea - I’m not under the impression that it cannot be done! This thread is about the problem that it cannot be done officially. Or at least, not anymore. Your suggestions for hacking together a solution for myself are definitely what I’ve been thinking about, but the point was to highlight the fact that it used to be supported semi-officially, so I wouldn’t have to. Always better not to re-invent the wheel!

Yeah, I can certainly make my own type. make it opaque, etc. This example code applies only to the alarm clock example, however, not my actual code, which I listed a little further down. To be clear: I wouldn’t just need a time of day, but a date, too, and basically everything that goes into an exact human time - without taking that final step into solidifying it into one Posix number. This way we can let the time zone wander without changing the time.

Right now, it’s looking like the best solution would be to use the parts bit from time-extra for this purpose:

type alias Parts =
    { year : Int
    , month : Month
    , day : Int
    , hour : Int
    , minute : Int
    , second : Int
    , millisecond : Int
    }

…But I’m open to hearing about better solutions. Of course, I’d only generate Parts safely using the library, to mitigate your next concern:

Already taken care of for me by the library! But you’re right, this is why the implementation of the opaque type DateTime in the old library was a simple integer offset from the Gregorian Date. Here is a link to that library if you’re unfamiliar.

From that package’s page:

This is a fork of elm-community/elm-time, upgraded to support Elm 0.19 . However, it’s not clear that this is the best way to model time; this package is intended only to make updating your app to 0.19 easier. It’s recommended to consider using newer libraries instead

Hence this post :grin:


#9

One approach I sometimes use for “Localized Time” (date & time which represents same “Human Time” value regardless of timezones or DST changes) is to just assume fixed UTC timezone and then use libraries like elm/time.

This can be considered a hack, as underlying Time.Posix is now “incorrect” - it doesn’t represent a fixed moment in time but is just a simple numerical representation of year/month/day/hours/minutes/seconds.


#10

I don’t think we need a core library for ‘human’ time - that only exists so we can get a Posix timestamp from the system.

Just add your own data type for local time. If you end making something good with some nice helper functions, publish it.


#11

If I get what you mean, then the problem is the new elm/time doesn’t do that. But yeah, I thought about doing that hack too.


#12

I guess you missed the part where I was saying this already exists - it was elm-community/elm-time. It’s something good. It’s published. It’s better than what I would have done myself, and has community support. It’s very nice, and it solved the issue of both posix time and human time.

The problem is it has been deprecated with 0.19 - under the reasoning that the new library solves all the same problems! My post shows that this is not the case.

Core or not, I should not, as a beginner, be writing my own library for doing something as simple as time in a todo list app. I want to maintain my app, not my ad hoc fill-in with no official support. This is why I want to know what options there are for zoneless time management in 0.19.