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