Idea for a unified date/time package using extensible records

It’s pretty typical that an app uses several different units or “granularities” of time. You might have timestamps that are in Posix/milliseconds or seconds, datepickers which have days as their smallest unit, and if you’re doing schedules you might also be using hours and/or minutes.

You could use Time.Posix for everything, treating dates as “the first millisecond of the day” etc. but that can lead to confusion and bugs where a value that’s supposed to represent a date like “2019-06-27” gets displayed as “2019-06-26” because of timezones or daylight savings.

We’re currently using justinmimbs/date for our dates, which avoids that problem, although we’re also using year/month pickers, and it doesn’t feel quite right to store their values as “the first day of the month”. We could of course create a custom “Period” type for this, but then we would dealing with three different time types with different interfaces.

I was thinking if there was a way to create a system that could handle all these units of time in a unified way, and thought maybe extensible records could be a solution?

Each unit of time would have a type alias like:

type alias Period r =
    { r
        | year : Int
        , month : Int
    }

type alias Date r =
    { r
        | year : Int
        , month : Int
        , day : Int
    }

type alias Millis r =
    { r
        | year : Int
        , month : Int
        , day : Int
        , hour : Int
        , minute : Int
        , second : Int
        , millis : Int
    }

This then lets you write functions like:

diffDays : Date a -> Date b -> Int
diffDays a b =
    diffMonths a b * 30 + (b.day - a.day)

diffDays can then be used to for example compare a Date with a Millis. But if you tried to use it with a Period the type system will prevent it, since Period doesn’t include days.

Here’s the full code: https://github.com/Herteby/elm-chrono/blob/master/src/Chrono.elm
(It’s just a mockup so far, it currently assumes all months are 30 days for example :P)

But what do you think, would this be worthwhile, or should we just stick to Time and Date? Is this whole thing just bit of OCD and not really worth worrying about?

Example of how formatting could be done:

toDateString: Date a -> String
toDateString date =
    String.fromInt date.year ++ "-" ++ paddedMonth date ++ "-" ++ paddedDay date

paddedMonth : Period a -> String
paddedMonth = .month >> String.fromInt >> String.padLeft 2 '0'

This function would work on any Date, Second, Millis etc.

The idea is interesting, but I see one shortcoming: yearDiff between two dates one second apart can be 1 if they sit on the two sides of the year charge, which is… Confusing? Or not, I’m not sure tbh. (also, you’re completely disregarding timezones, which is limiting, consider dst or when an offset change happens)

Also, do check out Joda/Noda time, time is tricky and that is one of the few libraries that Get It Right

Small side note: The Problem with Time & Timezones - Computerphile should be seen by anyone contemplating touching time.

5 Likes

@miniBill yeah that’s pretty confusing. I mostly wrote the diff functions as an example to try out the extensible records. Will have a look at Joda/Noda :+1:

Actually, instead of storing the time as a record, maybe I could just use the records as phantom types, and store it as Time.Posix internally.

type Time unit = Time Posix

@pdamoc yep I’ve seen that :expressionless:
Although if I use Time.Posix internally that would make things a lot simpler for me as Evan has already dealt with it!

1 Like

This topic was automatically closed 10 days after the last reply. New replies are no longer allowed.