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.
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!
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)
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:
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.
but I think you don't want to make Duration
open in that case, for your own convenience
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
very cool, this is awesome to see! :heart_eyes:
some things I love about elm/time
's design that I think are good ideas to replicate:
elm/time
has a Posix
type for this, but personally I like the name Utc
betterCalendar
module might be worth exploring?) and you can only convert between Calendar Time and UTC Time by specifying a specific time zoneelm/time
doesn't come out and say it this way, but I will. :stuck_out_tongue: (as far as I can tell, their primary use case is to specify incorrect approximations of time zones for the purpose of causing bugs around edge cases like Daylight Saving Time)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"
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:
(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!)
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 :)
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:
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
but having a dedicated data type for them kinda seems like footgun to me, even though it's a very common design :big_smile:
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.
I think the footgun is storing time zone alongside the date/time
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
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
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
That's fair. Also, an end user can always make simple wrapper if they want (Utc, Timezone)
yep! but that way you have to go out of your way to do it, so it's no longer a default
So as long as you have function to help with display using Utc
and Timezone
, I guess it doesn't really matter much.
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.
Side question: do any UTC libraries deal with leap seconds and the such when calculating durations?
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.
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
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
Upgrading from I64
to I128
should be a non-breaking change, so that shouldn't matter, right?
those are still type-incompatible
like you'd have to do Num.toI28
on your I64
Sure
So you get an input of I64
and convert it.
That's fine
The type is opaque. An end user has no idea how the data is stored.
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