View Source Custom Formatters

how-to-implement-a-custom-formatter-for-use-with-timex

How to implement a custom formatter for use with Timex

Implementing your own custom formatter is pretty straightforward if you plan to use one of the built-in format string tokenizers (default or strftime). The following example will use a custom tokenizer, and implement the formatter over the top of that.

The tokenizer is implemented as described in the Custom Parsers section, and the source code can be found in the Timex repo here.

getting-started

Getting Started

In brief, all you need to know is the following:

  • Extend the Formatter behavior, by adding use Timex.Format.DateTime.Formatter to the top of your module.
  • Implement tokenize/1 callback.
  • Implement format/2 callback.
  • Implement format!/2 callback.

The tokenize/1 callback can simply be delegated to the tokenizer you wish to use, while the implementation of format/2 and format!/2 are what we really are interested in here.

implementing-the-humanized-formatter

Implementing the Humanized formatter

The implementation of the formatter for our "humanized" date format would like something like the following:

defmodule MyApp.DateTimeFormatters.Humanized do
  use Timex.Format.DateTime.Formatter

  alias Timex.Format.FormatError
  alias MyApp.DateTimeTokenizers.Humanized, as: Tokenizer

  @days [
    "first", "second", "third", "fourth", "fifth",
    "sixth", "seventh", "eighth", "ninth", "tenth",
    "eleventh", "twelfth", "thirteenth", "fourteenth", "fifteenth",
    "sixteenth", "seventeenth", "eighteenth", "nineteenth", "twentieth",
    "twenty-first", "twenty-second", "twenty-third", "twenty-fourth", "twenty-fifth",
    "twenty-sixth", "twenty-seventh", "twenty-eighth", "twenty-ninth", "thirtieth",
    "thirty-first"
  ]

  defdelegate tokenize(format_string), to: Tokenizer

  def format!(date, format_string) do
    case format(date, format_string) do
      {:ok, result}    -> result
      {:error, reason} -> raise FormatError, message: reason
    end
  end

  def format(date, format_string) do
    case tokenize(format_string) do
      {:ok, []} ->
        {:error, "There were no formatting directives in the provided string."}
      {:ok, dirs} when is_list(dirs) ->
        do_format(Timex.to_naive_datetime(date), dirs, <<>>)
      {:error, reason} -> {:error, {:format, reason}}
    end
  end

  defp do_format(_date, [], result),             do: {:ok, result}
  defp do_format(_date, _, {:error, _} = error), do: error
  defp do_format(date, [%Directive{type: :literal, value: char} | dirs], result) when is_binary(char) do
    do_format(date, dirs, <<result::binary, char::binary>>)
  end
  defp do_format(%NaiveDateTime{day: day} = date, [%Directive{type: :oday_phonetic} | dirs], result) do
    phonetic = Enum.at(@days, day - 1)
    do_format(date, dirs, <<result::binary, phonetic::binary>>)
  end
  defp do_format(date, [%Directive{type: :date_shift} | dirs], result) do
    do_format(date, dirs, <<result::binary, "currently"::binary>>)
  end
  defp do_format(date, [%Directive{type: type, modifiers: mods, flags: flags, width: width} | dirs], result) do
    case format_token(type, date, mods, flags, width) do
      {:error, _} = err -> err
      formatted         -> do_format(date, dirs, <<result::binary, formatted::binary>>)
    end
  end

end

As you can see the implementation is pretty straightforward. You'll notice that the last do_format implementation calls an imported function format_token/5, this allows you to delegate the formatting of known directives to the formatter, which will use standard formatting rules. You can of course override the formatting of directives using the style above (pattern matching on the directive type and handling the formatting directly).

To use our new formatter with Timex.format:

iex> use Timex
iex> alias MyApp.DateTimeFormatters.Humanized, as: HumanFormat
iex> alias MyApp.DateTimeTokenizers.Humanized
iex> format = "{shift} the {day} of {month}, {year}"
iex> Timex.format(Timex.now, format, HumanFormat)
{:ok, "currently the eleventh of August, 2015"}