Localize.Number.Parser (Localize v1.5.0)

Copy Markdown View Source

Functions for parsing numbers and currencies from strings in a locale-aware manner.

The parser handles locale-specific digit transliteration, grouping separators, and decimal separators to convert localized number strings back into Elixir numeric values.

Parsing reads with a locale's own data or not at all. When the locale is not installed and :allow_runtime_locale_download is false, formatting falls back to a parent locale's data or to English, but every function here returns {:error, %Localize.LocaleNotFoundInCacheError{}}, since with English data en-AU "$5" would be US dollars. Install the locale with mix localize.download_locales or allow it to be downloaded.

Summary

Functions

Finds and replaces substrings from a map at the beginning and/or end of a string.

Parses a string in a locale-aware manner and returns a number.

Parses a string that holds one currency amount into its currency and its amount.

Maps a list applying a resolver function to each binary element.

Resolves currencies from strings within a list.

Resolves a currency from the beginning and/or end of a string.

Resolves percent or permille from a string.

Resolves percent and permille symbols from strings within a list.

Scans a string in a locale-aware manner and returns a list of strings and numbers.

Types

per()

@type per() :: :percent | :permille

Functions

find_and_replace(string_map, string, fuzzy \\ nil)

@spec find_and_replace(map(), String.t(), float() | nil) ::
  {:ok, list()} | {:error, Exception.t()}

Finds and replaces substrings from a map at the beginning and/or end of a string.

Arguments

  • string_map is a map of %{search_string => replacement}.

  • string is the string to search.

  • fuzzy is an optional float for fuzzy matching. Of equally close strings, the alphabetically first is taken.

Returns

  • {:ok, list} with replacements applied.

  • {:error, exception} if no match found.

Examples

iex> Localize.Number.Parser.find_and_replace(%{"usd" => :USD}, "USD 100")
{:ok, [:USD, " 100"]}

iex> Localize.Number.Parser.find_and_replace(%{"%" => :percent}, "11%")
{:ok, ["11", :percent]}

parse(string, options \\ [])

@spec parse(String.t(), Keyword.t()) ::
  {:ok, integer() | float() | Decimal.t()} | {:error, Exception.t()}

Parses a string in a locale-aware manner and returns a number.

Arguments

  • string is any string. An underscore between two digits is Elixir's digit separator and is ignored, as in "1_000_000.34"; anywhere else it makes the text no number.

  • options is a keyword list of options.

Options

  • :number is one of :integer, :float, :decimal, or nil. The default is nil (auto-detect).

  • :locale is a locale identifier. The default is :en.

  • :number_system is a number system name or type.

  • :lenient governs how strictly grouping separators must be positioned. true, the default, requires each group to be at least two digits, which is what keeps "3 4 5" three numbers rather than one. false requires each group to be exactly the locale's grouping size, so fr takes "1 234 567" and refuses "1 23".

  • :grouping set to :sizes checks each group against the sizes the locale writes rather than as :lenient says: the last group of the locale's grouping size, the others of that size or its secondary one, so de refuses "1.23", which a lenient reading takes as 123, and en-IN takes both "12,34,567" and "1,234,567". Suited to amounts of money, where a misread group is a wrong amount. The default is nil.

  • :currency is a currency code or a Localize.Currency.t/0. When set, the string is read as an amount of that currency, with the separators Localize.Number.to_string/2 formats it with. Two locales separate a currency amount differently from a plain number, de-AT, which formats € 1.234,56 but writes 1 234,56 plain, and fr-CH, which writes a franc amount 1'234.56 but a plain number 1'234,56. There the string is read with both, and refused should both readings be valid and give different numbers. de-AT groups the euro with a period, so "1.234" is 1234 euros. The default is nil, a plain number.

  • :root_separators set to true also reads the string with the root locale's separators, the locale-neutral 1,234.56, for an amount of something CLDR has no data for, such as a digital token, which is commonly written that way. A string both read, as different numbers, is refused: de "1.234" is 1234 or 1.234. The default is false.

  • :separators names the set of decimal and grouping separators to read with, for a locale that has more than one. en-ZA writes 1 234,56 with its :standard set and 1,234.56 with its :us set. A locale without the named set reads with its standard one. The default is :standard.

