elixir-cldr / cldr_dates_times

Date & times formatting functions for the Common Locale Data Repository (CLDR) package https://github.com/elixir-cldr/cldr
Other
69 stars 13 forks source link

Feature request: DateTime intervals with mixed formats #33

Closed jmoldrich2b closed 1 year ago

jmoldrich2b commented 2 years ago

Hi Kip,

Thanks again for this amazing library. It's been so useful to me.

I've been using the DateTime Intervals and I'm wishing there was a way to have :medium formatted dates and :short formatted times. For e.g.,

Apr 22, 2022, 2:00:00 AM – 3:00:00 AM -> Apr 22, 2022, 2:00 AM – 3:00 AM

... or ideally this ...

Apr 22, 2022, 2:00:00 AM – 3:00:00 AM -> Apr 22, 2022, 2 AM – 3 AM

I feel like remove the redundant parts of the time makes it more legible, but a shorter date is less legible and less friendly, and doesn't generalise if I don't have the exact locale (e.g., 'en-GB' vs 'en-US'). So ideally I want:

Cldr.DateTime.Interval.to_string(start, finish, locale: locale, date_format: :medium, time_format: :short)

Looking at cldr_dates_times/blob/master/lib/cldr/interval/date_time.ex it appears that formats could be percolated down to the output functions, but the format for DateTimes are coarse at the moment, and the 'common date time formats' don't feature formats with both date and time. So I'm thinking the DateTime to_string method could be changed to handle mixed formats.

In the meantime I've hacked together something that works for me. It might be useful for someone else, so here it is:

  def localise_time_range(from, to, zone, locale) do
    from = DateTime.shift_zone!(from, zone)
    to = DateTime.shift_zone!(to, zone)
    {:ok, %{medium: format_dt}} = BackEnd.Cldr.DateTime.Format.date_time_formats("en")
    template = Cldr.Substitution.parse(format_dt)
    {:ok, %{medium: format_d}} = BackEnd.Cldr.DateTime.Format.date_formats("en")
    {:ok, %{short: format_t}} = BackEnd.Cldr.DateTime.Format.time_formats("en")
    format_dt = Cldr.Substitution.substitute([format_t, format_d], template)
      |> IO.iodata_to_binary()
    to_format = if Date.compare(from, to) == :eq, do: format_t, else: format_dt
    {:ok, from} = BackEnd.Cldr.DateTime.to_string(from, locale: "en", format: format_dt)
    {:ok, to} = BackEnd.Cldr.DateTime.to_string(to, locale: "en", format: to_format)
    interval_format = BackEnd.Cldr.DateTime.Format.date_time_interval_fallback("en")
    Cldr.Substitution.substitute([from, to], interval_format)
      |> IO.iodata_to_binary()
  end

Perhaps there's already a better way to do this with what's already there? Maybe it doesn't make sense to have this kind of complexity within the library? But I'd be curious to know what you think.

Cheers,

Jarrod

kipcole9 commented 2 years ago

@jmoldrich2b thanks for the suggestion! There are parts of the CLDR specification and data that are not yet exposed in the ex_cldr libs, typically because matching the spec to an implementation isn't always obvious. But I understand the ask and it makes sense to me. In part because as you say, for date/times the formatting is the date part and then the time part.

I won't be able to investigate very fully until the weekend but I'll get back to you after then if there is any path forward that can make this happen without breaking everything else.

Thanks again, much appreciated.

jarrodmoldrich commented 2 years ago

Thank you for the prompt reply! As I have a working solution there's no urgency from my end, but looking forward to seeing what you come up with (and using it).

p.s., I was accidentally logged in to a different account when I posted

kipcole9 commented 2 years ago

@jarrodmoldrich sorry for taking a long time to get to this. After review I don't see a practical way to do what you're after using format code (like :medium). I think the simplest way forward might be to use your own format string. Using your example:

iex> MyApp.Cldr.DateTime.Interval.to_string ~U[2022-04-22 02:00:00Z], ~U[2022-04-22 03:00:00Z], 
...> format: "MMM d, y, hh:mm a - hh:mm a"
{:ok, "Apr 22, 2022, 02:00 AM - 03:00 AM"}

To make deciding which format string to use I have published ex_cldr_dates_times version 2.12.0 that exposes the public function Cldr.DateTime.Interval.greatest_difference/2 that will return the format code of the greatest difference. That will allow differentiating amongst interval formats without having to dive deep into formatting yourself.

iex> Cldr.DateTime.Interval.greatest_difference ~U[2022-04-22 02:00:00Z], ~U[2022-04-22 03:00:00Z]                  
{:ok, :H} 

Hopefully that give you an easier way forward? Let me know if that sparks any better ideas? You've been a long way down the rabbit hole of the code!

