Stream: show and tell

Topic: Roc Datetimes


view this post on Zulip Hannes (May 01 2023 at 05:12):

Hi, wanted to share my first proper Roc project, a WIP datetime library. The internals are based on Rust's chrono crate, but I would like to make the API more idiomatic for Roc. I was planning to look at Elm's datetimes library for inspiration, but if anyone has any other suggestions I'd appreciate it.

Here's a link to the repo, and here's a link to the docs.

view this post on Zulip Hannes (May 01 2023 at 05:28):

For example, with the Duration interface I wrote a bunch of functions for converting from numbers of seconds/minutes/hours, etc, but I was wondering if a more idiomatic Roc interface would use tags, e.g. instead of Duration.fromSeconds 5 a better function would be something like Duration.fromNumber (Seconds 5)? I've not used a language with tags like Roc before, so I'm looking for more experienced opinions :) Thanks!

view this post on Zulip Brendan Hansknecht (May 01 2023 at 06:07):

You might just define Duration as an open tag. Then when needed convert to the underlying type that is more similar to rusts Chrono. So an end user would most interact with Seconds 5 or similar. Then when needed, it would use the exact Chrono type. Though that is obviously limiting on expression, so maybe it isn't reasonable.

Also, if you go with the tag approach, i would probably just do Duration.from (Seconds 5)

view this post on Zulip Hannes (May 01 2023 at 09:03):

Brendan Hansknecht said:

Also, if you go with the tag approach, i would probably just do Duration.from (Seconds 5)

Ah, thank you, I was trying to think of a good name for the function, but couldn't! I'll probably try a few different ideas at the same time and then settle on one consistent API, I like the use of tags because they can be zero cost :+1:

view this post on Zulip Agus Zubiaga (May 01 2023 at 13:45):

If it was Duration.from Seconds 5 (number is now an argument to from instead of the tag), you could use the same tag for a Duration.to Seconds function.

view this post on Zulip Agus Zubiaga (May 01 2023 at 13:48):

but I think you don't want to make Duration open in that case, for your own convenience

view this post on Zulip Brendan Hansknecht (May 01 2023 at 14:26):

That is probably a better way to do things. I was looking at the impl of Duration, and I don't think you would actually want it to be an open tag.

Though I would switch the arg order: Duration.from 5 Seconds. Then it reads better and you can do something like: someCalcOfSeconds |> Duration.from Seconds

view this post on Zulip Richard Feldman (May 01 2023 at 17:08):

very cool, this is awesome to see! :heart_eyes:

view this post on Zulip Richard Feldman (May 01 2023 at 17:15):

some things I love about elm/time's design that I think are good ideas to replicate:

view this post on Zulip Richard Feldman (May 01 2023 at 17:17):

I also agree with elm-time's stance on ISO-8601 timestamps which is why when I made a separate package to deal with them I took the design approach that "this is a legacy mistake that should be translated into another format as soon as possible"

view this post on Zulip Richard Feldman (May 01 2023 at 17:18):

in general I think that dates and times are similar to Unicode in that there's a big opportunity to create a "pit of success" where it becomes natural to do the right thing around edge case handling, as opposed to something that appears to be correct but turns out to bite you in edge cases :sweat_smile:

view this post on Zulip Richard Feldman (May 01 2023 at 17:19):

(of which there are many opportunities when it comes to both Unicode as well as dates/times, especially when it comes to time zones and DST!)

view this post on Zulip Hannes (May 02 2023 at 01:21):

Thanks everyone for the feedback, lots to think about as I continue working on the library. I'll probably ask for more help with the design in the future, and if anyone has more ideas I'm happy to hear them :)

view this post on Zulip Hannes (May 02 2023 at 01:35):

Thanks for your detailed sugegstions @Richard Feldman, my priorities for the library are 1. Correctness, 2. Good UX and then 3. Speed, because I suspect there will need to be some compromise in UX to make sure the calculations are always correct.

I based the initial design on Rust's chrono crate because it was the first datetime library I was aware of that very carefully distinguished between a DateTime which has a timezone and a NaiveDateTime which is like the Calendar module you suggested.

Storing all DateTimes as UTC like you suggested aligns with my experience, I always pushed for everything being UTC at work, but I also found that when working with humans some kind of timezone specific output is needed. My current idea is to only implement date calculations for UTC types , but allow converting to and from a LocalDateTime type that doesn't let you do calculations. I suspect that design will break under some timezone corner cases, but possibly less than the alternaives :thinking:

view this post on Zulip Richard Feldman (May 02 2023 at 01:56):

yeah so my general sense is that displaying times in time zones is a good idea, and also receiving them from user input in that format is also a good idea

view this post on Zulip Richard Feldman (May 02 2023 at 01:56):

but having a dedicated data type for them kinda seems like footgun to me, even though it's a very common design :big_smile:

view this post on Zulip Brendan Hansknecht (May 02 2023 at 01:59):

If it is an opaque type only for storage, display, input, and conversion to/from the utc format, it shouldn't really have any footguns.

view this post on Zulip Richard Feldman (May 02 2023 at 02:04):

I think the footgun is storing time zone alongside the date/time

view this post on Zulip Richard Feldman (May 02 2023 at 02:05):

most often it's correct to use the viewer's time zone and not the creator's time zone, but putting those together into one datatype makes the default be to work in terms of the creator's time zone

view this post on Zulip Richard Feldman (May 02 2023 at 02:06):

e.g. consider functions like this:

Calendar.date : Utc, TimeZone -> { month : Month, day : U8, year : U32 }
Calendar.time : Utc, TimeZone -> { hour : U8, minute : U8, second : U8, millisecond : U32, nanosecond : U32 }
Calendar.hourToAmPm : U8 -> (U8, [Am, Pm])
Calendar.toUtc : TimeZone, { month : Month, day : U8, year : U32, hour : …etc } -> Utc

view this post on Zulip Richard Feldman (May 02 2023 at 02:06):

if you always have to provide a TimeZone at the last minute, instead of having it baked into a time, it makes it natural to stop and think which time zone to use

view this post on Zulip Brendan Hansknecht (May 02 2023 at 02:07):

That's fair. Also, an end user can always make simple wrapper if they want (Utc, Timezone)

view this post on Zulip Richard Feldman (May 02 2023 at 02:08):

yep! but that way you have to go out of your way to do it, so it's no longer a default

view this post on Zulip Brendan Hansknecht (May 02 2023 at 02:08):

So as long as you have function to help with display using Utc and Timezone, I guess it doesn't really matter much.

view this post on Zulip Luke Boswell (May 02 2023 at 02:11):

basic-cli/Utc stores the Utc internally as a U128, I wonder if that was the wrong decision? Should we update that toI64 as you have done here? TBH I didn't do a lot of research when I made it, was more focussed on getting something in there for the platform.

view this post on Zulip Brendan Hansknecht (May 02 2023 at 02:11):

Side question: do any UTC libraries deal with leap seconds and the such when calculating durations?

view this post on Zulip Luke Boswell (May 02 2023 at 02:15):

I'm also particularly interested in the interaction/interface with basic-cli as this is a good example for these two things working together. I'm not sure there are many other packages that have explored here before. I've done a bit with packages and Json, but it's not a great package as we currently have it as a builtin so that cuases conflicts. @Hannes have you packaged it as a URL yet? I suspect we will surface some new bugs... but appreciate that is beyond the scope of what your working on right now. Looking forward to testing it out and even adding a new Example that uses it.

view this post on Zulip Brendan Hansknecht (May 02 2023 at 02:18):

Should we update that to I64 as you have done here?

Probably yes. I64 is all the precision you get with time, not U128 Also, lets you represent a time before 1970

view this post on Zulip Richard Feldman (May 02 2023 at 02:24):

a nice thing to keep in mind is that since it's opaque, if in the future you want to change it to I128, you can introduce a new function to go from I128 to Utc as a nonbreaking change, and then make the existing one that accepts I64 convert to the internal I128 representation behind the scenes

view this post on Zulip Brendan Hansknecht (May 02 2023 at 02:52):

Upgrading from I64 to I128 should be a non-breaking change, so that shouldn't matter, right?

view this post on Zulip Richard Feldman (May 02 2023 at 03:18):

those are still type-incompatible

view this post on Zulip Richard Feldman (May 02 2023 at 03:19):

like you'd have to do Num.toI28 on your I64

view this post on Zulip Brendan Hansknecht (May 02 2023 at 03:44):

Sure

view this post on Zulip Brendan Hansknecht (May 02 2023 at 03:45):

So you get an input of I64 and convert it.

view this post on Zulip Brendan Hansknecht (May 02 2023 at 03:45):

That's fine

view this post on Zulip Brendan Hansknecht (May 02 2023 at 03:45):

The type is opaque. An end user has no idea how the data is stored.

view this post on Zulip Hannes (May 02 2023 at 03:51):

Luke Boswell said:

Hannes have you packaged it as a URL yet?

Just tagged a release and the URL is available, here's an example app:

app "hello-world"
    packages {
        pf: "https://github.com/roc-lang/basic-cli/releases/download/0.3.1/97mY3sUwo433-pcnEQUlMhn-sWiIf_J9bPhcAFZoqY4.tar.br",
        dt: "https://github.com/Hasnep/roc-datetimes/releases/download/v0.0.2/8CYrpsnUJCgbWGdctD-11mvWf9nDEV66CzPqt19A6qI.tar.br",
    }
    imports [
        pf.Stdout,
        dt.NaiveDate,
    ]
    provides [main] to pf

main =
    message =
        when NaiveDate.fromYmd 2023 1 1 is
            Ok date ->
                dateStr = NaiveDate.toIsoStr date
                "Hello, World! The date is \(dateStr)."

            Err _ ->
                "Hello, World!"

    Stdout.line message

Last updated: Jul 06 2025 at 12:14 UTC