Returns

  • {:ok, number} on success, or

  • {:error, %Localize.LocaleNotFoundInCacheError{}} when the locale's data is not installed (see the module documentation), or

  • {:error, exception} if parsing fails.

Examples

iex> Localize.Number.Parser.parse("-1_000_000.34")
{:ok, -1000000.34}

parse_currency_amount(string, options \\ [])

@spec parse_currency_amount(String.t(), Keyword.t()) ::
  {:ok, {Localize.Currency.currency_code() | term(), Decimal.t()}}
  | {:error, Exception.t()}

Parses a string that holds one currency amount into its currency and its amount.

The currency, a code, symbol or name, may come before or after the amount, with or without a space between them. A sign may stand before the currency, between the currency and the amount, or after the amount, or the parentheses of an accounting format may hold it, but not both. The amount is read with the separators the locale uses for that currency, so fr-CH "1’234.56 CHF" reads as it is written, and in de-AT and fr-CH, which write plain numbers otherwise, with those too. Its groups must be the sizes the locale writes (grouping: :sizes of parse/2), and a period that is none of its separators is read only where it can be nothing but a decimal point. Where the text could be two amounts it is refused rather than one guessed: de "1.23 EUR" is not €123, fr "1.234 €" is not €1.234, "($-5)" is neither -5 nor 5 dollars, "-$.50" is refused since Swiss "Fr. –.15" writes a dash for the zero, and fr "$2.00" is refused since several dollars share "$" there, where resolve_currency/2 takes the Canadian one. A narrow symbol of another currency than the locale's own is read only when it is a currency sign that names that currency alone, in the locale and in CLDR's root data, so en-AU "€5" is the euro and de-CH "€ 15.40" too, though de-CH writes the euro "EUR", but Swiss "15 Rp." (15 Rappen) is not the rupiah, whose narrow symbol de-CH gives as "Rp", and bg "₩5" is refused, since root gives "₩" to both wons. A historic currency is read only by its code or its name, since its symbols are reused: ru "6 300 Р" is not the ruble of 1991–1998. One to three digits, a decimal mark and three more are refused in a currency of fewer than three decimal places, since they may be a thousands group: de "1,599 €" and en "$59.568". The currency text, but not the number, is matched after NFKC normalization, so `ja` "¥1.00" is the yen, which `ja` writes "¥", while "$5²" is refused. Bidi controls are ignored. Nothing else may surround the amount.

ex_money's Money.parse/2 is built on this function, and its parsing guide describes what is read and what is refused, with examples.

Arguments

  • string is the string to parse.

  • options is a keyword list of options.

Options

  • :locale is a locale identifier. The default is Localize.get_locale/0.

  • :number_system and :separators are as for parse/2.

  • :only, :except and :fuzzy are as for resolve_currency/2, except that a fuzzy match is refused where another currency's string is as close: fr "100 dollars" is neither the Fiji nor the Rhodesian dollar.

  • :default_currency is the currency of an amount that names none: a currency code, or a name or symbol, resolved as currency text in the string would be. Without it, such an amount is an error.

  • :caller_currency is a function of one argument, called with the currency text, for currencies of the caller's own such as digital tokens. It returns {:ok, currency} when the text names one, or anything else. Text that names no currency of the locale is read as the caller's currency; text that names one of the locale's and a different one of the caller's is ambiguous and refused, as ₮ is the tugrik and a digital token's symbol, except where it names the locale's own currency: mn "₮5" is the tugrik. Pass it only when the caller has such currencies, since it is called for every amount. Their amounts are read with the locale's separators and the root locale's (root_separators: true of parse/2), and refused where the two differ. A caller's currency text may have digits in it, as a digital token's identifier "Q9PB7XDNF" does. It is then the first word of the amount or the last, which a space separates from the number, and it is read only where the amount has no other reading: fr "USD1 234" is also 1 234 US dollars, so it is neither.

Returns

  • {:ok, {currency, amount}} where currency is a currency code, or what :caller_currency returned, and amount is a Decimal.t/0, or

  • {:error, %Localize.UnknownCurrencyError{currency: text}} when the currency text names no currency, or

  • {:error, %Localize.ParseError{reason: :missing_currency}} when the string names no currency and there is no :default_currency, or

  • {:error, %Localize.InvalidAmountError{currency: currency, amount: text}} when the string names a currency but has no number, or one that cannot be read with the locale's separators for it, or

  • {:error, %Localize.ParseError{reason: :unexpected_input}} when the string is not one currency amount, or

  • {:error, %Localize.LocaleNotFoundInCacheError{}} when the locale's data is not installed (see the module documentation), or

  • {:error, exception} for an invalid locale or option.

