PhoenixKit.Utils.Number (phoenix_kit v2.29.1)

Copy Markdown View Source

Number formatting and parsing utilities for PhoenixKit.

Formatting: thousand separators, abbreviations, percentages. Parsing: parse_decimal/2 turns what a person typed into a form field ("2,5", "1 234.56") into a Decimal — pair it with PhoenixKitWeb.Components.Core.DecimalInput.decimal_input/1.

Summary

Functions

Formats a number with thousand separators.

Renders a number the way decimal_input/1 shows it: a normalized plain string (2.5, 1000, never 1E+3 or 2.500), "" for nil, and a binary unchanged — so the raw text a person typed round-trips through a re-render exactly as typed.

Formats a number as a percentage.

Formats a number with abbreviations (K, M, B).

Parses a number a person typed into a form field — the counterpart of PhoenixKitWeb.Components.Core.DecimalInput.decimal_input/1.

Same as parse_decimal/2, returning the Decimal or raising ArgumentError with the reason.

Types

parse_error()

@type parse_error() :: :empty | :invalid | :below_min | :above_max

Functions

format(number)

@spec format(integer() | nil) :: String.t()

Formats a number with thousand separators.

Examples

iex> PhoenixKit.Utils.Number.format(1234567)
"1,234,567"

iex> PhoenixKit.Utils.Number.format(0)
"0"

iex> PhoenixKit.Utils.Number.format(nil)
"0"

format_decimal(text)

@spec format_decimal(term()) :: String.t()

Renders a number the way decimal_input/1 shows it: a normalized plain string (2.5, 1000, never 1E+3 or 2.500), "" for nil, and a binary unchanged — so the raw text a person typed round-trips through a re-render exactly as typed.

Examples

iex> PhoenixKit.Utils.Number.format_decimal(Decimal.new("2.500"))
"2.5"

iex> PhoenixKit.Utils.Number.format_decimal(nil)
""

iex> PhoenixKit.Utils.Number.format_decimal("2,5")
"2,5"

format_percentage(rate)

@spec format_percentage(float() | integer() | nil) :: String.t()

Formats a number as a percentage.

Examples

iex> PhoenixKit.Utils.Number.format_percentage(95.5)
"95.5%"

iex> PhoenixKit.Utils.Number.format_percentage(100)
"100%"

iex> PhoenixKit.Utils.Number.format_percentage(nil)
"0%"

format_short(number)

@spec format_short(integer() | nil) :: String.t()

Formats a number with abbreviations (K, M, B).

Examples

iex> PhoenixKit.Utils.Number.format_short(1_234_567)
"1.2M"

iex> PhoenixKit.Utils.Number.format_short(5_432)
"5.4K"

iex> PhoenixKit.Utils.Number.format_short(123)
"123"

parse_decimal(raw, opts \\ [])

@spec parse_decimal(term(), keyword()) :: {:ok, Decimal.t()} | {:error, parse_error()}

Parses a number a person typed into a form field — the counterpart of PhoenixKitWeb.Components.Core.DecimalInput.decimal_input/1.

Keyboards in most of Europe produce a decimal COMMA, browsers and Decimal.parse/1 want a dot, and Decimal.parse/1 alone returns whatever prefix it could read ("2,5" silently became 2). This function takes the text as a person meant it and returns a normalized Decimal or a reason:

  • a dot or a comma is the decimal point ("2.5", "2,5", ",5");
  • spaces (no-break, thin and narrow no-break spaces too) around the number are ignored; inside it they are thousands grouping of the integer part and are dropped ("1 234,56") — like the separators below, only between 3-digit groups, so "12 34" and "1,5 25" are :invalid rather than silently merged; tabs and line breaks are not spaces — a value pasted with one is :invalid;
  • with both a dot and a comma present, the LAST one is the decimal point and the other is grouping ("1.234,56", "1,234.56");
  • one kind repeated is grouping ("1,234,567");
  • an optional leading sign; nothing else — exponents ("1e9"), NaN, Infinity, hex, stray letters are all {:error, :invalid};
  • blank (or nil) is {:error, :empty}, so a caller can tell "left empty" from "typed garbage";
  • more than 64 bytes is {:error, :invalid} without further inspection;
  • integers, floats and decimals pass straight through as a Decimal.

The result has no trailing fraction zeros ("2.500" → 2.5) and no exponent ("10" stays 10, never 1E+1), so it compares with == against Decimal.new/1 of the same text and prints plainly everywhere; a zero is always unsigned — "-0" → 0.

Options

  • :min / :max — any number shape (0, "0.25", Decimal); a value outside them is {:error, :below_min} / {:error, :above_max}, never clamped. A magnitude of 10¹² or more is {:error, :above_max} even without :max.

Examples

iex> PhoenixKit.Utils.Number.parse_decimal("2,5")
{:ok, Decimal.new("2.5")}

iex> PhoenixKit.Utils.Number.parse_decimal("1 234,56")
{:ok, Decimal.new("1234.56")}

iex> PhoenixKit.Utils.Number.parse_decimal("")
{:error, :empty}

iex> PhoenixKit.Utils.Number.parse_decimal("1e9")
{:error, :invalid}

iex> PhoenixKit.Utils.Number.parse_decimal("-1", min: 0)
{:error, :below_min}

parse_decimal!(raw, opts \\ [])

@spec parse_decimal!(term(), keyword()) :: Decimal.t()

Same as parse_decimal/2, returning the Decimal or raising ArgumentError with the reason.