josevalim

josevalim

Creator of Elixir

NOTE: this is a focused thread, so we appreciate if everybody stayed on topic. Feel free to comment anything in regards to calendar formatting but avoid off-topic or loosely related topics. For example, if you would like to discuss or propose other Calendar/DateTime features, please use a separate thread.

Hi everyone,

This is take two for calendar/datetime formatting in Elixir. This time, we are exploring strftime-based syntax which is much simpler in scope than the Unicode’s Locale Date Markup Language discussed previously.

Here is how the API will look like:

Calendar.format(date_or_time_or_datetime, "%Y-%m-%d %H:%M:%S.%f")
#=> {:ok, "2018-11-29 13:19:41.032412"}

The Calendar.format/2 entry point accepts any calendar type, using structural typing. This means we will be able to format any map that has the fields being formatted. In case a map field is missing, an appropriate error message will be raised.

The formatting function will also support multiple options to customize different aspects of formatting. Let’s take a look at them:

Options

The options can be broken into 2 distinct categories.

The first one is about localization:

  • :preferred_date - configures the default date
  • :preferred_time - configures the default time
  • :preferred_datetime - configures the default datetime
  • :hours_in_am_pm - a function that receives hour, minute, second and returns the hours_in_am_pm tuple (as seen in c:Calendar.hours_in_am_pm/3)

Then we have options that control translations:

  • :am_pm_names - a function that receives :am, :pm and returns the relevant “am”/“pm” string
  • :month_names - a function that receives the month as an integer and returns the month name as a string. For example, fn index -> {"January", "February", ...} |> elem(index - 1) end
  • :abbreviated_month_names - a function that receives the month as an integer and returns the abbreviated month name as a string. For example, fn index -> {"Jan", "Feb", ...} |> elem(index - 1) end
  • :day_of_week_names - a function that receives the day of the week as an integer and returns the day of the week as a string. For example, fn index -> {"Monday", "Tuesday", ...} |> elem(index - 1) end
  • :abbreviated_day_of_week_names - a function that receives the day of the week as an integer and returns the abbreviated day of the week as a string. For example, fn index -> {"Mon", "Tue", ...} |> elem(index - 1) end

The default values of all options will be returned by the calendar, which should implement a formatter_config callback.

With the options out of the way, let’s talk about the formatting syntax.

strftime syntax

strftime has a simpler notation while still covering a wide range of use cases. This leaves it open for the community to support more complex formats such as ICU/Unicode/CLDR if desired.

The proposed syntax is an extension of strftime that also allows the padding width to be given as argument:

%<flag>?<width>?<format>

The flag is limited to certain characters, the width is a positive integer without leading zeros and the format is always a letter. Examples are %d. %-d, %4d and %_4d.

Format Description Example (in ISO) Source
%a Abbreviated name of day Mon Calendar.day_of_week + :abbreviated_day_of_week_names
%A Full name of day Monday Calendar.day_of_week + :day_of_week_names
%b Abbreviated month name Jan struct.month + :abbreviated_month_names
%B Full month name January struct.month + :month_names
%c Preferred date+time representation 2018-10-17 12:34:56 :preferred_datetime
%d Day of the month 01, 12 struct.month
%f Microseconds 000000, 999999, 0123 struct.microsecond
%H Hour using a 24-hour clock 00, 23 struct.hour
%I Hour using a 12-hour clock 01, 12 struct.hour
%j Day of the year 001, 366 Calendar.day_of_year
%m Month 01, 12 struct.month
%M Minute 00, 59 struct.minute
%p “AM” or “PM” (noon is “PM”, midnight as “AM”) AM, PM Calendar.hours_in_am_pm + :am_pm_names
%P “am” or “pm” (noon is “pm”, midnight as “am”) am, pm Calendar.hours_in_am_pm + :am_pm_names
%q Quarter 1, 2, 3, 4 Calendar.quarter_of_year
%S Second 00, 59, 60 struct.second
%u Day of the week 01 (monday), 07 (sunday) Calendar.day_of_week
%x Preferred date (without time) representation 2018-10-17 :preferred_date
%X Preferred time (without date) representation 12:34:56 :preferred_time
%y Year as 2-digits 01, 01, 86, 18 struct.year
%Y Year -0001, 0001, 1986 struct.year
%z +hhmm/-hhmm time zone offset from UTC (empty string if naive) +0300, -0530 struct.utc_offset + struct.std_offset
%Z Time zone abbreviation (empty string if naive) CET, BRST struct.zone_abbr
%% Literal “%” character %