Examples

iex> Localize.Number.Parser.parse_currency_amount("USD 100", locale: :en)
{:ok, {:USD, Decimal.new("100")}}

iex> Localize.Number.Parser.parse_currency_amount("(1 234,56 €)", locale: :fr)
{:ok, {:EUR, Decimal.new("-1234.56")}}

iex> Localize.Number.Parser.parse_currency_amount("kr-127,54", locale: :da)
{:ok, {:DKK, Decimal.new("-127.54")}}

iex> token = fn "Q9PB7XDNF" -> {:ok, "Q9PB7XDNF"}; _other -> :error end
iex> Localize.Number.Parser.parse_currency_amount("Q9PB7XDNF 5", locale: :en, caller_currency: token)
{:ok, {"Q9PB7XDNF", Decimal.new("5")}}

resolve(list, resolver, options)

@spec resolve(list(), (String.t(), Keyword.t() -> term()), Keyword.t()) ::
  list() | {:error, Exception.t()}

Maps a list applying a resolver function to each binary element.

Arguments

  • list is a list of terms.

  • resolver is a function that takes a string and options.

  • options is a keyword list passed to the resolver.

Options

  • The options are passed through unchanged to resolver for each binary element. See the resolver function (for example resolve_currency/2 or resolve_per/2) for the options it supports.

Returns

  • The list with binary elements resolved.

Examples

iex> resolver = &Localize.Number.Parser.resolve_currency/2
iex> Localize.Number.Parser.resolve(["100", "USD"], resolver, locale: :en)
["100", :USD]

resolve_currencies(list, options \\ [])

@spec resolve_currencies([String.t() | number()], Keyword.t()) ::
  [atom() | String.t() | number()] | {:error, Exception.t()}

Resolves currencies from strings within a list.

Arguments

  • list is a list of strings and numbers.

  • options is a keyword list of options.

Options

  • :locale is a locale identifier. The default is :en.

  • :only is a filter for currencies to include.

  • :except is a filter for currencies to exclude.

  • :fuzzy is a float for fuzzy matching via String.jaro_distance/2. Of equally close currency strings, the alphabetically first is taken.

Returns

  • A list with currency strings replaced by currency code atoms.

Examples

iex> Localize.Number.Parser.scan("100 US dollars")
...> |> Localize.Number.Parser.resolve_currencies()
[100, :USD]

resolve_currency(string, options \\ [])

@spec resolve_currency(String.t(), Keyword.t()) ::
  [atom() | String.t()] | {:error, Exception.t()}

Resolves a currency from the beginning and/or end of a string.

A string several of the locale's currencies share names the currency of the territory whose CLDR locale is nearest the locale, when that locale is a good fit and no other claimant's is as near: "$" is the Canadian dollar in fr, as in fr-CA. Otherwise it stays unknown.

Arguments

  • string is a string potentially containing a currency name or symbol.

  • options is a keyword list of options.

Options

  • :locale is a locale identifier. The default is :en.

  • :only is a filter for currencies to include.

  • :except is a filter for currencies to exclude.

  • :fuzzy is a float for fuzzy matching. Of equally close currency strings, the alphabetically first is taken.

Returns

  • A list with the currency code and remainder, or {:error, exception}.

Examples

iex> Localize.Number.Parser.resolve_currency("US dollars")
[:USD]

iex> Localize.Number.Parser.resolve_currency("$100")
[:USD, "100"]

resolve_per(string, options \\ [])

@spec resolve_per(String.t(), Keyword.t()) ::
  [per() | String.t()] | {:error, Exception.t()}

Resolves percent or permille from a string.

Arguments

  • string is a string potentially containing percent or permille symbols.

  • options is a keyword list of options.

Options

  • :locale is a locale identifier atom, string, or a Localize.LanguageTag.t/0. The default is Localize.get_locale().

  • :number_system is a number system name or type atom. The default is the number system of :locale.