jarrodmoldrich commented 2 years ago

Thanks for looking into this!

I think the simplest way forward might be to use your own format string.

I think that's basically what I'm doing in my code snippet, except I'm building the format strings from the patterns provided by your library. This approach could be generalised to find a 'mixed format' date time:

fun date_time_format(dt_format, d_format, t_format) do
    # get formats
    {:ok, dt_formats} = BackEnd.Cldr.DateTime.Format.date_time_formats("en")
    {:ok, d_formats} = BackEnd.Cldr.DateTime.Format.date_formats("en")
    {:ok, t_formats} = BackEnd.Cldr.DateTime.Format.time_formats("en")
    # select formats
    dt_format = Map.fetch!(dt_formats, dt_format)
    d_format = Map.fetch!(d_formats, d_format)
    t_format = Map.fetch!(t_formats, t_format)
    # combine formats
    template = Cldr.Substitution.parse(dt_format)
    Cldr.Substitution.substitute([t_format, d_format], template)
      |> IO.iodata_to_binary()
end

So for the from and to in the interval it could either use the result of date_time_format(..) or simply use the appropriate time format for the to depending on the result of greatest_difference(..)

Passing the format triplet down might be polluting Interval module with unnecessary complexity, but even having just this utility function to create blended date time formats could be a useful.

kipcole9 commented 1 year ago

I've been very tardy on this but am getting back to it now.

kipcole9 commented 1 year ago

I've pushed some commits that I believe provide the functionality you are looking for (finally!). I need to add some tests before I can publish but you're welcome to try it out from Github now. Here are a couple of examples:

iex> MyApp.Cldr.DateTime.Interval.to_string ~U[2020-01-01 00:00:00.0Z], ~U[2020-12-31 10:00:00.0Z], 
...> format: :medium, date_format: :long, time_format: :short
{:ok, "January 1, 2020, 12:00 AM – December 31, 2020, 10:00 AM"}

iex> MyApp.Cldr.DateTime.Interval.to_string ~U[2020-01-01 00:00:00.0Z], ~U[2020-01-01 10:00:00.0Z], 
...> format: :medium, date_format: :long, time_format: :short
{:ok, "January 1, 2020, 12:00 AM – 10:00 AM"}
kipcole9 commented 1 year ago

The same options are also added to Cldr.DateTime.to_string/2 as well (and required to support intervals anyway):

iex> MyApp.Cldr.DateTime.to_string ~U[2020-01-01 00:00:00.0Z], format: :medium, date_format: :long, time_format: :short
{:ok, "January 1, 2020, 12:00 AM"}

iex> MyApp.Cldr.DateTime.to_string ~U[2020-01-01 00:00:00.0Z], format: :medium, date_format: :short, time_format: :long
{:ok, "1/1/20, 12:00:00 AM UTC"}

iex> MyApp.Cldr.DateTime.to_string ~U[2020-01-01 00:00:00.0Z], format: :short, date_format: :long, time_format: :short
{:ok, "January 1, 2020, 12:00 AM"}
jarrodmoldrich commented 1 year ago

Thanks @kipcole9 . Love this! I'll try this out on the weekend.

I might be pushing my luck here (and stretching my memory), but is there a :thin option from time (e.g., 12AM) in the CLDR standard? Wondering if I can get a DateTime range even smaller e.g., January 1, 2020, 12AM – 10AM . Either way, this looks really good.

kipcole9 commented 1 year ago

but is there a :thin option from time (e.g., 12AM)

Nope, :short is as short as it gets. I could, I think, make it so you can specify the format string directly for the date and time parts. So you could use time_fomat: "Ha" but that then makes you less locale aware.

jarrodmoldrich commented 1 year ago

that then makes you less locale aware

That's fair. It would obviate the point of using a cldr library in the first place. Thanks for getting back to this. I'll let you know how I go

kipcole9 commented 1 year ago

I've added some tests so when you're happy its operating as expected I'll publish to hex.

jarrodmoldrich commented 1 year ago

Thanks, Kip. I was sick last weekend, but I should be able to have a crack at it this week.

kipcole9 commented 1 year ago

Jarrod, sorry to hear that. No rush at all - I'm the one that has slowed down resolution.

kipcole9 commented 1 year ago

I wanted to publish a release that fixes compiler warnings on Elixir 1.16 so I've published ex_cldr_dates_times version 2.16.0 with the following changelog entry:

Bug Fixes

Enhancements

I'll close this issue as completed but of course please reopen if it's not operating as you expected. Thanks again for y our support - and especially patience.

jarrodmoldrich commented 1 year ago

Thanks Kip. Really appreciate this. I'll probably have a chance to try it out tomorrow