The source column is used as a reference for the implementation and it won’t be present in the final documentation.

Flags

By default the modifiers above are all padded with zeros according to the ISO standard. The user can disable padding or use spaces with the flags below:

  • _ (underscore) - pad a result with spaces, such as %_d
  • - (dash) - do not pad a result, such as %-d
  • 0 (zero) - pad with zeros, such as %0d

Rationale

Last but not least, it is worth discussing the rationale for date/time formatting. If you have an application that works with calendar types, it is likely that you have to format them at some point. If your application mostly interfaces with other systems, then there is a chance the built-in ISO format is enough, but not always. For example, some HTTP headers use a different format than the recommended ISO one. Therefore adding formatting to the standard library feels like a natural next step to the existing functionality. Furthermore, by choosing to support strftime, we guarantee that the implementation will have tiny footprint compared to larger standards.

Another discussion, which may or may not impact this one, is about parsing. The parsing specification is often the same as the formatting specification but we have explicitly decided to not support parsing in Elixir. First of all, it is really hard to support a general but efficient runtime date/time parsing strategy. If you expect certain formats, it is almost always better to define functions that parse specifically those formats. Things get trickier if we consider the fact we need to support internalization, which is trivial for formatting, but quite expensive for parsing. In other words, while we can provide a general and efficient implementation for formatting, we can’t do so for parsing. Since different trade-offs can be made here, ranging from performance to flexibility, we are not comfortable in picking one or another.

Roadmap

We don’t plan to add this functionality directly to Elixir. Instead we will develop it as a library and collect feedback. The complexity of the implementation will also dictate if this will become part of core or not, but we believe the implementation will be relatively simple.

Log

Log of changes done to the proposal.

  • 2018/12/14 - proposal submitted
  • 2018/12/15 - removed the Formatter callback from the proposal in favor of an option/config based API
  • 2018/12/17 - removed week_of_year to align with current Elixir master
  • 2018/12/18 - added width and %q
  • 2018/12/19 - remove calendar extensions section

Feedback

Your turn.

Showing Posts 40 to 31

josevalim

josevalim OP

Creator of Elixir

So there is no way to get the actual era name in strftime?

kip

kip

ex_cldr Core Team

Looks like I have misunderstood how %EC works (at least on MAC OSX). It just returns the century years:

kip@Kips-iMac-Pro strftime % ./a.out '%EC'
Result string is "20"

Therefore I don’t see a compatible path forward for including era formatting in strftime/2.

josevalim

josevalim OP

Creator of Elixir

I believe we can add %V but I would make it calendar specific. So once you change calendars, it will return the week number of said calendars. The default is of course ISO. Does strftime specify a way to get the week of year? Without week of year, I don’t see this being very useful.

About %U and %W, I am honestly not sure, as they seem to be based on gregorian calendars. What would they return for a japanese calendar, for example?

I agree with this proposal. day_of_era works but we will probably need to use year_of_era to implement all of the modifiers (except the %EC itself).

Regarding “(AD versus CE and BC versus BCE)”, let’s just pick whatever strftime uses/returns.

I like this proposal too but I wonder if it should simply be an option called :number_formatter. The number formatter is a function that receives the number as an integer and the padding, and it returns a string. You can replace the number formatter to use any numeric alphabet and padding that you want. This seems simpler overall. WDYT?


Thanks for writing these down @kip. If you want, we can proceed with a PR for era while we discuss the remaining topics. :slight_smile:

kip

kip

ex_cldr Core Team

With Calendar.strftime/2 being merged into Elixir master (from the NimbleStrftime lib) I’d like to open a proposal on some additional formatting options and gain community feedback.

There are three sub-proposals and any and all feedback is welcome.

Weeks (yes, again with the weeks conversation :slight_smile: )

