Packages
localize
0.32.0
1.0.0-rc.6
1.0.0-rc.5
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/number/plural_rules/ordinal.ex
defmodule Localize.Number.PluralRule.Ordinal do
@moduledoc """
Implements ordinal plural rules for numbers.
Ordinal plural rules are used for ordering sequences
(e.g., "1st", "2nd", "3rd", "4th"). Each locale has its
own set of rules that classify a number into one of the
plural categories: `:zero`, `:one`, `:two`, `:few`,
`:many`, or `:other`.
All plural rule functions are generated at compile time
from the CLDR plural rules data.
"""
alias Localize.Utils.Digits
alias Localize.Utils.Math
import Digits,
only: [number_of_integer_digits: 1, remove_trailing_zeros: 1, fraction_as_integer: 2]
import Math, only: [mod: 2, within: 2]
import Localize.Number.PluralRule.Transformer
alias Localize.LanguageTag
alias Localize.SupplementalData
# Load rules at compile time for function generation only.
# The attribute is deleted after generation so the raw data
# is not embedded in the BEAM file.
@rules_at_compile Localize.Number.PluralRule.Loader.load_plural_rules(:ordinal)
@doc """
Returns the locale names for which ordinal plural rules
are defined.
### Returns
* A sorted list of locale name atoms.
"""
@spec available_locale_names :: [atom(), ...]
def available_locale_names do
SupplementalData.plural_rules_locales(:ordinal)
end
@doc """
Returns all the ordinal plural rules defined in CLDR.
### Returns
* A map of locale names to their parsed plural rule definitions.
"""
def plural_rules do
SupplementalData.plural_rules(:ordinal)
end
@doc """
Pluralize a number using ordinal plural rules and a
substitution map.
### Arguments
* `number` is an integer, float, or Decimal.
* `locale` is a locale name string or atom, or a
`t:Localize.LanguageTag.t/0`.
* `substitutions` is a map that maps plural categories to
substitution values. The valid keys are `:zero`, `:one`,
`:two`, `:few`, `:many`, and `:other`.
### Returns
* The substitution value for the plural category of the
given number, or `nil` if no matching substitution is found.
### Examples
iex> Localize.Number.PluralRule.Ordinal.pluralize(1, "en", %{one: "st", two: "nd", few: "rd", other: "th"})
"st"
iex> Localize.Number.PluralRule.Ordinal.pluralize(2, "en", %{one: "st", two: "nd", few: "rd", other: "th"})
"nd"
"""
@default_substitution :other
def pluralize(number, locale_name, substitutions)
when is_atom(locale_name) or is_binary(locale_name) do
with {:ok, parsed} <- LanguageTag.parse(to_string(locale_name)),
{:ok, language_tag} <- LanguageTag.canonicalize(parsed) do
pluralize(number, language_tag, substitutions)
end
end
def pluralize(number, %LanguageTag{} = locale, %{} = substitutions)
when is_number(number) do
do_pluralize(number, locale, substitutions)
end
def pluralize(%Decimal{sign: _sign, coef: coef, exp: 0} = number, locale, substitutions)
when is_integer(coef) do
number
|> Decimal.to_integer()
|> do_pluralize(locale, substitutions)
end
def pluralize(%Decimal{sign: sign, coef: coef, exp: exp}, locale, substitutions)
when is_integer(coef) and exp > 0 do
number = %Decimal{sign: sign, coef: coef * 10, exp: exp - 1}
pluralize(number, locale, substitutions)
end
def pluralize(%Decimal{sign: sign, coef: coef, exp: exp}, locale, substitutions)
when is_integer(coef) and exp < 0 and rem(coef, 10) == 0 do
number = %Decimal{sign: sign, coef: Kernel.div(coef, 10), exp: exp + 1}
pluralize(number, locale, substitutions)
end
def pluralize(%Decimal{} = number, %LanguageTag{} = locale, %{} = substitutions) do
number
|> Decimal.to_float()
|> do_pluralize(locale, substitutions)
end
defp do_pluralize(number, %LanguageTag{} = locale, %{} = substitutions) do
plural = plural_rule(number, locale)
number = if (truncated = trunc(number)) == number, do: truncated, else: number
substitutions[number] || substitutions[plural] || substitutions[@default_substitution]
end
@doc """
Return the plural rules for a locale.
### Arguments
* `locale` is a locale name string or atom, or a
`t:Localize.LanguageTag.t/0`.
### Returns
* A keyword list of `{category, parsed_rule}` pairs.
"""
def plural_rules_for(%LanguageTag{cldr_locale_id: cldr_locale_id, language: language}) do
plural_rules()[cldr_locale_id] || plural_rules()[language]
end
def plural_rules_for(locale_name) when is_atom(locale_name) or is_binary(locale_name) do
with {:ok, parsed} <- LanguageTag.parse(to_string(locale_name)),
{:ok, locale} <- LanguageTag.canonicalize(parsed) do
plural_rules_for(locale)
end
end
# ── Plural rule function: public API ──────────────────────────
@doc """
Return the plural category for a given number in a given locale.
Returns which plural category (`:zero`, `:one`, `:two`, `:few`,
`:many`, or `:other`) a given number belongs to within the
context of a given locale.
### Arguments
* `number` is any integer, float, or Decimal.
* `locale` is a locale name string or a
`t:Localize.LanguageTag.t/0`.
* `rounding` is a positive integer specifying the number of
fractional digits to consider. The default is `3`.
### Returns
* A plural category atom.
* `{:error, exception}` if no plural rules are available
for the locale.
### Examples
iex> Localize.Number.PluralRule.Ordinal.plural_rule(1, "en")
:one
iex> Localize.Number.PluralRule.Ordinal.plural_rule(2, "en")
:two
iex> Localize.Number.PluralRule.Ordinal.plural_rule(3, "en")
:few
iex> Localize.Number.PluralRule.Ordinal.plural_rule(4, "en")
:other
"""
@spec plural_rule(
number() | Decimal.t(),
String.t() | LanguageTag.t(),
pos_integer()
) :: Localize.Number.PluralRule.plural_type() | {:error, Exception.t()}
def plural_rule(number, locale, rounding \\ Math.default_rounding())
def plural_rule(number, %LanguageTag{} = locale, rounding) when is_binary(number) do
plural_rule(Decimal.new(number), locale, rounding)
end
# Plural rule for an integer
def plural_rule(number, %LanguageTag{} = locale, _rounding) when is_integer(number) do
n = abs(number)
i = n
v = 0
w = 0
f = 0
t = 0
e = 0
do_plural_rule(locale, n, i, v, w, f, t, e)
end
# For a compact integer
def plural_rule({number, e}, %LanguageTag{} = locale, _rounding) when is_integer(number) do
n = abs(number)
i = n
v = 0
w = 0
f = 0
t = 0
do_plural_rule(locale, n, i, v, w, f, t, e)
end
# Plural rule for a float
def plural_rule(number, %LanguageTag{} = locale, rounding)
when is_float(number) and is_integer(rounding) and rounding > 0 do
n = Float.round(abs(number), rounding)
i = trunc(n)
v = rounding
t = fraction_as_integer(n - i, rounding)
w = number_of_integer_digits(t)
f = trunc(t * Math.power_of_10(v - w))
e = 0
do_plural_rule(locale, n, i, v, w, f, t, e)
end
# Plural rule for a compact float
def plural_rule({number, e}, %LanguageTag{} = locale, rounding)
when is_float(number) and is_integer(rounding) and rounding > 0 do
n = Float.round(abs(number), rounding)
i = trunc(n)
v = rounding
t = fraction_as_integer(n - i, rounding)
w = number_of_integer_digits(t)
f = trunc(t * Math.power_of_10(v - w))
do_plural_rule(locale, n, i, v, w, f, t, e)
end
# Plural rule for a %Decimal{}
def plural_rule(%Decimal{} = number, %LanguageTag{} = locale, rounding)
when is_integer(rounding) and rounding > 0 do
n = Decimal.abs(number)
i = Decimal.round(n, 0, :floor)
v = abs(n.exp)
f =
n
|> Decimal.sub(i)
|> Decimal.mult(Decimal.new(Math.power_of_10(v)))
|> Decimal.round(0, :floor)
|> Decimal.to_integer()
t = remove_trailing_zeros(f)
w = number_of_integer_digits(t)
i = Decimal.to_integer(i)
n = Math.to_float(n)
e = 0
do_plural_rule(locale, n, i, v, w, f, t, e)
end
# Plural rule for a compact %Decimal{}
def plural_rule({%Decimal{} = number, e}, %LanguageTag{} = locale, rounding)
when is_integer(rounding) and rounding > 0 do
n = Decimal.abs(number)
i = Decimal.round(n, 0, :floor)
v = abs(n.exp)
f =
n
|> Decimal.sub(i)
|> Decimal.mult(Decimal.new(Math.power_of_10(v)))
|> Decimal.round(0, :floor)
|> Decimal.to_integer()
t = remove_trailing_zeros(f)
w = number_of_integer_digits(t)
i = Decimal.to_integer(i)
n = Math.to_float(n)
do_plural_rule(locale, n, i, v, w, f, t, e)
end
# Fallback: parse locale string and retry
def plural_rule(number, locale_name, rounding)
when is_binary(locale_name) or is_atom(locale_name) do
with {:ok, parsed} <- LanguageTag.parse(to_string(locale_name)),
{:ok, locale} <- LanguageTag.canonicalize(parsed) do
plural_rule(number, locale, rounding)
end
end
# ── Generated plural rule functions ───────────────────────────
@dialyzer {:nowarn_function, [do_plural_rule: 8]}
@spec do_plural_rule(
LanguageTag.t(),
number(),
Localize.Number.PluralRule.operand(),
Localize.Number.PluralRule.operand(),
Localize.Number.PluralRule.operand(),
Localize.Number.PluralRule.operand(),
[integer(), ...] | integer(),
number()
) :: :zero | :one | :two | :few | :many | :other | {:error, Exception.t()}
for locale_name <- @rules_at_compile |> Map.keys() |> Enum.sort() do
function_body =
@rules_at_compile
|> Map.get(locale_name)
|> rules_to_condition_statement(__MODULE__)
defp do_plural_rule(
%LanguageTag{cldr_locale_id: unquote(locale_name)},
n,
i,
v,
w,
f,
t,
e
) do
_ = {n, i, v, w, f, t, e}
unquote(function_body)
end
end
Module.delete_attribute(__MODULE__, :rules_at_compile)
# If the locale doesn't have a plural rule, try the language
defp do_plural_rule(%LanguageTag{} = language_tag, n, i, v, w, f, t, e) do
# `language_tag.language` is already a validated atom (or nil) after
# `LanguageTag.parse/1`. The previous `String.to_atom(to_string(...))`
# round-trip was a no-op for atoms but would atomise `""` from a nil
# language; using the field directly avoids both.
language_atom = language_tag.language
if language_atom == language_tag.cldr_locale_id do
{:error,
Localize.UnknownPluralRulesError.exception(
locale_id: language_tag.cldr_locale_id,
type: :ordinal
)}
else
language_tag
|> Map.put(:cldr_locale_id, language_atom)
|> do_plural_rule(n, i, v, w, f, t, e)
end
end
end