josevalim

josevalim

Creator of Elixir

Proposal: strftime-based calendar/datetime formatting

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.

Most Liked

josevalim

josevalim

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.

axelson

axelson

Scenic Core Team

I like the reduced scope! I think it will cover the majority of use-cases very well.

Is there a chance of providing a means for developers to flexibly provide their own custom formatting? It could be used to provide the ordinal formatting that @Nicd is requesting.

Here’s one example syntax:

iex> Calendar.format(now, `"%B %{d_ord}"`, MyApp.OrdinalFormatter)
December 14th

Another syntax might be more like %B %Cd where the C indicates that the next character represents a custom formatting directive.

Of course a setup like this would mean that you’d always have to pass in your formatter when formatting your date strings, but you could relatively easily create a MyApp.calendar_format/2 that would bake it in.

kip

kip

ex_cldr Core Team

@josevalim very practical proposal and probably easier to consume in most use cases than the CLDR encoding I’d agree.

No surprise that I look at this and consider how this could be used in a locale-specific way. Injecting a formatter is great. But I think it would be even better if there is an option to inject an ma tuple instead of just the module. That way a locale could also be injected (or any other parameter). Otherwise for a locale aware application one would need to either:

  1. Set a locale prior to calling strftime which seems very brittle and not very clear.
  2. Or, as your original example illustrates, there would need to be one module per locale and a lookup table to translate a locale into a module name which is a lot of scaffolding just to inject the right formatter for a locale.

An example of the ma approach would be:

 Calendar.format(date_or_time_or_datetime, "%Y-%m-%d %H:%M:%S.%f", {MyApp, ["pt-BR"]})

This would make the intent clearer and be easier to adapt for Gettext and Cldr and any other locale-specific library. Implementation is just another function head that can easily be pattern matched. The actual date being formatted would be prepended to the other tuple arguments.

Where Next?

Popular in Proposals Top

josevalim
I am resubmitting the proposal from earlier today with more context and more focus on the important parts. Some concerns and praises stay...
452 18627 123
New
josevalim
Hi everyone, This is a proposal for introducing local accumulators to Elixir. This is another attempt of solving the comprehension probl...
1043 11268 245
New
josevalim
Hi everyone, We are considering deprecating 'charlists' in Elixir in favor of ~c"charlist". In many languages, 'foobar' is equivalent to...
New
josevalim
One of the major differences between running your application as a release and as a Mix project is the differences in configuration. Mix ...
382 18742 108
New
josevalim
NOTE: this is a focused thread, so we appreciate if everybody stayed on topic. Feel free to comment anything in regards to calendar forma...
New
josevalim
Hi everyone, One of the features added to Elixir early on to help integration with Erlang code was the idea of overridable function defi...
New
josevalim
Hi everyone, Erlang/OTP 21 comes with two new guards contributed by @michalmuskala: map_get/2 and is_map_key/2. Now we need to discuss h...
New
josevalim
Hi everyone, as you may be aware, we are researching a type system for Elixir. As preparation for my upcoming ElixirConf US talk, I woul...
New
josevalim
UPDATE: This proposal has been retracted. Read the new proposal here: Local accumulators for cleaner comprehensions Hi everyone, This i...
New
michalmuskala
TL;DR: The Elixir Core team is announcing a call for proposals to extend support for time zones in Elixir’s standard library. The reason...
New

Other popular topics Top

Harrisonl
We have an ECS cluster with 4 services, where each task joins a single cluster, via discovery ECS discovery service. Currently when I de...
New
Darmani72
If I have a post route which an argument: post /my_post_route/:my_param1, MyController.my_post_handler How would get the post params ...
New
gshaw
What is the idiomatic way of matching for not nil in Elixir? E.g., First way: defp halt_if_not_signed_in(conn, signed_in_account) when...
New
JeremM34
Hello, how can I check the Phoenix version ? Thanks !
New
dblack
I’ve got an issue with an app and I’ve no idea of how to troubleshoot it. I’m hoping someone here might have seen something similar. I p...
New
klo
Got a question about when to concat vs. prepending items to list then reversing to achieve appending. So i know lists boil down to [1 | ...
New
hariharasudhan94
I would like to know what is the best IDE for elixir development?
New
PeterCarter
There are pre-rolled solutions for other frameworks that do work. However, Phoenix does not seem to have these. Have people had good expe...
New
jaysoifer
Is there a way to rollback a specific migration and only that one (“skipping” all the other ones)? Would mix ecto.rollback -v 200809061...
New
JorisKok
I have a server on AWS, and was running a load test using artillery. When looking at the Phoenix dashboard I see the Ports going to 100% ...
New

We're in Beta

About us Mission Statement