Linux strftime has formatting flags for weeks. These being:

   %U     The week number of the current year as a decimal number, range
          00 to 53, starting with the first Sunday as the first day of
          week 01.  See also %V and %W.  (Calculated from tm_yday and
          tm_wday.)

   %V     The ISO 8601 week number (see NOTES) of the current year as a
          decimal number, range 01 to 53, where week 1 is the first week
          that has at least 4 days in the new year.  See also %U and %W.
          (Calculated from tm_year, tm_yday, and tm_wday.)  (SU)

   %W     The week number of the current year as a decimal number, range
          00 to 53, starting with the first Monday as the first day of
          week 01.  (Calculated from tm_yday and tm_wday.)

Adding this formatting directives would require an update to the Calendar behaviour to provide support.

Options:

  1. No need, don’t add
  2. Just add %V for the ISO week number and add Calendar.iso_week_of_year/1 to the Calendar behaviour
  3. Maximise compatibility, go with all three and add the callbacks

Add support for Era

Calendar.day_of_era/1 returns {day, era} but that’s an integer, not a display format. Calendar behaviour doesn’t do any display format translation today. The new Calendar.strftime/2 does display format translation for AM and PM (and variants). So one approach is to simply enhance Calendar.strftime/2 to also map Calendar.ISO eras 0 -> display_name and 1 -> display_name. And there would need to be agreement on display name (AD versus CE and BC versus BCE).

Era isn’t used in day-to-day formatting for Gregorian calendars except for dates before year 1. It is used as a standard part of formatting Japanese calendars and for some format of Chinese calendars (and derivatives on the 60 year cycle).

strftime defines the following directives:

Specifier Meaning
%Ec Date/time for current era.
%EC Era name.
%Ex Date for current era.
%EX Time for current era.
%Ey Era year. This is the offset from the base year.
%EY Year for current era.

