Packages
localize
0.10.0
1.0.0-rc.4
1.0.0-rc.3
1.0.0-rc.2
1.0.0-rc.1
1.0.0-rc.0
0.50.0
0.49.0
0.48.0
0.47.0
0.46.0
0.45.0
0.44.0
0.41.3
0.41.2
0.41.1
0.41.0
0.40.0
0.39.0
0.38.0
0.37.0
0.36.0
0.35.0
0.34.0
0.33.0
0.32.0
0.31.0
0.30.1
0.30.0
retired
0.29.0
0.28.0
0.27.0
0.26.0
0.25.0
0.24.0
0.23.0
0.22.0
0.21.0
0.20.0
0.19.0
0.18.0
0.16.0
0.15.0
0.14.0
0.13.0
0.12.0
0.11.0
0.10.0
0.9.0
0.8.0
0.7.0
0.6.0
0.5.0
0.4.0
0.3.0
0.2.0
0.1.0
0.1.0-alpha.1
Localization (parsing, formatting) of numbers, dates/time/calendar, units of measure, messages and lists. Includes localized collation.
Current section
Files
Jump to
Current section
Files
lib/localize/datetime.ex
defmodule Localize.DateTime do
@moduledoc """
Provides localized formatting of `DateTime`, `NaiveDateTime`,
and datetime-like maps.
The primary function is `to_string/2` which accepts a datetime
value and an options keyword list. Format patterns are defined
in CLDR and described in
[TR35](http://unicode.org/reports/tr35/tr35-dates.html).
## Predefined formats
* `:short` — abbreviated date and time (e.g., "1/2/25, 3:04 PM").
* `:medium` — standard date and time (default).
* `:long` — includes time zone name.
* `:full` — verbose day-of-week, date, and time zone.
Custom CLDR skeleton strings and raw format patterns are also
supported via the `:format` option.
"""
import Kernel, except: [to_string: 1]
@default_format :medium
@standard_formats [:short, :medium, :long, :full]
@doc """
Formats a datetime according to a CLDR format pattern.
### Arguments
* `datetime` is a `t:DateTime.t/0`, `t:NaiveDateTime.t/0`,
or any map with date and time keys.
* `options` is a keyword list of options.
### Options
* `:format` is a standard format name (`:short`, `:medium`,
`:long`, `:full`) or a format pattern string. The default
is `:medium`.
* `:locale` is a locale identifier. The default is `:en`.
* `:prefer` is `:unicode` or `:ascii` for variant selection.
The default is `:unicode`.
### Returns
* `{:ok, formatted_string}` on success.
* `{:error, exception}` if the datetime cannot be formatted.
### Examples
iex> Localize.DateTime.to_string(~N[2017-07-10 14:30:00], locale: :en, prefer: :ascii)
{:ok, "Jul 10, 2017, 2:30:00 PM"}
iex> Localize.DateTime.to_string(~N[2017-07-10 14:30:00], format: :short, locale: :en, prefer: :ascii)
{:ok, "7/10/17, 2:30 PM"}
"""
@spec to_string(map(), Keyword.t()) :: {:ok, String.t()} | {:error, Exception.t()}
def to_string(datetime, options \\ [])
def to_string(%{year: _, month: _, day: _, hour: _, minute: _} = datetime, options) do
locale = Keyword.get(options, :locale, Localize.get_locale())
format = Keyword.get(options, :format, @default_format)
style = Keyword.get(options, :style, :default)
with {:ok, locale_id} <- resolve_locale_id(locale) do
cond do
# Explicit pattern string — format directly
is_binary(format) ->
Localize.DateTime.Formatter.format(datetime, format, locale_id, Map.new(options))
# Standard format with separate date/time formats — use wrapper
format in @standard_formats or
(Keyword.has_key?(options, :date_format) and
Keyword.has_key?(options, :time_format)) ->
format_with_wrapper(datetime, options, locale_id, format, style)
# Skeleton atom — resolve to a pattern from available_formats
is_atom(format) ->
format_with_skeleton(datetime, options, locale_id, format)
true ->
{:error,
Localize.DateTimeFormatError.exception(format: format, reason: :invalid_format)}
end
end
end
def to_string(datetime, options) when is_map(datetime) do
# Try as date-only or time-only
cond do
Map.has_key?(datetime, :year) and Map.has_key?(datetime, :month) ->
Localize.Date.to_string(datetime, options)
Map.has_key?(datetime, :hour) ->
Localize.Time.to_string(datetime, options)
true ->
{:error, Localize.DateTimeInvalidInputError.exception(type: :datetime)}
end
end
def to_string(_invalid, _options) do
{:error, Localize.DateTimeInvalidInputError.exception(type: :datetime)}
end
@doc """
Same as `to_string/2` but raises on error.
"""
@spec to_string!(map(), Keyword.t()) :: String.t()
def to_string!(datetime, options \\ []) do
case to_string(datetime, options) do
{:ok, string} -> string
{:error, exception} -> raise exception
end
end
defp format_with_wrapper(datetime, options, locale_id, format, style) do
date_format = Keyword.get(options, :date_format, format)
time_format = Keyword.get(options, :time_format, format)
# The wrapper style should match the date format level
# (e.g., full date + short time → use full wrapper)
wrapper_format =
if Keyword.has_key?(options, :date_format),
do: date_format,
else: format
options_map =
options
|> Map.new()
|> Map.put(:date_format, date_format)
|> Map.put(:time_format, time_format)
with {:ok, wrapper} <- resolve_wrapper(wrapper_format, locale_id, style) do
Localize.DateTime.Formatter.format(datetime, wrapper, locale_id, options_map)
end
end
defp format_with_skeleton(datetime, options, locale_id, skeleton) do
prefer = Keyword.get(options, :prefer, :unicode)
with {:ok, available} <- Localize.DateTime.Format.available_formats(locale_id) do
case Map.get(available, skeleton) do
nil ->
# Try best-match algorithm for skeletons not found exactly
case Localize.DateTime.Format.Match.best_match(skeleton, locale_id) do
{:ok, matched_skeleton} when is_atom(matched_skeleton) ->
case Map.get(available, matched_skeleton) do
nil ->
{:error,
Localize.DateTimeUnresolvedFormatError.exception(
format: skeleton,
locale: locale_id
)}
matched_pattern ->
format_resolved_pattern(
resolve_prefer(matched_pattern, prefer),
datetime,
options,
locale_id,
skeleton
)
end
{:ok, {date_skeleton, time_skeleton}} ->
date_pattern = resolve_prefer(Map.get(available, date_skeleton, ""), prefer)
time_pattern = resolve_prefer(Map.get(available, time_skeleton, ""), prefer)
if is_binary(date_pattern) and is_binary(time_pattern) do
options_map =
options
|> Map.new()
|> Map.put(:date_format, :medium)
|> Map.put(:time_format, :medium)
with {:ok, wrapper} <- resolve_wrapper(:medium, locale_id, :default) do
combined = String.replace(wrapper, "{0}", time_pattern)
combined = String.replace(combined, "{1}", date_pattern)
Localize.DateTime.Formatter.format(datetime, combined, locale_id, options_map)
end
else
{:error,
Localize.DateTimeUnresolvedFormatError.exception(
format: skeleton,
locale: locale_id
)}
end
_ ->
{:error,
Localize.DateTimeUnresolvedFormatError.exception(
format: skeleton,
locale: locale_id
)}
end
%{} = variant_map ->
format_resolved_pattern(
resolve_prefer(variant_map, prefer),
datetime,
options,
locale_id,
skeleton
)
pattern when is_binary(pattern) ->
Localize.DateTime.Formatter.format(datetime, pattern, locale_id, Map.new(options))
end
end
end
defp format_resolved_pattern(nil, _datetime, _options, locale_id, skeleton) do
{:error,
Localize.DateTimeUnresolvedFormatError.exception(
format: skeleton,
locale: locale_id
)}
end
defp format_resolved_pattern(pattern, datetime, options, locale_id, _skeleton)
when is_binary(pattern) do
Localize.DateTime.Formatter.format(datetime, pattern, locale_id, Map.new(options))
end
defp resolve_prefer(%{} = variant_map, prefer) do
cond do
# `:unicode` / `:ascii` prefer-style variant.
Map.has_key?(variant_map, :unicode) or Map.has_key?(variant_map, :ascii) ->
Map.get(variant_map, prefer) || Map.get(variant_map, :unicode) ||
Map.get(variant_map, :ascii)
# CLDR plural-keyed variant (`:zero`, `:one`, `:two`, `:few`,
# `:many`, `:other`). These appear in skeleton-derived formats
# such as `MMMMW` ("week W of MMMM"). Without knowing the
# numeric value at this stage we fall back to the `:other`
# category, which CLDR guarantees to be present.
Map.has_key?(variant_map, :other) ->
Map.get(variant_map, :other)
true ->
nil
end
end
defp resolve_prefer(pattern, _prefer) when is_binary(pattern), do: pattern
defp resolve_prefer(_other, _prefer), do: nil
defp resolve_wrapper(format, locale_id, style) do
standard_format = if is_atom(format), do: format, else: :medium
case style do
:at ->
# Use at-style format (e.g., "{1} 'at' {0}")
with {:ok, at_formats} <-
Localize.DateTime.Format.date_time_at_formats(locale_id) do
pattern =
get_in(at_formats, [:standard, standard_format]) ||
fallback_wrapper(standard_format, locale_id)
{:ok, pattern}
else
_ -> {:ok, fallback_wrapper(standard_format, locale_id)}
end
_ ->
# Use standard wrapper format (e.g., "{1}, {0}")
{:ok, fallback_wrapper(standard_format, locale_id)}
end
end
defp fallback_wrapper(standard_format, locale_id) do
case Localize.DateTime.Format.date_time_formats(locale_id) do
{:ok, dt_formats} -> Map.get(dt_formats, standard_format, "{1}, {0}")
_ -> "{1}, {0}"
end
end
defp resolve_locale_id(%Localize.LanguageTag{cldr_locale_id: id}), do: {:ok, id}
defp resolve_locale_id(locale) when is_atom(locale) or is_binary(locale) do
case Localize.validate_locale(locale) do
{:ok, tag} -> {:ok, tag.cldr_locale_id}
error -> error
end
end
defp resolve_locale_id(invalid) do
{:error, Localize.InvalidLocaleError.exception(locale_id: inspect(invalid))}
end
end