Skip to content

Conversation

@mflatt
Copy link
Member

@mflatt mflatt commented Nov 15, 2025

This attempt to add time and date functions in a rhombus/date library is based on the gregor library for Racket and Java's date.LocalTime, etc., classes. It doesn't try to do everything that those do, but instead provides basic functionality that is available from racket/base and racket/date, but in a form that hopefully does not preclude improvements and compatible generalization by new libraries (unlike the way Gregor has to avoid the Racket date structure type).

I think I get why Java puts Local in the names of classes, but I have opted not to do that, but the name ZonedDateTime does come from Java.

API sumary:

export:
  Time
  Date
  DateTime
  ZonedDateTime
  Format  

class Time(~hour: hour :: Int.in(0, 23) = 0,
           ~minute: minute :: Int.in(0, 59) = 0,
           ~second: second :: Int.in(0, 60) = 0,
           ~nanosecond: nanosecond :: Int.in(0, 999_999_999) = 0):
  private implements Comparable

  fun now(~local = #true) :: Time

  fun from_seconds(secs :: Real,
                   ~local = #true) :: Time             

  method to_string(~show_nanosecond = #false) :: String

class Date(~year: year :: Int,
           ~month: month :: Int.in(1, 12 ~inclusive),
           ~day: day :: Int.in(1, 31 ~inclusive)):
  private implements Comparable

  fun now(~local = #true) :: Date

  fun from_seconds(secs :: Real,
                   ~local = #true) :: Date

  method to_datetime(~time: time :: date.Time = Time()) :: DateTime
  
  method to_seconds() :: Real

  method to_string(~format: format :: date.Format = default_format) :: String

class DateTime(~year: year :: Int,
               ~month: month :: Int.in(1, 12),
               ~day: day :: Int.in(1, 31),
               ~hour: hour :: Int.in(0, 23) = 0,
               ~minute: minute :: Int.in(0, 59) = 0,
               ~second: second :: Int.in(0, 60) = 0,
               ~nanosecond: nanosecond :: Int.in(0, 999_999_999) = 0):
  private implements Comparable

  fun now(~local = #true) :: DateTime

  fun from_seconds(secs :: Real,
                   ~local = #true) :: DateTime

  method to_seconds() :: Real

  method to_zoned() :: ZonedDateTime

  method to_string(~format: format :: date.Format = default_format,
                   ~show_time = #true,
                   ~show_nanosecond = #false) :: String

class ZonedDateTime(~year: year :: Int,
                    ~month: month :: Int.in(1, 12),
                    ~day: day :: Int.in(1, 31),
                    ~hour: hour :: Int.in(0, 23) = 0,
                    ~minute: minute :: Int.in(0, 59) = 0,
                    ~second: second :: Int.in(0, 60) = 0,
                    ~nanosecond: nanosecond :: Int.in(0, 999_999_999) = 0,
                    ~week_day: week_day :: Int.in(0, 6) = 0,
                    ~year_day: year_day :: Int.in(0, 365) = 0,
                    ~is_dst: is_dst :: Boolean = #false,
                    ~time_zone_offset: time_zone_offset :: Int = 0,
                    ~time_zone_name: time_zone_name :: String = "UTC"):
  private implements Comparable

  fun now() :: ZonedDateTime

  fun from_seconds(secs :: Real,
                   ~local: local = #true)

  fun find_seconds(~second: second :: Int.in(0, 60) = 0,
                   ~minute: minute :: Int.in(0, 59) = 0,
                   ~hour: hour :: Int.in(0, 23) = 0,
                   ~day: day :: Int.in(1, 31),
                   ~month: month :: Int.in(1, 12),
                   ~year: year :: Int,
                   ~local: local = #true) :: Int

  method to_string(~format: format :: date.Format = default_format,
                   ~show_time = #true,
                   ~show_nanosecond = #false,
                   ~show_time_zone = #true) :: String

  method to_seconds() :: Real

enum Format:
  rfc2822
  rfc3339
  iso8601
  american
  european
  julian

@mflatt mflatt marked this pull request as ready for review November 15, 2025 15:53
@samth
Copy link
Member

samth commented Nov 15, 2025

Can you say more about why you decided not to use gregor directly? Also, gregor provides a bunch of functionality that requires various tables (eg the tz database). Do you think that should eventually be a part of this library, some other standard library, or something else?

@mflatt
Copy link
Member Author

mflatt commented Nov 15, 2025

Can you say more about why you decided not to use gregor directly?

It's a large dependency. A (require gregor) by itself takes quite a bit longer than (require rhombus). So, I imagine that the functionality of gregor eventually would be added by rhombus-gregor package, or something like that.

@LiberalArtist
Copy link

Along similar lines, re:

in a form that hopefully does not preclude improvements and compatible generalization by new libraries (unlike the way Gregor has to avoid the Racket date structure type).

Can you say more about which problems with Racket's date this tries to avoid?

I noticed that there are separate fields for is_dst, time_zone_offset, time_zone_name, making it possible to represent nonsensical states, which I think of as one of the problems with Racket's date.

I looked back at racket/racket#1220 for some context. In racket/racket#1220 (comment), @97jaz wrote:

Gregor's date struct really just represents a date. Racket's represents a date, a time, a UTC offset, and a few other things like the day-of-week, which Gregor treats as a function of the data rather than part of it. Racket's date is similar to what Gregor calls a "moment," but the two still aren't quite compatible. Racket will let me construct, for example, a date representing Sunday, January 21, 2016 (at some time or other, etc.), but Gregor keeps to the Gregorian calendar in which January 21, 2016 is a Thursday. Also, Gregor is picky about what will count as a time zone.

Re @samth:

Also, gregor provides a bunch of functionality that requires various tables (eg the tz database).

In discussion around racket/racket#1220 (comment), the size of data files was mentioned as an obstacle to including Gregor in the main Racket distribution, but I hope that can be overcome.

I need to refresh my memory about what needs the cldr-core data as opposed to just the tz database. For the tz database specifically, though, it is provided by the operating system except on Windows, where it's sourced from a platform-specific dependency on the tzdata Racket package. Today I discovered PEP 615, which adopted the same approach for the tz database in Python (but I don't see discussion of cldr-core data there).

Another development since previous discussion is that currently-supported Windows versions provide ICU's C API as a system library (see also racket/racket#4844), and ICU integrates the tz database and CLDR data. Potentially, if we could limit ourselves to functionality exposed by the ICU C API, we could avoid distributing data files for Windows altogether. PEP 615 has this to say about that rejected alternative:

Providing bindings for this would allow us to support Windows “out of the box” without the need to install the tzdata package, but unfortunately the C headers provided by Windows do not provide any access to the underlying time zone data — only an API to query the system for transition and offset information is available. This would constrain the semantics of any ICU-based implementation in ways that may not be compatible with a non-ICU-based implementation — particularly around the behavior of the cache.

@mflatt
Copy link
Member Author

mflatt commented Nov 15, 2025

Can you say more about which problems with Racket's date this tries to avoid?

The main one I know is using the word "date" for "date and time". I believe the Time, Date, and DateTime classes here line up with same-named structure types in gregor.

As you note, there's no attempt here to ensure that a ZonedDateTime has a consistent set of fields (because that's not easy), and that's partly why the class is not called Moment. I view ZonedDateTime primary as a way to report back the OS facility for converting from sections. Maybe there could be some bridge between Moment and ZonedDateTime in an eventual Rhombus variant of gregor, but the intent is to have a clearer point of departure where they do not have to be the same.

@97jaz
Copy link

97jaz commented Nov 15, 2025

FWIW, I agree that gregor is too large of a dependency. That's mostly due to the CLDR i18n/l10n data and code, which in hindsight I would have omitted from the library and instead provided simple strftime/strptime-like functionality (and left anything more fancy to add-on libraries).

On the other hand, I still wouldn't have allowed the construction of nonsense dates and times. That's pretty easy to do with two exceptions:

  • Leap seconds, which gregor completely ignores; it does not allow 60 as a valid second of the hour and so cannot represent all times that your OS might report). Note that java.time also ignores leap seconds.
  • Local timelines, where hours are skipped or repeated due to daylight saving time adjustments.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants