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 thehours_in_am_pm
tuple (as seen inc: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.