defmodule Momento.Format do @moduledoc """ This module holds all the `format/2` method. """ @doc """ Provide a `DateTime` struct and token string to get back a formatted date/datetime string. ## Examples iex> Momento.date |> Momento.format("YYYY-MM-DD") "2016-07-01" ...> Momento.date |> Momento.format("M-D-YY") "7-1-16" """ @tokens ~r/YYYY|YY?|Mo|MM?M?M?|Do|DD?D?D?|HH?|hh?|mm?|ss?|X|x/ @spec format(DateTime.t, String.t) :: String.t # An implementation of the Moment.js formats listed here: http://momentjs.com/docs/#/displaying/format/ # TODO: Add support for escaping characters within square brackets [] def format(%DateTime{} = datetime, format_string) when is_bitstring(format_string) do # Split the format_string into pieces by tokens Regex.split(@tokens, format_string, include_captures: true, trim: true) # Convert each token |> Enum.map(fn token -> case token do # 1970 1971 ... 2029 2030 "YYYY" -> datetime.year |> Integer.to_string # 70 71 ... 29 30 "YY" -> rem(datetime.year, 100) |> Integer.to_string # TODO: 1970 1971 ... 9999 +10000 +10001 # "Y" -> datetime.year |> Integer.to_string) # January February ... November December "MMMM" -> datetime.month |> get_month_name # Jan Feb ... Nov Dec "MMM" -> datetime.month |> get_month_name(:MMM) # 01 02 ... 11 12 "MM" -> datetime.month |> Integer.to_string |> String.rjust(2, ?0) # 1 2 ... 11 12 "M" -> datetime.month |> Integer.to_string # 1st 2nd ... 11th 12th "Mo" -> datetime.month |> get_ordinal_form # TODO: 001 002 ... 364 365 # "DDDD" -> datetime.day |> Integer.to_string # TODO: 1st 2nd ... 364th 365th # "DDDo" -> datetime.day |> Integer.to_string # TODO: 1 2 ... 364 365 # "DDD" -> datetime.day |> Integer.to_string # 01 02 ... 30 31 "DD" -> datetime.day |> Integer.to_string |> String.rjust(2, ?0) # 1st 2nd ... 30th 31st "Do" -> datetime.day |> get_ordinal_form # 1 2 ... 30 31 "D" -> datetime.day |> Integer.to_string # TODO: Sunday Monday ... Friday Saturday # "dddd" -> datetime.day |> Integer.to_string # TODO: Sun Mon ... Fri Sat # "ddd" -> datetime.day |> Integer.to_string # TODO: Su Mo ... Fr Sa # "dd" -> datetime.day |> Integer.to_string # TODO: 0th 1st ... 5th 6th # "do" -> datetime.day |> Integer.to_string # TODO: 0 1 ... 5 6 # "d" -> datetime.day |> Integer.to_string # 00 01 ... 22 23 "HH" -> datetime.hour |> Integer.to_string |> String.rjust(2, ?0) # 0 1 ... 22 23 "H" -> datetime.hour |> Integer.to_string # 01 02 ... 11 12 "hh" -> datetime.hour |> twelve_hour_format(:hh) # 1 2 ... 11 12 "h" -> datetime.hour |> twelve_hour_format # TODO: 01 02 ... 23 24 # "kk" -> datetime.hour |> Integer.to_string # TODO: 1 2 ... 23 24 # "k" -> datetime.hour |> Integer.to_string # 00 01 ... 58 59 "mm" -> datetime.minute |> Integer.to_string |> String.rjust(2, ?0) # 0 1 ... 58 59 "m" -> datetime.minute |> Integer.to_string # 00 01 ... 58 59 "ss" -> datetime.second |> Integer.to_string |> String.rjust(2, ?0) # 0 1 ... 58 59 "s" -> datetime.second |> Integer.to_string # TODO: 000[0..] 001[0..] ... 998[0..] 999[0..] # ~r/S{4,9}/ -> datetime.second |> Integer.to_string # TODO: 000 001 ... 998 999 # "SSS" -> datetime.second |> Integer.to_string # TODO: 00 01 ... 98 99 # "SS" -> datetime.second |> Integer.to_string # TODO: 0 1 ... 8 9 # "S" -> datetime.second |> Integer.to_string # TODO: -0700 -0600 ... +0600 +0700 # "ZZ" -> datetime.time_zone # TODO: -07:00 -06:00 ... +06:00 +07:00 # "Z" -> datetime.time_zone # TODO: AM PM # "A" -> datetime.hour |> Integer.to_string # TODO: am pm # "a" -> datetime.hour |> Integer.to_string # TODO: 1 2 3 4 # "Q" -> datetime.month |> Integer.to_string # TODO: 1st 2nd 3rd 4th # "Qo" -> datetime.month |> Integer.to_string # TODO: 1 2 ... 6 7 # "E" -> datetime # TODO: 0 1 ... 5 6 # "e" -> datetime # TODO: 01 02 ... 52 53 # "WW" -> datetime # TODO: 1st 2nd ... 52nd 53rd # "Wo" -> datetime # TODO: 1 2 ... 52 53 # "W" -> datetime # TODO: 01 02 ... 52 53 # "ww" -> datetime # TODO: 1st 2nd ... 52nd 53rd # "wo" -> datetime # TODO: 1 2 ... 52 53 # "w" -> datetime # TODO: 1970 1971 ... 2029 2030 # "GGGG" -> datetime # TODO: 70 71 ... 29 30 # "GG" -> datetime # TODO: 1970 1971 ... 2029 2030 # "gggg" -> datetime # TODO: 70 71 ... 29 30 # "gg" -> datetime # TODO: Thursday, September 4 1986 8:30 PM # "LLLL" -> datetime # TODO: September 4 1986 8:30 PM # "LLL" -> datetime # TODO: September 4 1986 # "LL" -> datetime # TODO: 8:30:25 PM # "LTS" -> datetime # TODO: 8:30 PM # "LT" -> datetime # TODO: 09/04/1986 # "L" -> datetime # TODO: Thu, Sep 4 1986 8:30 PM # "llll" -> datetime # TODO: Sep 4 1986 8:30 PM # "lll" -> datetime # TODO: Sep 4 1986 # "ll" -> datetime # TODO: 9/4/1986 # "l" -> datetime # 1360013296 "X" -> datetime |> DateTime.to_unix |> Integer.to_string # 1360013296123 "x" -> datetime |> DateTime.to_unix(:milliseconds) |> Integer.to_string _ -> token end end) # Recombine the pieces |> Enum.join end defp get_month_name(month_number, token \\ :MMMM) do month_name = elem({"January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"}, month_number-1) case token do :MMM -> String.slice(month_name, 0..2) _ -> month_name end end defp get_ordinal_form(number) do number_rem_hundred = rem(number, 100) ordinal_form = if number_rem_hundred == 11 || number_rem_hundred == 12 || number_rem_hundred == 13 do "th" else number_rem_ten = rem(number, 10) cond do number_rem_ten == 1 -> "st" number_rem_ten == 2 -> "nd" number_rem_ten == 3 -> "rd" true -> "th" end end Integer.to_string(number) <> ordinal_form end defp twelve_hour_format(hour, token \\ :h) do adjusted_hour = cond do hour >= 13 -> hour - 12 hour == 0 -> 12 true -> hour end adjusted_hour = Integer.to_string(adjusted_hour) case token do :hh -> adjusted_hour |> String.pad_leading(2, "0") _ -> adjusted_hour end end end