These can be implemented without a change to the calendar behaviour if Calendar.strftime/2 treats era like it treats am/pm and provides an internal translation. It would perhaps be better that both of these (ie am/pm and era translations became behaviours since that would also help in international projects using Gettext or Cldr.

Options

  1. Era? Who cares, forget about it
  2. Compatibility is a good idea and implementation seems simple. Do it.
  3. I care about calendars beyond Calendar.ISO so I need this (I’m not holding my breath on this one :slight_smile: )

Localised number systems (ie not Latin alphabet)

strftime supports localising the date format to use non-latin alphabets. These is also supported in ex_cldr and friends. The default for these directives is to use the Latin alphabet so implementing these directives is quite trivial. If no configuration is provided in options, use the fallback to the Latin characters.

The formatting directives are:

Specifier Meaning
%Od Represents the day of the month, using the locale’s alternative numeric symbols, filled as needed with leading 0’s if an alternative symbol for 0 exists. If an alternative symbol for 0 does not exist, the %Od modified conversion specifier uses leading space characters.
%Oe Represents the day of the month, using the locale’s alternative numeric symbols, filled as needed with leading 0’s if an alternative symbol for 0 exists. If an alternative symbol for 0 does not exist, the %Oe modified conversion specifier uses leading space characters.
%OH Represents the hour in 24-hour clock time, using the locale’s alternative numeric symbols.
%OI Represents the hour in 12-hour clock time, using the locale’s alternative numeric symbols.
%Om Represents the month, using the locale’s alternative numeric symbols.
%OM Represents the minutes, using the locale’s alternative numeric symbols.
%OS Represents the seconds, using the locale’s alternative numeric symbols.
%Ou Represents the weekday as a number using the locale’s alternative numeric symbols.
%OU Represents the week number of the year, using the locale’s alternative numeric symbols. Sunday is considered the first day of the week. Use the rules corresponding to the %U conversion specifier.
%OV Represents the week number of the year (Monday as the first day of the week, rules corresponding to %V) using the locale’s alternative numeric symbols.
%Ow Represents the number of the weekday (with Sunday equal to 0), using the locale’s alternative numeric symbols.
%OW Represents the week number of the year using the locale’s alternative numeric symbols. Monday is considered the first day of the week. Use the rules corresponding to the %W conversion specifier.
%Oy Represents the year (offset from %C) using the locale’s alternative numeric symbols.

Options

  1. Who cares about countries and people that don’t use the Latin alphabet. Forget it.
  2. Good idea - easy to implement, no impact if I’m not doing non-latin alphabet code. Glad to see Elixir embracing global cultures (ok, I’m pitching I know :slight_smile: )
kip

kip

ex_cldr Core Team

@josevalim, all good. I think the only thing maybe missing is era, but I see that’s not part of the substitutions anyway.

For those looking to localise formatting with NimbleStrftime, there is now an ex_cldr function to generate the options for NimbleStrftime.format/3. ex_cldr_dates_times is another option for those looking for CLDR-based formatting.

MyApp.Cldr.Calendar.strftime_options!/2 (where MyApp is any Cldr backend module you’ve defined) provides these options. You’ll need to update to ex_cldr_calendars version 1.5.0.

Its a ! function because if the specified locale is unknown it will raise.

Examples

iex> NimbleStrftime.format(~D[2019-11-03], "%A, %b %d %Y", MyApp.Cldr.Calendar.strftime_options!())
"Sunday, Nov 03 2019"

iex> NimbleStrftime.format(~D[2019-11-03], "%A, %b %d %Y", MyApp.Cldr.Calendar.strftime_options!("fr")) ==
"dimanche, nov. 03 2019"
josevalim

josevalim OP

Creator of Elixir

We have finally implemented a library based on this proposal: GitHub - dashbitco/nimble_strftime: A simple and fast strftime-based datetime formatter · GitHub

Everyone, please do give it a try in your application! And @kip, let us know if it provides the necessary hooks for i18n/l10n.

kip

kip

ex_cldr Core Team

With my CLDR-based date/time formatter now finally out the door I look forward to serving configuration for strftime whenever its ready for a road test …

bjunc

bjunc

Ha, well I’d be happy to give it a shot.

josevalim

josevalim OP

Creator of Elixir

That someone can even be you! :smiley:

But yes, a formatter is just a module, so you can just define your own module if you want to.

bjunc

bjunc

I think there is more than enough rationale to justify something like this. To be honest, when I first got into Elixir, this was something I looked around for and just assumed it already existed.

Having used datetime formatters in a few languages, I think my favorite has been Moment.js (which isn’t strftime, and is closer to LDML). I even created a version for Elixir (sorry, never shared it). I think the formatting tokens are more intuitive than strftime.

For example, what I created looked like this:

DateTimeFormatter.format(datetime, "YYYY-MM-DD")
{:ok, "2018-12-22"}

DateTimeFormatter.format!(datetime, "M/D, h:mm a")
"12/22, 1:14 pm"

DateTimeFormatter.format(datetime, "[today is] dddd")
{:ok, "today is Saturday"}

One key difference is multiple characters for the token. Eg:

M              1 2 ... 11 12
Mo             1st 2nd ... 11th 12th
MM             01 02 ... 11 12
MMM            Jan Feb ... Nov Dec
MMMM           January February ... November December

That said, it sounds like you are proposing the ability to substitute formatters? So the default would be strftime, but someone could build a LDML formatter?

Where Next? Top

Trending in Proposals Top

Other Trending Topics Top

garrison
Hobbes is a low-level distributed database for the Elixir programming language. Hobbes provides a simple, safe, and scalable storage lay...
New
mcass19
ExRatatui lets you cook up rich terminal UIs in Elixir, powered by Rust’s ratatui via Rustler NIFs. Build interactive terminal applicatio...
New
Damirados
Hello everyone. After busy few months I am happy to announce v0.1.0 of Emerge &amp; Solve. They are GUI (Emerge) and State management (S...
New
netoum
Corex is an accessible, unstyled UI component library for Phoenix that integrates Zag.js state machines using Vanilla JavaScript and Live...
New
wintermeyer
There are three potential reasons for members of this forum to have a look at https://vutuv.de You are tired or annoyed of LinkedIn. Yo...
New
aseigo
ICal is a library for interacting with iCalendar data. It parses iCalendars into typed Elixir structs via ICal.from_ics, and can prepare ...
New

We're in Beta

About us Mission Statement

Options

Thread Display Mode




Thread Preview

Skip Thread Previews