1defmodule Timex.Format.DateTime.Formatter do
2  @moduledoc """
3  This module defines the behaviour for custom DateTime formatters.
4  """
5
6  alias Timex.{Timezone, Translator, Types}
7  alias Timex.Translator
8  alias Timex.Format.FormatError
9  alias Timex.Format.DateTime.Formatters.{Default, Strftime, Relative}
10  alias Timex.Parse.DateTime.Tokenizers.Directive
11
12  @callback tokenize(format_string :: String.t)
13    :: {:ok, [Directive.t]} | {:error, term}
14  @callback format(date :: Types.calendar_types, format_string :: String.t)
15    :: {:ok, String.t} | {:error, term}
16  @callback format!(date :: Types.calendar_types, format_string :: String.t)
17    :: String.t | no_return
18  @callback lformat(date :: Types.calendar_types, format_string :: String.t, locale :: String.t)
19    :: {:ok, String.t} | {:error, term}
20  @callback lformat!(date :: Types.calendar_types, format_string :: String.t, locale :: String.t)
21    :: String.t | no_return
22
23  @doc false
24  defmacro __using__(_opts) do
25    quote do
26      @behaviour Timex.Format.DateTime.Formatter
27
28      alias Timex.Parse.DateTime.Tokenizers.Directive
29      import Timex.Format.DateTime.Formatter, only: [format_token: 5, format_token: 6]
30    end
31  end
32
33  @doc """
34  Formats a Date, DateTime, or NaiveDateTime as a string, using the provided format string,
35  locale, and formatter. If the locale does not have translations, "en" will be used by
36  default. If a formatter is not provided, the formatter used is `Timex.Format.DateTime.Formatters.DefaultFormatter`
37
38  If an error is encountered during formatting, `lformat!` will raise
39  """
40  @spec lformat!(Types.valid_datetime, String.t, String.t, atom | nil) :: String.t | no_return
41  def lformat!(date, format_string, locale, formatter \\ Default)
42
43  def lformat!({:error, reason}, _format_string, _locale, _formatter),
44    do: raise ArgumentError, to_string(reason)
45  def lformat!(datetime, format_string, locale, :strftime),
46    do: lformat!(datetime, format_string, locale, Strftime)
47  def lformat!(datetime, format_string, locale, :relative),
48    do: lformat!(datetime, format_string, locale, Relative)
49  def lformat!(%{__struct__: struct} = date, format_string, locale, formatter)
50    when struct in [Date, DateTime, NaiveDateTime, Time] and is_binary(format_string)
51        and is_binary(locale) and is_atom(formatter) do
52    case formatter.lformat(date, format_string, locale) do
53      {:ok, result}    -> result
54      {:error, reason} -> raise FormatError, message: reason
55    end
56  end
57  def lformat!(date, format_string, locale, formatter)
58    when is_binary(format_string) and is_binary(locale) and is_atom(formatter) do
59    case Timex.to_naive_datetime(date) do
60      {:error, reason} -> raise ArgumentError, to_string(reason)
61      datetime ->
62        case formatter.lformat(datetime, format_string, locale) do
63          {:ok, result}    -> result
64          {:error, reason} -> raise FormatError, message: reason
65        end
66      end
67  end
68  def lformat!(a,b,c,d),
69    do: raise "invalid argument(s) to lformat!/4: #{inspect a}, #{inspect b}, #{inspect c}, #{inspect d}"
70
71  @doc """
72  Formats a Date, DateTime, or NaiveDateTime as a string, using the provided format string,
73  locale, and formatter. If the locale provided does not have translations, "en" is used by
74  default. If a formatter is not provided, the formatter used is `Timex.Format.DateTime.Formatters.DefaultFormatter`
75  """
76  @spec lformat(Types.valid_datetime, String.t, String.t, atom | nil) :: {:ok, String.t} | {:error, term}
77  def lformat(date, format_string, locale, formatter \\ Default)
78
79  def lformat({:error, _} = err, _format_string, _locale, _formatter),
80    do: err
81  def lformat(datetime, format_string, locale, :strftime),
82    do: lformat(datetime, format_string, locale, Strftime)
83  def lformat(datetime, format_string, locale, :relative),
84    do: lformat(datetime, format_string, locale, Relative)
85  def lformat(date, format_string, locale, formatter)
86    when is_binary(format_string) and is_binary(locale) and is_atom(formatter) do
87      try do
88        {:ok, lformat!(date, format_string, locale, formatter)}
89      catch
90        _type, %{:message => msg} ->
91          {:error, msg}
92        _type, reason ->
93          {:error, reason}
94      end
95  end
96  def lformat(_, _, _, _),
97    do: {:error, :badarg}
98
99
100  @doc """
101  Formats a Date, DateTime, or NaiveDateTime as a string, using the provided format
102  string and formatter. If a formatter is not provided, the formatter
103  used is `Timex.Format.DateTime.Formatters.DefaultFormatter`.
104
105  Formatting will use the configured default locale, "en" if no other default is given.
106
107  If an error is encountered during formatting, `format!` will raise.
108  """
109  @spec format!(Types.valid_datetime, String.t, atom | nil) :: String.t | no_return
110  def format!(date, format_string, formatter \\ Default)
111
112  def format!(date, format_string, formatter),
113    do: lformat!(date, format_string, Translator.default_locale, formatter)
114
115  @doc """
116  Formats a Date, DateTime, or NaiveDateTime as a string, using the provided format
117  string and formatter. If a formatter is not provided, the formatter
118  used is `Timex.Format.DateTime.Formatters.DefaultFormatter`.
119
120  Formatting will use the configured default locale, "en" if no other default is given.
121  """
122  @spec format(Types.valid_datetime, String.t, atom | nil) :: {:ok, String.t} | {:error, term}
123  def format(date, format_string, formatter \\ Default)
124
125  def format(datetime, format_string, :strftime),
126    do: lformat(datetime, format_string, Translator.default_locale, Strftime)
127  def format(datetime, format_string, :relative),
128    do: lformat(datetime, format_string, Translator.default_locale, Relative)
129  def format(datetime, format_string, formatter),
130    do: lformat(datetime, format_string, Translator.default_locale, formatter)
131
132  @doc """
133  Validates the provided format string, using the provided formatter,
134  or if none is provided, the default formatter. Returns `:ok` when valid,
135  or `{:error, reason}` if not valid.
136  """
137  @spec validate(String.t, atom | nil) :: :ok | {:error, term}
138  def validate(format_string, formatter \\ Default)
139  def validate(format_string, formatter) when is_binary(format_string) and is_atom(formatter) do
140    try do
141      formatter = case formatter do
142                    :strftime -> Strftime
143                    :relative -> Relative
144                    _         -> formatter
145                  end
146      case formatter.tokenize(format_string) do
147        {:error, _} = error -> error
148        {:ok, []} -> {:error, "There were no formatting directives in the provided string."}
149        {:ok, directives} when is_list(directives)-> :ok
150      end
151    rescue
152      x -> {:error, x}
153    end
154  end
155  def validate(_, _), do: {:error, :badarg}
156
157  @doc """
158  Given a token (as found in `Timex.Parsers.Directive`), and a Date, DateTime, or NaiveDateTime struct,
159  produce a string representation of the token using values from the struct, using the default locale.
160  """
161  @spec format_token(atom, Types.calendar_types, list(), list(), list()) :: String.t | {:error, term}
162  def format_token(token, date, modifiers, flags, width) do
163    format_token(Translator.default_locale, token, date, modifiers, flags, width)
164  end
165
166  @doc """
167  Given a token (as found in `Timex.Parsers.Directive`), and a Date, DateTime, or NaiveDateTime struct,
168  produce a string representation of the token using values from the struct.
169  """
170  @spec format_token(String.t, atom, Types.calendar_types, list(), list(), list()) :: String.t | {:error, term}
171  def format_token(locale, token, date, modifiers, flags, width)
172
173  # Formats
174  def format_token(locale, :iso_date, date, modifiers, _flags, _width) do
175    flags = [padding: :zeroes]
176    year  = format_token(locale, :year4, date, modifiers, flags, width_spec(4..4))
177    month = format_token(locale, :month, date, modifiers, flags, width_spec(2..2))
178    day   = format_token(locale, :day, date, modifiers, flags, width_spec(2..2))
179    "#{year}-#{month}-#{day}"
180  end
181  def format_token(locale, :iso_time, date, modifiers, _flags, _width) do
182    flags  = [padding: :zeroes]
183    hour   = format_token(locale, :hour24, date, modifiers, flags, width_spec(2..2))
184    minute = format_token(locale, :min, date, modifiers, flags, width_spec(2..2))
185    sec    = format_token(locale, :sec, date, modifiers, flags, width_spec(2..2))
186    ms     = format_token(locale, :sec_fractional, date, modifiers, flags, width_spec(-1, nil))
187    "#{hour}:#{minute}:#{sec}#{ms}"
188  end
189  def format_token(locale, token, date, modifiers, _flags, _width)
190    when token in [:iso_8601_extended, :iso_8601_extended_z] do
191    date  = case token do
192      :iso_8601_extended   -> date
193      :iso_8601_extended_z -> Timezone.convert(date, "UTC")
194    end
195    flags = [padding: :zeroes]
196    year  = format_token(locale, :year4, date, modifiers, flags, width_spec(4..4))
197    month = format_token(locale, :month, date, modifiers, flags, width_spec(2..2))
198    day   = format_token(locale, :day, date, modifiers, flags, width_spec(2..2))
199    hour  = format_token(locale, :hour24, date, modifiers, flags, width_spec(2..2))
200    min   = format_token(locale, :min, date, modifiers, flags, width_spec(2..2))
201    sec   = format_token(locale, :sec, date, modifiers, flags, width_spec(2..2))
202    ms    = format_token(locale, :sec_fractional, date, modifiers, flags, width_spec(-1, nil))
203    case token do
204      :iso_8601_extended ->
205        case format_token(locale, :zoffs_colon, date, modifiers, flags, width_spec(-1, nil)) do
206          "" ->
207            {:error, {:missing_timezone_information, date}}
208          tz ->
209            "#{year}-#{month}-#{day}T#{hour}:#{min}:#{sec}#{ms}#{tz}"
210        end
211      :iso_8601_extended_z ->
212        "#{year}-#{month}-#{day}T#{hour}:#{min}:#{sec}#{ms}Z"
213    end
214  end
215  def format_token(locale, token, date, modifiers, _flags, _width)
216    when token in [:iso_8601_basic, :iso_8601_basic_z] do
217    date  = case token do
218      :iso_8601_basic  -> date
219      :iso_8601_basic_z -> Timezone.convert(date, "UTC")
220    end
221    flags = [padding: :zeroes]
222    year  = format_token(locale, :year4, date, modifiers, flags, width_spec(4..4))
223    month = format_token(locale, :month, date, modifiers, flags, width_spec(2..2))
224    day   = format_token(locale, :day, date, modifiers, flags, width_spec(2..2))
225    hour  = format_token(locale, :hour24, date, modifiers, flags, width_spec(2..2))
226    min   = format_token(locale, :min, date, modifiers, flags, width_spec(2..2))
227    sec   = format_token(locale, :sec, date, modifiers, flags, width_spec(2..2))
228    ms    = format_token(locale, :sec_fractional, date, modifiers, flags, width_spec(-1, nil))
229    case token do
230      :iso_8601_basic ->
231        case format_token(locale, :zoffs, date, modifiers, flags, width_spec(-1, nil)) do
232          "" ->
233            {:error, {:missing_timezone_information, date}}
234          tz ->
235            "#{year}#{month}#{day}T#{hour}#{min}#{sec}#{ms}#{tz}"
236        end
237      :iso_8601_basic_z ->
238        "#{year}#{month}#{day}T#{hour}#{min}#{sec}#{ms}Z"
239    end
240  end
241  def format_token(locale, token, date, modifiers, _flags, _width)
242    when token in [:rfc_822, :rfc_822z] do
243    # Mon, 05 Jun 14 23:20:59 +0200
244    date = case token do
245      :rfc_822  -> date
246      :rfc_822z -> Timezone.convert(date, "UTC")
247    end
248    flags = [padding: :zeroes]
249    year  = format_token(locale, :year2, date, modifiers, flags, width_spec(2..2))
250    month = format_token(locale, :mshort, date, modifiers, flags, width_spec(-1, nil))
251    day   = format_token(locale, :day, date, modifiers, flags, width_spec(2..2))
252    hour  = format_token(locale, :hour24, date, modifiers, flags, width_spec(2..2))
253    min   = format_token(locale, :min, date, modifiers, flags, width_spec(2..2))
254    sec   = format_token(locale, :sec, date, modifiers, flags, width_spec(2..2))
255    wday  = format_token(locale, :wdshort, date, modifiers, flags, width_spec(-1, nil))
256    case token do
257      :rfc_822 ->
258        case format_token(locale, :zoffs, date, modifiers, flags, width_spec(-1, nil)) do
259          "" ->
260            {:error, {:missing_timezone_information, date}}
261          tz ->
262            "#{wday}, #{day} #{month} #{year} #{hour}:#{min}:#{sec} #{tz}"
263        end
264      :rfc_822z ->
265        "#{wday}, #{day} #{month} #{year} #{hour}:#{min}:#{sec} Z"
266    end
267  end
268  def format_token(locale, token, date, modifiers, _flags, _width)
269    when token in [:rfc_1123, :rfc_1123z] do
270    # `Tue, 05 Mar 2013 23:25:19 GMT`
271    date = case token do
272      :rfc_1123  -> date
273      :rfc_1123z -> Timezone.convert(date, "UTC")
274    end
275    flags = [padding: :zeroes]
276    year  = format_token(locale, :year4, date, modifiers, flags, width_spec(4..4))
277    month = format_token(locale, :mshort, date, modifiers, flags, width_spec(-1, nil))
278    day   = format_token(locale, :day, date, modifiers, flags, width_spec(2..2))
279    hour  = format_token(locale, :hour24, date, modifiers, flags, width_spec(2..2))
280    min   = format_token(locale, :min, date, modifiers, flags, width_spec(2..2))
281    sec   = format_token(locale, :sec, date, modifiers, flags, width_spec(2..2))
282    wday  = format_token(locale, :wdshort, date, modifiers, flags, width_spec(-1, nil))
283    case token do
284      :rfc_1123 ->
285        case format_token(locale, :zoffs, date, modifiers, flags, width_spec(-1, nil)) do
286          "" ->
287            {:error, {:missing_timezone_information, date}}
288          tz ->
289            "#{wday}, #{day} #{month} #{year} #{hour}:#{min}:#{sec} #{tz}"
290        end
291      :rfc_1123z ->
292        "#{wday}, #{day} #{month} #{year} #{hour}:#{min}:#{sec} Z"
293    end
294  end
295  def format_token(locale, token, date, modifiers, _flags, _width)
296    when token in [:rfc_3339, :rfc_3339z] do
297    # `2013-03-05T23:25:19+02:00`
298    date  = case token do
299      :rfc_3339  -> date
300      :rfc_3339z -> Timezone.convert(date, "UTC")
301    end
302    flags = [padding: :zeroes]
303    year  = format_token(locale, :year4, date, modifiers, flags, width_spec(4..4))
304    month = format_token(locale, :month, date, modifiers, flags, width_spec(2..2))
305    day   = format_token(locale, :day, date, modifiers, flags, width_spec(2..2))
306    hour  = format_token(locale, :hour24, date, modifiers, flags, width_spec(2..2))
307    min   = format_token(locale, :min, date, modifiers, flags, width_spec(2..2))
308    sec   = format_token(locale, :sec, date, modifiers, flags, width_spec(2..2))
309    ms    = format_token(locale, :sec_fractional, date, modifiers, flags, width_spec(-1, nil))
310    case token do
311      :rfc_3339 ->
312        case format_token(locale, :zoffs_colon, date, modifiers, flags, width_spec(-1, nil)) do
313          "" ->
314            {:error, {:missing_timezone_information, date}}
315          tz ->
316            "#{year}-#{month}-#{day}T#{hour}:#{min}:#{sec}#{ms}#{tz}"
317        end
318      :rfc_3339z ->
319        "#{year}-#{month}-#{day}T#{hour}:#{min}:#{sec}#{ms}Z"
320    end
321  end
322  def format_token(locale, :unix, date, modifiers, _flags, _width) do
323    # Tue Mar  5 23:25:19 PST 2013`
324    flags = [padding: :zeroes]
325    year  = format_token(locale, :year4, date, modifiers, [padding: :spaces], width_spec(4..4))
326    month = format_token(locale, :mshort, date, modifiers, flags, width_spec(-1, nil))
327    day   = format_token(locale, :day, date, modifiers, [padding: :spaces], width_spec(2..2))
328    hour  = format_token(locale, :hour24, date, modifiers, [padding: :zeroes], width_spec(2..2))
329    min   = format_token(locale, :min, date, modifiers, [padding: :zeroes], width_spec(2..2))
330    sec   = format_token(locale, :sec, date, modifiers, [padding: :zeroes], width_spec(2..2))
331    wday  = format_token(locale, :wdshort, date, modifiers, flags, width_spec(-1, nil))
332    tz    = format_token(locale, :zabbr, date, modifiers, flags, width_spec(-1, nil))
333    "#{wday} #{month} #{day} #{hour}:#{min}:#{sec} #{tz} #{year}"
334  end
335  def format_token(locale, :ansic, date, modifiers, flags, _width) do
336    # Tue Mar  5 23:25:19 2013`
337    year  = format_token(locale, :year4, date, modifiers, [padding: :spaces], width_spec(4..4))
338    month = format_token(locale, :mshort, date, modifiers, flags, width_spec(-1, nil))
339    day   = format_token(locale, :day, date, modifiers, [padding: :spaces], width_spec(2..2))
340    hour  = format_token(locale, :hour24, date, modifiers, [padding: :zeroes], width_spec(2..2))
341    min   = format_token(locale, :min, date, modifiers, [padding: :zeroes], width_spec(2..2))
342    sec   = format_token(locale, :sec, date, modifiers, [padding: :zeroes], width_spec(2..2))
343    wday  = format_token(locale, :wdshort, date, modifiers, flags, width_spec(-1, nil))
344    "#{wday} #{month} #{day} #{hour}:#{min}:#{sec} #{year}"
345  end
346  def format_token(locale, :asn1_utc_time, date, modifiers, _flags, _width) do
347    # `130305232519Z`
348    date = Timezone.convert(date, "UTC")
349    flags = [padding: :zeroes]
350    year  = format_token(locale, :year2, date, modifiers, flags, width_spec(2..2))
351    month = format_token(locale, :month, date, modifiers, flags, width_spec(2..2))
352    day   = format_token(locale, :day, date, modifiers, flags, width_spec(2..2))
353    hour  = format_token(locale, :hour24, date, modifiers, flags, width_spec(2..2))
354    min   = format_token(locale, :min, date, modifiers, flags, width_spec(2..2))
355    sec   = format_token(locale, :sec, date, modifiers, flags, width_spec(2..2))
356    "#{year}#{month}#{day}#{hour}#{min}#{sec}Z"
357  end
358  def format_token(locale, :asn1_generalized_time, date, modifiers, _flags, _width) do
359    # `130305232519`
360    flags = [padding: :zeroes]
361    year  = format_token(locale, :year4, date, modifiers, flags, width_spec(4..4))
362    month = format_token(locale, :month, date, modifiers, flags, width_spec(2..2))
363    day   = format_token(locale, :day, date, modifiers, flags, width_spec(2..2))
364    hour  = format_token(locale, :hour24, date, modifiers, flags, width_spec(2..2))
365    min   = format_token(locale, :min, date, modifiers, flags, width_spec(2..2))
366    sec   = format_token(locale, :sec, date, modifiers, flags, width_spec(2..2))
367    "#{year}#{month}#{day}#{hour}#{min}#{sec}"
368  end
369  def format_token(locale, :asn1_generalized_time_z, date, modifiers, flags, width) do
370    # `130305232519Z`
371    date = Timezone.convert(date, "UTC")
372    base = format_token(locale, :asn1_generalized_time, date, modifiers, flags, width)
373    base <> "Z"
374  end
375  def format_token(locale, :asn1_generalized_time_tz, date, modifiers, flags, width) do
376    # `130305232519-0500`
377    offset = format_token(locale, :zoffs, date, modifiers, flags, width)
378    base = format_token(locale, :asn1_generalized_time, date, modifiers, flags, width)
379    base <> offset
380  end
381  def format_token(locale, :kitchen, date, modifiers, _flags, _width) do
382    # `3:25PM`
383    hour  = format_token(locale, :hour12, date, modifiers, [], width_spec(2..2))
384    min   = format_token(locale, :min, date, modifiers, [padding: :zeroes], width_spec(2..2))
385    ampm  = format_token(locale, :AM, date, modifiers, [], width_spec(-1, nil))
386    "#{hour}:#{min}#{ampm}"
387  end
388  def format_token(locale, :slashed, date, modifiers, _flags, _width) do
389    # `04/12/1987`
390    flags = [padding: :zeroes]
391    year  = format_token(locale, :year2, date, modifiers, flags, width_spec(2..2))
392    month = format_token(locale, :month, date, modifiers, flags, width_spec(2..2))
393    day   = format_token(locale, :day, date, modifiers, flags, width_spec(2..2))
394    "#{month}/#{day}/#{year}"
395  end
396  def format_token(locale, token, date, modifiers, _flags, _width)
397    when token in [:strftime_iso_clock, :strftime_iso_clock_full] do
398    # `23:30:05`
399    flags = [padding: :zeroes]
400    hour  = format_token(locale, :hour24, date, modifiers, flags, width_spec(2..2))
401    min   = format_token(locale, :min, date, modifiers, flags, width_spec(2..2))
402    case token do
403      :strftime_iso_clock -> "#{hour}:#{min}"
404      :strftime_iso_clock_full ->
405        sec = format_token(locale, :sec, date, modifiers, flags, width_spec(2..2))
406        "#{hour}:#{min}:#{sec}"
407    end
408  end
409  def format_token(locale, :strftime_kitchen, date, modifiers, _flags, _width) do
410    # `04:30:01 PM`
411    hour  = format_token(locale, :hour12, date, modifiers, [padding: :zeroes], width_spec(2..2))
412    min   = format_token(locale, :min, date, modifiers, [padding: :zeroes], width_spec(2..2))
413    sec   = format_token(locale, :sec, date, modifiers, [padding: :zeroes], width_spec(2..2))
414    ampm  = format_token(locale, :AM, date, modifiers, [], width_spec(-1, nil))
415    "#{hour}:#{min}:#{sec} #{ampm}"
416  end
417  def format_token(locale, :strftime_iso_shortdate, date, modifiers, _flags, _width) do
418    # ` 5-Jan-2014`
419    flags = [padding: :zeroes]
420    year  = format_token(locale, :year4, date, modifiers, flags, width_spec(4..4))
421    month = format_token(locale, :mshort, date, modifiers, flags, width_spec(-1, nil))
422    day   = format_token(locale, :day, date, modifiers, [padding: :spaces], width_spec(2..2))
423    "#{day}-#{month}-#{year}"
424  end
425  def format_token(locale, :iso_week, date, modifiers, _flags, _width) do
426    # 2015-W04
427    flags = [padding: :zeroes]
428    year = format_token(locale, :year4, date, modifiers, flags, width_spec(4..4))
429    week = format_token(locale, :iso_weeknum, date, modifiers, flags, width_spec(2..2))
430    "#{year}-W#{week}"
431  end
432  def format_token(locale, :iso_weekday, date, modifiers, _flags, _width) do
433    # 2015-W04-1
434    flags = [padding: :zeroes]
435    year = format_token(locale, :year4, date, modifiers, flags, width_spec(4..4))
436    week = format_token(locale, :iso_weeknum, date, modifiers, flags, width_spec(2..2))
437    day  = format_token(locale, :wday_mon, date, modifiers, flags, width_spec(1, 1))
438    "#{year}-W#{week}-#{day}"
439  end
440  def format_token(locale, :iso_ordinal, date, modifiers, _flags, _width) do
441    # 2015-180
442    flags = [padding: :zeroes]
443    year = format_token(locale, :year4, date, modifiers, flags, width_spec(4..4))
444    day  = format_token(locale, :oday, date, modifiers, flags, width_spec(3..3))
445    "#{year}-#{day}"
446  end
447
448  # Years
449  def format_token(_locale, :year4, date, _modifiers, flags, width),
450    do: pad_numeric(date.year, flags, width)
451  def format_token(_locale, :year2, date, _modifiers, flags, width),
452    do: pad_numeric(rem(date.year, 100), flags, width)
453  def format_token(_locale, :century, date, _modifiers, flags, width),
454    do: pad_numeric(div(date.year, 100), flags, width)
455  def format_token(_locale, :iso_year4,  date, _modifiers, flags, width) do
456    {iso_year, _} = Timex.iso_week(date)
457    pad_numeric(iso_year, flags, width)
458  end
459  def format_token(_locale, :iso_year2,  date, _modifiers, flags, width) do
460    {iso_year, _} = Timex.iso_week(date)
461    pad_numeric(rem(iso_year, 100), flags, width)
462  end
463  # Months
464  def format_token(_locale, :month, date, _modifiers, flags, width),
465    do: pad_numeric(date.month, flags, width)
466  def format_token(locale, :mshort, date, _, _, _) do
467    months = Translator.get_months_abbreviated(locale)
468    Map.get(months, date.month)
469  end
470  def format_token(locale, :mfull, date, _, _, _)  do
471    months = Translator.get_months(locale)
472    Map.get(months, date.month)
473  end
474  # Days
475  def format_token(_locale, :day, date, _modifiers, flags, width),
476    do: pad_numeric(date.day, flags, width)
477  def format_token(_locale, :oday, date, _modifiers, flags, width),
478    do: pad_numeric(Timex.day(date), flags, width)
479  # Weeks
480  def format_token(_locale, :iso_weeknum, date, _modifiers, flags, width) do
481    {_, week} = Timex.iso_week(date)
482    pad_numeric(week, flags, width)
483  end
484  def format_token(_locale, :week_mon, %{:year => year} = date, _modifiers, flags, width) do
485    {:ok, jan1} = Date.new(year,1,1)
486    Timex.Interval.new(from: jan1, until: Timex.shift(date, days: 1))
487    |> Enum.reduce(0, fn d, acc ->
488      case Timex.weekday(d) do
489        1 -> acc+1
490        _ -> acc
491      end
492    end)
493    |> pad_numeric(flags, width)
494  end
495  def format_token(_locale, :week_sun, %{:year => year} = date, _modifiers, flags, width) do
496    {:ok, jan1} = Date.new(year,1,1)
497    Timex.Interval.new(from: jan1, until: Timex.shift(date, days: 1))
498    |> Enum.reduce(0, fn d, acc ->
499      case Timex.weekday(d) do
500        7 -> acc+1
501        _ -> acc
502      end
503    end)
504    |> pad_numeric(flags, width)
505  end
506  def format_token(_locale, :wday_mon, date, _modifiers, flags, width),
507    do: pad_numeric(Timex.weekday(date), flags, width)
508  def format_token(_locale, :wday_sun, date, _modifiers, flags, width) do
509    # from 1..7 to 0..6
510    weekday = case Timex.weekday(date) do
511      7   -> 0
512      day -> day
513    end
514    pad_numeric(weekday, flags, width)
515  end
516  def format_token(locale, :wdshort, date, _modifiers, _flags, _width) do
517    day = Timex.weekday(date)
518    day_names = Translator.get_weekdays_abbreviated(locale)
519    Map.get(day_names, day)
520  end
521  def format_token(locale, :wdfull, date, _modifiers, _flags, _width) do
522    day = Timex.weekday(date)
523    day_names = Translator.get_weekdays(locale)
524    Map.get(day_names, day)
525  end
526  # Hours
527  def format_token(_locale, :hour24, %{:hour => hour}, _modifiers, flags, width),
528    do: pad_numeric(hour, flags, width)
529  def format_token(_locale, :hour24, _date, _modifiers, flags, width),
530    do: pad_numeric(0, flags, width)
531  def format_token(_locale, :hour12, %{:hour => hour}, _modifiers, flags, width) do
532    {h, _} = Timex.Time.to_12hour_clock(hour)
533    pad_numeric(h, flags, width)
534  end
535  def format_token(_locale, :hour12, _date, _modifiers, flags, width) do
536    {h, _} = Timex.Time.to_12hour_clock(0)
537    pad_numeric(h, flags, width)
538  end
539  def format_token(_locale, :min, %{:minute => min}, _modifiers, flags, width),
540    do: pad_numeric(min, flags, width)
541  def format_token(_locale, :min, _date, _modifiers, flags, width),
542    do: pad_numeric(0, flags, width)
543  def format_token(_locale, :sec, %{:second => sec}, _modifiers, flags, width),
544    do: pad_numeric(sec, flags, width)
545  def format_token(_locale, :sec, _date, _modifiers, flags, width),
546    do: pad_numeric(0, flags, width)
547  def format_token(_locale, :sec_fractional, %{microsecond: {us, precision}}, _modifiers, _flags, width) when precision > 0 do
548    min_width =
549      case Keyword.get(width, :min) do
550        nil -> precision
551        n when n < 0 -> precision
552        n -> n
553      end
554    max_width =
555      case Keyword.get(width, :max) do
556        nil -> precision
557        n when n < min_width -> min_width
558        n -> n
559      end
560
561    us_str = "#{us}"
562    padded_us_str = String.duplicate(pad_char(:zeroes), 6 - byte_size(us_str)) <> us_str
563    padded = pad_numeric(padded_us_str, [padding: :zeroes], width_spec(min_width..max_width))
564    ".#{padded}"
565  end
566  def format_token(_locale, :sec_fractional, _date, _modifiers, _flags, width) do
567    case Keyword.get(width, :min) do
568      n when is_integer(n) and n > 0 ->
569        padded = pad_numeric(0, [padding: :zeroes], width_spec(n..n))
570        ".#{padded}"
571      _ ->
572        ""
573    end
574  end
575  def format_token(_locale, :sec_epoch, date, _modifiers, flags, width) do
576    case get_in(flags, [:padding]) do
577      padding when padding in [:zeroes, :spaces] ->
578        {:error, {:formatter, "Invalid directive flag: Cannot pad seconds from epoch, as it is not a fixed width integer."}}
579      _ ->
580        pad_numeric(Timex.to_unix(date), flags, width)
581    end
582  end
583  def format_token(_locale, :us, %{microsecond: {us, _precision}}, _modifiers, flags, width) do
584    min =
585      case Keyword.get(width, :min) do
586        nil -> 6
587        n when n < 0 -> 6
588        n -> n
589      end
590    max =
591      case Keyword.get(width, :max) do
592        nil -> 6
593        n when n > 6 -> n
594        _ -> 6
595      end
596    pad_numeric(us, flags, width_spec(min..max))
597  end
598  def format_token(_locale, :us, _date, _modifiers, flags, width) do
599    pad_numeric(0, flags, width)
600  end
601  def format_token(locale, :am, %{hour: hour}, _modifiers, _flags, _width) do
602    day_periods = Translator.get_day_periods(locale)
603    {_, am_pm} = Timex.Time.to_12hour_clock(hour)
604    Map.get(day_periods, am_pm)
605  end
606  def format_token(locale, :am, _date, _modifiers, _flags, _width) do
607    day_periods = Translator.get_day_periods(locale)
608    {_, am_pm} = Timex.Time.to_12hour_clock(0)
609    Map.get(day_periods, am_pm)
610  end
611  def format_token(locale, :AM, %{hour: hour}, _modifiers, _flags, _width) do
612    day_periods = Translator.get_day_periods(locale)
613    case Timex.Time.to_12hour_clock(hour) do
614      {_, :am} ->
615        Map.get(day_periods, :AM)
616      {_, :pm} ->
617        Map.get(day_periods, :PM)
618    end
619  end
620  def format_token(locale, :AM, _date, _modifiers, _flags, _width) do
621    day_periods = Translator.get_day_periods(locale)
622    case Timex.Time.to_12hour_clock(0) do
623      {_, :am} ->
624        Map.get(day_periods, :AM)
625      {_, :pm} ->
626        Map.get(day_periods, :PM)
627    end
628  end
629  # Timezones
630  def format_token(_locale, :zname, %{time_zone: tz}, _modifiers, _flags, _width),
631    do: tz
632  def format_token(_locale, :zname, _date, _modifiers, _flags, _width),
633    do: ""
634  def format_token(_locale, :zabbr, %{zone_abbr: abbr}, _modifiers, _flags, _width),
635    do: abbr
636  def format_token(_locale, :zabbr, _date, _modifiers, _flags, _width),
637    do: ""
638  def format_token(_locale, :zoffs, %{std_offset: std, utc_offset: utc}, _modifiers, flags, _width) do
639    case get_in(flags, [:padding]) do
640      padding when padding in [:spaces, :none] ->
641        {:error, {:formatter, "Invalid directive flag: Timezone offsets require 0-padding to remain unambiguous."}}
642      _ ->
643        total_offset = Timezone.total_offset(std, utc)
644        offset_hours = div(total_offset, 60 * 60)
645        offset_mins  = div(rem(total_offset, 60 * 60), 60)
646        hour  = pad_numeric(offset_hours, [padding: :zeroes], width_spec(2..2))
647        min   = pad_numeric(offset_mins, [padding: :zeroes], width_spec(2..2))
648        cond do
649          (offset_hours + offset_mins) >= 0 -> "+#{hour}#{min}"
650          true -> "#{hour}#{min}"
651        end
652    end
653  end
654  def format_token(_locale, :zoffs, _date, _modifiers, _flags, _width),
655    do: ""
656  def format_token(locale, :zoffs_colon, date, modifiers, flags, width) do
657    case format_token(locale, :zoffs, date, modifiers, flags, width) do
658      {:error, _} = err -> err
659      "" -> ""
660      offset ->
661        case String.split(offset, "", [trim: true, parts: 2]) do
662          [qualifier, <<hour::binary-size(2), min::binary-size(2)>>] ->
663            <<qualifier::binary, hour::binary, ?:, min::binary>>
664          [qualifier, <<hour::binary-size(2), "-", min::binary-size(2)>>] ->
665            <<qualifier::binary, hour::binary, ?:, min::binary>>
666        end
667    end
668  end
669  def format_token(locale, :zoffs_sec, %{std_offset: std, utc_offset: utc} = date, modifiers, flags, width) do
670    case format_token(locale, :zoffs_colon, date, modifiers, flags, width) do
671      {:error,_} = err -> err
672      "" -> ""
673      offset ->
674        total_offset = Timezone.total_offset(std, utc)
675        offset_secs = rem(rem(total_offset, 60*60), 60)
676        "#{offset}:#{pad_numeric(offset_secs, [padding: :zeroes], width_spec(2..2))}"
677    end
678  end
679  def format_token(_locale, :zoffs_sec, _date, _modifiers, _flags, _width),
680    do: ""
681  def format_token(_locale, token, _, _, _, _) do
682    {:error, {:formatter, :unsupported_token, token}}
683  end
684
685  defp pad_numeric(number, flags, width) when is_integer(number), do: pad_numeric("#{number}", flags, width)
686  defp pad_numeric(number_str, [], _width), do: number_str
687  defp pad_numeric(<<?-, number_str::binary>>, flags, width) do
688    res = pad_numeric(number_str, flags, width)
689    <<?-, res::binary>>
690  end
691  defp pad_numeric(number_str, flags, [min: min_width, max: max_width]) do
692    case get_in(flags, [:padding]) do
693      pad_type when pad_type in [nil, :none] ->
694        number_str
695      pad_type ->
696        len = byte_size(number_str)
697        cond do
698          len == min_width                    -> number_str
699          min_width == -1 && max_width == nil -> number_str
700          len < min_width                     -> String.duplicate(pad_char(pad_type), min_width - len) <> number_str
701          len > min_width && len > max_width  -> binary_part(number_str, 0, max_width)
702          len > min_width                     -> number_str
703        end
704    end
705  end
706  defp pad_char(:zeroes), do: <<?0>>
707  defp pad_char(:spaces), do: <<32>>
708
709  defp width_spec(min..max), do: [min: min, max: max]
710  defp width_spec(min, max), do: [min: min, max: max]
711end
712