Returns

  • A list with the symbol replaced by :percent or :permille.

  • {:error, exception} if no symbol found.

Examples

iex> Localize.Number.Parser.resolve_per("11%")
["11", :percent]

resolve_pers(list, options \\ [])

@spec resolve_pers([String.t() | number()], Keyword.t()) ::
  [per() | String.t() | number()] | {:error, Exception.t()}

Resolves percent and permille symbols from strings within a list.

Arguments

  • list is a list of strings and numbers.

  • options is a keyword list of options.

Options

  • :locale is a locale identifier atom, string, or a Localize.LanguageTag.t/0. The default is Localize.get_locale().

  • :number_system is a number system name or type atom. The default is the number system of :locale.

Returns

  • A list with percent/permille strings replaced by :percent or :permille atoms.

Examples

iex> Localize.Number.Parser.scan("100%")
...> |> Localize.Number.Parser.resolve_pers()
[100, :percent]

scan(string, options \\ [])

@spec scan(String.t(), Keyword.t()) ::
  [String.t() | integer() | float() | Decimal.t()] | {:error, Exception.t()}

Scans a string in a locale-aware manner and returns a list of strings and numbers.

In a locale that groups with a space, a space between digits is read as a grouping separator, which is what lets "1 234,5" be found as one number under fr. The consequence is that adjacent numbers separated by a space can be read as a single number when each run is two digits or more — a list of room numbers or the parts of a phone number among them. Pass lenient: false to require exact grouping sizes, which reads those as separate numbers again:

iex> Localize.Number.Parser.scan("chambres 12 14 16", locale: "fr")
["chambres ", 121416]

iex> Localize.Number.Parser.scan("chambres 12 14 16", locale: "fr", lenient: false)
["chambres ", 12, " ", 14, " ", 16]

A number in the text has the shape Localize.Number.Scanner reads: a sign before the digits and touching them, the locale's group and decimal separators, an exponent, and leniently a zero fraction written as a dash, "5.–". Each such run is read by parse/2 with these options and is a number where that succeeds; otherwise it is text, as written. An underscore between two digits is Elixir's digit separator, so "1_000" is 1000, and anywhere else it makes the run text, so "5_" stays "5_".

Arguments

  • string is any string.

  • options is a keyword list of options.

Options

  • :number is one of :integer, :float, :decimal, or nil. The default is nil (auto-detect).

  • :locale is a locale identifier. The default is :en.

  • :number_system is a number system name or type.

  • :lenient governs how strictly grouping separators must be positioned. true, the default, requires each group to be at least two digits, which is what keeps "3 4 5" three numbers rather than one. false requires each group to be exactly the locale's grouping size, so fr takes "1 234 567" and refuses "1 23".

  • :grouping set to :sizes checks each group against the sizes the locale writes rather than as :lenient says: the last group of the locale's grouping size, the others of that size or its secondary one, so de refuses "1.23", which a lenient reading takes as 123, and en-IN takes both "12,34,567" and "1,234,567". Suited to amounts of money, where a misread group is a wrong amount. The default is nil.

  • :currency is a currency code or a Localize.Currency.t/0. When set, the string is read as an amount of that currency, with the separators Localize.Number.to_string/2 formats it with. Two locales separate a currency amount differently from a plain number, de-AT, which formats € 1.234,56 but writes 1 234,56 plain, and fr-CH, which writes a franc amount 1'234.56 but a plain number 1'234,56. There the string is read with both, and refused should both readings be valid and give different numbers. de-AT groups the euro with a period, so "1.234" is 1234 euros. The default is nil, a plain number.

  • :root_separators set to true also reads the string with the root locale's separators, the locale-neutral 1,234.56, for an amount of something CLDR has no data for, such as a digital token, which is commonly written that way. A string both read, as different numbers, is refused: de "1.234" is 1234 or 1.234. The default is false.

  • :separators names the set of decimal and grouping separators to read with, for a locale that has more than one. en-ZA writes 1 234,56 with its :standard set and 1,234.56 with its :us set. A locale without the named set reads with its standard one. The default is :standard.

Returns

  • A list of strings and numbers.

Examples

iex> Localize.Number.Parser.scan("The prize is 23")
["The prize is ", 23]

iex> Localize.Number.Parser.scan("1kg")
[1, "kg"]