├── .dialyzer_ignore_warnings ├── .formatter.exs ├── .github └── workflows │ └── ci.yml ├── .gitignore ├── CHANGELOG.md ├── LICENSE.md ├── README.md ├── bench ├── locale_and_options.exs ├── parse.exs ├── profile_number_parser.ex ├── profile_to_string.exs └── to_string.exs ├── config ├── config.exs ├── dev.exs ├── prod.exs ├── release.exs └── test.exs ├── lib └── cldr │ ├── exception.ex │ ├── number.ex │ ├── number │ ├── backend │ │ ├── compiler.ex │ │ ├── decimal_formatter.ex │ │ ├── format.ex │ │ ├── number.ex │ │ ├── rbnf.ex │ │ ├── symbol.ex │ │ ├── system.ex │ │ └── transliterate.ex │ ├── format.ex │ ├── format │ │ ├── compiler.ex │ │ ├── meta.ex │ │ └── options.ex │ ├── formatter │ │ ├── currency_formatter.ex │ │ ├── decimal_formatter.ex │ │ └── short_formatter.ex │ ├── parse.ex │ ├── rbnf.ex │ ├── rbnf │ │ ├── processor.ex │ │ └── rule.ex │ ├── string.ex │ ├── symbol.ex │ ├── system.ex │ └── transliterate.ex │ └── protocol │ └── cldr_chars.ex ├── logo.png ├── mix.exs ├── mix.lock ├── mix ├── clause_debugger.ex_ ├── for_dialyzer.ex ├── rbnf_test_data_gen.ex ├── tasks │ └── generate_rbnf_test_data.ex └── test_backend.ex ├── src ├── decimal_formats_lexer.xrl ├── decimal_formats_parser.yrl ├── rbnf_lexer.xrl └── rbnf_parser.yrl └── test ├── cldr_numbers_test.exs ├── doc_test.exs ├── meta_test.exs ├── number ├── currency_test.exs ├── decimal_format_compiler_test.exs ├── exponent_test.exs ├── number_format_test.exs ├── number_parser_test.exs ├── number_string_test.exs ├── number_symbol_test.exs ├── number_system_test.exs ├── rbnf_test.exs ├── short_formatter_test.exs ├── significant_digits_test.exs ├── split_format_test.exs ├── transliteration_test.exs └── wrapper_test.exs ├── rbnf └── rbnf_rule_compiler_test.exs ├── support ├── .gitkeep ├── icu_test_cases.txt ├── number_format_test_data.exs ├── rbnf │ ├── af │ │ └── rbnf_test.json │ ├── ar-EG │ │ └── rbnf_test.json │ ├── be │ │ └── rbnf_test.json │ ├── bg │ │ └── rbnf_test.json │ ├── bn │ │ └── rbnf_test.json │ ├── ca │ │ └── rbnf_test.json │ ├── cs │ │ └── rbnf_test.json │ ├── cy │ │ └── rbnf_test.json │ ├── da │ │ └── rbnf_test.json │ ├── de-CH │ │ └── rbnf_test.json │ ├── de │ │ └── rbnf_test.json │ ├── el │ │ └── rbnf_test.json │ ├── en-150 │ │ └── rbnf_test.json │ ├── en-AU │ │ └── rbnf_test.json │ ├── en-CA │ │ └── rbnf_test.json │ ├── en-GB │ │ └── rbnf_test.json │ ├── en-IE │ │ └── rbnf_test.json │ ├── en-SG │ │ └── rbnf_test.json │ ├── en-ZA │ │ └── rbnf_test.json │ ├── en │ │ └── rbnf_test.json │ ├── es-419 │ │ └── rbnf_test.json │ ├── es-CO │ │ └── rbnf_test.json │ ├── es-MX │ │ └── rbnf_test.json │ ├── es-US │ │ └── rbnf_test.json │ ├── es │ │ └── rbnf_test.json │ ├── eu │ │ └── rbnf_test.json │ ├── fa │ │ └── rbnf_test.json │ ├── fi │ │ └── rbnf_test.json │ ├── fil │ │ └── rbnf_test.json │ ├── fr-BE │ │ └── rbnf_test.json │ ├── fr-CA │ │ └── rbnf_test.json │ ├── fr-CH │ │ └── rbnf_test.json │ ├── fr │ │ └── rbnf_test.json │ ├── ga │ │ └── rbnf_test.json │ ├── gl │ │ └── rbnf_test.json │ ├── gu │ │ └── rbnf_test.json │ ├── he │ │ └── rbnf_test.json │ ├── hi │ │ └── rbnf_test.json │ ├── hr │ │ └── rbnf_test.json │ ├── hu │ │ └── rbnf_test.json │ ├── id │ │ └── rbnf_test.json │ ├── is │ │ └── rbnf_test.json │ ├── it-CH │ │ └── rbnf_test.json │ ├── it │ │ └── rbnf_test.json │ ├── ja │ │ └── rbnf_test.json │ ├── kn │ │ └── rbnf_test.json │ ├── ko │ │ └── rbnf_test.json │ ├── lv │ │ └── rbnf_test.json │ ├── mr │ │ └── rbnf_test.json │ ├── ms │ │ └── rbnf_test.json │ ├── nb │ │ └── rbnf_test.json │ ├── nl │ │ └── rbnf_test.json │ ├── pl │ │ └── rbnf_test.json │ ├── pt │ │ └── rbnf_test.json │ ├── ro │ │ └── rbnf_test.json │ ├── ru │ │ └── rbnf_test.json │ ├── sk │ │ └── rbnf_test.json │ ├── sq │ │ └── rbnf_test.json │ ├── sr │ │ └── rbnf_test.json │ ├── sv │ │ └── rbnf_test.json │ ├── ta │ │ └── rbnf_test.json │ ├── th │ │ └── rbnf_test.json │ ├── tr │ │ └── rbnf_test.json │ ├── uk │ │ └── rbnf_test.json │ ├── ur │ │ └── rbnf_test.json │ ├── vi │ │ └── rbnf_test.json │ ├── zh-Hant │ │ └── rbnf_test.json │ └── zh │ │ └── rbnf_test.json ├── rbnf_test_support.exs ├── split_format_test_data.exs ├── test_backend.ex └── wrapper.ex ├── sync_test.exs └── test_helper.exs /.dialyzer_ignore_warnings: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /.formatter.exs: -------------------------------------------------------------------------------- 1 | [ 2 | inputs: ["mix.exs", "{config,lib,test,mix}/**/*.{ex,exs}"], 3 | locals_without_parens: [docp: 1] 4 | ] 5 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: Elixir CI 2 | 3 | # Define workflow that runs when changes are pushed to the 4 | # `main` branch or pushed to a PR branch that targets the `main` 5 | # branch. Change the branch name if your project uses a 6 | # different name for the main branch like "master" or "production". 7 | on: 8 | push: 9 | branches: [ "main" ] # adapt branch for project 10 | pull_request: 11 | branches: [ "main" ] # adapt branch for project 12 | 13 | # Sets the ENV `MIX_ENV` to `test` for running tests 14 | env: 15 | MIX_ENV: test 16 | 17 | permissions: 18 | contents: read 19 | 20 | jobs: 21 | test: 22 | # Set up a Postgres DB service. By default, Phoenix applications 23 | # use Postgres. This creates a database for running tests. 24 | # Additional services can be defined here if required. 25 | # services: 26 | # db: 27 | # image: postgres:12 28 | # ports: ['5432:5432'] 29 | # env: 30 | # POSTGRES_PASSWORD: postgres 31 | # options: >- 32 | # --health-cmd pg_isready 33 | # --health-interval 10s 34 | # --health-timeout 5s 35 | # --health-retries 5 36 | 37 | runs-on: ubuntu-latest 38 | name: Test on OTP ${{matrix.otp}} / Elixir ${{matrix.elixir}} 39 | strategy: 40 | # Specify the OTP and Elixir versions to use when building 41 | # and running the workflow steps. 42 | matrix: 43 | otp: ['26.2.4'] # Define the OTP version [required] 44 | elixir: ['1.16.2-otp-26'] # Define the elixi 45 | steps: 46 | # Step: Setup Elixir + Erlang image as the base. 47 | - name: Set up Elixir 48 | uses: erlef/setup-beam@v1 49 | with: 50 | otp-version: ${{matrix.otp}} 51 | elixir-version: ${{matrix.elixir}} 52 | 53 | # Step: Check out the code. 54 | - name: Checkout code 55 | uses: actions/checkout@v3 56 | 57 | # Step: Define how to cache deps. Restores existing cache if present. 58 | - name: Cache deps 59 | id: cache-deps 60 | uses: actions/cache@v3 61 | env: 62 | cache-name: cache-elixir-deps 63 | with: 64 | path: deps 65 | key: ${{ runner.os }}-mix-${{ env.cache-name }}-${{ hashFiles('**/mix.lock') }} 66 | restore-keys: | 67 | ${{ runner.os }}-mix-${{ env.cache-name }}- 68 | 69 | # Step: Define how to cache the `_build` directory. After the first run, 70 | # this speeds up tests runs a lot. This includes not re-compiling our 71 | # project's downloaded deps every run. 72 | - name: Cache compiled build 73 | id: cache-build 74 | uses: actions/cache@v3 75 | env: 76 | cache-name: cache-compiled-build 77 | with: 78 | path: _build 79 | key: ${{ runner.os }}-mix-${{ env.cache-name }}-${{ hashFiles('**/mix.lock') }} 80 | restore-keys: | 81 | ${{ runner.os }}-mix-${{ env.cache-name }}- 82 | ${{ runner.os }}-mix- 83 | 84 | # Step: Download project dependencies. If unchanged, uses 85 | # the cached version. 86 | - name: Install dependencies 87 | run: mix deps.get 88 | 89 | # Step: Compile the project treating any warnings as errors. 90 | # Customize this step if a different behavior is desired. 91 | - name: Compiles without warnings 92 | run: mix compile --warnings-as-errors 93 | 94 | # Step: Check that the checked in code has already been formatted. 95 | # This step fails if something was found unformatted. 96 | # Customize this step as desired. 97 | # - name: Check Formatting 98 | # run: mix format --check-formatted 99 | 100 | # Step: Execute the tests. 101 | - name: Run tests 102 | run: mix test 103 | 104 | # Step: Execute dialyzer. 105 | - name: Run dialyzer 106 | run: mix dialyzer 107 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /_build 2 | /cover 3 | /deps 4 | /doc 5 | /references 6 | *.snapshot 7 | erl_crash.dump 8 | *.ez 9 | *.tar 10 | .DS_Store 11 | /src/*.erl 12 | 13 | # The xml downloaded from Unicode 14 | /downloads 15 | 16 | # Ignore the generated json files 17 | /data 18 | 19 | # erl is always generated 20 | *.erl 21 | 22 | # asdf 23 | .tool-versions 24 | 25 | # mis 26 | mise.toml 27 | 28 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | ## License 2 | 3 | Copyright 2017-2023 Kip Cole 4 | 5 | Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in 6 | compliance with the License. You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software distributed under the License 11 | is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 12 | implied. See the License for the specific language governing permissions and limitations under the 13 | License. 14 | -------------------------------------------------------------------------------- /bench/locale_and_options.exs: -------------------------------------------------------------------------------- 1 | Benchee.run( 2 | %{ 3 | "Cldr.locale_and_backend_from" => fn -> Cldr.locale_and_backend_from([]) end 4 | }, 5 | time: 10, 6 | memory_time: 2 7 | ) -------------------------------------------------------------------------------- /bench/parse.exs: -------------------------------------------------------------------------------- 1 | number = "1234567.00" 2 | locale = Cldr.Locale.new!("en", MyApp.Cldr) 3 | 4 | parse = fn x -> 5 | case Integer.parse(x) do 6 | {integer, ""} -> integer 7 | _other -> case Float.parse(x) do 8 | {float, ""} -> float 9 | _other -> x 10 | end 11 | end 12 | end 13 | 14 | Benchee.run( 15 | %{ 16 | "Cldr.Number.Parser.parse" => fn -> Cldr.Number.Parser.parse(number) end, 17 | "Cldr.Number.Parser.parse(locale)" => fn -> Cldr.Number.Parser.parse(number, locale: locale) end, 18 | "Float.parse" => fn -> Float.parse(number) end, 19 | "Flexi parser" => fn -> parse.(number) end 20 | }, 21 | time: 10, 22 | memory_time: 2 23 | ) -------------------------------------------------------------------------------- /bench/profile_number_parser.ex: -------------------------------------------------------------------------------- 1 | defmodule ProfileRunner do 2 | import ExProf.Macro 3 | 4 | @doc "analyze with profile macro" 5 | def do_analyze do 6 | profile do 7 | Cldr.Number.Parser.parse("1234.00") 8 | end 9 | end 10 | 11 | @doc "get analysis records and sum them up" 12 | def run do 13 | {records, _block_result} = do_analyze() 14 | 15 | records 16 | |> Enum.filter(&String.contains?(&1.function, "Cldr.Number.Parser")) 17 | |> ExProf.Analyzer.print 18 | end 19 | 20 | end 21 | 22 | ProfileRunner.run 23 | -------------------------------------------------------------------------------- /bench/profile_to_string.exs: -------------------------------------------------------------------------------- 1 | defmodule ProfileRunner do 2 | import ExProf.Macro 3 | 4 | @doc "analyze with profile macro" 5 | def do_analyze do 6 | {:ok, options} = Cldr.Number.Format.Options.validate_options(0, MyApp.Cldr, []) 7 | profile do 8 | MyApp.Cldr.Number.to_string 1234, options 9 | end 10 | end 11 | 12 | @doc "get analysis records and sum them up" 13 | def run do 14 | {records, _block_result} = do_analyze() 15 | 16 | records 17 | |> Enum.filter(&String.contains?(&1.function, "Cldr.Number")) 18 | |> ExProf.Analyzer.print 19 | end 20 | 21 | end 22 | 23 | ProfileRunner.run 24 | 25 | # 26 | # Total: 215 100.00% 328 [ 1.53] 27 | # %Prof{ 28 | # calls: 1, 29 | # function: "'Elixir.Cldr.Number.Formatter.Decimal':add_first_group/3", 30 | # percent: 0.0, 31 | # time: 0, 32 | # us_per_call: 0.0 33 | # } -------------------------------------------------------------------------------- /bench/to_string.exs: -------------------------------------------------------------------------------- 1 | {:ok, options} = Cldr.Number.Format.Options.validate_options([], MyApp.Cldr, locale: Cldr.get_locale()) 2 | number = 100000.55 3 | 4 | Benchee.run( 5 | %{ 6 | "Number to_string" => fn -> MyApp.Cldr.Number.to_string number end, 7 | "Number to_string preformatted options" => fn -> MyApp.Cldr.Number.to_string number, options end, 8 | "Float to_string" => fn -> Float.to_string 10000.55 end, 9 | ":erlang.float_to_binary" => fn -> :erlang.float_to_binary(number, [:compact, decimals: 3]) end 10 | }, 11 | time: 10, 12 | memory_time: 2 13 | ) -------------------------------------------------------------------------------- /config/config.exs: -------------------------------------------------------------------------------- 1 | # This file is responsible for configuring your application 2 | # and its dependencies with the aid of the Mix.Config module. 3 | import Config 4 | 5 | import_config "#{Mix.env()}.exs" 6 | -------------------------------------------------------------------------------- /config/dev.exs: -------------------------------------------------------------------------------- 1 | import Config 2 | 3 | config :ex_cldr, 4 | default_backend: MyApp.Cldr, 5 | json_library: JSON 6 | -------------------------------------------------------------------------------- /config/prod.exs: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elixir-cldr/cldr_numbers/1fb2f82370c262f83dc348a9781211926ee97fa9/config/prod.exs -------------------------------------------------------------------------------- /config/release.exs: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elixir-cldr/cldr_numbers/1fb2f82370c262f83dc348a9781211926ee97fa9/config/release.exs -------------------------------------------------------------------------------- /config/test.exs: -------------------------------------------------------------------------------- 1 | # In test mode we compile and test all locales 2 | import Config 3 | 4 | config :ex_unit, 5 | case_load_timeout: 220_000, 6 | timeout: 120_000 7 | 8 | config :ex_cldr, 9 | default_backend: TestBackend.Cldr 10 | -------------------------------------------------------------------------------- /lib/cldr/exception.ex: -------------------------------------------------------------------------------- 1 | defmodule Cldr.Rbnf.NoRuleForNumber do 2 | @moduledoc """ 3 | Exception raised when an attempt is made to invoke an RBNF rule for a number 4 | that is not supported by that rule. 5 | """ 6 | defexception [:message] 7 | 8 | def exception(message) do 9 | %__MODULE__{message: message} 10 | end 11 | end 12 | 13 | defmodule Cldr.Rbnf.NoRule do 14 | @moduledoc """ 15 | Exception raised when an attempt is made to invoke an RBNF rule that 16 | is not supported for a given locale 17 | """ 18 | defexception [:message] 19 | 20 | def exception(message) do 21 | %__MODULE__{message: message} 22 | end 23 | end 24 | 25 | defmodule Cldr.CurrencyAlreadyDefined do 26 | @moduledoc """ 27 | Exception raised when an attempt is made to define a currency 28 | that already exists. 29 | """ 30 | defexception [:message] 31 | 32 | def exception(message) do 33 | %__MODULE__{message: message} 34 | end 35 | end 36 | 37 | defmodule Cldr.CurrencyCodeInvalid do 38 | @moduledoc """ 39 | Exception raised when an attempt is made to define a currency 40 | code that is invalid. 41 | """ 42 | defexception [:message] 43 | 44 | def exception(message) do 45 | %__MODULE__{message: message} 46 | end 47 | end 48 | 49 | defmodule Cldr.NoNumberSymbols do 50 | @moduledoc """ 51 | Exception raised when when there are no number 52 | symbols for a locale and number system. 53 | """ 54 | defexception [:message] 55 | 56 | def exception(message) do 57 | %__MODULE__{message: message} 58 | end 59 | end 60 | 61 | defmodule Cldr.Number.ParseError do 62 | @moduledoc """ 63 | Exception raised when when trying to parse 64 | a string into a number and the string is 65 | not parseable. 66 | """ 67 | defexception [:message] 68 | 69 | def exception(message) do 70 | %__MODULE__{message: message} 71 | end 72 | end 73 | -------------------------------------------------------------------------------- /lib/cldr/number/backend/compiler.ex: -------------------------------------------------------------------------------- 1 | defmodule Cldr.Number.Backend do 2 | @moduledoc false 3 | 4 | def define_number_modules(config) do 5 | quote location: :keep do 6 | unquote(Cldr.Number.Backend.Number.define_number_module(config)) 7 | unquote(Cldr.Number.Backend.Format.define_number_module(config)) 8 | unquote(Cldr.Number.Backend.Transliterate.define_number_module(config)) 9 | unquote(Cldr.Number.Backend.System.define_number_module(config)) 10 | unquote(Cldr.Number.Backend.Symbol.define_number_module(config)) 11 | unquote(Cldr.Number.Backend.Decimal.Formatter.define_number_module(config)) 12 | unquote(Cldr.Number.Backend.Rbnf.define_number_modules(config)) 13 | 14 | # Number requires Currency 15 | unquote(Cldr.Currency.Backend.define_currency_module(config)) 16 | end 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /lib/cldr/number/backend/decimal_formatter.ex: -------------------------------------------------------------------------------- 1 | defmodule Cldr.Number.Backend.Decimal.Formatter do 2 | @moduledoc false 3 | 4 | def define_number_module(config) do 5 | alias Cldr.Number.Formatter.Decimal 6 | 7 | backend = config.backend 8 | 9 | quote location: :keep do 10 | defmodule Number.Formatter.Decimal do 11 | unless Cldr.Config.include_module_docs?(unquote(config.generate_docs)) do 12 | @moduledoc false 13 | end 14 | 15 | alias Cldr.Number.Formatter.Decimal 16 | alias Cldr.Number.Format.Compiler 17 | alias Cldr.Number.Format.Meta 18 | alias Cldr.Number.Format.Options 19 | alias Cldr.Math 20 | 21 | @doc """ 22 | Formats a number according to a decimal format string. 23 | 24 | ## Arguments 25 | 26 | * `number` is an integer, float or Decimal 27 | 28 | * `format` is a format string. See `#{inspect(unquote(backend))}.Number` for further information. 29 | 30 | * `options` is a map of options. See `#{inspect(unquote(backend))}.Number.to_string/2` 31 | for further information. 32 | 33 | """ 34 | @dialyzer {:nowarn_function, to_string: 3} 35 | 36 | @spec to_string( 37 | Math.number_or_decimal(), 38 | String.t() | Meta.t(), 39 | Keyword.t() | Options.t() 40 | ) :: 41 | {:ok, String.t()} | {:error, {module(), String.t()}} 42 | 43 | def to_string(number, format, options \\ []) 44 | 45 | def to_string(number, format, options) when is_binary(format) and is_list(options) do 46 | with {:ok, options} <- Options.validate_options(number, unquote(backend), options) do 47 | to_string(number, format, options) 48 | end 49 | end 50 | 51 | # Precompile the known formats and build the formatting pipeline 52 | # specific to this format thereby optimizing the performance. 53 | unquote(Decimal.define_to_string(backend)) 54 | 55 | # Other number formatting systems may create the formatting 56 | # metadata by other means (like a printf function) in which 57 | # case we don't do anything except format 58 | def to_string(number, %Meta{} = meta, %Options{} = options) do 59 | meta = Decimal.update_meta(meta, number, unquote(backend), options) 60 | Decimal.do_to_string(number, meta, unquote(backend), options) 61 | end 62 | 63 | def to_string(number, %Meta{} = meta, options) do 64 | with {:ok, options} <- Options.validate_options(number, unquote(backend), options) do 65 | to_string(number, meta, options) 66 | end 67 | end 68 | 69 | # For formats not precompiled we need to compile first 70 | # and then process. This will be slower than a compiled 71 | # format since we have to (a) compile the format and (b) 72 | # execute the full formatting pipeline. 73 | require Compiler 74 | 75 | def to_string(number, format, %Options{} = options) when is_binary(format) do 76 | Compiler.maybe_log_compile_warning( 77 | format, 78 | unquote(config.suppress_warnings), 79 | "ex_cldr_numbers: number format #{inspect(format)} is being compiled. " <> 80 | "For performance reasons please consider adding this format to the " <> 81 | "`precompile_number_formats` list in the backend configuration." 82 | ) 83 | 84 | case Compiler.format_to_metadata(format) do 85 | {:ok, meta} -> 86 | meta = Decimal.update_meta(meta, number, unquote(backend), options) 87 | Decimal.do_to_string(number, meta, unquote(backend), options) 88 | 89 | {:error, message} -> 90 | {:error, {Cldr.FormatCompileError, message}} 91 | end 92 | end 93 | end 94 | end 95 | end 96 | end 97 | -------------------------------------------------------------------------------- /lib/cldr/number/backend/rbnf.ex: -------------------------------------------------------------------------------- 1 | defmodule Cldr.Number.Backend.Rbnf do 2 | @moduledoc false 3 | 4 | def define_number_modules(config) do 5 | backend = config.backend 6 | root_locale = Cldr.Config.root_locale_name() 7 | 8 | quote location: :keep do 9 | defmodule Rbnf.NumberSystem do 10 | @moduledoc false 11 | if Cldr.Config.include_module_docs?(unquote(config.generate_docs)) do 12 | @moduledoc """ 13 | Functions to implement the number system rule-based-number-format rules of CLDR. 14 | 15 | These rules are defined only on the "und" locale and represent specialised 16 | number formatting. 17 | 18 | The standard public API for RBNF is via the `Cldr.Number.to_string/2` function. 19 | 20 | The functions on this module are defined at compile time based upon the RBNF rules 21 | defined in the Unicode CLDR data repository. Available rules are identified by: 22 | 23 | iex> #{inspect(__MODULE__)}.rule_sets(#{inspect(unquote(root_locale))}) 24 | ...> |> Enum.sort() 25 | [ 26 | :armenian_lower, 27 | :armenian_upper, 28 | :cyrillic_lower, 29 | :ethiopic, 30 | :georgian, 31 | :greek_lower, 32 | :greek_upper, 33 | :hebrew, 34 | :hebrew_item, 35 | :roman_lower, 36 | :roman_upper, 37 | :tamil, 38 | :zz_default 39 | ] 40 | 41 | A rule can then be invoked on an available rule_set. For example 42 | 43 | iex> #{inspect(__MODULE__)}.roman_upper(123, #{inspect(unquote(root_locale))}) 44 | "CXXIII" 45 | 46 | This particular call is equivalent to the call through the public API of: 47 | 48 | iex> #{inspect(unquote(backend))}.Number.to_string(123, format: :roman) 49 | {:ok, "CXXIII"} 50 | 51 | """ 52 | end 53 | 54 | import Kernel, except: [and: 2] 55 | use Cldr.Rbnf.Processor, backend: unquote(backend) 56 | 57 | define_rules(:NumberingSystemRules, unquote(backend), __ENV__) 58 | end 59 | 60 | defmodule Rbnf.Spellout do 61 | @moduledoc false 62 | if Cldr.Config.include_module_docs?(unquote(config.generate_docs)) do 63 | @moduledoc """ 64 | Functions to implement the spellout rule-based-number-format rules of CLDR. 65 | 66 | As CLDR notes, the data is incomplete or non-existent for many languages. It 67 | is considered complete for English however. 68 | 69 | The standard public API for RBNF is via the `Cldr.Number.to_string/2` function. 70 | 71 | The functions on this module are defined at compile time based upon the RBNF rules 72 | defined in the Unicode CLDR data repository. Available rules are identified by: 73 | 74 | iex> #{inspect(__MODULE__)}.rule_sets("en") 75 | ...> |> Enum.sort() 76 | [ 77 | :spellout_cardinal, 78 | :spellout_cardinal_verbose, 79 | :spellout_numbering, 80 | :spellout_numbering_verbose, 81 | :spellout_numbering_year, 82 | :spellout_ordinal, 83 | :spellout_ordinal_verbose 84 | ] 85 | 86 | A rule can then be invoked on an available rule_set. For example: 87 | 88 | iex> #{inspect(__MODULE__)}.spellout_ordinal(123, "en") 89 | "one hundred twenty-third" 90 | 91 | This call is equivalent to the call through the public API of: 92 | 93 | iex> #{inspect(unquote(backend))}.Number.to_string(123, format: :spellout) 94 | {:ok, "one hundred twenty-three"} 95 | 96 | """ 97 | end 98 | 99 | import Kernel, except: [and: 2] 100 | use Cldr.Rbnf.Processor, backend: unquote(backend) 101 | 102 | define_rules(:SpelloutRules, unquote(backend), __ENV__) 103 | end 104 | 105 | defmodule Rbnf.Ordinal do 106 | @moduledoc false 107 | if Cldr.Config.include_module_docs?(unquote(config.generate_docs)) do 108 | @moduledoc """ 109 | Functions to implement the ordinal rule-based-number-format rules of CLDR. 110 | 111 | As CLDR notes, the data is incomplete or non-existent for many languages. It 112 | is considered complete for English however. 113 | 114 | The standard public API for RBNF is via the `Cldr.Number.to_string/2` function. 115 | 116 | The functions on this module are defined at compile time based upon the RBNF rules 117 | defined in the Unicode CLDR data repository. Available rules are identified by: 118 | 119 | iex> #{inspect(__MODULE__)}.rule_sets(:en) 120 | [:digits_ordinal] 121 | 122 | iex> #{inspect(__MODULE__)}.rule_sets("fr") 123 | ...> |> Enum.sort() 124 | [ 125 | :digits_ordinal, 126 | :digits_ordinal_feminine, 127 | :digits_ordinal_feminine_plural, 128 | :digits_ordinal_masculine, 129 | :digits_ordinal_masculine_plural 130 | ] 131 | 132 | A rule can then be invoked on an available rule_set. For example 133 | 134 | iex> #{inspect(__MODULE__)}.digits_ordinal(123, :en) 135 | "123rd" 136 | 137 | This call is equivalent to the call through the public API of: 138 | 139 | iex> #{inspect(unquote(backend))}.Number.to_string(123, format: :ordinal) 140 | {:ok, "123rd"} 141 | 142 | """ 143 | end 144 | 145 | import Kernel, except: [and: 2] 146 | use Cldr.Rbnf.Processor, backend: unquote(backend) 147 | 148 | define_rules(:OrdinalRules, unquote(backend), __ENV__) 149 | end 150 | end 151 | end 152 | end 153 | -------------------------------------------------------------------------------- /lib/cldr/number/backend/symbol.ex: -------------------------------------------------------------------------------- 1 | defmodule Cldr.Number.Backend.Symbol do 2 | @moduledoc false 3 | 4 | def define_number_module(config) do 5 | module = inspect(__MODULE__) 6 | backend = config.backend 7 | config = Macro.escape(config) 8 | 9 | quote location: :keep, bind_quoted: [module: module, backend: backend, config: config] do 10 | defmodule Number.Symbol do 11 | unless Cldr.Config.include_module_docs?(config.generate_docs) do 12 | @moduledoc false 13 | end 14 | 15 | all_symbols = 16 | for locale <- Cldr.Locale.Loader.known_locale_names(config) do 17 | symbols = 18 | locale 19 | |> Cldr.Locale.Loader.get_locale(config) 20 | |> Map.get(:number_symbols) 21 | |> Enum.map(fn 22 | {number_system, nil} -> {number_system, nil} 23 | {number_system, symbols} -> {number_system, struct(Cldr.Number.Symbol, symbols)} 24 | end) 25 | |> Enum.into(%{}) 26 | 27 | {locale, symbols} 28 | end 29 | |> Map.new() 30 | 31 | @doc """ 32 | Returns a map of `Cldr.Number.Symbol.t` structs of the number symbols for each 33 | of the number systems of a locale. 34 | 35 | ## Options 36 | 37 | * `locale` is any valid locale name returned by 38 | `#{inspect(backend)}.known_locale_names/0` 39 | or a `Cldr.LanguageTag` struct returned by 40 | `#{inspect(backend)}.Locale.new!/1`. The default 41 | is `#{inspect(backend)}.get_locale/0`. 42 | 43 | ## Example: 44 | 45 | iex> #{inspect(__MODULE__)}.number_symbols_for(:th) 46 | { 47 | :ok, 48 | %{ 49 | latn: %Cldr.Number.Symbol{ 50 | decimal: %{standard: "."}, 51 | exponential: "E", 52 | group: %{standard: ","}, 53 | infinity: "∞", 54 | list: ";", 55 | minus_sign: "-", 56 | nan: "NaN", 57 | per_mille: "‰", 58 | percent_sign: "%", 59 | plus_sign: "+", 60 | superscripting_exponent: "×", 61 | time_separator: ":" 62 | }, 63 | thai: %Cldr.Number.Symbol{ 64 | decimal: %{standard: "."}, 65 | exponential: "E", 66 | group: %{standard: ","}, 67 | infinity: "∞", 68 | list: ";", 69 | minus_sign: "-", 70 | nan: "NaN", 71 | per_mille: "‰", 72 | percent_sign: "%", 73 | plus_sign: "+", 74 | superscripting_exponent: "×", 75 | time_separator: ":" 76 | } 77 | } 78 | } 79 | 80 | """ 81 | @spec number_symbols_for(LanguageTag.t() | Cldr.Locale.locale_name()) :: 82 | {:ok, map()} | {:error, {module(), String.t()}} 83 | 84 | def number_symbols_for(locale \\ unquote(backend).get_locale()) 85 | 86 | for {locale, symbols} <- all_symbols do 87 | def number_symbols_for(%LanguageTag{cldr_locale_name: unquote(locale)}) do 88 | {:ok, unquote(Macro.escape(symbols))} 89 | end 90 | end 91 | 92 | def number_symbols_for(locale_name) do 93 | with {:ok, locale} <- unquote(backend).validate_locale(locale_name) do 94 | number_symbols_for(locale) 95 | end 96 | end 97 | 98 | def number_symbols_for(locale, number_system) do 99 | with {:ok, system_name} <- 100 | unquote(backend).Number.System.system_name_from(number_system, locale), 101 | {:ok, symbols} <- number_symbols_for(locale) do 102 | symbols 103 | |> Map.get(system_name) 104 | |> Cldr.Number.Symbol.symbols_return(locale, number_system) 105 | end 106 | end 107 | 108 | all_decimal_symbols = 109 | for {_locale, locale_symbols} <- all_symbols, 110 | {_number_system, symbols} <- locale_symbols, 111 | !is_nil(symbols) do 112 | Map.values(symbols.decimal) 113 | end 114 | |> Elixir.List.flatten() 115 | |> Enum.uniq() 116 | 117 | all_grouping_symbols = 118 | for {_locale, locale_symbols} <- all_symbols, 119 | {_number_system, symbols} <- locale_symbols, 120 | !is_nil(symbols) do 121 | Map.values(symbols.group) 122 | end 123 | |> Elixir.List.flatten() 124 | |> Enum.uniq() 125 | 126 | @doc """ 127 | Returns a list of all decimal symbols defined 128 | by the locales configured in this backend as 129 | a list. 130 | 131 | """ 132 | def all_decimal_symbols do 133 | unquote(Macro.escape(all_decimal_symbols)) 134 | end 135 | 136 | @doc """ 137 | Returns a list of all grouping symbols defined 138 | by the locales configured in this backend as 139 | a list. 140 | 141 | """ 142 | def all_grouping_symbols do 143 | unquote(Macro.escape(all_grouping_symbols)) 144 | end 145 | 146 | @doc """ 147 | Returns a list of all decimal symbols defined 148 | by the locales configured in this backend as 149 | a string. 150 | 151 | This string can be used as a character class 152 | when builing a regular expression. 153 | 154 | """ 155 | def all_decimal_symbols_class do 156 | unquote(Macro.escape(all_decimal_symbols)) 157 | end 158 | 159 | @doc """ 160 | Returns a list of all grouping symbols defined 161 | by the locales configured in this backend as 162 | a string. 163 | 164 | This string can be used as a character class 165 | when builing a regular expression. 166 | 167 | """ 168 | def all_grouping_symbols_class do 169 | unquote(Enum.join(all_grouping_symbols)) 170 | end 171 | end 172 | end 173 | end 174 | end 175 | -------------------------------------------------------------------------------- /lib/cldr/number/backend/transliterate.ex: -------------------------------------------------------------------------------- 1 | defmodule Cldr.Number.Backend.Transliterate do 2 | @moduledoc false 3 | 4 | def define_number_module(config) do 5 | module = inspect(__MODULE__) 6 | backend = config.backend 7 | config = Macro.escape(config) 8 | 9 | quote location: :keep, bind_quoted: [module: module, backend: backend, config: config] do 10 | defmodule Number.Transliterate do 11 | @moduledoc false 12 | if Cldr.Config.include_module_docs?(config.generate_docs) do 13 | @moduledoc """ 14 | Transliteration for digits and separators. 15 | 16 | Transliterating a string is an expensive business. First the string has to 17 | be exploded into its component graphemes. Then for each grapheme we have 18 | to map to the equivalent in the other `{locale, number_system}`. Then we 19 | have to reassemble the string. 20 | 21 | Effort is made to short circuit where possible. Transliteration is not 22 | required for any `{locale, number_system}` that is the same as `{"en", 23 | "latn"}` since the implementation uses this combination for the placeholders during 24 | formatting already. When short circuiting is possible (typically the en-* 25 | locales with "latn" number_system - the total number of short circuited 26 | locales is 211 of the 537 in CLDR) the overall number formatting is twice as 27 | fast than when formal transliteration is required. 28 | 29 | ### Configuring precompilation of digit transliterations 30 | 31 | This module includes `Cldr.Number.Transliterate.transliterate_digits/3` which transliterates 32 | digits between number systems. For example from :arabic to :latn. Since generating a 33 | transliteration map is slow, pairs of transliterations can be configured so that the 34 | transliteration map is created at compile time and therefore speeding up transliteration at 35 | run time. 36 | 37 | To configure these transliteration pairs, add the to the `use Cldr` configuration 38 | in a backend module: 39 | 40 | defmodule MyApp.Cldr do 41 | use Cldr, 42 | locale: ["en", "fr", "th"], 43 | default_locale: "en", 44 | precompile_transliterations: [{:latn, :thai}, {:arab, :thai}] 45 | end 46 | 47 | Where each tuple in the list configures one transliteration map. In this example, two maps are 48 | configured: from `:latn` to `:thai` and from `:arab` to `:thai`. 49 | 50 | A list of configurable number systems is returned by `Cldr.Number.System.numeric_systems/0`. 51 | 52 | If a transliteration is requested between two number pairs that have not been configured for 53 | precompilation, a warning is logged. 54 | 55 | """ 56 | end 57 | 58 | alias Cldr.Number.System 59 | alias Cldr.Number.Symbol 60 | alias Cldr.Number.Format.Compiler 61 | alias Cldr.LanguageTag 62 | alias Cldr.Config 63 | 64 | @doc """ 65 | Transliterates from latin digits to another number system's digits. 66 | 67 | Transliterates the latin digits 0..9 to their equivalents in 68 | another number system. Also transliterates the decimal and grouping 69 | separators as well as the plus, minus and exponent symbols. Any other character 70 | in the string will be returned "as is". 71 | 72 | ## Arguments 73 | 74 | * `sequence` is the string to be transliterated. 75 | 76 | * `locale` is any known locale, defaulting to `#{inspect(backend)}.get_locale/0`. 77 | 78 | * `number_system` is any known number system. If expressed as a `string` it 79 | is the actual name of a known number system. If epressed as an `atom` it is 80 | used as a key to look up a number system for the locale (the usual keys are 81 | `:default` and `:native` but :traditional and :finance are also part of the 82 | standard). See `#{inspect(backend)}.Number.System.number_systems_for/1` for a locale to 83 | see what number system types are defined. The default is `:default`. 84 | 85 | For available number systems see `Cldr.Number.System.number_systems/0` 86 | and `#{inspect(backend)}.Number.System.number_systems_for/1`. Also see 87 | `#{inspect(backend)}.Number.Symbol.number_symbols_for/1`. 88 | 89 | 90 | ## Examples 91 | 92 | iex> #{inspect(__MODULE__)}.transliterate("123556") 93 | "123556" 94 | 95 | iex> #{inspect(__MODULE__)}.transliterate("123,556.000", "fr", :default) 96 | "123 556,000" 97 | 98 | iex> #{inspect(__MODULE__)}.transliterate("123556", "th", :default) 99 | "123556" 100 | 101 | iex> #{inspect(__MODULE__)}.transliterate("123556", "th", "thai") 102 | "๑๒๓๕๕๖" 103 | 104 | iex> #{inspect(__MODULE__)}.transliterate("123556", "th", :native) 105 | "๑๒๓๕๕๖" 106 | 107 | iex> #{inspect(__MODULE__)}.transliterate("Some number is: 123556", "th", "thai") 108 | "Some number is: ๑๒๓๕๕๖" 109 | 110 | """ 111 | 112 | @spec transliterate( 113 | String.t(), 114 | LanguageTag.t() | Cldr.Locale.locale_name(), 115 | Cldr.Number.System.system_name() | Cldr.Number.System.types(), 116 | map() | Keyword.t() 117 | ) :: 118 | String.t() | {:error, {module(), String.t()}} 119 | 120 | def transliterate( 121 | sequence, 122 | locale \\ unquote(backend).get_locale(), 123 | number_system \\ System.default_number_system_type(), 124 | options \\ %{} 125 | ) 126 | 127 | def transliterate(sequence, locale, number_system, options) when is_list(options) do 128 | transliterate(sequence, locale, number_system, Map.new(options)) 129 | end 130 | 131 | # No transliteration required when the digits and separators as the same 132 | # as the ones we use in formatting. 133 | with {:ok, systems} <- Config.known_number_systems_like(:en, :latn, config) do 134 | for {locale, system} <- systems do 135 | def transliterate( 136 | sequence, 137 | %LanguageTag{cldr_locale_name: unquote(locale)}, 138 | unquote(system), 139 | options 140 | ) do 141 | sequence 142 | end 143 | end 144 | end 145 | 146 | # We can only transliterate if the target {locale, number_system} has defined 147 | # digits. Some systems don't have digits, just rules. 148 | for {number_system, %{digits: _digits}} <- System.numeric_systems() do 149 | def transliterate(sequence, locale, unquote(number_system), options) do 150 | sequence 151 | |> String.graphemes() 152 | |> Enum.map(&transliterate_char(&1, locale, unquote(number_system), options)) 153 | |> Elixir.List.to_string() 154 | end 155 | end 156 | 157 | # String locale name needs validation 158 | def transliterate(sequence, locale_name, number_system, options) when is_binary(locale_name) do 159 | with {:ok, locale} <- Module.concat(unquote(backend), :Locale).new(locale_name) do 160 | transliterate(sequence, locale, number_system, options) 161 | end 162 | end 163 | 164 | # For when the system name is not known (because its probably a system type 165 | # like :default, or :native) 166 | def transliterate(sequence, locale_name, number_system, options) do 167 | with {:ok, system_name} <- 168 | System.system_name_from(number_system, locale_name, unquote(backend)) do 169 | transliterate(sequence, locale_name, system_name, options) 170 | end 171 | end 172 | 173 | def transliterate!(sequence, locale, number_system, options) do 174 | case transliterate(sequence, locale, number_system, options) do 175 | {:error, {exception, reason}} -> raise exception, reason 176 | string -> string 177 | end 178 | end 179 | 180 | # Functions to transliterate the symbols 181 | for locale_name <- Cldr.Locale.Loader.known_locale_names(config), 182 | {name, symbols} <- Config.number_symbols_for!(locale_name, config), 183 | !is_nil(symbols) do 184 | # Mapping for the grouping separator 185 | defp transliterate_char( 186 | unquote(Compiler.placeholder(:group)), 187 | %LanguageTag{cldr_locale_name: unquote(locale_name)}, 188 | unquote(name), 189 | options 190 | ) do 191 | group_symbols = unquote(Macro.escape(symbols.group)) 192 | group_option = Map.get(options, :separators, :standard) 193 | Map.get(group_symbols, group_option) 194 | end 195 | 196 | # Mapping for the decimal separator 197 | defp transliterate_char( 198 | unquote(Compiler.placeholder(:decimal)), 199 | %LanguageTag{cldr_locale_name: unquote(locale_name)}, 200 | unquote(name), 201 | options 202 | ) do 203 | decimal_symbols = unquote(Macro.escape(symbols.decimal)) 204 | decimal_option = Map.get(options, :separators, :standard) 205 | Map.get(decimal_symbols, decimal_option) 206 | end 207 | 208 | # Mapping for the exponent 209 | defp transliterate_char( 210 | unquote(Compiler.placeholder(:exponent)), 211 | %LanguageTag{cldr_locale_name: unquote(locale_name)}, 212 | unquote(name), 213 | _options 214 | ) do 215 | unquote(symbols.exponential) 216 | end 217 | 218 | # Mapping for the plus sign 219 | defp transliterate_char( 220 | unquote(Compiler.placeholder(:plus)), 221 | %LanguageTag{cldr_locale_name: unquote(locale_name)}, 222 | unquote(name), 223 | _options 224 | ) do 225 | unquote(symbols.plus_sign) 226 | end 227 | 228 | # Mapping for the minus sign 229 | defp transliterate_char( 230 | unquote(Compiler.placeholder(:minus)), 231 | %LanguageTag{cldr_locale_name: unquote(locale_name)}, 232 | unquote(name), 233 | _options 234 | ) do 235 | unquote(symbols.minus_sign) 236 | end 237 | end 238 | 239 | # Functions to transliterate the digits 240 | for {name, %{digits: digits}} <- System.numeric_systems() do 241 | graphemes = String.graphemes(digits) 242 | 243 | for latin_digit <- 0..9 do 244 | grapheme = :lists.nth(latin_digit + 1, graphemes) 245 | latin_char = Integer.to_string(latin_digit) 246 | 247 | defp transliterate_char(unquote(latin_char), _locale, unquote(name), _options) do 248 | unquote(grapheme) 249 | end 250 | end 251 | end 252 | 253 | # Any unknown mapping gets returned as is 254 | defp transliterate_char(digit, _locale, _name, _options) do 255 | digit 256 | end 257 | 258 | @doc """ 259 | Transliterates digits from one number system to another number system 260 | 261 | * `digits` is binary representation of a number 262 | 263 | * `from_system` and `to_system` are number system names in atom form. See 264 | `Cldr.Number.System.numeric_systems/0` for available number systems. 265 | 266 | ## Example 267 | 268 | iex> #{inspect(__MODULE__)}.transliterate_digits "٠١٢٣٤٥٦٧٨٩", :arab, :latn 269 | "0123456789" 270 | 271 | """ 272 | @spec transliterate_digits(String.t(), System.system_name(), System.system_name()) :: 273 | String.t() 274 | 275 | for {from_system, to_system} <- Map.get(config, :precompile_transliterations, []) do 276 | with {:ok, from} = System.number_system_digits(from_system), 277 | {:ok, to} = System.number_system_digits(to_system), 278 | map = System.generate_transliteration_map(from, to) do 279 | def transliterate_digits(digits, unquote(from_system), unquote(to_system)) do 280 | do_transliterate_digits(digits, unquote(Macro.escape(map))) 281 | end 282 | end 283 | end 284 | 285 | def transliterate_digits(digits, from_system, to_system) when is_binary(digits) do 286 | Cldr.Number.Transliterate.transliterate_digits(digits, from_system, to_system) 287 | end 288 | 289 | defp do_transliterate_digits(digits, map) do 290 | digits 291 | |> String.graphemes() 292 | |> Enum.map(&Map.get(map, &1, &1)) 293 | |> Enum.join() 294 | end 295 | end 296 | end 297 | end 298 | end 299 | -------------------------------------------------------------------------------- /lib/cldr/number/format/meta.ex: -------------------------------------------------------------------------------- 1 | defmodule Cldr.Number.Format.Meta do 2 | @moduledoc """ 3 | Describes the metadata that drives 4 | number formatting and provides functions to 5 | update the struct. 6 | 7 | ## Format definition 8 | 9 | The `:format` is a keyword list that with two 10 | elements: 11 | 12 | * `:positive` which is a keyword list for 13 | formatting a number >= zero 14 | 15 | * `:negative` which is a keyword list for 16 | formatting negative number 17 | 18 | There are two formats because we can format in 19 | an accounting style (that is, numbers surrounded 20 | by `()`) or any other arbitrary style. Typically 21 | the format for a negative number is the same as 22 | that for a positive number with a prepended 23 | minus sign. 24 | 25 | ## Localisation of number formatting 26 | 27 | Number formatting is always localised to either 28 | the currency processes locale or a locale 29 | provided as an option to `Cldr.Number.to_string/3`. 30 | 31 | The metadata is independent of the localisation 32 | process. Signs (`+`/`-`), grouping (`,`), decimal markers 33 | (`.`) and exponent signs are all localised when 34 | the number is formatted. 35 | 36 | ## Formatting directives 37 | 38 | The formats - positive and negative - are defined 39 | in the metadata struct, as a keyword list of keywords 40 | and values. 41 | 42 | The simplest formatting list might be: 43 | ``` 44 | [format: _]` 45 | ``` 46 | The `:format` keyword indicates 47 | that this is where the formatting number will be 48 | substituted into the format pattern. 49 | 50 | Another example would be for formatting a negative 51 | number: 52 | ``` 53 | [minus: _, format: _] 54 | ``` 55 | which will format with a localised minus sign 56 | followed by the formatted number. Note that the 57 | keyword value for `:minus` and `:format` are 58 | ignored. 59 | 60 | ## List of formatting keywords 61 | 62 | The following is the full list of formatting 63 | keywords which can be used to format a 64 | number. A `_` in the keyword format is 65 | used to denote `:dont_care`. 66 | 67 | * `[format: _]` inserts the formatted number 68 | exclusive of any sign 69 | 70 | * `[minus: _]` inserts a localised minus 71 | sign 72 | 73 | * `[plus: _]` inserts a localised plus sign 74 | 75 | * `[percent: _]` inserts a localised percent sign 76 | 77 | * `[permille: _]` inserts a localised permille sign 78 | 79 | * `[literal: "string"]` inserts `string` into the 80 | format without any processing 81 | 82 | * `[currency: 1..4]` inserts a localised currency 83 | symbol of the given `type`. A `:currency` must be 84 | provided as an option to `Cldr.Number.Formatter.Decimal.to_string/4`. 85 | 86 | * `[pad: "char"]` inserts the correct number of `char`s 87 | to pad the number format to the width specified by 88 | `:padding_length` in the `%Meta{}` struct. The `:pad` 89 | can be anywhere in the format list but it is most 90 | typically inserted before or after the `:format` 91 | keyword. The assumption is that the `char` is a single 92 | binary character but this is not checked. 93 | 94 | ## Currency symbol formatting 95 | 96 | Currency are localised and have four ways of being 97 | presented. The different types are defined in the 98 | `[currency: type]` keyword where `type` is an integer 99 | in the range `1..4` These types will insert 100 | into the final format: 101 | 102 | 1. The standard currency symbol like `$`,`¥` or `€` 103 | 2. The ISO currency code (like `USD` and `JPY`) 104 | 3. The localised and pluralised currency display name 105 | like "Australian dollar" or "Australian dollars" 106 | 4. The narrow currency symbol if defined for a locale 107 | 108 | """ 109 | defstruct integer_digits: %{max: 0, min: 1}, 110 | fractional_digits: %{max: 0, min: 0}, 111 | significant_digits: %{max: 0, min: 0}, 112 | exponent_digits: 0, 113 | exponent_sign: false, 114 | scientific_rounding: 0, 115 | grouping: %{ 116 | fraction: %{first: 0, rest: 0}, 117 | integer: %{first: 0, rest: 0} 118 | }, 119 | round_nearest: 0, 120 | padding_length: 0, 121 | padding_char: " ", 122 | multiplier: 1, 123 | currency: nil, 124 | format: [ 125 | positive: [format: "#"], 126 | negative: [minus: ~c"-", format: :same_as_positive] 127 | ], 128 | number: 0 129 | 130 | @typedoc "Metadata type that drives how to format a number" 131 | @type t :: %__MODULE__{ 132 | :currency => nil | Cldr.Currency.t(), 133 | :exponent_digits => non_neg_integer(), 134 | :exponent_sign => boolean(), 135 | :format => [{:negative, [any(), ...]} | {:positive, [any(), ...]}, ...], 136 | :fractional_digits => %{:max => non_neg_integer(), :min => non_neg_integer()}, 137 | :grouping => %{ 138 | :fraction => %{:first => non_neg_integer(), :rest => non_neg_integer()}, 139 | :integer => %{:first => non_neg_integer(), :rest => non_neg_integer()} 140 | }, 141 | :integer_digits => %{:max => non_neg_integer(), :min => non_neg_integer()}, 142 | :multiplier => non_neg_integer(), 143 | :number => non_neg_integer(), 144 | :padding_char => String.t(), 145 | :padding_length => non_neg_integer(), 146 | :round_nearest => non_neg_integer(), 147 | :scientific_rounding => non_neg_integer(), 148 | :significant_digits => %{:max => non_neg_integer(), :min => non_neg_integer()} 149 | } 150 | 151 | @doc """ 152 | Returns a new number formatting metadata 153 | struct. 154 | 155 | """ 156 | @spec new :: t() 157 | def new do 158 | %__MODULE__{} 159 | end 160 | 161 | @doc """ 162 | Set the minimum, and optionally maximum, integer digits to 163 | format. 164 | 165 | """ 166 | @spec put_integer_digits(t(), non_neg_integer, non_neg_integer) :: t() 167 | def put_integer_digits(%__MODULE__{} = meta, min, max \\ 0) 168 | when is_integer(min) and is_integer(max) do 169 | meta 170 | |> Map.put(:integer_digits, %{min: min, max: max}) 171 | end 172 | 173 | @doc """ 174 | Set the minimum, and optionally maximum, fractional digits to 175 | format. 176 | 177 | """ 178 | @spec put_fraction_digits(t(), non_neg_integer, non_neg_integer) :: t() 179 | def put_fraction_digits(%__MODULE__{} = meta, min, max \\ 0) 180 | when is_integer(min) and is_integer(max) do 181 | meta 182 | |> Map.put(:fractional_digits, %{min: min, max: max}) 183 | end 184 | 185 | @doc """ 186 | Set the minimum, and optionally maximum, significant digits to 187 | format. 188 | 189 | """ 190 | @spec put_significant_digits(t(), non_neg_integer, non_neg_integer) :: t() 191 | def put_significant_digits(%__MODULE__{} = meta, min, max \\ 0) 192 | when is_integer(min) and is_integer(max) do 193 | meta 194 | |> Map.put(:significant_digits, %{min: min, max: max}) 195 | end 196 | 197 | @doc """ 198 | Set the number of exponent digits to 199 | format. 200 | 201 | """ 202 | @spec put_exponent_digits(t(), non_neg_integer) :: t() 203 | def put_exponent_digits(%__MODULE__{} = meta, digits) when is_integer(digits) do 204 | meta 205 | |> Map.put(:exponent_digits, digits) 206 | end 207 | 208 | @doc """ 209 | Set whether to add the sign of the exponent to 210 | the format. 211 | 212 | """ 213 | @spec put_exponent_sign(t(), boolean) :: t() 214 | def put_exponent_sign(%__MODULE__{} = meta, flag) when is_boolean(flag) do 215 | meta 216 | |> Map.put(:exponent_sign, flag) 217 | end 218 | 219 | @doc """ 220 | Set the increment to which the number should 221 | be rounded. 222 | 223 | """ 224 | @spec put_round_nearest_digits(t(), non_neg_integer) :: t() 225 | def put_round_nearest_digits(%__MODULE__{} = meta, digits) when is_integer(digits) do 226 | meta 227 | |> Map.put(:round_nearest, digits) 228 | end 229 | 230 | @doc """ 231 | Set the number of scientific digits to which the number should 232 | be rounded. 233 | 234 | """ 235 | @spec put_scientific_rounding_digits(t(), non_neg_integer) :: t() 236 | def put_scientific_rounding_digits(%__MODULE__{} = meta, digits) when is_integer(digits) do 237 | meta 238 | |> Map.put(:scientific_rounding, digits) 239 | end 240 | 241 | @spec put_padding_length(t(), non_neg_integer) :: t() 242 | def put_padding_length(%__MODULE__{} = meta, digits) when is_integer(digits) do 243 | meta 244 | |> Map.put(:padding_length, digits) 245 | end 246 | 247 | @doc """ 248 | Set the padding character to be used when 249 | padding the formatted number. 250 | 251 | """ 252 | @spec put_padding_char(t(), String.t()) :: t() 253 | def put_padding_char(%__MODULE__{} = meta, char) when is_binary(char) do 254 | meta 255 | |> Map.put(:padding_char, char) 256 | end 257 | 258 | @doc """ 259 | Sets the multiplier for the number. 260 | 261 | Before formatting, the number is multiplied 262 | by this amount. This is useful when 263 | formatting as a percent or permille. 264 | 265 | """ 266 | @spec put_multiplier(t(), non_neg_integer) :: t() 267 | def put_multiplier(%__MODULE__{} = meta, multiplier) when is_integer(multiplier) do 268 | meta 269 | |> Map.put(:multiplier, multiplier) 270 | end 271 | 272 | @doc """ 273 | Sets the number of digits in a group or 274 | optionally the first group and subsequent 275 | groups for the integer part of a number. 276 | 277 | The grouping character is defined by the locale 278 | defined for the current process or supplied 279 | as the `:locale` option to `to_string/3`. 280 | 281 | """ 282 | @spec put_integer_grouping(t(), non_neg_integer, non_neg_integer) :: t() 283 | def put_integer_grouping(%__MODULE__{} = meta, first, rest) 284 | when is_integer(first) and is_integer(rest) do 285 | grouping = 286 | meta 287 | |> Map.get(:grouping) 288 | |> Map.put(:integer, %{first: first, rest: rest}) 289 | 290 | Map.put(meta, :grouping, grouping) 291 | end 292 | 293 | @spec put_integer_grouping(t(), non_neg_integer) :: t() 294 | def put_integer_grouping(%__MODULE__{} = meta, all) when is_integer(all) do 295 | grouping = 296 | meta 297 | |> Map.get(:grouping) 298 | |> Map.put(:integer, %{first: all, rest: all}) 299 | 300 | Map.put(meta, :grouping, grouping) 301 | end 302 | 303 | @doc """ 304 | Sets the number of digits in a group or 305 | optionally the first group and subsequent 306 | groups for the fractional part of a number. 307 | 308 | The grouping character is defined by the locale 309 | defined for the current process or supplied 310 | as the `:locale` option to `to_string/3`. 311 | 312 | """ 313 | @spec put_fraction_grouping(t(), non_neg_integer, non_neg_integer) :: t() 314 | def put_fraction_grouping(%__MODULE__{} = meta, first, rest) 315 | when is_integer(first) and is_integer(rest) do 316 | grouping = 317 | meta 318 | |> Map.get(:grouping) 319 | |> Map.put(:fraction, %{first: first, rest: rest}) 320 | 321 | Map.put(meta, :grouping, grouping) 322 | end 323 | 324 | @spec put_fraction_grouping(t(), non_neg_integer) :: t() 325 | def put_fraction_grouping(%__MODULE__{} = meta, all) when is_integer(all) do 326 | grouping = 327 | meta 328 | |> Map.get(:grouping) 329 | |> Map.put(:fraction, %{first: all, rest: all}) 330 | 331 | Map.put(meta, :grouping, grouping) 332 | end 333 | 334 | @doc """ 335 | Set the metadata format for the positive 336 | and negative number format. 337 | 338 | Note that this is the parsed format as a simple keyword 339 | list, not a binary representation. 340 | 341 | Its up to each formatting engine to transform its input 342 | into this form. See `Cldr.Number.Format.Meta` module 343 | documentation for the available keywords. 344 | 345 | """ 346 | @spec put_format(t(), Keyword.t(), Keyword.t()) :: t() 347 | def put_format(%__MODULE__{} = meta, positive_format, negative_format) do 348 | meta 349 | |> Map.put(:format, positive: positive_format, negative: negative_format) 350 | end 351 | 352 | @spec put_format(t(), Keyword.t()) :: t() 353 | def put_format(%__MODULE__{} = meta, positive_format) do 354 | put_format(meta, positive_format, minus: ~c"-", format: :same_as_positive) 355 | end 356 | end 357 | -------------------------------------------------------------------------------- /lib/cldr/number/formatter/currency_formatter.ex: -------------------------------------------------------------------------------- 1 | defmodule Cldr.Number.Formatter.Currency do 2 | @moduledoc """ 3 | Number formatter for the `:currency` `:long` format. 4 | 5 | This formatter implements formatting a currency in a long form. This 6 | is not the same as decimal formatting with a currency placeholder. 7 | 8 | To explain the difference, look at the following examples: 9 | 10 | iex> Cldr.Number.to_string 123, TestBackend.Cldr, format: :currency, currency: "USD" 11 | {:ok, "$123.00"} 12 | 13 | iex> Cldr.Number.to_string 123, TestBackend.Cldr, format: :long, currency: "USD" 14 | {:ok, "123 US dollars"} 15 | 16 | In the first example the format is defined by a decimal mask. In this example 17 | the format mask comes from: 18 | 19 | iex> {:ok, formats} = Cldr.Number.Format.all_formats_for("en", TestBackend.Cldr) 20 | ...> formats.latn.currency 21 | "¤#,##0.00" 22 | 23 | In the second example we are using a format that combines the number with 24 | a language translation of the currency name. In this example the format 25 | comes from: 26 | 27 | iex> {:ok, formats} = Cldr.Number.Format.all_formats_for("en", TestBackend.Cldr) 28 | ...> formats.latn.currency_long 29 | %{one: [0, " ", 1], other: [0, " ", 1]} 30 | 31 | Where "{0}" is replaced with the number formatted using the `:standard` 32 | decimal format and "{1} is replaced with locale-specific name of the 33 | currency adjusted for the locales plural rules." 34 | 35 | **This module is not part of the public API and is subject 36 | to change at any time.** 37 | 38 | """ 39 | 40 | alias Cldr.Number.{Format, System} 41 | alias Cldr.{Substitution, Currency} 42 | alias Cldr.Number.Format.Options 43 | alias Cldr.Number.Formatter 44 | 45 | import DigitalToken, only: [is_digital_token: 1] 46 | 47 | @doc false 48 | def to_string(number, _format, _backend, _options) when is_binary(number) do 49 | {:error, 50 | { 51 | ArgumentError, 52 | "Not a number: #{inspect(number)}. Currency long formats only support number or Decimal arguments" 53 | }} 54 | end 55 | 56 | # The format :currency_long_with_symbol is a composition of :currency_long 57 | # and the default :currency format. It is a derived format, not one 58 | # defined by CLDR. 59 | 60 | def to_string(number, :currency_long_with_symbol, backend, options) do 61 | decimal_options = decimal_options(options, backend, number) 62 | decimal_format = decimal_options.format 63 | 64 | number 65 | |> Cldr.Number.to_string!(backend, long_options(options)) 66 | |> Formatter.Decimal.to_string(decimal_format, backend, decimal_options) 67 | end 68 | 69 | def to_string(number, :currency_long, backend, options) do 70 | locale = options.locale 71 | number_system = System.system_name_from!(options.number_system, locale, backend) 72 | cardinal = Module.concat(backend, Number.Cardinal) 73 | 74 | if !(formats = Format.formats_for!(locale, number_system, backend).currency_long) do 75 | raise ArgumentError, 76 | message: 77 | "No :currency_long format known for " <> 78 | "locale #{inspect(locale)} and number system #{inspect(number_system)}." 79 | end 80 | 81 | options = 82 | options 83 | |> Map.put(:format, :standard) 84 | |> set_fractional_digits(options.currency, options.fractional_digits) 85 | |> Options.resolve_standard_format(backend) 86 | 87 | currency_string = currency_string(number, options.currency, cardinal, locale, backend) 88 | number_string = Cldr.Number.to_string!(number, backend, options) 89 | format = cardinal.pluralize(number, locale, formats) 90 | 91 | Substitution.substitute([number_string, currency_string], format) 92 | |> :erlang.iolist_to_binary() 93 | end 94 | 95 | defp currency_string(number, %Cldr.Currency{} = currency, cardinal, locale, _backend) do 96 | cardinal.pluralize(number, locale, currency.count) 97 | end 98 | 99 | defp currency_string(_number, currency, _cardinal, _locale, _backend) 100 | when is_digital_token(currency) do 101 | {:ok, currency_string} = DigitalToken.long_name(currency) 102 | currency_string 103 | end 104 | 105 | defp set_fractional_digits(options, %Cldr.Currency{}, nil) do 106 | Map.put(options, :fractional_digits, 0) 107 | end 108 | 109 | defp set_fractional_digits(options, _currency, _digits) do 110 | options 111 | end 112 | 113 | defp long_options(options) do 114 | options 115 | |> Map.put(:format, :decimal_long) 116 | |> Map.put(:currency, nil) 117 | end 118 | 119 | defp decimal_options(options, backend, number) do 120 | currency_format = Currency.currency_format_from_locale(options.locale) 121 | options = Map.put(options, :format, currency_format) 122 | 123 | options 124 | |> Options.resolve_standard_format(backend) 125 | |> Options.maybe_expand_currency_symbol(number) 126 | end 127 | end 128 | -------------------------------------------------------------------------------- /lib/cldr/number/formatter/short_formatter.ex: -------------------------------------------------------------------------------- 1 | defmodule Cldr.Number.Formatter.Short do 2 | @moduledoc """ 3 | Formats a number according to the locale-specific `:short` formats 4 | 5 | This is best explained by some 6 | examples: 7 | 8 | iex> Cldr.Number.to_string 123, TestBackend.Cldr, format: :short 9 | {:ok, "123"} 10 | 11 | iex> Cldr.Number.to_string 1234, TestBackend.Cldr, format: :short 12 | {:ok, "1K"} 13 | 14 | iex> Cldr.Number.to_string 523456789, TestBackend.Cldr, format: :short 15 | {:ok, "523M"} 16 | 17 | iex> Cldr.Number.to_string 7234567890, TestBackend.Cldr, format: :short 18 | {:ok, "7B"} 19 | 20 | iex> Cldr.Number.to_string 7234567890, TestBackend.Cldr, format: :long 21 | {:ok, "7 billion"} 22 | 23 | These formats are compact representations however they do lose 24 | precision in the presentation in favour of human readability. 25 | 26 | Note that for a `:currency` short format the number of decimal places 27 | is retrieved from the currency definition itself. You can see the difference 28 | in the following examples: 29 | 30 | iex> Cldr.Number.to_string 1234, TestBackend.Cldr, format: :short, currency: "EUR" 31 | {:ok, "€1K"} 32 | 33 | iex> Cldr.Number.to_string 1234, TestBackend.Cldr, format: :short, currency: "EUR", fractional_digits: 2 34 | {:ok, "€1.23K"} 35 | 36 | iex> Cldr.Number.to_string 1234, TestBackend.Cldr, format: :short, currency: "JPY" 37 | {:ok, "¥1K"} 38 | 39 | **This module is not part of the public API and is subject 40 | to change at any time.** 41 | 42 | """ 43 | 44 | alias Cldr.Math 45 | alias Cldr.Number.{System, Format, Formatter} 46 | alias Cldr.Locale 47 | alias Cldr.LanguageTag 48 | alias Cldr.Number.Format.Options 49 | 50 | # Notes from Unicode TR35 on formatting short formats: 51 | # 52 | # To format a number N, the greatest type less than or equal to N is 53 | # used, with the appropriate plural category. N is divided by the type, after 54 | # removing the number of zeros in the pattern, less 1. APIs supporting this 55 | # format should provide control over the number of significant or fraction 56 | # digits. 57 | # 58 | # If the value is precisely 0, or if the type is less than 1000, then the 59 | # normal number format pattern for that sort of object is supplied. For 60 | # example, formatting 1200 would result in “$1.2K”, while 990 would result in 61 | # simply “$990”. 62 | # 63 | # Thus N=12345 matches 00 K . N 64 | # is divided by 1000 (obtained from 10000 after removing "00" and restoring one 65 | # "0". The result is formatted according to the normal decimal pattern. With no 66 | # fractional digits, that yields "12 K". 67 | 68 | @spec to_string(Math.number_or_decimal(), atom(), Cldr.backend(), Options.t()) :: 69 | {:ok, String.t()} | {:error, {module(), String.t()}} 70 | 71 | def to_string(number, _style, _backend, _options) when is_binary(number) do 72 | {:error, 73 | { 74 | ArgumentError, 75 | "Not a number: #{inspect(number)}. Long and short formats only support number or Decimal arguments" 76 | }} 77 | end 78 | 79 | def to_string(number, style, backend, options) do 80 | locale = options.locale || backend.default_locale() 81 | 82 | with {:ok, locale} <- Cldr.validate_locale(locale, backend), 83 | {:ok, number_system} <- System.system_name_from(options.number_system, locale, backend) do 84 | short_format_string(number, style, locale, number_system, backend, options) 85 | end 86 | end 87 | 88 | @spec short_format_string( 89 | Math.number_or_decimal(), 90 | atom, 91 | Locale.locale_name() | LanguageTag.t(), 92 | System.system_name(), 93 | Cldr.backend(), 94 | Options.t() 95 | ) :: {:ok, String.t()} | {:error, {module(), String.t()}} 96 | 97 | defp short_format_string(number, style, locale, number_system, backend, options) do 98 | format_rules = 99 | locale 100 | |> Format.formats_for!(number_system, backend) 101 | |> Map.fetch!(style) 102 | 103 | {normalized_number, format} = choose_short_format(number, format_rules, options, backend) 104 | 105 | options = 106 | options 107 | |> digits(options.fractional_digits) 108 | |> Map.put(:format, format) 109 | |> Options.maybe_expand_currency_symbol(number) 110 | 111 | Formatter.Decimal.to_string(normalized_number, format, backend, options) 112 | end 113 | 114 | @doc """ 115 | Returns the exponent that will be applied 116 | when formatting the given number as a short 117 | format. 118 | 119 | This function is primarily intended to support 120 | pluralization for compact numbers (numbers 121 | formatted with the `format: :short` option) since 122 | some languages pluralize compact numbers differently 123 | to a fully expressed number. 124 | 125 | Such rules are defined for the locale "fr" from 126 | CLDR version 38 with the intention that additional 127 | rules will be added in later versions. 128 | 129 | ## Examples 130 | 131 | iex> Cldr.Number.Formatter.Short.short_format_exponent 1234 132 | {1000, 1} 133 | 134 | iex> Cldr.Number.Formatter.Short.short_format_exponent 12345 135 | {10000, 2} 136 | 137 | iex> Cldr.Number.Formatter.Short.short_format_exponent 123456789 138 | {100000000, 3} 139 | 140 | iex> Cldr.Number.Formatter.Short.short_format_exponent 123456789, locale: "th" 141 | {100000000, 3} 142 | 143 | """ 144 | def short_format_exponent(number, options \\ []) when is_list(options) do 145 | with {locale, backend} = Cldr.locale_and_backend_from(options), 146 | number_system = Keyword.get(options, :number_system, :default), 147 | {:ok, number_system} <- System.system_name_from(number_system, locale, backend), 148 | {:ok, all_formats} <- Format.formats_for(locale, number_system, backend) do 149 | formats = Map.fetch!(all_formats, :decimal_short) 150 | pluralizer = Module.concat(backend, Number.Cardinal) 151 | 152 | options = 153 | options 154 | |> Map.new() 155 | |> Map.put_new(:locale, locale) 156 | |> Map.put_new(:number_system, number_system) 157 | |> Map.put_new(:currency, nil) 158 | 159 | case get_short_format_rule(number, formats, options, backend) do 160 | [range, plural_selectors] -> 161 | normalized_number = normalise_number(number, range, plural_selectors.other) 162 | plural_key = pluralization_key(normalized_number, options) 163 | 164 | [_format, number_of_zeros] = 165 | pluralizer.pluralize(plural_key, options.locale, plural_selectors) 166 | 167 | {range, number_of_zeros} 168 | 169 | {number, _format} -> 170 | {number, 0} 171 | end 172 | end 173 | end 174 | 175 | # For short formats the fractional digits should be 0 unless otherwise specified, 176 | # even for currencies 177 | defp digits(options, nil) do 178 | Map.put(options, :fractional_digits, 0) 179 | end 180 | 181 | defp digits(options, _digits) do 182 | options 183 | end 184 | 185 | defp choose_short_format(number, format_rules, options, backend) 186 | when is_number(number) and number < 0 do 187 | {number, format} = choose_short_format(abs(number), format_rules, options, backend) 188 | {number * -1, format} 189 | end 190 | 191 | defp choose_short_format(%Decimal{sign: -1 = sign} = number, format_rules, options, backend) do 192 | {normalized_number, format} = 193 | choose_short_format(Decimal.abs(number), format_rules, options, backend) 194 | 195 | {Decimal.mult(normalized_number, sign), format} 196 | end 197 | 198 | defp choose_short_format(number, format_rules, options, backend) do 199 | pluralizer = Module.concat(backend, Number.Cardinal) 200 | 201 | case get_short_format_rule(number, format_rules, options, backend) do 202 | # Its a short format 203 | [range, plural_selectors] -> 204 | normalized_number = normalise_number(number, range, plural_selectors.other) 205 | plural_key = pluralization_key(normalized_number, options) 206 | 207 | [format, _number_of_zeros] = 208 | pluralizer.pluralize(plural_key, options.locale, plural_selectors) 209 | 210 | {normalized_number, format} 211 | 212 | # Its a standard format 213 | {number, format} -> 214 | {number, format} 215 | end 216 | end 217 | 218 | defp get_short_format_rule(number, _format_rules, options, backend) 219 | when is_number(number) and number < 1000 do 220 | format = 221 | options.locale 222 | |> Format.formats_for!(options.number_system, backend) 223 | |> Map.get(standard_or_currency(options)) 224 | 225 | {number, format} 226 | end 227 | 228 | defp get_short_format_rule(number, format_rules, options, backend) when is_number(number) do 229 | format_rules 230 | |> Enum.filter(fn [range, _rules] -> range <= number end) 231 | |> Enum.reverse() 232 | |> hd 233 | |> maybe_get_default_format(number, options, backend) 234 | end 235 | 236 | defp get_short_format_rule(%Decimal{} = number, format_rules, options, backend) do 237 | rule = 238 | number 239 | |> Decimal.round(0, :floor) 240 | |> Decimal.to_integer() 241 | |> get_short_format_rule(format_rules, options, backend) 242 | 243 | case rule do 244 | {_ignore, format} -> {number, format} 245 | rule -> rule 246 | end 247 | end 248 | 249 | defp maybe_get_default_format([_range, %{other: ["0", _]}], number, options, backend) do 250 | {_, format} = get_short_format_rule(0, [], options, backend) 251 | {number, format} 252 | end 253 | 254 | defp maybe_get_default_format(rule, _number, _options, _backend) do 255 | rule 256 | end 257 | 258 | defp standard_or_currency(options) do 259 | if options.currency do 260 | :currency 261 | else 262 | :standard 263 | end 264 | end 265 | 266 | @one_thousand Decimal.new(1000) 267 | defp normalise_number(%Decimal{} = number, range, number_of_zeros) do 268 | if Cldr.Decimal.compare(number, @one_thousand) == :lt do 269 | number 270 | else 271 | Decimal.div(number, Decimal.new(adjustment(range, number_of_zeros))) 272 | end 273 | end 274 | 275 | defp normalise_number(number, _range, _number_of_zeros) when number < 1000 do 276 | number 277 | end 278 | 279 | defp normalise_number(number, _range, ["0", _number_of_zeros]) do 280 | number 281 | end 282 | 283 | defp normalise_number(number, range, [_format, number_of_zeros]) do 284 | number / adjustment(range, number_of_zeros) 285 | end 286 | 287 | # TODO: We can precompute these at compile time which would 288 | # save this lookup 289 | defp adjustment(range, number_of_zeros) when is_integer(number_of_zeros) do 290 | (range / Math.power_of_10(number_of_zeros - 1)) 291 | |> trunc 292 | end 293 | 294 | defp adjustment(range, [_, number_of_zeros]) when is_integer(number_of_zeros) do 295 | adjustment(range, number_of_zeros) 296 | end 297 | 298 | # The pluralization key has to consider when there is an 299 | # exact match and when the number would be rounded up. When 300 | # rounded up it also has to not be an exact match. 301 | defp pluralization_key(number, options) when is_number(number) do 302 | rounding_mode = Map.get_lazy(options, :rounding_mode, &Cldr.Math.default_rounding_mode/0) 303 | 304 | if (rounded = Cldr.Math.round(number, 0, rounding_mode)) <= number do 305 | # Rounded number <= number means that the 306 | # pluralization key is the same integer part 307 | # so no issue 308 | number 309 | else 310 | # The rounded number is greater than the normalized 311 | # number so the plural key is different but not exactly 312 | # equal so we add an offset so pluralization works 313 | # correctly (we don't want to trigger an exact match; 314 | # although this relies on exact matches always being integers 315 | # which as of CLDR39 they are). 316 | rounded + 0.1 317 | end 318 | end 319 | 320 | defp pluralization_key(%Decimal{} = number, options) do 321 | number 322 | |> Decimal.to_float() 323 | |> pluralization_key(options) 324 | end 325 | end 326 | -------------------------------------------------------------------------------- /lib/cldr/number/rbnf.ex: -------------------------------------------------------------------------------- 1 | defmodule Cldr.Rbnf do 2 | @moduledoc """ 3 | Functions to implement Rules Based Number Formatting (rbnf) 4 | 5 | During compilation RBNF rules are extracted and generated 6 | as function bodies by `Cldr.Rbnf.Ordinal`, `Cldr.Rbnf.Cardinal` 7 | and `Cldr.Rbnf.NumberSystem`. 8 | 9 | The functions in this module would not normally be of common 10 | use outside of supporting the compilation phase. 11 | """ 12 | 13 | alias Cldr.LanguageTag 14 | 15 | @doc """ 16 | Returns the list of locales that that have RBNF defined 17 | 18 | This list is the set of known locales for which 19 | there are rbnf rules defined. 20 | 21 | Delegates to `Cldr.known_rbnf_locale_names/1` 22 | 23 | """ 24 | defdelegate known_locale_names(backend), to: Cldr, as: :known_rbnf_locale_names 25 | 26 | @categories [:NumberSystem, :Spellout, :Ordinal] 27 | 28 | @doc """ 29 | Returns the list of RBNF rules for a locale. 30 | 31 | A rule name can be used as the `:format` parameter 32 | in `Cldr.Number.to_string/3`. 33 | 34 | ## Arguments 35 | 36 | * `locale` is any `Cldr.LanguageTag.t()` 37 | 38 | ## Returns 39 | 40 | * `{:ok, [list_of_rule_names_as_atoms]}` or 41 | 42 | * `{:error, {exception, reason}}` 43 | 44 | ## Examples 45 | 46 | iex> Cldr.Rbnf.rule_names_for_locale "zh" 47 | {:ok, 48 | [:spellout_cardinal_alternate2, :spellout_ordinal, :spellout_cardinal, 49 | :spellout_cardinal_financial, :spellout_numbering, :spellout_numbering_days, 50 | :spellout_numbering_year, :digits_ordinal]} 51 | 52 | iex> Cldr.Rbnf.rule_names_for_locale "fp" 53 | {:error, {Cldr.InvalidLanguageError, "The language \"fp\" is invalid"}} 54 | 55 | """ 56 | @spec rule_names_for_locale(Cldr.LanguageTag.t()) :: 57 | {:ok, list(atom())} | {:error, {module(), String.t()}} 58 | 59 | def rule_names_for_locale(%LanguageTag{rbnf_locale_name: nil} = language_tag) do 60 | {:error, rbnf_locale_error(language_tag)} 61 | end 62 | 63 | def rule_names_for_locale(%LanguageTag{rbnf_locale_name: rbnf_locale_name, backend: backend}) do 64 | rule_names = 65 | Enum.flat_map(@categories, fn category -> 66 | rbnf_module = Module.concat([backend, :Rbnf, category]) 67 | rule_names = rbnf_module.rule_sets(rbnf_locale_name) 68 | if rule_names, do: rule_names, else: [] 69 | end) 70 | 71 | {:ok, rule_names} 72 | end 73 | 74 | def rule_names_for_locale(locale_name, backend \\ Cldr.default_backend!()) 75 | when is_binary(locale_name) or is_atom(locale_name) do 76 | with {:ok, locale} <- Cldr.Locale.canonical_language_tag(locale_name, backend) do 77 | rule_names_for_locale(locale) 78 | end 79 | end 80 | 81 | @doc """ 82 | Returns the list of RBNF rules for a locale. 83 | 84 | A rule name can be used as the `:format` parameter 85 | in `Cldr.Number.to_string/3`. 86 | 87 | ## Arguments 88 | 89 | * `locale` is any `Cldr.LanguageTag.t()` 90 | 91 | ## Returns 92 | 93 | * `[list_of_rule_names_as_atoms]`, or 94 | 95 | * raises an exception 96 | 97 | ## Examples 98 | 99 | iex> Cldr.Rbnf.rule_names_for_locale! "zh" 100 | [:spellout_cardinal_alternate2, :spellout_ordinal, :spellout_cardinal, 101 | :spellout_cardinal_financial, :spellout_numbering, :spellout_numbering_days, 102 | :spellout_numbering_year, :digits_ordinal] 103 | 104 | """ 105 | @spec rule_names_for_locale!(Cldr.LanguageTag.t()) :: list(atom()) | no_return() 106 | 107 | def rule_names_for_locale!(locale) do 108 | case rule_names_for_locale(locale) do 109 | {:ok, rule_names} -> rule_names 110 | {:error, {exception, reason}} -> raise exception, reason 111 | end 112 | end 113 | 114 | @doc """ 115 | Returns {:ok, rbnf_rules} for a `locale` or `{:error, {Cldr.NoRbnf, info}}` 116 | 117 | * `locale` is any `t:Cldr.LanguageTag` 118 | 119 | This function reads the raw locale definition and therefore 120 | should *not* be called at runtime. 121 | 122 | """ 123 | @spec for_locale(LanguageTag.t()) :: 124 | {:ok, map()} | {:error, {module(), String.t()}} 125 | 126 | def for_locale(%LanguageTag{rbnf_locale_name: nil} = language_tag) do 127 | {:error, rbnf_locale_error(language_tag)} 128 | end 129 | 130 | def for_locale(%LanguageTag{rbnf_locale_name: rbnf_locale_name, backend: backend}) do 131 | rbnf_data = 132 | rbnf_locale_name 133 | |> Cldr.Locale.Loader.get_locale(backend) 134 | |> Map.get(:rbnf) 135 | 136 | {:ok, rbnf_data} 137 | end 138 | 139 | @doc """ 140 | Returns {:ok, rbnf_rules} for a `locale` or `{:error, {Cldr.NoRbnf, info}}` 141 | 142 | * `locale` is any locale name returned by `Cldr.Rbnf.known_locale_names/1` 143 | 144 | * `backend` is any module that includes `use Cldr` and therefore 145 | is a `Cldr` backend module 146 | 147 | """ 148 | @spec for_locale(Cldr.Locale.locale_name() | LanguageTag.t(), Cldr.backend()) :: 149 | {:ok, map()} | {:error, {module(), String.t()}} 150 | 151 | def for_locale(locale, backend) do 152 | with {:ok, language_tag} <- Cldr.Locale.canonical_language_tag(locale, backend) do 153 | for_locale(language_tag) 154 | end 155 | end 156 | 157 | @doc """ 158 | Returns rbnf_rules for a `locale` or raises an exception if 159 | there are no rules. 160 | 161 | * `locale` is any `Cldr.LanguageTag` 162 | 163 | * `backend` is any module that includes `use Cldr` and therefore 164 | is a `Cldr` backend module 165 | 166 | """ 167 | def for_locale!(%LanguageTag{} = locale) do 168 | case for_locale(locale) do 169 | {:ok, rules} -> rules 170 | {:error, {exception, reason}} -> raise exception, reason 171 | end 172 | end 173 | 174 | @doc """ 175 | Returns rbnf_rules for a `locale` and `backend` or raises an exception if 176 | there are no rules. 177 | 178 | * `locale` is any locale name returned by `Cldr.Rbnf.known_locale_names/1` 179 | 180 | * `backend` is any module that includes `use Cldr` and therefore 181 | is a `Cldr` backend module 182 | 183 | """ 184 | def for_locale!(locale, backend) when is_atom(backend) do 185 | case for_locale(locale, backend) do 186 | {:ok, rules} -> rules 187 | {:error, {exception, reason}} -> raise exception, reason 188 | end 189 | end 190 | 191 | @doc false 192 | def categories_for_locale!(%LanguageTag{} = locale) do 193 | Enum.reduce(@categories, [], fn category, acc -> 194 | rbnf_module = Module.concat([locale.backend, :Rbnf, category]) 195 | 196 | case rbnf_module.rule_sets(locale) do 197 | nil -> acc 198 | _rules -> [category | acc] 199 | end 200 | end) 201 | end 202 | 203 | # Returns a map that merges all rules by the primary dimension of 204 | # RuleGroup, within which rbnf rules are keyed by locale. 205 | # 206 | # This function is primarily intended to support compile-time generation 207 | # of functions to process rbnf rules. 208 | @doc false 209 | @spec for_all_locales(Cldr.backend()) :: %{} 210 | def for_all_locales(backend) do 211 | config = Module.get_attribute(backend, :config) 212 | known_rbnf_locale_names = Cldr.Locale.Loader.known_rbnf_locale_names(config) 213 | 214 | Enum.map(known_rbnf_locale_names, fn locale_name -> 215 | locale = 216 | locale_name 217 | |> Cldr.Locale.Loader.get_locale(config) 218 | |> Map.get(:rbnf) 219 | 220 | Enum.map(locale, fn {group, sets} -> 221 | {group, %{locale_name => sets}} 222 | end) 223 | |> Enum.into(%{}) 224 | end) 225 | |> Cldr.Map.merge_map_list() 226 | end 227 | 228 | def rbnf_locale_error(%LanguageTag{} = locale) do 229 | {Cldr.Rbnf.NotAvailable, "RBNF is not available for locale #{inspect(locale)}"} 230 | end 231 | 232 | def rbnf_rule_error(%LanguageTag{} = locale, format) do 233 | { 234 | Cldr.Rbnf.NoRule, 235 | "RBNF rule #{inspect(format)} is unknown to locale #{inspect(locale)}" 236 | } 237 | end 238 | 239 | # Function names need to start with a letter. At least one 240 | # rule in CLDR does not (2d_year). We prepend an "r" to the 241 | # rule. This is also done in rbnf_parse.yrl so the strategies 242 | # need to match. 243 | 244 | @doc false 245 | def force_valid_function_name(rule_group) do 246 | function = to_string(rule_group) 247 | 248 | case function do 249 | <> when digit in ?0..?9 -> 250 | "r" <> function 251 | 252 | _other -> 253 | function 254 | end 255 | |> String.to_atom() 256 | end 257 | 258 | if Mix.env() == :test do 259 | # Returns all the rules in rbnf without any tagging for rulegroup or set. 260 | # This is helpful for testing only. 261 | @doc false 262 | def all_rules(backend) do 263 | # Get sets from groups 264 | # Get rules from set 265 | # Get the list of rules 266 | known_locale_names(backend) 267 | |> Enum.map(&Cldr.Locale.new!(&1, backend)) 268 | |> Enum.map(&for_locale!(&1)) 269 | |> Enum.flat_map(&Map.values/1) 270 | |> Enum.flat_map(&Map.values/1) 271 | |> Enum.flat_map(& &1.rules) 272 | end 273 | 274 | # Returns a list of unique rule definitions. Used for testing. 275 | @doc false 276 | def all_rule_definitions(backend) do 277 | all_rules(backend) 278 | |> Enum.map(& &1.definition) 279 | |> Enum.uniq() 280 | end 281 | end 282 | end 283 | -------------------------------------------------------------------------------- /lib/cldr/number/rbnf/rule.ex: -------------------------------------------------------------------------------- 1 | defmodule Cldr.Rbnf.Rule do 2 | @moduledoc """ 3 | Tokenizer and Parser for RBNF rules. 4 | 5 | """ 6 | defstruct [:base_value, :radix, :definition, :range, :divisor] 7 | alias Cldr.Rbnf.Rule 8 | 9 | @doc """ 10 | Scan and tokenize rule definition 11 | 12 | Using a leex lexer, tokenize a rule definition 13 | """ 14 | def tokenize(definition) when is_binary(definition) do 15 | definition 16 | |> String.trim_leading("'") 17 | |> String.to_charlist() 18 | |> :rbnf_lexer.string() 19 | end 20 | 21 | def tokenize(%Rule{definition: definition} = _rule) do 22 | tokenize(definition) 23 | end 24 | 25 | @doc """ 26 | Parse an RBNF rule definition 27 | 28 | Returns a list of rule subparts that can then be used for 29 | further processing or for turning into an AST for execution. 30 | """ 31 | def parse(definition) when is_binary(definition) do 32 | {:ok, tokens, _end_line} = tokenize(definition) 33 | parse(tokens) 34 | end 35 | 36 | def parse([]) do 37 | {:ok, []} 38 | end 39 | 40 | def parse(tokens) when is_list(tokens) do 41 | tokens 42 | |> :rbnf_parser.parse() 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /lib/cldr/number/string.ex: -------------------------------------------------------------------------------- 1 | defmodule Cldr.Number.String do 2 | @moduledoc false 3 | 4 | @doc """ 5 | Returns a regex which matches all latin1 characters. 6 | 7 | """ 8 | @latin1 "([\\x00-\\x7F])" 9 | def latin1 do 10 | ~r/#{@latin1}/ 11 | end 12 | 13 | @doc """ 14 | Returns a regex which matches all non-latin1 characters. 15 | 16 | """ 17 | @not_latin1 "([^\\x00-\\x7F])" 18 | def not_latin1 do 19 | ~r/#{@not_latin1}/ 20 | end 21 | 22 | @doc """ 23 | Replaces characters with a string hex representation. 24 | """ 25 | def hex_string(string) do 26 | String.to_charlist(string) 27 | |> Enum.map(&("\\x" <> Integer.to_string(&1))) 28 | |> Enum.join() 29 | end 30 | 31 | @doc """ 32 | Pad a a string (representing a number) with leading "0"'s to the 33 | specified length. 34 | 35 | ## Options 36 | 37 | * `number` is a string representation of a number. 38 | 39 | * `count` is the final length required of the string. 40 | 41 | """ 42 | @spec pad_leading_zeros(String.t(), integer) :: String.t() 43 | def pad_leading_zeros(number_string, count) when count <= 0 do 44 | number_string 45 | end 46 | 47 | def pad_leading_zeros(number_string, count) do 48 | :binary.copy("0", count - byte_size(number_string)) <> number_string 49 | end 50 | 51 | @doc """ 52 | Pad a a string (representing a number) with trailing "0"'s to the 53 | specified length. 54 | 55 | ## Options 56 | 57 | * `number` is a string representation of a number. 58 | 59 | * `count` is the final length required of the string. 60 | 61 | """ 62 | @spec pad_trailing_zeros(String.t(), integer) :: String.t() 63 | def pad_trailing_zeros(number_string, count) when count <= 0 do 64 | number_string 65 | end 66 | 67 | def pad_trailing_zeros(number_string, count) do 68 | number_string <> :binary.copy("0", count - byte_size(number_string)) 69 | end 70 | 71 | @doc """ 72 | Split a string up into fixed size chunks. 73 | 74 | Returns a list of strings the size of `size` plus potentially 75 | one more chunk at the end that is the remainder of the string 76 | after chunking. 77 | 78 | ## Examples 79 | 80 | iex> Cldr.Number.String.chunk_string("This is a string", 3) 81 | ["Thi", "s i", "s a", " st", "rin", "g"] 82 | 83 | iex> Cldr.Number.String.chunk_string("1234", 4) 84 | ["1234"] 85 | 86 | iex> Cldr.Number.String.chunk_string("1234", 3) 87 | ["123","4"] 88 | 89 | iex> Cldr.Number.String.chunk_string("1234", 3, :reverse) 90 | ["1", "234"] 91 | 92 | """ 93 | @spec chunk_string(String.t(), integer, :forward | :reverse) :: [String.t()] 94 | def chunk_string(string, size, direction \\ :forward) 95 | 96 | def chunk_string(string, 0, _direction) do 97 | [string] 98 | end 99 | 100 | def chunk_string("", _size, _) do 101 | [""] 102 | end 103 | 104 | if Version.compare(System.version(), "1.6.0") in [:gt, :eq] do 105 | def chunk_string(string, size, :forward) do 106 | string 107 | |> String.to_charlist() 108 | |> Enum.chunk_every(size, size, []) 109 | |> Enum.map(&List.to_string/1) 110 | end 111 | else 112 | def chunk_string(string, size, :forward) do 113 | string 114 | |> String.to_charlist() 115 | |> Enum.chunk(size, size, []) 116 | |> Enum.map(&List.to_string/1) 117 | end 118 | end 119 | 120 | def chunk_string(string, size, :reverse) do 121 | len = String.length(string) 122 | remainder = rem(len, size) 123 | 124 | if remainder > 0 do 125 | {head, last} = String.split_at(string, remainder) 126 | [head] ++ do_chunk_string(last, size) 127 | else 128 | do_chunk_string(string, size) 129 | end 130 | end 131 | 132 | defp do_chunk_string("", _size) do 133 | [] 134 | end 135 | 136 | defp do_chunk_string(string, size) do 137 | {chunk, rest} = String.split_at(string, size) 138 | [chunk] ++ do_chunk_string(rest, size) 139 | end 140 | end 141 | -------------------------------------------------------------------------------- /lib/cldr/number/symbol.ex: -------------------------------------------------------------------------------- 1 | defmodule Cldr.Number.Symbol do 2 | @moduledoc """ 3 | Functions to manage the symbol definitions for a locale and 4 | number system. 5 | 6 | """ 7 | 8 | alias Cldr.Locale 9 | alias Cldr.LanguageTag 10 | alias Cldr.Number.System 11 | 12 | defstruct [ 13 | :decimal, 14 | :group, 15 | :exponential, 16 | :infinity, 17 | :list, 18 | :minus_sign, 19 | :nan, 20 | :per_mille, 21 | :percent_sign, 22 | :plus_sign, 23 | :superscripting_exponent, 24 | :time_separator 25 | ] 26 | 27 | @type t :: %__MODULE__{ 28 | decimal: String.t(), 29 | group: String.t(), 30 | exponential: String.t(), 31 | infinity: String.t(), 32 | list: String.t(), 33 | minus_sign: String.t(), 34 | nan: String.t(), 35 | per_mille: String.t(), 36 | percent_sign: String.t(), 37 | plus_sign: String.t(), 38 | superscripting_exponent: String.t(), 39 | time_separator: String.t() 40 | } 41 | 42 | @doc """ 43 | Returns a map of `Cldr.Number.Symbol.t` structs of the number symbols for each 44 | of the number systems of a locale. 45 | 46 | ## Arguments 47 | 48 | * `locale` is any valid locale name returned by `Cldr.known_locale_names/1` 49 | or a `Cldr.LanguageTag` struct returned by `Cldr.Locale.new!/2`. The 50 | default is `Cldr.get_locale/1`. 51 | 52 | * `backend` is any module that includes `use Cldr` and therefore 53 | is a `Cldr` backend module 54 | 55 | ## Example: 56 | 57 | iex> Cldr.Number.Symbol.number_symbols_for("th", TestBackend.Cldr) 58 | { 59 | :ok, 60 | %{ 61 | latn: %Cldr.Number.Symbol{ 62 | decimal: %{standard: "."}, 63 | exponential: "E", 64 | group: %{standard: ","}, 65 | infinity: "∞", 66 | list: ";", 67 | minus_sign: "-", 68 | nan: "NaN", 69 | per_mille: "‰", 70 | percent_sign: "%", 71 | plus_sign: "+", 72 | superscripting_exponent: "×", 73 | time_separator: ":" 74 | }, 75 | thai: %Cldr.Number.Symbol{ 76 | decimal: %{standard: "."}, 77 | exponential: "E", 78 | group: %{standard: ","}, 79 | infinity: "∞", 80 | list: ";", 81 | minus_sign: "-", 82 | nan: "NaN", 83 | per_mille: "‰", 84 | percent_sign: "%", 85 | plus_sign: "+", 86 | superscripting_exponent: "×", 87 | time_separator: ":" 88 | } 89 | } 90 | } 91 | 92 | """ 93 | @spec number_symbols_for(Locale.locale_reference(), Cldr.backend()) :: 94 | {:ok, map()} | {:error, {module(), String.t()}} 95 | 96 | def number_symbols_for(locale, backend) do 97 | Module.concat(backend, Number.Symbol).number_symbols_for(locale) 98 | end 99 | 100 | @doc """ 101 | Returns the number symbols for a specific locale and number system. 102 | 103 | ## Options 104 | 105 | * `locale` is any valid locale name returned by `Cldr.known_locale_names/1` 106 | or a `Cldr.LanguageTag` struct returned by `Cldr.Locale.new!/2`. The 107 | default is `Cldr.get_locale/1`. 108 | 109 | * `number_system` is any number system name returned by 110 | `Cldr.known_number_systems/0` or a number system type 111 | returned by `Cldr.known_number_system_types/0` 112 | 113 | * `backend` is any module that includes `use Cldr` and therefore 114 | is a `Cldr` backend module 115 | 116 | ## Example 117 | 118 | iex> Cldr.Number.Symbol.number_symbols_for("th", "thai", TestBackend.Cldr) 119 | { 120 | :ok, 121 | %Cldr.Number.Symbol{ 122 | decimal: %{standard: "."}, 123 | exponential: "E", 124 | group: %{standard: ","}, 125 | infinity: "∞", 126 | list: ";", 127 | minus_sign: "-", 128 | nan: "NaN", 129 | per_mille: "‰", 130 | percent_sign: "%", 131 | plus_sign: "+", 132 | superscripting_exponent: "×", 133 | time_separator: ":" 134 | } 135 | } 136 | 137 | """ 138 | @spec number_symbols_for( 139 | Locale.locale_reference(), 140 | System.system_name(), 141 | Cldr.backend() 142 | ) :: {:ok, map()} | {:error, {Cldr.NoNumberSymbols, String.t()}} 143 | 144 | def number_symbols_for(%LanguageTag{} = locale, number_system, backend) do 145 | with {:ok, system_name} <- 146 | Cldr.Number.System.system_name_from(number_system, locale, backend), 147 | {:ok, symbols} <- number_symbols_for(locale, backend) do 148 | symbols 149 | |> Map.get(system_name) 150 | |> symbols_return(locale, number_system) 151 | end 152 | end 153 | 154 | def number_symbols_for(locale_name, number_system, backend) do 155 | with {:ok, locale} <- Cldr.validate_locale(locale_name, backend) do 156 | number_symbols_for(locale, number_system, backend) 157 | end 158 | end 159 | 160 | @doc """ 161 | Returns a list of all decimal symbols defined 162 | by the locales configured in the given backend as 163 | a list. 164 | 165 | ## Arguments 166 | 167 | * `backend` is any module that includes `use Cldr` and therefore 168 | is a `Cldr` backend module 169 | 170 | """ 171 | def all_decimal_symbols(backend) do 172 | Module.concat(backend, Number.Symbol).all_decimal_symbols 173 | end 174 | 175 | @doc """ 176 | Returns a list of all grouping symbols defined 177 | by the locales configured in the given backend as 178 | a list. 179 | 180 | ## Arguments 181 | 182 | * `backend` is any module that includes `use Cldr` and therefore 183 | is a `Cldr` backend module 184 | 185 | """ 186 | def all_grouping_symbols(backend) do 187 | Module.concat(backend, Number.Symbol).all_grouping_symbols 188 | end 189 | 190 | @doc """ 191 | Returns a list of all decimal symbols defined 192 | by the locales configured in the given backend as 193 | a string. 194 | 195 | This string can be used as a character class 196 | when builing a regular expression. 197 | 198 | ## Arguments 199 | 200 | * `backend` is any module that includes `use Cldr` and therefore 201 | is a `Cldr` backend module 202 | 203 | """ 204 | def all_decimal_symbols_class(backend) do 205 | Module.concat(backend, Number.Symbol).all_decimal_symbols_class 206 | end 207 | 208 | @doc """ 209 | Returns a list of all grouping symbols defined 210 | by the locales configured in the given backend as 211 | a string. 212 | 213 | This string can be used as a character class 214 | when builing a regular expression. 215 | 216 | ## Arguments 217 | 218 | * `backend` is any module that includes `use Cldr` and therefore 219 | is a `Cldr` backend module 220 | 221 | """ 222 | def all_grouping_symbols_class(backend) do 223 | Module.concat(backend, Number.Symbol).all_grouping_symbols_class 224 | end 225 | 226 | @doc false 227 | def symbols_return(nil, locale, number_system) do 228 | { 229 | :error, 230 | { 231 | Cldr.NoNumberSymbols, 232 | "The locale #{inspect(locale)} does not have " <> 233 | "any symbols for number system #{inspect(number_system)}" 234 | } 235 | } 236 | end 237 | 238 | @doc false 239 | def symbols_return(symbols, _locale, _number_system) do 240 | {:ok, symbols} 241 | end 242 | end 243 | -------------------------------------------------------------------------------- /lib/cldr/number/transliterate.ex: -------------------------------------------------------------------------------- 1 | defmodule Cldr.Number.Transliterate do 2 | @moduledoc """ 3 | Transliteration for digits and separators. 4 | 5 | Transliterating a string is an expensive business. First the string has to 6 | be exploded into its component graphemes. Then for each grapheme we have 7 | to map to the equivalent in the other `{locale, number_system}`. Then we 8 | have to reassemble the string. 9 | 10 | Effort is made to short circuit where possible. Transliteration is not 11 | required for any `{locale, number_system}` that is the same as `{"en", 12 | "latn"}` since the implementation uses this combination for the placeholders during 13 | formatting already. When short circuiting is possible (typically the en-* 14 | locales with "latn" number_system - the total number of short circuited 15 | locales is 211 of the 537 in CLDR) the overall number formatting is twice as 16 | fast than when formal transliteration is required. 17 | 18 | ### Configuring precompilation of digit transliterations 19 | 20 | This module includes `Cldr.Number.Transliterate.transliterate_digits/3` which transliterates 21 | digits between number systems. For example from :arabic to :latn. Since generating a 22 | transliteration map is slow, pairs of transliterations can be configured so that the 23 | transliteration map is created at compile time and therefore speeding up transliteration at 24 | run time. 25 | 26 | To configure these transliteration pairs, add the following to your backend configuration: 27 | 28 | defmodule MyApp.Cldr do 29 | use Cldr, 30 | locale: ["en", "fr", "th"], 31 | default_locale: "en", 32 | precompile_transliterations: [{:latn, :thai}, {:arab, :thai}] 33 | end 34 | 35 | Where each tuple in the list configures one transliteration map. In this example, two maps are 36 | configured: from :latn to :thai and from :arab to :thai. 37 | 38 | A list of configurable number systems is returned by `Cldr.Number.System.numeric_systems/0`. 39 | 40 | If a transliteration is requested between two number pairs that have not been configured for 41 | precompilation, a warning is logged. 42 | 43 | """ 44 | 45 | require Logger 46 | alias Cldr.Number.System 47 | 48 | @doc """ 49 | Transliterates from latin digits to another number system's digits. 50 | 51 | Transliterates the latin digits 0..9 to their equivalents in 52 | another number system. Also transliterates the decimal and grouping 53 | separators as well as the plus, minus and exponent symbols. Any other character 54 | in the string will be returned "as is". 55 | 56 | * `sequence` is the string to be transliterated. 57 | 58 | * `locale` is any known locale, defaulting to `Cldr.get_locale/0`. 59 | 60 | * `number_system` is any known number system. If expressed as a `string` it 61 | is the actual name of a known number system. If epressed as an `atom` it is 62 | used as a key to look up a number system for the locale (the usual keys are 63 | `:default` and `:native` but :traditional and :finance are also part of the 64 | standard). See `Cldr.Number.System.number_systems_for/2` for a locale to 65 | see what number system types are defined. The default is `:default`. 66 | 67 | For available number systems see `Cldr.Number.System.number_systems/0` 68 | and `Cldr.Number.System.number_systems_for/2`. Also see 69 | `Cldr.Number.Symbol.number_symbols_for/2`. 70 | 71 | 72 | ## Examples 73 | 74 | iex> Cldr.Number.Transliterate.transliterate("123556", "en", :default, TestBackend.Cldr) 75 | "123556" 76 | 77 | iex> Cldr.Number.Transliterate.transliterate("123,556.000", "fr", :default, TestBackend.Cldr) 78 | "123 556,000" 79 | 80 | iex> Cldr.Number.Transliterate.transliterate("123556", "th", :default, TestBackend.Cldr) 81 | "123556" 82 | 83 | iex> Cldr.Number.Transliterate.transliterate("123556", "th", "thai", TestBackend.Cldr) 84 | "๑๒๓๕๕๖" 85 | 86 | iex> Cldr.Number.Transliterate.transliterate("123556", "th", :native, TestBackend.Cldr) 87 | "๑๒๓๕๕๖" 88 | 89 | iex> Cldr.Number.Transliterate.transliterate("Some number is: 123556", "th", "thai", TestBackend.Cldr) 90 | "Some number is: ๑๒๓๕๕๖" 91 | 92 | """ 93 | def transliterate(sequence, locale, number_system, backend, options \\ []) do 94 | backend = Module.concat(backend, Number.Transliterate) 95 | backend.transliterate(sequence, locale, number_system, options) 96 | end 97 | 98 | def transliterate_digits(digits, from_system, from_system) do 99 | digits 100 | end 101 | 102 | def transliterate_digits(digits, from_system, to_system) when is_binary(digits) do 103 | with {:ok, from} <- System.number_system_digits(from_system), 104 | {:ok, to} <- System.number_system_digits(to_system) do 105 | log_warning( 106 | "Transliteration from number system #{inspect(from_system)} to " <> 107 | "#{inspect(to_system)} requires dynamic generation of a transliteration map for " <> 108 | "each function call which is slow. Please consider configuring this transliteration pair. " <> 109 | "See `Cldr.Number.Transliteration` for further information." 110 | ) 111 | 112 | map = System.generate_transliteration_map(from, to) 113 | do_transliterate_digits(digits, map) 114 | else 115 | {:error, message} -> {:error, message} 116 | end 117 | end 118 | 119 | if macro_exported?(Logger, :warning, 2) do 120 | defp log_warning(message) do 121 | Logger.warning(fn -> message end) 122 | end 123 | else 124 | defp log_warning(message) do 125 | Logger.warn(message) 126 | end 127 | end 128 | 129 | defp do_transliterate_digits(digits, map) do 130 | digits 131 | |> String.graphemes() 132 | |> Enum.map(&Map.get(map, &1, &1)) 133 | |> Enum.join() 134 | end 135 | end 136 | -------------------------------------------------------------------------------- /lib/cldr/protocol/cldr_chars.ex: -------------------------------------------------------------------------------- 1 | defimpl Cldr.Chars, for: [Float, Integer, Decimal] do 2 | def to_string(number) do 3 | locale = Cldr.get_locale() 4 | Cldr.Number.to_string!(number, locale.backend, locale: locale) 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elixir-cldr/cldr_numbers/1fb2f82370c262f83dc348a9781211926ee97fa9/logo.png -------------------------------------------------------------------------------- /mix.exs: -------------------------------------------------------------------------------- 1 | defmodule Cldr.Numbers.Mixfile do 2 | @moduledoc false 3 | 4 | use Mix.Project 5 | 6 | @version "2.35.1" 7 | 8 | def project do 9 | [ 10 | app: :ex_cldr_numbers, 11 | version: @version, 12 | elixir: "~> 1.12", 13 | name: "Cldr Numbers", 14 | description: description(), 15 | source_url: "https://github.com/elixir-cldr/cldr_numbers", 16 | docs: docs(), 17 | build_embedded: Mix.env() == :prod, 18 | start_permanent: Mix.env() == :prod, 19 | elixirc_paths: elixirc_paths(Mix.env()), 20 | deps: deps(), 21 | package: package(), 22 | compilers: [:leex, :yecc] ++ Mix.compilers(), 23 | dialyzer: [ 24 | ignore_warnings: ".dialyzer_ignore_warnings", 25 | plt_add_apps: ~w(inets jason mix)a, 26 | flags: [ 27 | :error_handling, 28 | :unknown, 29 | :underspecs, 30 | :extra_return, 31 | :missing_return 32 | ] 33 | ] 34 | ] 35 | end 36 | 37 | def application do 38 | [ 39 | extra_applications: [:logger] 40 | ] 41 | end 42 | 43 | defp description do 44 | """ 45 | Number and currency localization and formatting functions for the Common Locale Data 46 | Repository (CLDR). 47 | """ 48 | end 49 | 50 | defp deps do 51 | [ 52 | {:ex_cldr, "~> 2.42"}, 53 | {:ex_cldr_currencies, "~> 2.16"}, 54 | 55 | {:digital_token, "~> 0.3 or ~> 1.0"}, 56 | {:decimal, "~> 1.6 or ~> 2.0"}, 57 | {:jason, "~> 1.0", optional: true}, 58 | {:ex_doc, "~> 0.18", only: [:dev, :release], optional: true, runtime: false}, 59 | {:dialyxir, "~> 1.0", only: [:dev, :test], optional: true, runtime: false}, 60 | {:phoenix_html, "~> 3.0", only: [:dev, :test, :release], optional: true, runtime: false}, 61 | {:exprof, "~> 0.2", only: :dev, optional: true, runtime: false}, 62 | # {:benchee, "~> 1.0", only: :dev, optional: true, runtime: false} 63 | ] 64 | end 65 | 66 | defp package do 67 | [ 68 | maintainers: ["Kip Cole"], 69 | licenses: ["Apache-2.0"], 70 | links: links(), 71 | files: [ 72 | "lib", 73 | "src/decimal_formats_lexer.xrl", 74 | "src/decimal_formats_parser.yrl", 75 | "src/rbnf_lexer.xrl", 76 | "src/rbnf_parser.yrl", 77 | "config", 78 | "mix.exs", 79 | "README*", 80 | "CHANGELOG*", 81 | "LICENSE*" 82 | ] 83 | ] 84 | end 85 | 86 | def docs do 87 | [ 88 | source_ref: "v#{@version}", 89 | main: "readme", 90 | extras: ["README.md", "CHANGELOG.md", "LICENSE.md"], 91 | logo: "logo.png", 92 | formatters: ["html"], 93 | skip_undefined_reference_warnings_on: ["changelog", "CHANGELOG.md"] 94 | ] 95 | end 96 | 97 | def links do 98 | %{ 99 | "GitHub" => "https://github.com/elixir-cldr/cldr_numbers", 100 | "Readme" => "https://github.com/elixir-cldr/cldr_numbers/blob/v#{@version}/README.md", 101 | "Changelog" => "https://github.com/elixir-cldr/cldr_numbers/blob/v#{@version}/CHANGELOG.md" 102 | } 103 | end 104 | 105 | defp elixirc_paths(:test), do: ["lib", "test", "mix"] 106 | defp elixirc_paths(:dev), do: ["lib", "mix"] 107 | defp elixirc_paths(_), do: ["lib"] 108 | end 109 | -------------------------------------------------------------------------------- /mix.lock: -------------------------------------------------------------------------------- 1 | %{ 2 | "benchee": {:hex, :benchee, "1.3.1", "c786e6a76321121a44229dde3988fc772bca73ea75170a73fd5f4ddf1af95ccf", [:mix], [{:deep_merge, "~> 1.0", [hex: :deep_merge, repo: "hexpm", optional: false]}, {:statistex, "~> 1.0", [hex: :statistex, repo: "hexpm", optional: false]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "76224c58ea1d0391c8309a8ecbfe27d71062878f59bd41a390266bf4ac1cc56d"}, 3 | "cldr_utils": {:hex, :cldr_utils, "2.28.2", "f500667164a9043369071e4f9dcef31f88b8589b2e2c07a1eb9f9fa53cb1dce9", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:certifi, "~> 2.5", [hex: :certifi, repo: "hexpm", optional: true]}, {:decimal, "~> 1.9 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}], "hexpm", "c506eb1a170ba7cdca59b304ba02a56795ed119856662f6b1a420af80ec42551"}, 4 | "decimal": {:hex, :decimal, "2.3.0", "3ad6255aa77b4a3c4f818171b12d237500e63525c2fd056699967a3e7ea20f62", [:mix], [], "hexpm", "a4d66355cb29cb47c3cf30e71329e58361cfcb37c34235ef3bf1d7bf3773aeac"}, 5 | "deep_merge": {:hex, :deep_merge, "1.0.0", "b4aa1a0d1acac393bdf38b2291af38cb1d4a52806cf7a4906f718e1feb5ee961", [:mix], [], "hexpm", "ce708e5f094b9cd4e8f2be4f00d2f4250c4095be93f8cd6d018c753894885430"}, 6 | "dialyxir": {:hex, :dialyxir, "1.4.5", "ca1571ac18e0f88d4ab245f0b60fa31ff1b12cbae2b11bd25d207f865e8ae78a", [:mix], [{:erlex, ">= 0.2.7", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "b0fb08bb8107c750db5c0b324fa2df5ceaa0f9307690ee3c1f6ba5b9eb5d35c3"}, 7 | "digital_token": {:hex, :digital_token, "1.0.0", "454a4444061943f7349a51ef74b7fb1ebd19e6a94f43ef711f7dae88c09347df", [:mix], [{:cldr_utils, "~> 2.17", [hex: :cldr_utils, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm", "8ed6f5a8c2fa7b07147b9963db506a1b4c7475d9afca6492136535b064c9e9e6"}, 8 | "earmark_parser": {:hex, :earmark_parser, "1.4.44", "f20830dd6b5c77afe2b063777ddbbff09f9759396500cdbe7523efd58d7a339c", [:mix], [], "hexpm", "4778ac752b4701a5599215f7030989c989ffdc4f6df457c5f36938cc2d2a2750"}, 9 | "erlex": {:hex, :erlex, "0.2.7", "810e8725f96ab74d17aac676e748627a07bc87eb950d2b83acd29dc047a30595", [:mix], [], "hexpm", "3ed95f79d1a844c3f6bf0cea61e0d5612a42ce56da9c03f01df538685365efb0"}, 10 | "ex_cldr": {:hex, :ex_cldr, "2.42.0", "17ea930e88b8802b330e1c1e288cdbaba52cbfafcccf371ed34b299a47101ffb", [:mix], [{:cldr_utils, "~> 2.28", [hex: :cldr_utils, repo: "hexpm", optional: false]}, {:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:gettext, "~> 0.19", [hex: :gettext, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:nimble_parsec, "~> 0.5 or ~> 1.0", [hex: :nimble_parsec, repo: "hexpm", optional: true]}], "hexpm", "07264a7225810ecae6bdd6715d8800c037a1248dc0063923cddc4ca3c4888df6"}, 11 | "ex_cldr_currencies": {:hex, :ex_cldr_currencies, "2.16.4", "d76770690699b6ba91f1fa253a299a905f9c22b45d91891b85f431b9dafa8b3b", [:mix], [{:ex_cldr, "~> 2.38", [hex: :ex_cldr, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm", "46a67d1387f14e836b1a24d831fa5f0904663b4f386420736f40a7d534e3cb9e"}, 12 | "ex_doc": {:hex, :ex_doc, "0.37.3", "f7816881a443cd77872b7d6118e8a55f547f49903aef8747dbcb345a75b462f9", [:mix], [{:earmark_parser, "~> 1.4.42", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_c, ">= 0.1.0", [hex: :makeup_c, repo: "hexpm", optional: true]}, {:makeup_elixir, "~> 0.14 or ~> 1.0", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1 or ~> 1.0", [hex: :makeup_erlang, repo: "hexpm", optional: false]}, {:makeup_html, ">= 0.1.0", [hex: :makeup_html, repo: "hexpm", optional: true]}], "hexpm", "e6aebca7156e7c29b5da4daa17f6361205b2ae5f26e5c7d8ca0d3f7e18972233"}, 13 | "exprintf": {:hex, :exprintf, "0.2.1", "b7e895dfb00520cfb7fc1671303b63b37dc3897c59be7cbf1ae62f766a8a0314", [:mix], [], "hexpm", "20a0e8c880be90e56a77fcc82533c5d60c643915c7ce0cc8aa1e06ed6001da28"}, 14 | "exprof": {:hex, :exprof, "0.2.4", "13ddc0575a6d24b52e7c6809d2a46e9ad63a4dd179628698cdbb6c1f6e497c98", [:mix], [{:exprintf, "~> 0.2", [hex: :exprintf, repo: "hexpm", optional: false]}], "hexpm", "0884bcb66afc421c75d749156acbb99034cc7db6d3b116c32e36f32551106957"}, 15 | "jason": {:hex, :jason, "1.4.4", "b9226785a9aa77b6857ca22832cffa5d5011a667207eb2a0ad56adb5db443b8a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "c5eb0cab91f094599f94d55bc63409236a8ec69a21a67814529e8d5f6cc90b3b"}, 16 | "makeup": {:hex, :makeup, "1.2.1", "e90ac1c65589ef354378def3ba19d401e739ee7ee06fb47f94c687016e3713d1", [:mix], [{:nimble_parsec, "~> 1.4", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "d36484867b0bae0fea568d10131197a4c2e47056a6fbe84922bf6ba71c8d17ce"}, 17 | "makeup_elixir": {:hex, :makeup_elixir, "1.0.1", "e928a4f984e795e41e3abd27bfc09f51db16ab8ba1aebdba2b3a575437efafc2", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "7284900d412a3e5cfd97fdaed4f5ed389b8f2b4cb49efc0eb3bd10e2febf9507"}, 18 | "makeup_erlang": {:hex, :makeup_erlang, "1.0.2", "03e1804074b3aa64d5fad7aa64601ed0fb395337b982d9bcf04029d68d51b6a7", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "af33ff7ef368d5893e4a267933e7744e46ce3cf1f61e2dccf53a111ed3aa3727"}, 19 | "nimble_parsec": {:hex, :nimble_parsec, "1.4.2", "8efba0122db06df95bfaa78f791344a89352ba04baedd3849593bfce4d0dc1c6", [:mix], [], "hexpm", "4b21398942dda052b403bbe1da991ccd03a053668d147d53fb8c4e0efe09c973"}, 20 | "phoenix_html": {:hex, :phoenix_html, "3.3.4", "42a09fc443bbc1da37e372a5c8e6755d046f22b9b11343bf885067357da21cb3", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "0249d3abec3714aff3415e7ee3d9786cb325be3151e6c4b3021502c585bf53fb"}, 21 | "statistex": {:hex, :statistex, "1.0.0", "f3dc93f3c0c6c92e5f291704cf62b99b553253d7969e9a5fa713e5481cd858a5", [:mix], [], "hexpm", "ff9d8bee7035028ab4742ff52fc80a2aa35cece833cf5319009b52f1b5a86c27"}, 22 | } 23 | -------------------------------------------------------------------------------- /mix/clause_debugger.ex_: -------------------------------------------------------------------------------- 1 | defmodule Cldr.FunctionClause do 2 | @moduledoc """ 3 | Format function clauses using Exception.blame/3 4 | """ 5 | 6 | @doc """ 7 | Given a `module`, `function`, and `args` see 8 | that function clause would match or not match. 9 | 10 | This is useful for helping diagnose function 11 | clause errors when many clauses are generated 12 | at compile time. 13 | 14 | """ 15 | @spec match(module(), atom(), list(any)) :: :ok | no_return() 16 | def match(module, function, args) do 17 | case Exception.blame_mfa(module, function, args) do 18 | {:ok, kind, clauses} -> 19 | formatted_clauses(function, kind, clauses, &blame_match/2) 20 | 21 | :error -> 22 | raise ArgumentError, 23 | "Function #{inspect(module)}.#{function}/#{length(args)} " <> 24 | "is not known." 25 | end 26 | end 27 | 28 | defp formatted_clauses(function, kind, clauses, ast_fun) do 29 | format_clause_fun = fn {args, guards} -> 30 | code = Enum.reduce(guards, {function, [], args}, &{:when, [], [&2, &1]}) 31 | " #{kind} " <> Macro.to_string(code, ast_fun) <> "\n" 32 | end 33 | 34 | clauses 35 | |> Enum.map(format_clause_fun) 36 | |> Enum.join() 37 | |> IO.puts() 38 | end 39 | 40 | defp blame_match(%{match?: true, node: node}, _), 41 | do: Macro.to_string(node) 42 | 43 | defp blame_match(%{match?: false, node: node}, _), 44 | do: IO.ANSI.red() <> Macro.to_string(node) <> IO.ANSI.reset() 45 | 46 | defp blame_match(_, string), do: string 47 | end 48 | -------------------------------------------------------------------------------- /mix/for_dialyzer.ex: -------------------------------------------------------------------------------- 1 | defmodule Cldr.Number.ForDialyzer do 2 | @moduledoc """ 3 | Includes functions intended only to give dialyzer some 4 | opportunity to check specs. 5 | 6 | """ 7 | 8 | def for_dialyzer do 9 | formats = MyApp.Cldr.Number.Format.all_formats_for!(:en) 10 | formats 11 | end 12 | end -------------------------------------------------------------------------------- /mix/rbnf_test_data_gen.ex: -------------------------------------------------------------------------------- 1 | defmodule Cldr.Rbnf.Gen do 2 | @moduledoc """ 3 | Generates test data values for RBNF. 4 | 5 | Only to be used if something changes in 6 | CLDR 7 | 8 | The json included here has the standard test 9 | keys used by RBNF testing. The values will be 10 | replaced with generated data. 11 | 12 | """ 13 | @json ~S""" 14 | { 15 | "-1141": "−1 141.", 16 | "-1142": "−1 142.", 17 | "-1143": "−1 143.", 18 | "-100": "−100.", 19 | "-75": "−75-и", 20 | "-50": "−50.", 21 | "-24": "−24.", 22 | "0": "0.", 23 | "1": "1-ви", 24 | "2": "2-и", 25 | "3": "3-и", 26 | "4": "4-ти", 27 | "5": "5-и", 28 | "6": "6-и", 29 | "7": "7-и", 30 | "8": "8-и", 31 | "9": "9-и", 32 | "10": "10-и", 33 | "11": "11-и", 34 | "12": "12-и", 35 | "13": "13-и", 36 | "14": "14-и", 37 | "15": "15-и", 38 | "16": "16-и", 39 | "17": "17-и", 40 | "18": "18-и", 41 | "19": "19-и", 42 | "20": "20-и", 43 | "21": "21-и", 44 | "22": "22-и", 45 | "23": "23-и", 46 | "24": "24-ти", 47 | "25": "25-и", 48 | "26": "26-и", 49 | "27": "27-и", 50 | "28": "28-и", 51 | "29": "29-и", 52 | "30": "30-и", 53 | "31": "31-и", 54 | "32": "32-и", 55 | "33": "33-и", 56 | "34": "34-и", 57 | "35": "35-и", 58 | "36": "36-и", 59 | "37": "37-и", 60 | "38": "38-и", 61 | "39": "39-и", 62 | "40": "40-и", 63 | "41": "41-и", 64 | "42": "42-ри", 65 | "43": "43-и", 66 | "44": "44-и", 67 | "45": "45-и", 68 | "46": "46-и", 69 | "47": "47-и", 70 | "48": "48-и", 71 | "49": "49-и", 72 | "50": "50-и", 73 | "51": "51-ви", 74 | "52": "52-и", 75 | "53": "53-и", 76 | "54": "54-и", 77 | "55": "55-и", 78 | "56": "56-и", 79 | "57": "57-и", 80 | "58": "58-и", 81 | "59": "59-и", 82 | "60": "60-и", 83 | "61": "61-и", 84 | "62": "62-и", 85 | "63": "63-и", 86 | "64": "64-и", 87 | "65": "65-и", 88 | "66": "66-и", 89 | "67": "67-и", 90 | "68": "68-и", 91 | "69": "69-и", 92 | "70": "70-и", 93 | "71": "71-и", 94 | "72": "72-и", 95 | "73": "73-и", 96 | "74": "74-и", 97 | "75": "75-и", 98 | "76": "76-и", 99 | "77": "77-и", 100 | "78": "78-и", 101 | "79": "79-и", 102 | "80": "80-и", 103 | "81": "81-и", 104 | "82": "82-и", 105 | "83": "83-и", 106 | "84": "84-и", 107 | "85": "85-и", 108 | "86": "86-и", 109 | "87": "87-и", 110 | "88": "88-и", 111 | "89": "89-и", 112 | "90": "90-и", 113 | "91": "91-и", 114 | "92": "92-и", 115 | "93": "93-и", 116 | "94": "94-и", 117 | "95": "95-и", 118 | "96": "96-и", 119 | "97": "97-и", 120 | "98": "98-и", 121 | "99": "99-и", 122 | "100": "100.", 123 | "321": "321.", 124 | "322": "322.", 125 | "323": "323.", 126 | "1141": "1141-и", 127 | "1142": "1142-и", 128 | "1143": "1143-и", 129 | "10311": "10 311.", 130 | "138400": "138 400-тен" 131 | } 132 | """ 133 | 134 | def gen(rule, locale) do 135 | @json 136 | |> Jason.decode!() 137 | |> Enum.map(fn {k, _v} -> 138 | i = String.to_integer(k) 139 | {:ok, v} = Cldr.Number.to_string(i, format: rule, locale: locale) 140 | {i, v} 141 | end) 142 | |> Enum.sort() 143 | |> Enum.map(fn {k, v} -> "\"#{k}\": \"#{v}\"" end) 144 | |> Enum.join(",\n") 145 | |> IO.puts() 146 | 147 | nil 148 | end 149 | end 150 | -------------------------------------------------------------------------------- /mix/tasks/generate_rbnf_test_data.ex: -------------------------------------------------------------------------------- 1 | defmodule Mix.Tasks.Cldr.Number.GenerateRbnfTestData do 2 | @moduledoc """ 3 | Generates RBNF test data 4 | """ 5 | 6 | use Mix.Task 7 | require Logger 8 | 9 | @shortdoc "Generate RBNF test data" 10 | 11 | @output_directory "test/support/rbnf" 12 | 13 | # Check out possible bug being surfaced in PL 14 | @locales [ 15 | :pl 16 | # :af, :be, :bg, :ca, :es, :gu, :he, :hi, :hr, :hu, :it, :ja, :ko, :ms, :ru, 17 | # :uk,:vi, :zh, :"zh-Hant" 18 | ] 19 | 20 | @doc false 21 | def run(_) do 22 | for locale <- @locales do 23 | IO.puts "Generating RBNF test data for locale #{locale}" 24 | 25 | tests = 26 | "#{@output_directory}/#{locale}/rbnf_test.json" 27 | |> File.read!() 28 | |> Jason.decode!() 29 | |> Enum.map(fn {rule_group, rule_examples} -> {rule_group, generate_tests(locale, rule_examples)} end) 30 | |> Map.new() 31 | |> Jason.encode!(pretty: true) 32 | 33 | File.write("#{@output_directory}/#{locale}/rbnf_test.json", tests) 34 | end 35 | end 36 | 37 | defp generate_tests(locale, rule_examples) do 38 | Enum.map(rule_examples, fn {rule_string, examples} -> 39 | rule = 40 | rule_string 41 | |> String.replace("-", "_") 42 | |> String.to_atom() 43 | 44 | IO.puts " Building examples for rule #{rule}" 45 | 46 | examples = 47 | Enum.map(examples, fn {number, _result} -> 48 | int = String.to_integer(number) 49 | {:ok, result} = Cldr.Number.to_string int, format: rule, locale: locale, backend: TestBackend.Cldr 50 | {number, result} 51 | end) 52 | |> Map.new() 53 | 54 | {rule_string, examples} 55 | end) 56 | |> Map.new() 57 | end 58 | end -------------------------------------------------------------------------------- /mix/test_backend.ex: -------------------------------------------------------------------------------- 1 | require Cldr.Number.Backend 2 | 3 | defmodule MyApp.Cldr do 4 | @moduledoc false 5 | 6 | use Cldr, 7 | default_locale: "en", 8 | locales: [ 9 | "en", 10 | "da", 11 | "bn", 12 | "zh", 13 | "zh-Hant", 14 | "it", 15 | "de", 16 | "th", 17 | "id", 18 | "ru", 19 | "he", 20 | "pl", 21 | "es", 22 | "hr", 23 | "nb", 24 | "no", 25 | "en-IN", 26 | "ur", 27 | "fr-CH", 28 | "fr-BE", 29 | "ta", 30 | "he", 31 | "pt-CV", 32 | "ar-EG", 33 | "en-ZA" 34 | ], 35 | precompile_transliterations: [{:latn, :arab}, {:arab, :thai}, {:arab, :latn}], 36 | providers: [Cldr.Number], 37 | suppress_warnings: true 38 | end 39 | 40 | defmodule MyApp.Cldr2 do 41 | @moduledoc false 42 | 43 | use Cldr, 44 | default_locale: "en-GB", 45 | locales: ["en-GB", "hu", "ar", "de"], 46 | precompile_transliterations: [{:latn, :arab}, {:arab, :thai}, {:arab, :latn}, {:thai, :latn}], 47 | providers: [Cldr.Number], 48 | suppress_warnings: true 49 | end 50 | -------------------------------------------------------------------------------- /src/decimal_formats_lexer.xrl: -------------------------------------------------------------------------------- 1 | % Tokenizes CLDR decimal formats which are described at 2 | % http://unicode.org/reports/tr35/tr35-numbers.html#Number_Format_Patterns 3 | 4 | Definitions. 5 | 6 | Number = ([@#,]*)?([0-9,]+)?(\.[0-9#,]+)?([Ee](\+)?[0-9]+)? 7 | Percent = % 8 | Permille = ‰ 9 | Plus = \+ 10 | Minus = \- 11 | Semicolon = ; 12 | Currency = ¤+ 13 | Pad = \*. 14 | Quoted = \'.\' 15 | Quote = \'\' 16 | Literal = [^*@#0-9¤\+\-;%\']+ 17 | 18 | Rules. 19 | 20 | {Number} : {token,{format,TokenLine,TokenChars}}. 21 | {Percent} : {token,{percent,TokenLine,TokenChars}}. 22 | {Permille} : {token,{permille,TokenLine,TokenChars}}. 23 | {Plus} : {token,{plus,TokenLine,TokenChars}}. 24 | {Minus} : {token,{minus,TokenLine,TokenChars}}. 25 | {Semicolon} : {token,{semicolon,TokenLine,TokenChars}}. 26 | {Currency} : {token,{currency_symbol(length(TokenChars)),TokenLine,length(TokenChars)}}. 27 | {Pad} : {token,{pad,TokenLine,[lists:nth(2,TokenChars)]}}. 28 | {Quoted} : {token,{quoted_char,TokenLine,[lists:nth(2, TokenChars)]}}. 29 | {Quote} : {token,{quote,TokenLine,["'"]}}. 30 | {Literal} : {token,{literal,TokenLine,TokenChars}}. 31 | 32 | Erlang code. 33 | 34 | currency_symbol(Len) -> 35 | list_to_atom("currency_" ++ integer_to_list(Len)). -------------------------------------------------------------------------------- /src/decimal_formats_parser.yrl: -------------------------------------------------------------------------------- 1 | % Parse CLDR decimal formats which are described at 2 | % http://unicode.org/reports/tr35/tr35-numbers.html#Number_Format_Patterns 3 | 4 | Nonterminals decimal_format positive_format negative_format number_format 5 | prefix suffix literal_list literal_elem currency_symbol. 6 | 7 | Terminals plus minus format currency_1 currency_2 currency_3 currency_4 percent 8 | permille literal semicolon pad quote quoted_char. 9 | 10 | Rootsymbol decimal_format. 11 | 12 | decimal_format -> positive_format semicolon negative_format : [{positive, '$1'}, {negative, '$3'}]. 13 | decimal_format -> positive_format : [{positive, '$1'}, negative('$1')]. 14 | 15 | positive_format -> prefix number_format suffix : '$1' ++ format('$2') ++ '$3'. 16 | positive_format -> prefix number_format : '$1' ++ format('$2'). 17 | positive_format -> number_format suffix : format('$1') ++ '$2'. 18 | positive_format -> number_format : format('$1'). 19 | positive_format -> prefix : '$1'. 20 | 21 | negative_format -> prefix number_format suffix : '$1' ++ neg_format('$2') ++ '$3'. 22 | negative_format -> prefix number_format : '$1' ++ neg_format('$2'). 23 | negative_format -> number_format suffix : neg_format('$1') ++ '$2'. 24 | negative_format -> number_format : neg_format('$1'). 25 | 26 | number_format -> format : unwrap('$1'). 27 | 28 | prefix -> literal_list pad : '$1' ++ pad('$2'). 29 | prefix -> pad literal_list : pad('$1') ++ '$2'. 30 | prefix -> literal_list : '$1'. 31 | prefix -> pad : pad('$1'). 32 | 33 | suffix -> prefix : '$1'. 34 | 35 | literal_list -> literal_elem literal_list : append('$1', '$2'). 36 | literal_list -> literal_elem : '$1'. 37 | 38 | literal_elem -> currency_symbol : [{currency, unwrap('$1')}]. 39 | literal_elem -> percent : [{percent, unwrap('$1')}]. 40 | literal_elem -> permille : [{permille, unwrap('$1')}]. 41 | literal_elem -> literal : [{literal, unwrap('$1')}]. 42 | literal_elem -> plus : [{plus, "+"}]. 43 | literal_elem -> minus : [{minus, "-"}]. 44 | literal_elem -> quote : [{quote, '\''}]. 45 | literal_elem -> quoted_char : [{quoted_char, unwrap('$1')}]. 46 | 47 | currency_symbol -> currency_1 : '$1'. 48 | currency_symbol -> currency_2 : '$1'. 49 | currency_symbol -> currency_3 : '$1'. 50 | currency_symbol -> currency_4 : '$1'. 51 | 52 | Erlang code. 53 | 54 | % If there is no negative pattern then build the default one. The default 55 | % is an exact copy of the positive one except we put `{minus, "-"}` 56 | % in front of the `format` token. 57 | negative(Positive) -> 58 | {negative, copy_positive(Positive)}. 59 | 60 | copy_positive([]) -> []; 61 | copy_positive([{format, Format} | Rest]) -> [{minus, "-"}, {format, Format} | copy_positive(Rest)]; 62 | copy_positive([Head | Rest]) -> [Head | copy_positive(Rest)]. 63 | 64 | % Append list items. Consolidate literals if possible into 65 | % a single list element. 66 | append([{literal, Literal1}], [{literal, Literal2} | Rest]) -> 67 | [{literal, list_to_binary([Literal1, Literal2])}] ++ Rest; 68 | append(A, B) when is_list(A) and is_list(B) -> 69 | A ++ B. 70 | 71 | format(F) -> 72 | [{format, F}]. 73 | 74 | % Doesn't matter what the negative format is 75 | % its always the same as the positive one 76 | % with potentially different suffix and prefix 77 | neg_format(_F) -> 78 | [{format, same_as_positive}]. 79 | 80 | pad(V) -> 81 | [{pad, unwrap(V)}]. 82 | 83 | % Return a token value 84 | unwrap({_,_,V}) when is_list(V) -> unicode:characters_to_binary(V); 85 | unwrap({_,_,V}) -> V. 86 | -------------------------------------------------------------------------------- /src/rbnf_lexer.xrl: -------------------------------------------------------------------------------- 1 | % Lexer for the ICU/CLDR Rule Based Number Formatting rule definitions. 2 | % Step 1 is to read in the json rule set. 3 | % Then for each ruleset, pass each rule to the parser in two parts: 4 | % First the rule name 5 | % THen the rule definition 6 | % 7 | % Can be invoked in elixir as: 8 | % 9 | % iex> :rbnf.string(rules_set_as_a_char_list) 10 | % 11 | 12 | Definitions. 13 | 14 | Rule_cardinal_start = (cardinal) 15 | Rule_ordinal_start = (ordinal) 16 | Plural_rules = (zero\{.+\})?(one\{.+\})?(two\{.+\})?(few\{.+\})?(many\{.+\})?(other\{.+\})? 17 | 18 | Rule_name = (%[%a-zA-Z0-9\-]+) 19 | Number_format = ([0#]([0#,]+)?)(\.([0#]+))?([eE]([-+]?[0#]+))? 20 | Conditional_start = \[ 21 | Conditional_end = \] 22 | Left_paren = \( 23 | Right_paren = \) 24 | Greater_than = [→>] 25 | Less_than = [←<] 26 | Equals = = 27 | Dollar = \$ 28 | Semicolon = ; 29 | Comma = , 30 | Char = . 31 | 32 | Rules. 33 | 34 | % This part is for the definitions. 35 | {Rule_cardinal_start} : {token,{rule_cardinal_start,TokenLine,TokenChars}}. 36 | {Rule_ordinal_start} : {token,{rule_ordinal_start,TokenLine,TokenChars}}. 37 | {Number_format} : {token,{number_format,TokenLine,TokenChars}}. 38 | {Rule_name} : {token,{rule_name,TokenLine,TokenChars}}. 39 | {Conditional_start} : {token,{conditional_start,TokenLine,TokenChars}}. 40 | {Conditional_end} : {token,{conditional_end,TokenLine,TokenChars}}. 41 | {Greater_than} : {token,{modulo_call,TokenLine,TokenChars}}. 42 | {Less_than} : {token,{quotient_call,TokenLine,TokenChars}}. 43 | {Equals} : {token,{rule_call,TokenLine,TokenChars}}. 44 | {Semicolon} : {token,{rule_end,TokenLine,TokenChars}}. 45 | {Plural_rules} : {token,{plural_rules,TokenLine,TokenChars}}. 46 | {Right_paren} : {token,{right_paren,TokenLine,TokenChars}}. 47 | {Left_paren} : {token,{left_paren,TokenLine,TokenChars}}. 48 | {Dollar} : {token,{dollar,TokenLine,TokenChars}}. 49 | {Comma} : {token,{comma,TokenLine,TokenChars}}. 50 | {Char} : {token,{char,TokenLine,TokenChars}}. 51 | 52 | Erlang code. 53 | -------------------------------------------------------------------------------- /src/rbnf_parser.yrl: -------------------------------------------------------------------------------- 1 | Nonterminals quotient_rule modulo_rule invoke_rule cardinal_rule ordinal_rule 2 | rule literal rbnf_rule rule_part character. 3 | 4 | Terminals rule_cardinal_start rule_ordinal_start number_format 5 | rule_name conditional_start conditional_end modulo_call 6 | quotient_call rule_call plural_rules right_paren 7 | left_paren dollar comma char. 8 | 9 | Rootsymbol rbnf_rule. 10 | 11 | Left 100 rbnf_rule. 12 | Right 400 modulo_call. 13 | Right 500 quotient_call. 14 | Right 600 character. 15 | 16 | rbnf_rule -> rule_part rbnf_rule : ['$1'] ++ '$2'. 17 | rbnf_rule -> rule_part : ['$1']. 18 | 19 | rule_part -> literal : '$1'. 20 | rule_part -> quotient_rule : '$1'. 21 | rule_part -> modulo_rule : '$1'. 22 | rule_part -> invoke_rule : '$1'. 23 | rule_part -> cardinal_rule : '$1'. 24 | rule_part -> ordinal_rule : '$1'. 25 | rule_part -> conditional_start rbnf_rule conditional_end : {conditional, '$2'}. 26 | 27 | quotient_rule -> quotient_call rule quotient_call quotient_call : {quotient, '$2'}. 28 | quotient_rule -> quotient_call rule quotient_call : {quotient, '$2'}. 29 | quotient_rule -> quotient_call quotient_call : {quotient, nil}. 30 | 31 | modulo_rule -> modulo_call rule modulo_call : {modulo, '$2'}. 32 | modulo_rule -> modulo_call modulo_call : {modulo, nil}. 33 | 34 | % Note that here we are treating >>> as equivalent to >> 35 | % This is not strictly true since the spec says we should 36 | % ... but bypass the normal rule-selection process and just 37 | % use the rule that precedes this one in this rule list. 38 | modulo_rule -> modulo_call modulo_call modulo_call : {modulo, nil}. 39 | 40 | 41 | invoke_rule -> rule_call rule rule_call : {call, '$2'}. 42 | 43 | cardinal_rule -> dollar left_paren rule_cardinal_start comma 44 | plural_rules right_paren dollar : {cardinal, to_map('$5')}. 45 | ordinal_rule -> dollar left_paren rule_ordinal_start comma 46 | plural_rules right_paren dollar : {ordinal, to_map('$5')}. 47 | 48 | rule -> rule_name : {rule, normalize_rule_name('$1')}. 49 | rule -> number_format : {format, unwrap('$1')}. 50 | 51 | literal -> character literal : append('$1', '$2'). 52 | literal -> character : {literal, unwrap('$1')}. 53 | 54 | character -> char : '$1'. 55 | character -> comma : '$1'. 56 | character -> number_format : '$1'. 57 | 58 | Erlang code. 59 | 60 | % Consolidate characters into a binary 61 | append({char, _, _} = Char, {literal, Literal}) -> 62 | {literal, list_to_binary([unwrap(Char), Literal])}; 63 | append({comma, _, _} = Char, {literal, Literal}) -> 64 | {literal, list_to_binary([unwrap(Char), Literal])}; 65 | append({number_format, _, _} = Char, {literal, Literal}) -> 66 | {literal, list_to_binary([unwrap(Char), Literal])}; 67 | append({literal, Literal1}, {literal, Literal2}) -> 68 | {literal, list_to_binary([Literal2, Literal1])}. 69 | 70 | % We will turn rule names into functions later on so 71 | % we normalise the names to a format that is acceptable. 72 | normalize_rule_name({_,_,[$%, $% | Name]}) -> 73 | erlang:binary_to_atom(unicode:characters_to_binary(force_valid_function_name(Name)), utf8); 74 | normalize_rule_name({_,_,[$% | Name]}) -> 75 | erlang:binary_to_atom(unicode:characters_to_binary(force_valid_function_name(Name)), utf8). 76 | 77 | % Return a token value as a binary 78 | unwrap({_,_,V}) when is_list(V) -> unicode:characters_to_binary(V); 79 | unwrap({_,_,V}) -> V. 80 | 81 | % Force a valid function name. This is in two parts: 82 | % 1. If the function name starts with a number, prepend 83 | % a letter 84 | % 2. Replace "-" with "_" 85 | force_valid_function_name([Digit | Rest]) when Digit >= $0 andalso Digit =< $9 -> 86 | underscore([$r, Digit | Rest]); 87 | force_valid_function_name(Other) -> 88 | underscore(Other). 89 | 90 | % Substitute "_" for "-" since we will use these rule names 91 | % as functions later on. 92 | underscore([$-| Rest]) -> 93 | [$_ | underscore(Rest)]; 94 | underscore([]) -> 95 | []; 96 | underscore([Char | Rest]) -> 97 | [Char | underscore(Rest)]. 98 | 99 | % Convert ordinal and cardinal rules into a map 100 | to_map(Plurals) -> 101 | String = unwrap(Plurals), 102 | Parts = binary:split(String, [<<"{">>,<<"}">>], [global, trim]), 103 | Proplist = to_proplist(Parts), 104 | maps:from_list(Proplist). 105 | 106 | % Convert a list into a proplist 107 | to_proplist([K,V | T]) -> [{erlang:binary_to_atom(K, utf8),V} | to_proplist(T)]; 108 | to_proplist([]) -> []. -------------------------------------------------------------------------------- /test/cldr_numbers_test.exs: -------------------------------------------------------------------------------- 1 | defmodule CldrNumbersTest do 2 | use ExUnit.Case, async: true 3 | 4 | if function_exported?(Code, :fetch_docs, 1) do 5 | @modules [ 6 | Number, 7 | Number.System, 8 | Number.Format, 9 | Number.Symbol, 10 | Number.Transliterate, 11 | Number.Formatter.Decimal, 12 | Rbnf.NumberSystem, 13 | Rbnf.Spellout, 14 | Rbnf.Ordinal 15 | ] 16 | 17 | test "that no module docs are generated for a backend" do 18 | for mod <- @modules do 19 | module = Module.concat(NoDoc.Cldr, mod) 20 | assert {:docs_v1, _, :elixir, _, :hidden, %{}, _} = Code.fetch_docs(module) 21 | end 22 | end 23 | 24 | assert "that module docs are generated for a backend" do 25 | for mod <- @modules do 26 | module = Module.concat(TestBackend.Cldr, mod) 27 | {:docs_v1, _, :elixir, "text/markdown", _, %{}, _} = Code.fetch_docs(module) 28 | end 29 | end 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /test/doc_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Cldr.Number.Doc.Test do 2 | use ExUnit.Case 3 | 4 | default = Application.compile_env(:ex_cldr, :default_backend) 5 | Application.put_env(:ex_cldr, :default_backend, TestBackend.Cldr) 6 | 7 | doctest Cldr.Number 8 | doctest Cldr.Number.String 9 | doctest Cldr.Number.Format 10 | doctest Cldr.Number.Symbol 11 | doctest Cldr.Number.System 12 | doctest Cldr.Number.Transliterate 13 | doctest Cldr.Number.Format.Compiler 14 | doctest Cldr.Number.Parser 15 | 16 | doctest Cldr.Number.Formatter.Decimal 17 | doctest Cldr.Number.Formatter.Short 18 | doctest Cldr.Number.Formatter.Currency 19 | 20 | doctest TestBackend.Cldr.Number.System 21 | doctest TestBackend.Cldr.Number 22 | doctest TestBackend.Cldr.Number.Symbol 23 | doctest TestBackend.Cldr.Number.Format 24 | doctest TestBackend.Cldr.Number.Transliterate 25 | 26 | doctest TestBackend.Cldr.Rbnf.Ordinal 27 | doctest TestBackend.Cldr.Rbnf.Spellout 28 | doctest TestBackend.Cldr.Rbnf.NumberSystem 29 | 30 | Application.put_env(:ex_cldr, :default_backend, default) 31 | end 32 | -------------------------------------------------------------------------------- /test/meta_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Cldr.Number.Format.Meta.Test do 2 | use ExUnit.Case, async: true 3 | 4 | test "that we can create a default metadata struct" do 5 | assert Cldr.Number.Format.Meta.new() == 6 | %Cldr.Number.Format.Meta{ 7 | exponent_digits: 0, 8 | exponent_sign: false, 9 | format: [ 10 | positive: [format: "#"], 11 | negative: [minus: ~c"-", format: :same_as_positive] 12 | ], 13 | fractional_digits: %{max: 0, min: 0}, 14 | grouping: %{fraction: %{first: 0, rest: 0}, integer: %{first: 0, rest: 0}}, 15 | integer_digits: %{max: 0, min: 1}, 16 | multiplier: 1, 17 | number: 0, 18 | padding_char: " ", 19 | padding_length: 0, 20 | round_nearest: 0, 21 | scientific_rounding: 0, 22 | significant_digits: %{max: 0, min: 0} 23 | } 24 | end 25 | 26 | test "setting the meta fields" do 27 | alias Cldr.Number.Format.Meta 28 | 29 | meta = 30 | Meta.new() 31 | |> Meta.put_integer_digits(2) 32 | |> Meta.put_fraction_digits(3) 33 | |> Meta.put_significant_digits(4) 34 | 35 | assert meta.integer_digits == %{max: 0, min: 2} 36 | assert meta.fractional_digits == %{max: 0, min: 3} 37 | assert meta.significant_digits == %{max: 0, min: 4} 38 | 39 | meta = 40 | meta 41 | |> Meta.put_exponent_digits(5) 42 | |> Meta.put_exponent_sign(true) 43 | |> Meta.put_scientific_rounding_digits(6) 44 | |> Meta.put_round_nearest_digits(7) 45 | |> Meta.put_padding_length(8) 46 | |> Meta.put_padding_char("Z") 47 | |> Meta.put_multiplier(9) 48 | 49 | assert meta.exponent_digits == 5 50 | assert meta.exponent_sign == true 51 | assert meta.scientific_rounding == 6 52 | assert meta.round_nearest == 7 53 | assert meta.padding_length == 8 54 | assert meta.padding_char == "Z" 55 | assert meta.multiplier == 9 56 | 57 | meta = 58 | meta 59 | |> Meta.put_fraction_grouping(10) 60 | |> Meta.put_integer_grouping(11) 61 | 62 | assert meta.grouping == %{ 63 | fraction: %{first: 10, rest: 10}, 64 | integer: %{first: 11, rest: 11} 65 | } 66 | 67 | meta = 68 | Meta.new() 69 | |> Meta.put_integer_digits(12, 13) 70 | |> Meta.put_fraction_digits(14, 15) 71 | |> Meta.put_significant_digits(16, 17) 72 | 73 | assert meta.integer_digits == %{max: 13, min: 12} 74 | assert meta.fractional_digits == %{max: 15, min: 14} 75 | assert meta.significant_digits == %{max: 17, min: 16} 76 | 77 | meta = 78 | Meta.new() 79 | |> Meta.put_format([format: "#"], format: "##") 80 | 81 | assert meta.format == [positive: [format: "#"], negative: [format: "##"]] 82 | end 83 | end 84 | -------------------------------------------------------------------------------- /test/number/currency_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Cldr.Number.CurrencyTest do 2 | use ExUnit.Case, async: true 3 | 4 | test "That the currency is derived from the locale" do 5 | assert {:ok, "AU$๑๒๓.๐๐"} == 6 | MyApp.Cldr.Number.to_string(123, 7 | locale: "th-u-cu-aud-nu-thai", 8 | currency: :from_locale 9 | ) 10 | 11 | assert {:ok, "฿๑๒๓.๐๐"} == 12 | MyApp.Cldr.Number.to_string(123, locale: "th-u-nu-thai", currency: :from_locale) 13 | 14 | assert {:ok, "฿123.00"} == 15 | MyApp.Cldr.Number.to_string(123, locale: "th", currency: :from_locale) 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /test/number/decimal_format_compiler_test.exs: -------------------------------------------------------------------------------- 1 | defmodule DecimalFormatCompiler.Test do 2 | use ExUnit.Case, async: true 3 | 4 | alias Cldr.Number.Format 5 | 6 | Enum.each(TestBackend.Cldr.Number.Format.decimal_format_list(), fn format -> 7 | test "Compile decimal format #{inspect(format)}" do 8 | assert {:ok, _result} = Format.Compiler.parse(unquote(format)) 9 | end 10 | end) 11 | 12 | test "compile fails on empty format" do 13 | assert {:error, _result} = Format.Compiler.parse("") 14 | end 15 | 16 | test "compile fails on nil format" do 17 | assert {:error, _result} = Format.Compiler.parse(nil) 18 | end 19 | 20 | test "that we can parse a token list" do 21 | {:ok, tokens, _} = Format.Compiler.tokenize("#") 22 | assert {:ok, _parse_tree} = Format.Compiler.parse(tokens) 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /test/number/exponent_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Cldr.Number.Exponent.Test do 2 | use ExUnit.Case, async: true 3 | 4 | test "An integer number exponent format with precision" do 5 | assert {:ok, "1.2E3"} = MyApp.Cldr.Number.to_string(1234, format: "0.0E0") 6 | assert {:ok, "1.23E3"} = MyApp.Cldr.Number.to_string(1234, format: "0.00E0") 7 | assert {:ok, "1.234E3"} = MyApp.Cldr.Number.to_string(1234, format: "0.000E0") 8 | end 9 | 10 | test "A float number exponent format with precision" do 11 | assert {:ok, "1.2E3"} = MyApp.Cldr.Number.to_string(1234.5678, format: "0.0E0") 12 | assert {:ok, "1.23E3"} = MyApp.Cldr.Number.to_string(1234.5678, format: "0.00E0") 13 | assert {:ok, "1.235E3"} = MyApp.Cldr.Number.to_string(1234.5678, format: "0.000E0") 14 | assert {:ok, "1.23457E3"} = MyApp.Cldr.Number.to_string(1234.5678, format: "0.00000E0") 15 | end 16 | 17 | test "A Decimal number exponent format with precision" do 18 | assert {:ok, "1.2E3"} = MyApp.Cldr.Number.to_string(Decimal.new("1234.5678"), format: "0.0E0") 19 | 20 | assert {:ok, "1.23E3"} = 21 | MyApp.Cldr.Number.to_string(Decimal.new("1234.5678"), format: "0.00E0") 22 | 23 | assert {:ok, "1.235E3"} = 24 | MyApp.Cldr.Number.to_string(Decimal.new("1234.5678"), format: "0.000E0") 25 | 26 | assert {:ok, "1.23457E3"} = 27 | MyApp.Cldr.Number.to_string(Decimal.new("1234.5678"), format: "0.00000E0") 28 | end 29 | 30 | test "An integer number exponent format with exponent precision" do 31 | assert {:ok, "1.2E03"} = MyApp.Cldr.Number.to_string(1234, format: "0.0E00") 32 | assert {:ok, "1.23E003"} = MyApp.Cldr.Number.to_string(1234, format: "0.00E000") 33 | assert {:ok, "1.234E0003"} = MyApp.Cldr.Number.to_string(1234, format: "0.000E0000") 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /test/number/number_format_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Number.Format.Test do 2 | use ExUnit.Case, async: true 3 | 4 | Enum.each(Cldr.Test.Number.Format.test_data(), fn {value, result, args} -> 5 | new_args = 6 | if args[:locale] do 7 | Keyword.put(args, :locale, TestBackend.Cldr.Locale.new!(Keyword.get(args, :locale))) 8 | else 9 | args 10 | end 11 | 12 | test "formatted #{inspect(value)} == #{inspect(result)} with args: #{inspect(args)}" do 13 | assert {:ok, unquote(result)} = 14 | TestBackend.Cldr.Number.to_string(unquote(value), unquote(Macro.escape(new_args))) 15 | end 16 | end) 17 | 18 | test "to_string with no arguments" do 19 | assert {:ok, "1,234"} = Cldr.Number.to_string(1234) 20 | end 21 | 22 | test "to_string with only options" do 23 | assert {:ok, "1.234"} = Cldr.Number.to_string(1234, locale: "de") 24 | end 25 | 26 | test "literal-only format returns the literal" do 27 | assert {:ok, "xxx"} = TestBackend.Cldr.Number.to_string(1234, format: "xxx") 28 | end 29 | 30 | test "formatted float with rounding" do 31 | assert {:ok, "1.40"} == TestBackend.Cldr.Number.to_string(1.4, fractional_digits: 2) 32 | end 33 | 34 | test "a currency format with no currency returns a currency formatted number" do 35 | assert {:ok, "1,234.00"} = TestBackend.Cldr.Number.to_string(1234, format: :currency) 36 | end 37 | 38 | test "that -0 is formatted as 0" do 39 | number = Decimal.new("-0") 40 | assert TestBackend.Cldr.Number.to_string(number) == {:ok, "0"} 41 | end 42 | 43 | test "minimum_grouping digits delegates to Cldr.Number.Symbol" do 44 | assert TestBackend.Cldr.Number.Format.minimum_grouping_digits_for!("en") == 1 45 | end 46 | 47 | test "that there are decimal formats for a locale" do 48 | assert Map.keys(TestBackend.Cldr.Number.Format.all_formats_for!("en")) == [:latn] 49 | end 50 | 51 | test "that there is an exception if we get formats for an unknown locale" do 52 | assert_raise Cldr.InvalidLanguageError, ~r/The language .* is invalid/, fn -> 53 | TestBackend.Cldr.Number.Format.formats_for!("zzz") 54 | end 55 | end 56 | 57 | test "that there is an exception if we get formats for an number system" do 58 | assert_raise Cldr.UnknownNumberSystemError, ~r/The number system \"zulu\" is invalid/, fn -> 59 | TestBackend.Cldr.Number.Format.formats_for!("en", "zulu") 60 | end 61 | end 62 | 63 | test "that an rbnf format request fails if the locale doesn't define the ruleset" do 64 | assert TestBackend.Cldr.Number.to_string(123, format: :spellout_ordinal_verbose, locale: "zh") == 65 | { 66 | :error, 67 | { 68 | Cldr.Rbnf.NoRule, 69 | "RBNF rule :spellout_ordinal_verbose is unknown to locale TestBackend.Cldr.Locale.new!(\"zh-Hans-CN\")" 70 | } 71 | } 72 | end 73 | 74 | test "that we get default formats_for" do 75 | assert TestBackend.Cldr.Number.Format.formats_for!().__struct__ == Cldr.Number.Format 76 | end 77 | 78 | test "setting currency_format: :iso" do 79 | assert TestBackend.Cldr.Number.to_string(123, currency: :USD, currency_symbol: :iso) == 80 | {:ok, "USD 123.00"} 81 | end 82 | 83 | test "round_nearest to_string parameter" do 84 | assert Cldr.Number.to_string(1234, MyApp.Cldr, round_nearest: 5) == {:ok, "1,235"} 85 | assert Cldr.Number.to_string(1231, MyApp.Cldr, round_nearest: 5) == {:ok, "1,230"} 86 | assert Cldr.Number.to_string(1234, MyApp.Cldr, round_nearest: 10) == {:ok, "1,230"} 87 | assert Cldr.Number.to_string(1231, MyApp.Cldr, round_nearest: 10) == {:ok, "1,230"} 88 | assert Cldr.Number.to_string(1235, MyApp.Cldr, round_nearest: 10) == {:ok, "1,240"} 89 | end 90 | 91 | test "fraction digits of 0" do 92 | assert Cldr.Number.to_string(50.12, MyApp.Cldr, fractional_digits: 0, currency: :USD) == 93 | {:ok, "$50"} 94 | 95 | assert Cldr.Number.to_string(50.82, MyApp.Cldr, fractional_digits: 0, currency: :USD) == 96 | {:ok, "$51"} 97 | end 98 | 99 | test "to_string with :percent format" do 100 | assert MyApp.Cldr.Number.to_string!(123.456, format: :percent, fractional_digits: 1) == 101 | "12,345.6%" 102 | end 103 | 104 | test "negative decimal short formatting" do 105 | assert MyApp.Cldr.Number.to_string(Decimal.new(1_000_000), format: :short) == {:ok, "1M"} 106 | assert MyApp.Cldr.Number.to_string(Decimal.new(-1_000_000), format: :short) == {:ok, "-1M"} 107 | assert MyApp.Cldr.Number.to_string(Decimal.new(100_000), format: :short) == {:ok, "100K"} 108 | assert MyApp.Cldr.Number.to_string(Decimal.new(-100_000), format: :short) == {:ok, "-100K"} 109 | assert MyApp.Cldr.Number.to_string(Decimal.new(1_000), format: :short) == {:ok, "1K"} 110 | assert MyApp.Cldr.Number.to_string(Decimal.new(-1_000), format: :short) == {:ok, "-1K"} 111 | end 112 | 113 | test "Decimal currency short formatting" do 114 | assert MyApp.Cldr.Number.to_string(Decimal.new(1_000_000), format: :currency_short) == 115 | {:ok, "$1M"} 116 | 117 | assert MyApp.Cldr.Number.to_string(Decimal.new(1_100_000), 118 | format: :currency_short, 119 | fractional_digits: 1 120 | ) == {:ok, "$1.1M"} 121 | 122 | assert MyApp.Cldr.Number.to_string(Decimal.new(1_000_000), format: :short) == {:ok, "1M"} 123 | 124 | assert MyApp.Cldr.Number.to_string(Decimal.new(1_100_000), 125 | format: :short, 126 | fractional_digits: 1 127 | ) == {:ok, "1.1M"} 128 | end 129 | 130 | test "Currency with :narrow formatting uses standard format with narrow symbol" do 131 | assert MyApp.Cldr.Number.to_string(Decimal.new(1_000_000), currency: :USD, format: :narrow) == 132 | {:ok, "$1,000,000.00"} 133 | end 134 | 135 | test "Decimal currency short with fractional digits formatting" do 136 | assert Cldr.Number.to_string(Decimal.new("214564569.50"), 137 | format: :short, 138 | currency: :USD, 139 | fractional_digits: 2 140 | ) == 141 | {:ok, "$214.56M"} 142 | 143 | assert Cldr.Number.to_string(Decimal.new("219.50"), 144 | format: :short, 145 | currency: :USD, 146 | fractional_digits: 2 147 | ) == 148 | {:ok, "$219.50"} 149 | 150 | assert Cldr.Number.to_string(Decimal.from_float(219.50), 151 | format: :short, 152 | currency: :USD, 153 | fractional_digits: 2 154 | ) == 155 | {:ok, "$219.50"} 156 | end 157 | 158 | test "Currency format long with symbol" do 159 | assert Cldr.Number.to_string(1_000_000_000, format: :currency_long_with_symbol, locale: "fr") == 160 | {:ok, "1 milliard €"} 161 | 162 | assert Cldr.Number.to_string(1_000_000_000, format: :currency_long_with_symbol) == 163 | {:ok, "$1 billion"} 164 | 165 | assert Cldr.Number.to_string(1_000_000_000, format: :currency_long_with_symbol, locale: "hr") == 166 | {:ok, "1 milijardi €"} 167 | 168 | assert Cldr.Number.to_string(1_000_000_000, 169 | format: :currency_long_with_symbol, 170 | locale: "hr", 171 | currency: :USD 172 | ) == 173 | {:ok, "1 milijardi USD"} 174 | 175 | assert Cldr.Number.to_string(1_234_545_656.456789, 176 | currency: "BTC", 177 | format: :currency_long_with_symbol 178 | ) == 179 | {:ok, "₿1 billion"} 180 | end 181 | 182 | test "NaN and Inf decimal number formatting" do 183 | assert {:ok, "NaN"} = Cldr.Number.to_string(Decimal.new("NaN")) 184 | assert {:ok, "-NaN"} = Cldr.Number.to_string(Decimal.new("-NaN")) 185 | assert {:ok, "∞"} = Cldr.Number.to_string(Decimal.new("Inf")) 186 | assert {:ok, "-∞"} = Cldr.Number.to_string(Decimal.new("-Inf")) 187 | assert {:ok, "∞"} = Cldr.Number.to_string(Decimal.new("Inf"), locale: :de) 188 | assert {:ok, "-∞"} = Cldr.Number.to_string(Decimal.new("-Inf"), locale: :de) 189 | 190 | assert {:ok, "-∞"} = 191 | Cldr.Number.to_string(Decimal.new("-Inf"), format: "########", locale: :de) 192 | 193 | assert {:ok, "∞ and beyond"} = 194 | Cldr.Number.to_string(Decimal.new("Inf"), format: "# and beyond") 195 | end 196 | 197 | test "A free form currency format where the currency symbol is not first or last" do 198 | assert {:ok, "US$1,234"} = 199 | MyApp.Cldr.Number.to_string(1234, currency: "USD", format: "US¤#,###") 200 | end 201 | 202 | if System.otp_release() < "28" do 203 | test "Digital tokens with overriden symbols" do 204 | assert {:ok, "₿ 1,234,545,656.456789"} = 205 | Cldr.Number.to_string(1_234_545_656.456789, 206 | currency: "BTC", 207 | currency_symbol: :narrow 208 | ) 209 | 210 | assert {:ok, "BTC 1,234,545,656.456789"} = 211 | Cldr.Number.to_string(1_234_545_656.456789, currency: "BTC", currency_symbol: :iso) 212 | 213 | assert {:ok, "₿ 1,234,545,656.456789"} = 214 | Cldr.Number.to_string(1_234_545_656.456789, 215 | currency: "BTC", 216 | currency_symbol: :symbol 217 | ) 218 | 219 | assert {:ok, "₿ 1,234,545,656.456789"} = 220 | Cldr.Number.to_string(1_234_545_656.456789, 221 | currency: "BTC", 222 | currency_symbol: :standard 223 | ) 224 | 225 | assert {:ok, "XBTC 1,234,545,656.456789"} = 226 | Cldr.Number.to_string(1_234_545_656.456789, currency: "BTC", currency_symbol: "XBTC") 227 | end 228 | else 229 | test "Digital tokens with overriden symbols" do 230 | assert {:ok, "₿1,234,545,656.456789"} = 231 | Cldr.Number.to_string(1_234_545_656.456789, 232 | currency: "BTC", 233 | currency_symbol: :narrow 234 | ) 235 | 236 | assert {:ok, "BTC 1,234,545,656.456789"} = 237 | Cldr.Number.to_string(1_234_545_656.456789, currency: "BTC", currency_symbol: :iso) 238 | 239 | assert {:ok, "₿1,234,545,656.456789"} = 240 | Cldr.Number.to_string(1_234_545_656.456789, 241 | currency: "BTC", 242 | currency_symbol: :symbol 243 | ) 244 | 245 | assert {:ok, "₿1,234,545,656.456789"} = 246 | Cldr.Number.to_string(1_234_545_656.456789, 247 | currency: "BTC", 248 | currency_symbol: :standard 249 | ) 250 | 251 | assert {:ok, "XBTC 1,234,545,656.456789"} = 252 | Cldr.Number.to_string(1_234_545_656.456789, currency: "BTC", currency_symbol: "XBTC") 253 | end 254 | end 255 | 256 | test "Formatting a number with standard format in a locale with no RBNF" do 257 | for {_locale, language_tag} <- Cldr.Config.all_language_tags(), 258 | is_nil(language_tag.rbnf_locale_name) do 259 | assert {:ok, _formatted_number} = Cldr.Number.to_string(1234, locale: language_tag) 260 | end 261 | end 262 | 263 | test "that each number system for each locale can format a number with standard format" do 264 | for locale <- TestBackend.Cldr.known_locale_names() do 265 | {:ok, systems} = TestBackend.Cldr.Number.System.number_systems_for(locale) 266 | number_systems = Enum.uniq(Map.keys(systems) ++ Map.values(systems)) 267 | 268 | for number_system <- number_systems do 269 | assert {:ok, _} = 270 | TestBackend.Cldr.Number.to_string(123, 271 | locale: locale, 272 | number_system: number_system 273 | ) 274 | end 275 | end 276 | end 277 | 278 | test "Formatting when a currency is specified but the format has no currency symbol" do 279 | assert {:ok, _} = 280 | Cldr.Number.to_string(1234, 281 | curency: :CAD, 282 | format: "#,##0.###;-#,##0.###", 283 | locale: :en 284 | ) 285 | end 286 | 287 | test "Formatting a currency the standard format if the it has a space before the currency placeholder" do 288 | assert {:ok, "123,45\u00A0kr."} = 289 | TestBackend.Cldr.Number.to_string(123.45, locale: :da, currency: :DKK) 290 | end 291 | end 292 | -------------------------------------------------------------------------------- /test/number/number_parser_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Cldr.Number.Parsing.Test do 2 | use ExUnit.Case, async: true 3 | 4 | test "parse numbers" do 5 | assert Cldr.Number.Parser.parse("100", backend: TestBackend.Cldr) == {:ok, 100} 6 | assert Cldr.Number.Parser.parse("100.0", backend: TestBackend.Cldr) == {:ok, 100.0} 7 | assert Cldr.Number.Parser.parse("1_000", backend: TestBackend.Cldr) == {:ok, 1000} 8 | assert Cldr.Number.Parser.parse("+1_000.0", backend: TestBackend.Cldr) == {:ok, 1000.0} 9 | assert Cldr.Number.Parser.parse("-100", backend: TestBackend.Cldr) == {:ok, -100} 10 | assert Cldr.Number.Parser.parse(" 100 ", backend: TestBackend.Cldr) == {:ok, 100} 11 | end 12 | 13 | test "parse numbers with locale other than en" do 14 | assert Cldr.Number.Parser.parse("1.000,00", backend: TestBackend.Cldr, locale: "de") == 15 | {:ok, 1000.0} 16 | end 17 | 18 | test "scan a number string" do 19 | assert Cldr.Number.Parser.scan("100 australian dollars", backend: TestBackend.Cldr) == 20 | [100, " australian dollars"] 21 | 22 | assert Cldr.Number.Parser.scan("us dollars 100", backend: TestBackend.Cldr) == 23 | ["us dollars ", 100] 24 | end 25 | 26 | test "resolving currency and value" do 27 | result = 28 | Cldr.Number.Parser.scan("us dollars 100", backend: TestBackend.Cldr) 29 | |> Cldr.Number.Parser.resolve_currencies(backend: TestBackend.Cldr) 30 | 31 | assert result == [:USD, 100] 32 | 33 | result2 = 34 | Cldr.Number.Parser.scan("$100", backend: TestBackend.Cldr) 35 | |> Cldr.Number.Parser.resolve_currencies(backend: TestBackend.Cldr) 36 | 37 | assert result2 == [:USD, 100] 38 | 39 | result3 = 40 | Cldr.Number.Parser.scan("R100", backend: TestBackend.Cldr) 41 | |> Cldr.Number.Parser.resolve_currencies(backend: TestBackend.Cldr) 42 | 43 | assert result3 == [:ZAR, 100] 44 | end 45 | 46 | test "scanning strings that have symbols in them" do 47 | assert Cldr.Number.Parser.scan("a string, which I think. Well, sometimes not £1_000_000.34") == 48 | ["a string, which I think. Well, sometimes not £", 1_000_000.34] 49 | end 50 | 51 | test "parse a decimal" do 52 | {:ok, parsed} = 53 | Cldr.Number.Parser.parse("1.000,00", 54 | number: :decimal, 55 | backend: TestBackend.Cldr, 56 | locale: "de" 57 | ) 58 | 59 | assert Cldr.Decimal.compare(parsed, Decimal.from_float(1000.0)) == :eq 60 | end 61 | 62 | test "Parse a list of numbers separated by comma" do 63 | assert Cldr.Number.Parser.scan("Here's my number list: 1111, 2222, 3333, 4444, 55,55", 64 | number: :decimal 65 | ) == 66 | [ 67 | "Here's my number list: ", 68 | Decimal.new(1111), 69 | ", ", 70 | Decimal.new(2222), 71 | ", ", 72 | Decimal.new(3333), 73 | ", ", 74 | Decimal.new(4444), 75 | ", ", 76 | Decimal.new(5555) 77 | ] 78 | end 79 | 80 | test "Parsing a locale with a grouping character that is a pop space" do 81 | string = Cldr.Number.to_string!(12345, locale: "fr") 82 | assert Cldr.Number.Parser.scan(string, locale: "fr") == [12345] 83 | assert Cldr.Number.Parser.parse(string, locale: "fr") == {:ok, 12345} 84 | end 85 | 86 | test "Parsing a locale with a grouping character that is a pop space but using 0x20 group char" do 87 | # pop space is 0x202c 88 | assert Cldr.Number.Parser.scan("This with normal space 12 345", locale: "fr") == 89 | ["This with normal space ", 12345] 90 | 91 | assert Cldr.Number.Parser.scan("This is with pop space 12 345", locale: "fr") == 92 | ["This is with pop space ", 12345] 93 | 94 | assert Cldr.Number.Parser.scan("This with normal space 12 345", locale: "en") == 95 | ["This with normal space ", 12, " ", 345] 96 | end 97 | 98 | test "Resolving currencies in a string" do 99 | scanned = 100 | Cldr.Number.Parser.scan( 101 | "Lets try this 123 US dollars, a bunch of US dollars 23 and 345 euros" 102 | ) 103 | 104 | assert Cldr.Number.Parser.resolve_currencies(scanned) == 105 | ["Lets try this ", 123, :USD, ", a bunch of ", :USD, 23, " and ", 345, :EUR] 106 | 107 | scanned = 108 | Cldr.Number.Parser.scan( 109 | "Lets try this 123 US dollars, a bunch of swiss francs 23 and 345 euros" 110 | ) 111 | 112 | assert Cldr.Number.Parser.resolve_currencies(scanned) == 113 | ["Lets try this ", 123, :USD, ", a bunch of ", :CHF, 23, " and ", 345, :EUR] 114 | end 115 | 116 | test "That resolving only happens when there is a non-alpha adjacent char" do 117 | scanned = Cldr.Number.Parser.scan("These are 100 us dollars and also nonswiss francs") 118 | 119 | assert Cldr.Number.Parser.resolve_currencies(scanned) == 120 | ["These are ", 100, :USD, " and also nonswiss francs"] 121 | 122 | scanned = Cldr.Number.Parser.scan("These are 100 us dollars and also non swiss francs") 123 | 124 | assert Cldr.Number.Parser.resolve_currencies(scanned) == 125 | ["These are ", 100, :USD, " and also non ", :CHF] 126 | 127 | scanned = Cldr.Number.Parser.scan("These are us dollars 100 and also nonswiss francs") 128 | 129 | assert Cldr.Number.Parser.resolve_currencies(scanned) == 130 | ["These are ", :USD, 100, " and also nonswiss francs"] 131 | 132 | scanned = Cldr.Number.Parser.scan("These areus dollars 100 and also nonswiss francs") 133 | 134 | assert Cldr.Number.Parser.resolve_currencies(scanned) == 135 | ["These areus dollars ", 100, " and also nonswiss francs"] 136 | end 137 | 138 | test "That parsing returns the same case as the search for strings" do 139 | scanned = Cldr.Number.Parser.scan("Try ThCA$2is") 140 | assert Cldr.Number.Parser.resolve_currencies(scanned) == [:TRY, " ThCA$", 2, "is"] 141 | 142 | scanned = Cldr.Number.Parser.scan("Try Th CA$2is") 143 | assert Cldr.Number.Parser.resolve_currencies(scanned) == [:TRY, " Th ", :CAD, 2, "is"] 144 | end 145 | 146 | test "Resolve currencies when the string is only whitespace" do 147 | string = [" "] 148 | assert Cldr.Number.Parser.resolve_currencies(string) == string 149 | end 150 | end 151 | -------------------------------------------------------------------------------- /test/number/number_string_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Number.String.Test do 2 | use ExUnit.Case, async: true 3 | 4 | alias Cldr.Number 5 | 6 | test "that the regexp for latin1 is correct" do 7 | assert Number.String.latin1().source == "([\\x00-\\x7F])" 8 | end 9 | 10 | test "that the regexp for not latin1 is correct" do 11 | assert Number.String.not_latin1().source == "([^\\x00-\\x7F])" 12 | end 13 | 14 | test "that we transliterate a non latin1 character" do 15 | s = Number.String.hex_string("¤") 16 | assert s == "\\x164" 17 | end 18 | 19 | test "that padding a string with a negative count returns the string" do 20 | assert Number.String.pad_leading_zeros("1234", -5) == "1234" 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /test/number/number_symbol_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Number.Symbol.Test do 2 | use ExUnit.Case, async: true 3 | 4 | test "that we can get number symbols for a known locale" do 5 | {:ok, symbols} = TestBackend.Cldr.Number.Symbol.number_symbols_for("en", "latn") 6 | 7 | assert symbols == 8 | %Cldr.Number.Symbol{ 9 | decimal: %{standard: "."}, 10 | exponential: "E", 11 | group: %{standard: ","}, 12 | infinity: "∞", 13 | list: ";", 14 | minus_sign: "-", 15 | nan: "NaN", 16 | per_mille: "‰", 17 | percent_sign: "%", 18 | plus_sign: "+", 19 | superscripting_exponent: "×", 20 | time_separator: ":" 21 | } 22 | end 23 | 24 | test "that we raise an error if we get minimum digits for an invalid locale" do 25 | assert_raise Cldr.InvalidLanguageError, ~r/The language .* is invalid/, fn -> 26 | TestBackend.Cldr.Number.Format.minimum_grouping_digits_for!("zzzzz") 27 | end 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /test/number/number_system_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Number.System.Test do 2 | use ExUnit.Case, async: true 3 | 4 | test "that number_system_for with a system name returns" do 5 | {:ok, system} = TestBackend.Cldr.Number.System.number_system_for("en", :latn) 6 | assert system == %{digits: "0123456789", type: :numeric} 7 | end 8 | 9 | test "that number_systems_for raises when the locale is not known" do 10 | assert_raise Cldr.InvalidLanguageError, ~r/The language \"zzz\" is invalid/, fn -> 11 | TestBackend.Cldr.Number.System.number_systems_for!("zzz") 12 | end 13 | end 14 | 15 | test "that number_system_names_for raises when the locale is not known" do 16 | assert_raise Cldr.InvalidLanguageError, ~r/The language .* is invalid/, fn -> 17 | TestBackend.Cldr.Number.System.number_system_names_for!("zzz") 18 | end 19 | end 20 | 21 | test "that number_systems_like returns a list" do 22 | {:ok, likes} = Cldr.Number.System.number_systems_like("en", :latn, TestBackend.Cldr) 23 | assert is_list(likes) 24 | assert Enum.count(likes) > 100 25 | end 26 | 27 | test "that locale u extension number system overrides default" do 28 | assert TestBackend.Cldr.Number.to_string(1234, locale: "th-u-nu-thai") == {:ok, "๑,๒๓๔"} 29 | assert TestBackend.Cldr.Number.to_string(1234, locale: "th-u-nu-latn") == {:ok, "1,234"} 30 | end 31 | 32 | test "that number_system parameter overrides the locale u number system" do 33 | assert TestBackend.Cldr.Number.to_string(1234, locale: "th-u-nu-latn", number_system: :thai) == 34 | {:ok, "๑,๒๓๔"} 35 | end 36 | 37 | test "that a locale u number system that is not valid for a locale returns an error" do 38 | assert TestBackend.Cldr.Number.to_string(1234, locale: "en-AU-u-nu-thai") == 39 | {:error, 40 | {Cldr.UnknownNumberSystemError, 41 | "The number system :thai is unknown for the locale named :\"en-AU\". Valid number systems are %{default: :latn, native: :latn}"}} 42 | end 43 | 44 | test "that an invalid nu returns an error" do 45 | assert TestBackend.Cldr.Number.to_string(123, locale: "en-AU-u-nu-xxx") == 46 | {:error, 47 | {Cldr.LanguageTag.ParseError, "The value \"xxx\" is not valid for the key \"nu\""}} 48 | end 49 | end 50 | -------------------------------------------------------------------------------- /test/number/rbnf_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Rbnf.Test do 2 | use ExUnit.Case, async: true 3 | 4 | alias TestBackend.Cldr 5 | 6 | test "rbnf spellout" do 7 | assert {:ok, "twenty-five thousand three hundred forty"} = 8 | Cldr.Number.to_string(25_340, format: :spellout) 9 | end 10 | 11 | test "rbnf spellout in german" do 12 | assert {:ok, "fünf­und­zwanzig­tausend­drei­hundert­vierzig"} = 13 | Cldr.Number.to_string(25_340, format: :spellout, locale: "de") 14 | end 15 | 16 | test "rbnf spellout in french" do 17 | assert {:ok, "vingt-cinq mille trois cent quarante"} = 18 | Cldr.Number.to_string(25_340, format: :spellout, locale: "fr") 19 | end 20 | 21 | test "rbnf spellout in spanish" do 22 | assert {:ok, "veinticinco mil trescientos cuarenta"} = 23 | Cldr.Number.to_string(25_340, format: :spellout, locale: "es") 24 | end 25 | 26 | test "rbnf spellout in mandarin" do 27 | assert {:ok, "二万五千三百四十"} = Cldr.Number.to_string(25_340, format: :spellout, locale: "zh") 28 | end 29 | 30 | test "rbnf spellout in hebrew" do 31 | assert {:ok, "עשרים וחמישה אלף שלוש מאות ארבעים"} = 32 | Cldr.Number.to_string(25_340, format: :spellout, locale: "he") 33 | end 34 | 35 | test "rbnf spellout ordinal in de" do 36 | assert {:ok, "fünf­und­zwanzig­tausend­drei­hundert­vierzigste"} = 37 | Cldr.Number.to_string(25_340, format: :spellout_ordinal, locale: "de") 38 | end 39 | 40 | test "rbnf spellout ordinal in ar" do 41 | assert {:ok, "خمسة وعشرون ألف وثلاثة مائة وأربعون"} = 42 | Cldr.Number.to_string(25_340, format: :spellout_ordinal_feminine, locale: "ar") 43 | 44 | assert {:ok, "خمسة وعشرون ألف وثلاثة مائة وأربعون"} = 45 | Cldr.Number.to_string(25_340, format: :spellout_ordinal_masculine, locale: "ar") 46 | end 47 | 48 | test "rbnf spellout in thai" do 49 | assert {:ok, "สอง​หมื่น​ห้า​พัน​สาม​ร้อย​สี่​สิบ"} = 50 | Cldr.Number.to_string(25_340, format: :spellout, locale: "th") 51 | end 52 | 53 | test "rbnf spellout ordinal verbose" do 54 | assert {:ok, "one hundred and twenty-three thousand, four hundred and fifty-sixth"} = 55 | Cldr.Number.to_string(123_456, format: :spellout_ordinal_verbose) 56 | end 57 | 58 | test "rbnf ordinal" do 59 | assert {:ok, "123,456th"} = Cldr.Number.to_string(123_456, format: :ordinal) 60 | 61 | assert {:ok, "123 456e"} = Cldr.Number.to_string(123_456, format: :ordinal, locale: "fr") 62 | end 63 | 64 | test "rbnf improper fraction" do 65 | assert Cldr.Rbnf.Spellout.spellout_cardinal_verbose(123.456, "en") == 66 | "one hundred and twenty-three point four five six" 67 | 68 | assert Cldr.Rbnf.Spellout.spellout_cardinal_verbose(-123.456, "en") == 69 | "minus one hundred and twenty-three point four five six" 70 | 71 | assert Cldr.Rbnf.Spellout.spellout_cardinal_verbose(-0.456, "en") == 72 | "minus zero point four five six" 73 | 74 | assert Cldr.Rbnf.Spellout.spellout_cardinal_verbose(0.456, "en") == "zero point four five six" 75 | 76 | assert Cldr.Rbnf.Spellout.spellout_cardinal(0.456, "en") == "zero point four five six" 77 | 78 | assert Cldr.Rbnf.Spellout.spellout_cardinal(0, "en") == "zero" 79 | assert Cldr.Rbnf.Spellout.spellout_ordinal(0, "en") == "zeroth" 80 | assert Cldr.Rbnf.Spellout.spellout_ordinal(0.0, "en") == "0" 81 | assert Cldr.Rbnf.Spellout.spellout_ordinal(0.1, "en") == "0.1" 82 | end 83 | 84 | test "decimal rbnf for decimal integers" do 85 | assert {:ok, "123,456th"} = Cldr.Number.to_string(Decimal.new(123_456), format: :ordinal) 86 | 87 | assert {:ok, "123 456e"} = 88 | Cldr.Number.to_string( 89 | Decimal.new(123_456), 90 | format: :ordinal, 91 | locale: "fr" 92 | ) 93 | 94 | assert {:ok, "one hundred and twenty-three thousand, four hundred and fifty-sixth"} = 95 | Cldr.Number.to_string(Decimal.new(123_456), format: :spellout_ordinal_verbose) 96 | 97 | assert {:ok, "twenty-five thousand three hundred forty"} = 98 | Cldr.Number.to_string(Decimal.new(25_340), format: :spellout) 99 | 100 | assert Cldr.Rbnf.Spellout.spellout_cardinal(Decimal.new(0), "en") == "zero" 101 | assert Cldr.Rbnf.Spellout.spellout_ordinal(Decimal.new(0), "en") == "zeroth" 102 | end 103 | 104 | test "roman numerals" do 105 | assert Cldr.Number.to_string(1, format: :roman) == {:ok, "I"} 106 | assert Cldr.Number.to_string(2, format: :roman) == {:ok, "II"} 107 | assert Cldr.Number.to_string(3, format: :roman) == {:ok, "III"} 108 | assert Cldr.Number.to_string(4, format: :roman) == {:ok, "IV"} 109 | assert Cldr.Number.to_string(5, format: :roman) == {:ok, "V"} 110 | assert Cldr.Number.to_string(6, format: :roman) == {:ok, "VI"} 111 | assert Cldr.Number.to_string(7, format: :roman) == {:ok, "VII"} 112 | assert Cldr.Number.to_string(8, format: :roman) == {:ok, "VIII"} 113 | assert Cldr.Number.to_string(9, format: :roman) == {:ok, "IX"} 114 | assert Cldr.Number.to_string(10, format: :roman) == {:ok, "X"} 115 | assert Cldr.Number.to_string(11, format: :roman) == {:ok, "XI"} 116 | assert Cldr.Number.to_string(20, format: :roman) == {:ok, "XX"} 117 | assert Cldr.Number.to_string(50, format: :roman) == {:ok, "L"} 118 | assert Cldr.Number.to_string(90, format: :roman) == {:ok, "XC"} 119 | assert Cldr.Number.to_string(100, format: :roman) == {:ok, "C"} 120 | assert Cldr.Number.to_string(1000, format: :roman) == {:ok, "M"} 121 | assert Cldr.Number.to_string(123, format: :roman) == {:ok, "CXXIII"} 122 | end 123 | 124 | test "no rule is available for number" do 125 | assert Cldr.Rbnf.Spellout.spellout_numbering_year(-24, "zh-Hant") == 126 | { 127 | :error, 128 | { 129 | Elixir.Cldr.Rbnf.NoRuleForNumber, 130 | "rule group :spellout_numbering_year for locale :\"zh-Hant\" does not know how to process -24" 131 | } 132 | } 133 | end 134 | 135 | test "that rbnf rules lookup fall back to the root locale (und)" do 136 | # implemented in und locale 137 | assert Cldr.Number.to_string(123, format: :digits_ordinal, locale: "de") == 138 | {:ok, "123."} 139 | 140 | # implemented in en locale 141 | assert Cldr.Number.to_string(123, format: :digits_ordinal, locale: "en") == 142 | {:ok, "123rd"} 143 | end 144 | 145 | test "RBNF Spellout for spanish" do 146 | assert TestBackend.Cldr.Number.to_string(123.456, 147 | format: :spellout_cardinal_masculine, 148 | locale: :es 149 | ) == 150 | {:ok, "ciento veintitrés punto cuatro cinco seis"} 151 | 152 | assert TestBackend.Cldr.Number.to_string(123.456, 153 | format: :spellout_cardinal_feminine, 154 | locale: :es 155 | ) == 156 | {:ok, "ciento veintitrés punto cuatro cinco seis"} 157 | end 158 | 159 | Elixir.Cldr.Rbnf.TestSupport.rbnf_tests(fn name, tests, module, function, locale -> 160 | test name do 161 | Enum.each(unquote(Macro.escape(tests)), fn {test_data, test_result} -> 162 | if apply(unquote(module), unquote(function), [ 163 | String.to_integer(test_data), 164 | unquote(Macro.escape(locale)) 165 | ]) != test_result do 166 | IO.puts( 167 | "Test is failing on locale #{unquote(locale.requested_locale_name)} for value #{test_data}" 168 | ) 169 | end 170 | 171 | assert apply(unquote(module), unquote(function), [ 172 | String.to_integer(test_data), 173 | unquote(Macro.escape(locale)) 174 | ]) == test_result 175 | end) 176 | end 177 | end) 178 | end 179 | -------------------------------------------------------------------------------- /test/number/short_formatter_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Cldr.Number.ShortFormatter.Test do 2 | use ExUnit.Case, async: true 3 | 4 | test "Short formatter for a decimal" do 5 | number = Decimal.new("940038.00000000") 6 | assert {:ok, "940K"} = MyApp.Cldr.Number.to_string(number, format: :short) 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /test/number/significant_digits_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Significant.Digits.Test do 2 | use ExUnit.Case, async: true 3 | 4 | test "round 1,239,451 to 3 significant digits and return 1,240,000" do 5 | assert 1_240_000 == Cldr.Math.round_significant(1_239_451, 3) 6 | end 7 | 8 | test "round 12.1257 to 3 significant digits and return 12.1" do 9 | assert 12.1 == Cldr.Math.round_significant(12.1257, 3) 10 | end 11 | 12 | test "round .0681 to 3 significant digits and return .0681" do 13 | assert 0.0681 == Cldr.Math.round_significant(0.0681, 3) 14 | end 15 | 16 | test "round 5 to 3 significant digits and return 5" do 17 | assert 5 == Cldr.Math.round_significant(5, 3) 18 | end 19 | 20 | # From TR35 Section 3.5 21 | test "round 12345 to 3 significant digits and return 12300" do 22 | assert 12300 == Cldr.Math.round_significant(12345, 3) 23 | end 24 | 25 | test "round 0.12345 to 3 significant digits and return 0.123" do 26 | assert 0.123 == Cldr.Math.round_significant(0.12345, 3) 27 | end 28 | 29 | test "round 3.14159 to 4 significant digits and return 3.142" do 30 | assert 3.142 == Cldr.Math.round_significant(3.14159, 4) 31 | end 32 | 33 | test "round 1.23004 to 4 significant digits and return 1.23" do 34 | assert 1.23 == Cldr.Math.round_significant(1.23004, 4) 35 | end 36 | 37 | # Decimal tests 38 | test "round decimal 12345 to 3 significant digits and return 12300" do 39 | assert Cldr.Decimal.reduce(Decimal.new(12300)) == 40 | Cldr.Math.round_significant(Decimal.new(12345), 3) 41 | end 42 | 43 | test "round decimal 0.12345 to 3 significant digits and return 0.123" do 44 | assert Decimal.new("0.123") == Cldr.Math.round_significant(Decimal.new("0.12345"), 3) 45 | end 46 | 47 | test "round decimal 3.14159 to 4 significant digits and return 3.142" do 48 | assert Decimal.new("3.142") == Cldr.Math.round_significant(Decimal.new("3.14159"), 4) 49 | end 50 | 51 | test "round decimal 1.23004 to 4 significant digits and return 1.23" do 52 | assert Decimal.new("1.23") == Cldr.Math.round_significant(Decimal.new("1.23004"), 4) 53 | end 54 | 55 | test "round negative decimal -1.23004 to 4 significant digits and return 1.23" do 56 | assert Decimal.new("-1.23") == Cldr.Math.round_significant(Decimal.new("-1.23004"), 4) 57 | end 58 | end 59 | -------------------------------------------------------------------------------- /test/number/split_format_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Split.Format.Test do 2 | use ExUnit.Case, async: true 3 | 4 | Enum.each(Cldr.Test.Number.Split.Format.test_data(), fn {format, result} -> 5 | test "that a format splits correctly for #{inspect(format)}" do 6 | regex = Cldr.Number.Format.Compiler.number_match_regex() 7 | assert Regex.named_captures(regex, unquote(format)) == unquote(Macro.escape(result)) 8 | end 9 | end) 10 | end 11 | -------------------------------------------------------------------------------- /test/number/transliteration_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Transliteration.Test do 2 | use ExUnit.Case, async: true 3 | 4 | import ExUnit.CaptureLog 5 | 6 | test "that a dynamic transliteration generates a log message" do 7 | assert capture_log(fn -> 8 | TestBackend.Cldr.Number.Transliterate.transliterate_digits( 9 | "٠١٢٣٤٥٦٧٨٩", 10 | :arab, 11 | :java 12 | ) 13 | end) =~ "Transliteration from number system :arab to :java" 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /test/number/wrapper_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Number.FormatWrapper.Test do 2 | use ExUnit.Case, async: true 3 | 4 | test "Wrapping a number" do 5 | assert {:ok, "100"} = 6 | Cldr.Number.to_string(100, wrapper: &NumberWrapper.wrapper/2) 7 | end 8 | 9 | test "Wrapping a currency symbol" do 10 | assert {:ok, "$100.00"} = 11 | Cldr.Number.to_string(100, currency: :USD, wrapper: &NumberWrapper.wrapper/2) 12 | end 13 | 14 | test "Wrapping a currency symbol with inserted space" do 15 | assert {:ok, "CHF 100.00"} = 16 | Cldr.Number.to_string(100, currency: :CHF, wrapper: &NumberWrapper.wrapper/2) 17 | end 18 | 19 | test "Wrapping a percent and permille" do 20 | assert {:ok, "10,000%"} = 21 | Cldr.Number.to_string(100, format: :percent, wrapper: &NumberWrapper.wrapper/2) 22 | end 23 | 24 | test "Wrapping plus and minus" do 25 | assert {:ok, "+100"} = 26 | Cldr.Number.to_string(100, format: "+##", wrapper: &NumberWrapper.wrapper/2) 27 | 28 | assert {:ok, "-100"} = 29 | Cldr.Number.to_string(-100, format: "##", wrapper: &NumberWrapper.wrapper/2) 30 | end 31 | 32 | test "Wrapping a literal" do 33 | assert {:ok, "some string 100"} = 34 | Cldr.Number.to_string(100, 35 | format: "some string ##", 36 | wrapper: &NumberWrapper.wrapper/2 37 | ) 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /test/rbnf/rbnf_rule_compiler_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Cldr.Rbnf.Compiler.Test do 2 | use ExUnit.Case, async: true 3 | 4 | test "that rbnf rules can parse" do 5 | Enum.each(Cldr.Rbnf.all_rule_definitions(TestBackend.Cldr), fn rule -> 6 | parse_result = Cldr.Rbnf.Rule.parse(rule) 7 | assert [{:ok, _}, _] = [parse_result, rule] 8 | end) 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /test/support/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elixir-cldr/cldr_numbers/1fb2f82370c262f83dc348a9781211926ee97fa9/test/support/.gitkeep -------------------------------------------------------------------------------- /test/support/number_format_test_data.exs: -------------------------------------------------------------------------------- 1 | defmodule Cldr.Test.Number.Format do 2 | def test_data do 3 | [ 4 | {1234, "1,234", []}, 5 | {1234, "1 234", [locale: "fr"]}, 6 | {0.000123456, "0", []}, 7 | {-0.000123456, "0", []}, 8 | 9 | # Data from http://unicode.org/reports/tr35/tr35-numbers.html 10 | 11 | # Number_Patterns 12 | {1234.567, "1 234,57", [format: "#,##0.##", locale: "fr"]}, 13 | {1234.567, "1 234,567", [format: "#,##0.###", locale: "fr"]}, 14 | {1234.567, "1234,567", [format: "###0.#####", locale: "fr"]}, 15 | {1234.567, "1234,5670", [format: "###0.0000#", locale: "fr"]}, 16 | {1234.567, "01234,5670", [format: "00000.0000", locale: "fr"]}, 17 | {1234.567, "1 234,57 €", [format: "#,##0.00 ¤", locale: "fr", currency: :EUR]}, 18 | {1234.567, "1 235 JPY", [format: "#,##0 ¤", locale: "fr", currency: "JPY"]}, 19 | {1234.567, "1 234,57 JPY", [format: "#,##0.00 ¤", locale: "fr", currency: "JPY"]}, 20 | {1234.567, "1 234,57 JPY", [format: "#,##0.## ¤", locale: "fr", currency: "JPY"]}, 21 | {1234.00, "1 234 JPY", [format: "#,##0.## ¤", locale: "fr", currency: "JPY"]}, 22 | {1234, "1 234 JPY", [format: "#,##0.## ¤", locale: "fr", currency: "JPY"]}, 23 | 24 | # Fraction grouping 25 | {1234.4353244565, "1234,435 324 456 5", [format: "#,###.###,#########", locale: "pl"]}, 26 | 27 | # Special_Pattern_Characters 28 | {3.1415, "3,14", [format: "0.00;-0.00", locale: "fr"]}, 29 | {-3.1415, "-3,14", [format: "0.00;-0.00", locale: "fr"]}, 30 | {3.1415, "3,14", [format: "0.00;0.00-", locale: "fr"]}, 31 | {-3.1415, "3,14-", [format: "0.00;0.00-", locale: "fr"]}, 32 | {3.1415, "3,14+", [format: "0.00+;0.00-", locale: "fr"]}, 33 | {-3.1415, "3,14-", [format: "0.00+;0.00-", locale: "fr"]}, 34 | 35 | # Minimum grouping digits 36 | {1000, "1000", [format: "#,##0.##", locale: "pl"]}, 37 | {10000, "10 000", [format: "#,##0.##", locale: "pl"]}, 38 | 39 | # Secondary grouping 40 | {1_234_567, "12,34,567", [format: "#,##,###"]}, 41 | 42 | # Padding 43 | {123, "$xx123.00", [format: "$*x#,##0.00"]}, 44 | {123, "xx$123.00", [format: "*x$#,##0.00"]}, 45 | {123, "$123.00xx", [format: "$#,##0.00*x"]}, 46 | {1234, "$1,234.00", [format: "$*x#,##0.00"]}, 47 | {123, "! $xx123.00", [format: "'!' $*x#,##0.00"]}, 48 | {123, "' $xx123.00", [format: "'' $*x#,##0.00"]}, 49 | 50 | # Currency 51 | {123.4, "123.40 A$", [format: "#,##0.00 ¤", currency: :AUD]}, 52 | {123.4, "123.40 AUD", [format: "#,##0.00 ¤¤", currency: :AUD]}, 53 | {123.4, "123.40 Australian dollars", [format: "#,##0.00 ¤¤¤", currency: :AUD]}, 54 | {123.4, "123.40 $", [format: "#,##0.00 ¤¤¤¤", currency: :AUD]}, 55 | {1234, "A$1,234.00", [currency: :AUD]}, 56 | {1234, "COP 1,234.00", [currency: :COP, currency_digits: :iso]}, 57 | {1234, "COP 1,234.00", [currency: :COP]}, 58 | {1234, "১,২৩৪.০০€", [currency: :EUR, locale: :bn]}, 59 | 60 | # Currency where the symbol replaces the decimal separator 61 | {1234, "1234$00", [currency: :CVE, locale: "pt-CV"]}, 62 | 63 | # Currency default fractional digits 64 | {1234, "¥1,234", [currency: :JPY]}, 65 | {1234, "TND 1,234.000", [currency: :TND]}, 66 | 67 | # Currency with varying currency symbol 68 | {1234, "CUSTOM 1,234.00", [currency: :AUD, currency_symbol: "CUSTOM"]}, 69 | {1234, "$1,234.00", [currency: :AUD, currency_symbol: :narrow]}, 70 | {1234, "A$1,234.00", [currency: :AUD, currency_symbol: :standard]}, 71 | {1234, "A$1,234.00", [currency: :AUD, currency_symbol: :symbol]}, 72 | {1234, "AUD 1,234.00", [currency: :AUD, currency_symbol: :iso]}, 73 | {1234, "1,234.00", [currency: :AUD, currency_symbol: :none]}, 74 | 75 | # Rounding 76 | {1234.21, "1,234.20", [format: "#,##0.05"]}, 77 | {1234.22, "1,234.20", [format: "#,##0.05"]}, 78 | {1234.23, "1,234.25", [format: "#,##0.05"]}, 79 | {1234, "1,250", [format: "#,#50"]}, 80 | 81 | # Percentage 82 | {0.1234, "12.34%", [format: "#0.0#%"]}, 83 | {-0.1234, "-12.34%", [format: "#0.0#%"]}, 84 | 85 | # Permille 86 | {0.1234, "123.4‰", [format: "#0.0#‰"]}, 87 | {-0.1234, "-123.4‰", [format: "#0.0#‰"]}, 88 | 89 | # Negative number format 90 | {-1234, "(1234.00)", [format: "#.00;(#.00)"]}, 91 | 92 | # Significant digits format 93 | {12345, "12300", [format: "@@#"]}, 94 | {0.12345, "0.123", [format: "@@#"]}, 95 | {3.14159, "3.142", [format: "@@##"]}, 96 | {1.23004, "1.23", [format: "@@##"]}, 97 | {-1.23004, "-1.23", [format: "@@##"]}, 98 | 99 | # Test for when padding specified but there is no padding possible 100 | {123_456_789, "123456789", [format: "*x#"]}, 101 | 102 | # Scientific formats 103 | {0.1234, "1.234E-1", [format: "#E0"]}, 104 | {1.234, "1.234E0", [format: "#E0"]}, 105 | {12.34, "1.234E1", [format: "#E0"]}, 106 | {123.4, "1.234E2", [format: "#E0"]}, 107 | {1234, "1.234E3", [format: "#E0"]}, 108 | {1234, "1.234E3", [format: :scientific]}, 109 | 110 | # Scientific with exponent sign 111 | {1234, "1.234E+3", [format: "#E+0"]}, 112 | {0.000012456, "1.2456E-5", [format: "#E+0"]}, 113 | 114 | # Maximum and minimum digits 115 | {1234, "34", [format: "00", maximum_integer_digits: 2]}, 116 | {1, "01.00", [format: "00.00"]}, 117 | 118 | # Scientific formats with grouping 119 | # {1234, "1.234E3", [format: "#,###E0"]}, 120 | # {12.34, "0.012E3", [format: "#,###E0"]} 121 | 122 | # Short formats 123 | {123, "123", [format: :short]}, 124 | {1234, "1K", [format: :short]}, 125 | {12345, "12K", [format: :short]}, 126 | {1234.5, "1K", [format: :short]}, 127 | {1234.5, "1.234", [format: :short, locale: "de"]}, 128 | {123_456, "123.456", [format: :short, locale: "de"]}, 129 | {12_345_678, "12M", [format: :short]}, 130 | {1_234_567_890, "1B", [format: :short]}, 131 | {1_234_567_890_000, "1T", [format: :short]}, 132 | {1234, "1 thousand", [format: :long]}, 133 | {1_234_567_890, "1 billion", [format: :long]}, 134 | {1234, "$1K", [format: :short, currency: :USD]}, 135 | {1234, "ZAR 1K", [format: :short, currency: :ZAR]}, 136 | {12345, "12,345 US dollars", [format: :long, currency: :USD]}, 137 | {123, "A$123", [format: :short, currency: :AUD]}, 138 | {12, "12 Thai baht", [format: :long, currency: :THB]}, 139 | {12, "12 bahts thaïlandais", [format: :long, currency: :THB, locale: "fr"]}, 140 | {2134, "A$2K", [format: :currency_short, currency: :AUD]}, 141 | {2134, "2,134 Australian dollars", [format: :currency_long, currency: :AUD]}, 142 | {499_999_999, "500 millions", [format: :long, locale: "fr"]}, 143 | {500_000_000, "500 millions", [format: :long, locale: "fr"]}, 144 | {9_900_000_000, "10 milliards", [format: :long, locale: "fr"]}, 145 | {1_000, "mille", [format: :long, locale: "fr"]}, 146 | {1_001, "1 millier", [format: :long, locale: "fr"]}, 147 | {1_499, "1 millier", [format: :long, locale: "fr"]}, 148 | {1_500, "2 mille", [format: :long, locale: "fr"]}, 149 | 150 | # Negative short formats 151 | {1_000_000, "1M", [format: :short]}, 152 | {-1_000_000, "-1M", [format: :short]}, 153 | {100_000, "100K", [format: :short]}, 154 | {-100_000, "-100K", [format: :short]}, 155 | {1_000, "1K", [format: :short]}, 156 | {-1_000, "-1K", [format: :short]}, 157 | 158 | # Notes for future tests in "fr" locale: 159 | # millier is used with the number 1 on front and strictly only for 1000 (1 millier). 160 | # mille is used for anything between 1001 (Mille 1) and 1999 (Mille 999) and with no number before the word mille. 161 | # milles is used for many as the plural form : 3 milles, 4 milles, etc. 162 | 163 | # Formats with literals 164 | {123, "This is a 123.00 format", [format: "This is a #,##0.00 format"]}, 165 | {-123, "This is a -123.00 format", [format: "This is a #,##0.00 format"]}, 166 | {0.1, "This is a 10% format", [format: "This is a #,##0% format"]}, 167 | {-0.1, "This is a -10% format", [format: "This is a #,##0% format"]}, 168 | 169 | # Specify a currency in the locale 170 | {123.4, "123.40 $", [format: "#,##0.00 ¤", locale: "en-AU-u-cu-aud"]}, 171 | {123.4, "123.40 USD", [format: "#,##0.00 ¤", locale: "en-AU-u-cu-aud", currency: :USD]}, 172 | 173 | # Specify accounting or currency in the locale doesn't change the format 174 | {-123.4, "-123.40", [format: :currency, locale: "en-AU-u-cu-aud-cf-standard"]}, 175 | {-123.4, "-123.40", [format: :currency, locale: "en-AU-u-cu-aud-cf-account"]}, 176 | ] 177 | |> merge_crypto_tests(System.otp_release()) 178 | end 179 | 180 | # Formatting digital tokens (Crypto currencies) 181 | # Because OTP 28 introduces a new re module, OTP 28 will correctly 182 | # detect that "₿" is a symbol whereas early versions do not. 183 | 184 | def merge_crypto_tests(tests, version) when version >= "28" do 185 | tests ++ [ 186 | {1_234_545_656.456789, "₿1,234,545,656.456789", [currency: "BTC"]}, 187 | {1_234_545_656.456789, "₿1B", [format: :short, currency: "BTC"]}, 188 | {1_234_545_656.456789, "1,234,545,656.456789 Bitcoin", [format: :long, currency: "BTC"]} 189 | ] 190 | end 191 | 192 | def merge_crypto_tests(tests, _version) do 193 | tests ++ [ 194 | {1_234_545_656.456789, "₿ 1,234,545,656.456789", [currency: "BTC"]}, 195 | {1_234_545_656.456789, "₿ 1B", [format: :short, currency: "BTC"]}, 196 | {1_234_545_656.456789, "1,234,545,656.456789 Bitcoin", [format: :long, currency: "BTC"]} 197 | ] 198 | end 199 | end 200 | -------------------------------------------------------------------------------- /test/support/rbnf/fa/rbnf_test.json: -------------------------------------------------------------------------------- 1 | { 2 | "SpelloutRules": { 3 | "spellout-cardinal": { 4 | "-1141": "منفی یک هزار و صد و چهل و یک", 5 | "-1142": "منفی یک هزار و صد و چهل و دو", 6 | "-1143": "منفی یک هزار و صد و چهل و سه", 7 | "-100": "منفی صد", 8 | "-75": "منفی هفتاد و پنج", 9 | "-50": "منفی پنجاه", 10 | "-24": "منفی بیست و چهار", 11 | "0": "صفر", 12 | "1": "یک", 13 | "2": "دو", 14 | "3": "سه", 15 | "4": "چهار", 16 | "5": "پنج", 17 | "6": "شش", 18 | "7": "هفت", 19 | "8": "هشت", 20 | "9": "نه", 21 | "10": "ده", 22 | "11": "یازده", 23 | "12": "دوازده", 24 | "13": "سیزده", 25 | "14": "چهارده", 26 | "15": "پانزده", 27 | "16": "شانزده", 28 | "17": "هفده", 29 | "18": "هجده", 30 | "19": "نوزده", 31 | "20": "بیست", 32 | "21": "بیست و یک", 33 | "22": "بیست و دو", 34 | "23": "بیست و سه", 35 | "24": "بیست و چهار", 36 | "25": "بیست و پنج", 37 | "26": "بیست و شش", 38 | "27": "بیست و هفت", 39 | "28": "بیست و هشت", 40 | "29": "بیست و نه", 41 | "30": "سی", 42 | "31": "سی و یک", 43 | "32": "سی و دو", 44 | "33": "سی و سه", 45 | "34": "سی و چهار", 46 | "35": "سی و پنج", 47 | "36": "سی و شش", 48 | "37": "سی و هفت", 49 | "38": "سی و هشت", 50 | "39": "سی و نه", 51 | "40": "چهل", 52 | "41": "چهل و یک", 53 | "42": "چهل و دو", 54 | "43": "چهل و سه", 55 | "44": "چهل و چهار", 56 | "45": "چهل و پنج", 57 | "46": "چهل و شش", 58 | "47": "چهل و هفت", 59 | "48": "چهل و هشت", 60 | "49": "چهل و نه", 61 | "50": "پنجاه", 62 | "51": "پنجاه و یک", 63 | "52": "پنجاه و دو", 64 | "53": "پنجاه و سه", 65 | "54": "پنجاه و چهار", 66 | "55": "پنجاه و پنج", 67 | "56": "پنجاه و شش", 68 | "57": "پنجاه و هفت", 69 | "58": "پنجاه و هشت", 70 | "59": "پنجاه و نه", 71 | "60": "شصت", 72 | "61": "شصت و یک", 73 | "62": "شصت و دو", 74 | "63": "شصت و سه", 75 | "64": "شصت و چهار", 76 | "65": "شصت و پنج", 77 | "66": "شصت و شش", 78 | "67": "شصت و هفت", 79 | "68": "شصت و هشت", 80 | "69": "شصت و نه", 81 | "70": "هفتاد", 82 | "71": "هفتاد و یک", 83 | "72": "هفتاد و دو", 84 | "73": "هفتاد و سه", 85 | "74": "هفتاد و چهار", 86 | "75": "هفتاد و پنج", 87 | "76": "هفتاد و شش", 88 | "77": "هفتاد و هفت", 89 | "78": "هفتاد و هشت", 90 | "79": "هفتاد و نه", 91 | "80": "هشتاد", 92 | "81": "هشتاد و یک", 93 | "82": "هشتاد و دو", 94 | "83": "هشتاد و سه", 95 | "84": "هشتاد و چهار", 96 | "85": "هشتاد و پنج", 97 | "86": "هشتاد و شش", 98 | "87": "هشتاد و هفت", 99 | "88": "هشتاد و هشت", 100 | "89": "هشتاد و نه", 101 | "90": "نود", 102 | "91": "نود و یک", 103 | "92": "نود و دو", 104 | "93": "نود و سه", 105 | "94": "نود و چهار", 106 | "95": "نود و پنج", 107 | "96": "نود و شش", 108 | "97": "نود و هفت", 109 | "98": "نود و هشت", 110 | "99": "نود و نه", 111 | "100": "صد", 112 | "321": "سیصد و بیست و یک", 113 | "322": "سیصد و بیست و دو", 114 | "323": "سیصد و بیست و سه", 115 | "1141": "یک هزار و صد و چهل و یک", 116 | "1142": "یک هزار و صد و چهل و دو", 117 | "1143": "یک هزار و صد و چهل و سه", 118 | "10311": "ده هزار و سیصد و یازده", 119 | "138400": "صد و سی و هشت هزار و چهارصد" 120 | }, 121 | "spellout-numbering": { 122 | "-1141": "منفی یک هزار و صد و چهل و یک", 123 | "-1142": "منفی یک هزار و صد و چهل و دو", 124 | "-1143": "منفی یک هزار و صد و چهل و سه", 125 | "-100": "منفی صد", 126 | "-75": "منفی هفتاد و پنج", 127 | "-50": "منفی پنجاه", 128 | "-24": "منفی بیست و چهار", 129 | "0": "صفر", 130 | "1": "یک", 131 | "2": "دو", 132 | "3": "سه", 133 | "4": "چهار", 134 | "5": "پنج", 135 | "6": "شش", 136 | "7": "هفت", 137 | "8": "هشت", 138 | "9": "نه", 139 | "10": "ده", 140 | "11": "یازده", 141 | "12": "دوازده", 142 | "13": "سیزده", 143 | "14": "چهارده", 144 | "15": "پانزده", 145 | "16": "شانزده", 146 | "17": "هفده", 147 | "18": "هجده", 148 | "19": "نوزده", 149 | "20": "بیست", 150 | "21": "بیست و یک", 151 | "22": "بیست و دو", 152 | "23": "بیست و سه", 153 | "24": "بیست و چهار", 154 | "25": "بیست و پنج", 155 | "26": "بیست و شش", 156 | "27": "بیست و هفت", 157 | "28": "بیست و هشت", 158 | "29": "بیست و نه", 159 | "30": "سی", 160 | "31": "سی و یک", 161 | "32": "سی و دو", 162 | "33": "سی و سه", 163 | "34": "سی و چهار", 164 | "35": "سی و پنج", 165 | "36": "سی و شش", 166 | "37": "سی و هفت", 167 | "38": "سی و هشت", 168 | "39": "سی و نه", 169 | "40": "چهل", 170 | "41": "چهل و یک", 171 | "42": "چهل و دو", 172 | "43": "چهل و سه", 173 | "44": "چهل و چهار", 174 | "45": "چهل و پنج", 175 | "46": "چهل و شش", 176 | "47": "چهل و هفت", 177 | "48": "چهل و هشت", 178 | "49": "چهل و نه", 179 | "50": "پنجاه", 180 | "51": "پنجاه و یک", 181 | "52": "پنجاه و دو", 182 | "53": "پنجاه و سه", 183 | "54": "پنجاه و چهار", 184 | "55": "پنجاه و پنج", 185 | "56": "پنجاه و شش", 186 | "57": "پنجاه و هفت", 187 | "58": "پنجاه و هشت", 188 | "59": "پنجاه و نه", 189 | "60": "شصت", 190 | "61": "شصت و یک", 191 | "62": "شصت و دو", 192 | "63": "شصت و سه", 193 | "64": "شصت و چهار", 194 | "65": "شصت و پنج", 195 | "66": "شصت و شش", 196 | "67": "شصت و هفت", 197 | "68": "شصت و هشت", 198 | "69": "شصت و نه", 199 | "70": "هفتاد", 200 | "71": "هفتاد و یک", 201 | "72": "هفتاد و دو", 202 | "73": "هفتاد و سه", 203 | "74": "هفتاد و چهار", 204 | "75": "هفتاد و پنج", 205 | "76": "هفتاد و شش", 206 | "77": "هفتاد و هفت", 207 | "78": "هفتاد و هشت", 208 | "79": "هفتاد و نه", 209 | "80": "هشتاد", 210 | "81": "هشتاد و یک", 211 | "82": "هشتاد و دو", 212 | "83": "هشتاد و سه", 213 | "84": "هشتاد و چهار", 214 | "85": "هشتاد و پنج", 215 | "86": "هشتاد و شش", 216 | "87": "هشتاد و هفت", 217 | "88": "هشتاد و هشت", 218 | "89": "هشتاد و نه", 219 | "90": "نود", 220 | "91": "نود و یک", 221 | "92": "نود و دو", 222 | "93": "نود و سه", 223 | "94": "نود و چهار", 224 | "95": "نود و پنج", 225 | "96": "نود و شش", 226 | "97": "نود و هفت", 227 | "98": "نود و هشت", 228 | "99": "نود و نه", 229 | "100": "صد", 230 | "321": "سیصد و بیست و یک", 231 | "322": "سیصد و بیست و دو", 232 | "323": "سیصد و بیست و سه", 233 | "1141": "یک هزار و صد و چهل و یک", 234 | "1142": "یک هزار و صد و چهل و دو", 235 | "1143": "یک هزار و صد و چهل و سه", 236 | "10311": "ده هزار و سیصد و یازده", 237 | "138400": "صد و سی و هشت هزار و چهارصد" 238 | }, 239 | "spellout-numbering-year": { 240 | "-1141": "منفی یک هزار و صد و چهل و یک", 241 | "-1142": "منفی یک هزار و صد و چهل و دو", 242 | "-1143": "منفی یک هزار و صد و چهل و سه", 243 | "-100": "منفی صد", 244 | "-75": "منفی هفتاد و پنج", 245 | "-50": "منفی پنجاه", 246 | "-24": "منفی بیست و چهار", 247 | "0": "صفر", 248 | "1": "یک", 249 | "2": "دو", 250 | "3": "سه", 251 | "4": "چهار", 252 | "5": "پنج", 253 | "6": "شش", 254 | "7": "هفت", 255 | "8": "هشت", 256 | "9": "نه", 257 | "10": "ده", 258 | "11": "یازده", 259 | "12": "دوازده", 260 | "13": "سیزده", 261 | "14": "چهارده", 262 | "15": "پانزده", 263 | "16": "شانزده", 264 | "17": "هفده", 265 | "18": "هجده", 266 | "19": "نوزده", 267 | "20": "بیست", 268 | "21": "بیست و یک", 269 | "22": "بیست و دو", 270 | "23": "بیست و سه", 271 | "24": "بیست و چهار", 272 | "25": "بیست و پنج", 273 | "26": "بیست و شش", 274 | "27": "بیست و هفت", 275 | "28": "بیست و هشت", 276 | "29": "بیست و نه", 277 | "30": "سی", 278 | "31": "سی و یک", 279 | "32": "سی و دو", 280 | "33": "سی و سه", 281 | "34": "سی و چهار", 282 | "35": "سی و پنج", 283 | "36": "سی و شش", 284 | "37": "سی و هفت", 285 | "38": "سی و هشت", 286 | "39": "سی و نه", 287 | "40": "چهل", 288 | "41": "چهل و یک", 289 | "42": "چهل و دو", 290 | "43": "چهل و سه", 291 | "44": "چهل و چهار", 292 | "45": "چهل و پنج", 293 | "46": "چهل و شش", 294 | "47": "چهل و هفت", 295 | "48": "چهل و هشت", 296 | "49": "چهل و نه", 297 | "50": "پنجاه", 298 | "51": "پنجاه و یک", 299 | "52": "پنجاه و دو", 300 | "53": "پنجاه و سه", 301 | "54": "پنجاه و چهار", 302 | "55": "پنجاه و پنج", 303 | "56": "پنجاه و شش", 304 | "57": "پنجاه و هفت", 305 | "58": "پنجاه و هشت", 306 | "59": "پنجاه و نه", 307 | "60": "شصت", 308 | "61": "شصت و یک", 309 | "62": "شصت و دو", 310 | "63": "شصت و سه", 311 | "64": "شصت و چهار", 312 | "65": "شصت و پنج", 313 | "66": "شصت و شش", 314 | "67": "شصت و هفت", 315 | "68": "شصت و هشت", 316 | "69": "شصت و نه", 317 | "70": "هفتاد", 318 | "71": "هفتاد و یک", 319 | "72": "هفتاد و دو", 320 | "73": "هفتاد و سه", 321 | "74": "هفتاد و چهار", 322 | "75": "هفتاد و پنج", 323 | "76": "هفتاد و شش", 324 | "77": "هفتاد و هفت", 325 | "78": "هفتاد و هشت", 326 | "79": "هفتاد و نه", 327 | "80": "هشتاد", 328 | "81": "هشتاد و یک", 329 | "82": "هشتاد و دو", 330 | "83": "هشتاد و سه", 331 | "84": "هشتاد و چهار", 332 | "85": "هشتاد و پنج", 333 | "86": "هشتاد و شش", 334 | "87": "هشتاد و هفت", 335 | "88": "هشتاد و هشت", 336 | "89": "هشتاد و نه", 337 | "90": "نود", 338 | "91": "نود و یک", 339 | "92": "نود و دو", 340 | "93": "نود و سه", 341 | "94": "نود و چهار", 342 | "95": "نود و پنج", 343 | "96": "نود و شش", 344 | "97": "نود و هفت", 345 | "98": "نود و هشت", 346 | "99": "نود و نه", 347 | "100": "صد", 348 | "321": "سیصد و بیست و یک", 349 | "322": "سیصد و بیست و دو", 350 | "323": "سیصد و بیست و سه", 351 | "1141": "یک هزار و صد و چهل و یک", 352 | "1142": "یک هزار و صد و چهل و دو", 353 | "1143": "یک هزار و صد و چهل و سه", 354 | "10311": "ده هزار و سیصد و یازده", 355 | "138400": "صد و سی و هشت هزار و چهارصد" 356 | } 357 | }, 358 | "OrdinalRules": { 359 | "digits-ordinal": { 360 | "-1141": "−۱٬۱۴۱.", 361 | "-1142": "−۱٬۱۴۲.", 362 | "-1143": "−۱٬۱۴۳.", 363 | "-100": "−۱۰۰.", 364 | "-75": "−۷۵.", 365 | "-50": "−۵۰.", 366 | "-24": "−۲۴.", 367 | "0": "۰.", 368 | "1": "۱.", 369 | "2": "۲.", 370 | "3": "۳.", 371 | "4": "۴.", 372 | "5": "۵.", 373 | "6": "۶.", 374 | "7": "۷.", 375 | "8": "۸.", 376 | "9": "۹.", 377 | "10": "۱۰.", 378 | "11": "۱۱.", 379 | "12": "۱۲.", 380 | "13": "۱۳.", 381 | "14": "۱۴.", 382 | "15": "۱۵.", 383 | "16": "۱۶.", 384 | "17": "۱۷.", 385 | "18": "۱۸.", 386 | "19": "۱۹.", 387 | "20": "۲۰.", 388 | "21": "۲۱.", 389 | "22": "۲۲.", 390 | "23": "۲۳.", 391 | "24": "۲۴.", 392 | "25": "۲۵.", 393 | "26": "۲۶.", 394 | "27": "۲۷.", 395 | "28": "۲۸.", 396 | "29": "۲۹.", 397 | "30": "۳۰.", 398 | "31": "۳۱.", 399 | "32": "۳۲.", 400 | "33": "۳۳.", 401 | "34": "۳۴.", 402 | "35": "۳۵.", 403 | "36": "۳۶.", 404 | "37": "۳۷.", 405 | "38": "۳۸.", 406 | "39": "۳۹.", 407 | "40": "۴۰.", 408 | "41": "۴۱.", 409 | "42": "۴۲.", 410 | "43": "۴۳.", 411 | "44": "۴۴.", 412 | "45": "۴۵.", 413 | "46": "۴۶.", 414 | "47": "۴۷.", 415 | "48": "۴۸.", 416 | "49": "۴۹.", 417 | "50": "۵۰.", 418 | "51": "۵۱.", 419 | "52": "۵۲.", 420 | "53": "۵۳.", 421 | "54": "۵۴.", 422 | "55": "۵۵.", 423 | "56": "۵۶.", 424 | "57": "۵۷.", 425 | "58": "۵۸.", 426 | "59": "۵۹.", 427 | "60": "۶۰.", 428 | "61": "۶۱.", 429 | "62": "۶۲.", 430 | "63": "۶۳.", 431 | "64": "۶۴.", 432 | "65": "۶۵.", 433 | "66": "۶۶.", 434 | "67": "۶۷.", 435 | "68": "۶۸.", 436 | "69": "۶۹.", 437 | "70": "۷۰.", 438 | "71": "۷۱.", 439 | "72": "۷۲.", 440 | "73": "۷۳.", 441 | "74": "۷۴.", 442 | "75": "۷۵.", 443 | "76": "۷۶.", 444 | "77": "۷۷.", 445 | "78": "۷۸.", 446 | "79": "۷۹.", 447 | "80": "۸۰.", 448 | "81": "۸۱.", 449 | "82": "۸۲.", 450 | "83": "۸۳.", 451 | "84": "۸۴.", 452 | "85": "۸۵.", 453 | "86": "۸۶.", 454 | "87": "۸۷.", 455 | "88": "۸۸.", 456 | "89": "۸۹.", 457 | "90": "۹۰.", 458 | "91": "۹۱.", 459 | "92": "۹۲.", 460 | "93": "۹۳.", 461 | "94": "۹۴.", 462 | "95": "۹۵.", 463 | "96": "۹۶.", 464 | "97": "۹۷.", 465 | "98": "۹۸.", 466 | "99": "۹۹.", 467 | "100": "۱۰۰.", 468 | "321": "۳۲۱.", 469 | "322": "۳۲۲.", 470 | "323": "۳۲۳.", 471 | "1141": "۱٬۱۴۱.", 472 | "1142": "۱٬۱۴۲.", 473 | "1143": "۱٬۱۴۳.", 474 | "10311": "۱۰٬۳۱۱.", 475 | "138400": "۱۳۸٬۴۰۰." 476 | } 477 | } 478 | } -------------------------------------------------------------------------------- /test/support/rbnf/ga/rbnf_test.json: -------------------------------------------------------------------------------- 1 | { 2 | "SpelloutRules": { 3 | "spellout-cardinal": { 4 | "-1141": "míneas míle, céad daichead a haon", 5 | "-1142": "míneas míle, céad daichead a dó", 6 | "-1143": "míneas míle, céad daichead a trí", 7 | "-100": "míneas céad", 8 | "-75": "míneas seachtó a cúig", 9 | "-50": "míneas caoga", 10 | "-24": "míneas fiche a ceathair", 11 | "0": "a náid", 12 | "1": "a haon", 13 | "2": "a dó", 14 | "3": "a trí", 15 | "4": "a ceathair", 16 | "5": "a cúig", 17 | "6": "a sé", 18 | "7": "a seacht", 19 | "8": "a hocht", 20 | "9": "a naoi", 21 | "10": "a deich", 22 | "11": "a haon déag", 23 | "12": "a dó dhéag", 24 | "13": "a trí déag", 25 | "14": "a ceathair déag", 26 | "15": "a cúig déag", 27 | "16": "a sé déag", 28 | "17": "a seacht déag", 29 | "18": "a hocht déag", 30 | "19": "a naoi déag", 31 | "20": "fiche", 32 | "21": "fiche a haon", 33 | "22": "fiche a dó", 34 | "23": "fiche a trí", 35 | "24": "fiche a ceathair", 36 | "25": "fiche a cúig", 37 | "26": "fiche a sé", 38 | "27": "fiche a seacht", 39 | "28": "fiche a hocht", 40 | "29": "fiche a naoi", 41 | "30": "tríocha", 42 | "31": "tríocha a haon", 43 | "32": "tríocha a dó", 44 | "33": "tríocha a trí", 45 | "34": "tríocha a ceathair", 46 | "35": "tríocha a cúig", 47 | "36": "tríocha a sé", 48 | "37": "tríocha a seacht", 49 | "38": "tríocha a hocht", 50 | "39": "tríocha a naoi", 51 | "40": "daichead", 52 | "41": "daichead a haon", 53 | "42": "daichead a dó", 54 | "43": "daichead a trí", 55 | "44": "daichead a ceathair", 56 | "45": "daichead a cúig", 57 | "46": "daichead a sé", 58 | "47": "daichead a seacht", 59 | "48": "daichead a hocht", 60 | "49": "daichead a naoi", 61 | "50": "caoga", 62 | "51": "caoga a haon", 63 | "52": "caoga a dó", 64 | "53": "caoga a trí", 65 | "54": "caoga a ceathair", 66 | "55": "caoga a cúig", 67 | "56": "caoga a sé", 68 | "57": "caoga a seacht", 69 | "58": "caoga a hocht", 70 | "59": "caoga a naoi", 71 | "60": "seasca", 72 | "61": "seasca a haon", 73 | "62": "seasca a dó", 74 | "63": "seasca a trí", 75 | "64": "seasca a ceathair", 76 | "65": "seasca a cúig", 77 | "66": "seasca a sé", 78 | "67": "seasca a seacht", 79 | "68": "seasca a hocht", 80 | "69": "seasca a naoi", 81 | "70": "seachtó", 82 | "71": "seachtó a haon", 83 | "72": "seachtó a dó", 84 | "73": "seachtó a trí", 85 | "74": "seachtó a ceathair", 86 | "75": "seachtó a cúig", 87 | "76": "seachtó a sé", 88 | "77": "seachtó a seacht", 89 | "78": "seachtó a hocht", 90 | "79": "seachtó a naoi", 91 | "80": "ochtó", 92 | "81": "ochtó a haon", 93 | "82": "ochtó a dó", 94 | "83": "ochtó a trí", 95 | "84": "ochtó a ceathair", 96 | "85": "ochtó a cúig", 97 | "86": "ochtó a sé", 98 | "87": "ochtó a seacht", 99 | "88": "ochtó a hocht", 100 | "89": "ochtó a naoi", 101 | "90": "nócha", 102 | "91": "nócha a haon", 103 | "92": "nócha a dó", 104 | "93": "nócha a trí", 105 | "94": "nócha a ceathair", 106 | "95": "nócha a cúig", 107 | "96": "nócha a sé", 108 | "97": "nócha a seacht", 109 | "98": "nócha a hocht", 110 | "99": "nócha a naoi", 111 | "100": "céad", 112 | "321": "trí chéad fiche a haon", 113 | "322": "trí chéad fiche a dó", 114 | "323": "trí chéad fiche a trí", 115 | "1141": "míle, céad daichead a haon", 116 | "1142": "míle, céad daichead a dó", 117 | "1143": "míle, céad daichead a trí", 118 | "10311": "deich míle, trí chéad a haon déag", 119 | "138400": "céad tríocha is ocht míle, ceithre chéad" 120 | }, 121 | "spellout-numbering": { 122 | "-1141": "míneas míle, céad daichead a haon", 123 | "-1142": "míneas míle, céad daichead a dó", 124 | "-1143": "míneas míle, céad daichead a trí", 125 | "-100": "míneas céad", 126 | "-75": "míneas seachtó a cúig", 127 | "-50": "míneas caoga", 128 | "-24": "míneas fiche a ceathair", 129 | "0": "a náid", 130 | "1": "a haon", 131 | "2": "a dó", 132 | "3": "a trí", 133 | "4": "a ceathair", 134 | "5": "a cúig", 135 | "6": "a sé", 136 | "7": "a seacht", 137 | "8": "a hocht", 138 | "9": "a naoi", 139 | "10": "a deich", 140 | "11": "a haon déag", 141 | "12": "a dó dhéag", 142 | "13": "a trí déag", 143 | "14": "a ceathair déag", 144 | "15": "a cúig déag", 145 | "16": "a sé déag", 146 | "17": "a seacht déag", 147 | "18": "a hocht déag", 148 | "19": "a naoi déag", 149 | "20": "fiche", 150 | "21": "fiche a haon", 151 | "22": "fiche a dó", 152 | "23": "fiche a trí", 153 | "24": "fiche a ceathair", 154 | "25": "fiche a cúig", 155 | "26": "fiche a sé", 156 | "27": "fiche a seacht", 157 | "28": "fiche a hocht", 158 | "29": "fiche a naoi", 159 | "30": "tríocha", 160 | "31": "tríocha a haon", 161 | "32": "tríocha a dó", 162 | "33": "tríocha a trí", 163 | "34": "tríocha a ceathair", 164 | "35": "tríocha a cúig", 165 | "36": "tríocha a sé", 166 | "37": "tríocha a seacht", 167 | "38": "tríocha a hocht", 168 | "39": "tríocha a naoi", 169 | "40": "daichead", 170 | "41": "daichead a haon", 171 | "42": "daichead a dó", 172 | "43": "daichead a trí", 173 | "44": "daichead a ceathair", 174 | "45": "daichead a cúig", 175 | "46": "daichead a sé", 176 | "47": "daichead a seacht", 177 | "48": "daichead a hocht", 178 | "49": "daichead a naoi", 179 | "50": "caoga", 180 | "51": "caoga a haon", 181 | "52": "caoga a dó", 182 | "53": "caoga a trí", 183 | "54": "caoga a ceathair", 184 | "55": "caoga a cúig", 185 | "56": "caoga a sé", 186 | "57": "caoga a seacht", 187 | "58": "caoga a hocht", 188 | "59": "caoga a naoi", 189 | "60": "seasca", 190 | "61": "seasca a haon", 191 | "62": "seasca a dó", 192 | "63": "seasca a trí", 193 | "64": "seasca a ceathair", 194 | "65": "seasca a cúig", 195 | "66": "seasca a sé", 196 | "67": "seasca a seacht", 197 | "68": "seasca a hocht", 198 | "69": "seasca a naoi", 199 | "70": "seachtó", 200 | "71": "seachtó a haon", 201 | "72": "seachtó a dó", 202 | "73": "seachtó a trí", 203 | "74": "seachtó a ceathair", 204 | "75": "seachtó a cúig", 205 | "76": "seachtó a sé", 206 | "77": "seachtó a seacht", 207 | "78": "seachtó a hocht", 208 | "79": "seachtó a naoi", 209 | "80": "ochtó", 210 | "81": "ochtó a haon", 211 | "82": "ochtó a dó", 212 | "83": "ochtó a trí", 213 | "84": "ochtó a ceathair", 214 | "85": "ochtó a cúig", 215 | "86": "ochtó a sé", 216 | "87": "ochtó a seacht", 217 | "88": "ochtó a hocht", 218 | "89": "ochtó a naoi", 219 | "90": "nócha", 220 | "91": "nócha a haon", 221 | "92": "nócha a dó", 222 | "93": "nócha a trí", 223 | "94": "nócha a ceathair", 224 | "95": "nócha a cúig", 225 | "96": "nócha a sé", 226 | "97": "nócha a seacht", 227 | "98": "nócha a hocht", 228 | "99": "nócha a naoi", 229 | "100": "céad", 230 | "321": "trí chéad fiche a haon", 231 | "322": "trí chéad fiche a dó", 232 | "323": "trí chéad fiche a trí", 233 | "1141": "míle, céad daichead a haon", 234 | "1142": "míle, céad daichead a dó", 235 | "1143": "míle, céad daichead a trí", 236 | "10311": "deich míle, trí chéad a haon déag", 237 | "138400": "céad tríocha is ocht míle, ceithre chéad" 238 | }, 239 | "spellout-numbering-year": { 240 | "-1141": "míneas aon déag daichead a haon", 241 | "-1142": "míneas aon déag daichead a dó", 242 | "-1143": "míneas aon déag daichead a trí", 243 | "-100": "míneas céad", 244 | "-75": "míneas seachtó a cúig", 245 | "-50": "míneas caoga", 246 | "-24": "míneas fiche a ceathair", 247 | "0": "a náid", 248 | "1": "a haon", 249 | "2": "a dó", 250 | "3": "a trí", 251 | "4": "a ceathair", 252 | "5": "a cúig", 253 | "6": "a sé", 254 | "7": "a seacht", 255 | "8": "a hocht", 256 | "9": "a naoi", 257 | "10": "a deich", 258 | "11": "a haon déag", 259 | "12": "a dó dhéag", 260 | "13": "a trí déag", 261 | "14": "a ceathair déag", 262 | "15": "a cúig déag", 263 | "16": "a sé déag", 264 | "17": "a seacht déag", 265 | "18": "a hocht déag", 266 | "19": "a naoi déag", 267 | "20": "fiche", 268 | "21": "fiche a haon", 269 | "22": "fiche a dó", 270 | "23": "fiche a trí", 271 | "24": "fiche a ceathair", 272 | "25": "fiche a cúig", 273 | "26": "fiche a sé", 274 | "27": "fiche a seacht", 275 | "28": "fiche a hocht", 276 | "29": "fiche a naoi", 277 | "30": "tríocha", 278 | "31": "tríocha a haon", 279 | "32": "tríocha a dó", 280 | "33": "tríocha a trí", 281 | "34": "tríocha a ceathair", 282 | "35": "tríocha a cúig", 283 | "36": "tríocha a sé", 284 | "37": "tríocha a seacht", 285 | "38": "tríocha a hocht", 286 | "39": "tríocha a naoi", 287 | "40": "daichead", 288 | "41": "daichead a haon", 289 | "42": "daichead a dó", 290 | "43": "daichead a trí", 291 | "44": "daichead a ceathair", 292 | "45": "daichead a cúig", 293 | "46": "daichead a sé", 294 | "47": "daichead a seacht", 295 | "48": "daichead a hocht", 296 | "49": "daichead a naoi", 297 | "50": "caoga", 298 | "51": "caoga a haon", 299 | "52": "caoga a dó", 300 | "53": "caoga a trí", 301 | "54": "caoga a ceathair", 302 | "55": "caoga a cúig", 303 | "56": "caoga a sé", 304 | "57": "caoga a seacht", 305 | "58": "caoga a hocht", 306 | "59": "caoga a naoi", 307 | "60": "seasca", 308 | "61": "seasca a haon", 309 | "62": "seasca a dó", 310 | "63": "seasca a trí", 311 | "64": "seasca a ceathair", 312 | "65": "seasca a cúig", 313 | "66": "seasca a sé", 314 | "67": "seasca a seacht", 315 | "68": "seasca a hocht", 316 | "69": "seasca a naoi", 317 | "70": "seachtó", 318 | "71": "seachtó a haon", 319 | "72": "seachtó a dó", 320 | "73": "seachtó a trí", 321 | "74": "seachtó a ceathair", 322 | "75": "seachtó a cúig", 323 | "76": "seachtó a sé", 324 | "77": "seachtó a seacht", 325 | "78": "seachtó a hocht", 326 | "79": "seachtó a naoi", 327 | "80": "ochtó", 328 | "81": "ochtó a haon", 329 | "82": "ochtó a dó", 330 | "83": "ochtó a trí", 331 | "84": "ochtó a ceathair", 332 | "85": "ochtó a cúig", 333 | "86": "ochtó a sé", 334 | "87": "ochtó a seacht", 335 | "88": "ochtó a hocht", 336 | "89": "ochtó a naoi", 337 | "90": "nócha", 338 | "91": "nócha a haon", 339 | "92": "nócha a dó", 340 | "93": "nócha a trí", 341 | "94": "nócha a ceathair", 342 | "95": "nócha a cúig", 343 | "96": "nócha a sé", 344 | "97": "nócha a seacht", 345 | "98": "nócha a hocht", 346 | "99": "nócha a naoi", 347 | "100": "céad", 348 | "321": "trí chéad fiche a haon", 349 | "322": "trí chéad fiche a dó", 350 | "323": "trí chéad fiche a trí", 351 | "1141": "aon déag daichead a haon", 352 | "1142": "aon déag daichead a dó", 353 | "1143": "aon déag daichead a trí", 354 | "10311": "deich míle, trí chéad a haon déag", 355 | "138400": "céad tríocha is ocht míle, ceithre chéad" 356 | } 357 | }, 358 | "OrdinalRules": { 359 | "digits-ordinal": { 360 | "-1141": "−1,141ú", 361 | "-1142": "−1,142ú", 362 | "-1143": "−1,143ú", 363 | "-100": "−100ú", 364 | "-75": "−75ú", 365 | "-50": "−50ú", 366 | "-24": "−24ú", 367 | "0": "0ú", 368 | "1": "1ú", 369 | "2": "2ú", 370 | "3": "3ú", 371 | "4": "4ú", 372 | "5": "5ú", 373 | "6": "6ú", 374 | "7": "7ú", 375 | "8": "8ú", 376 | "9": "9ú", 377 | "10": "10ú", 378 | "11": "11ú", 379 | "12": "12ú", 380 | "13": "13ú", 381 | "14": "14ú", 382 | "15": "15ú", 383 | "16": "16ú", 384 | "17": "17ú", 385 | "18": "18ú", 386 | "19": "19ú", 387 | "20": "20ú", 388 | "21": "21ú", 389 | "22": "22ú", 390 | "23": "23ú", 391 | "24": "24ú", 392 | "25": "25ú", 393 | "26": "26ú", 394 | "27": "27ú", 395 | "28": "28ú", 396 | "29": "29ú", 397 | "30": "30ú", 398 | "31": "31ú", 399 | "32": "32ú", 400 | "33": "33ú", 401 | "34": "34ú", 402 | "35": "35ú", 403 | "36": "36ú", 404 | "37": "37ú", 405 | "38": "38ú", 406 | "39": "39ú", 407 | "40": "40ú", 408 | "41": "41ú", 409 | "42": "42ú", 410 | "43": "43ú", 411 | "44": "44ú", 412 | "45": "45ú", 413 | "46": "46ú", 414 | "47": "47ú", 415 | "48": "48ú", 416 | "49": "49ú", 417 | "50": "50ú", 418 | "51": "51ú", 419 | "52": "52ú", 420 | "53": "53ú", 421 | "54": "54ú", 422 | "55": "55ú", 423 | "56": "56ú", 424 | "57": "57ú", 425 | "58": "58ú", 426 | "59": "59ú", 427 | "60": "60ú", 428 | "61": "61ú", 429 | "62": "62ú", 430 | "63": "63ú", 431 | "64": "64ú", 432 | "65": "65ú", 433 | "66": "66ú", 434 | "67": "67ú", 435 | "68": "68ú", 436 | "69": "69ú", 437 | "70": "70ú", 438 | "71": "71ú", 439 | "72": "72ú", 440 | "73": "73ú", 441 | "74": "74ú", 442 | "75": "75ú", 443 | "76": "76ú", 444 | "77": "77ú", 445 | "78": "78ú", 446 | "79": "79ú", 447 | "80": "80ú", 448 | "81": "81ú", 449 | "82": "82ú", 450 | "83": "83ú", 451 | "84": "84ú", 452 | "85": "85ú", 453 | "86": "86ú", 454 | "87": "87ú", 455 | "88": "88ú", 456 | "89": "89ú", 457 | "90": "90ú", 458 | "91": "91ú", 459 | "92": "92ú", 460 | "93": "93ú", 461 | "94": "94ú", 462 | "95": "95ú", 463 | "96": "96ú", 464 | "97": "97ú", 465 | "98": "98ú", 466 | "99": "99ú", 467 | "100": "100ú", 468 | "321": "321ú", 469 | "322": "322ú", 470 | "323": "323ú", 471 | "1141": "1,141ú", 472 | "1142": "1,142ú", 473 | "1143": "1,143ú", 474 | "10311": "10,311ú", 475 | "138400": "138,400ú" 476 | } 477 | } 478 | } -------------------------------------------------------------------------------- /test/support/rbnf_test_support.exs: -------------------------------------------------------------------------------- 1 | defmodule Cldr.Rbnf.TestSupport do 2 | def rbnf_tests(fun) when is_function(fun) do 3 | 4 | # Possible bug being surfaced by :pl 5 | locales = TestBackend.Cldr.known_locale_names() 6 | 7 | _force_atom_instantiation = [OrdinalRules, CardinalRules] 8 | 9 | for locale_name <- locales do 10 | json_data_file = "./test/support/rbnf/#{locale_name}/rbnf_test.json" 11 | file_data = File.read(json_data_file) 12 | 13 | case file_data do 14 | {:error, _} -> 15 | :no_such_locale_test_file 16 | 17 | {:ok, json_string} -> 18 | json_data = 19 | json_string 20 | |> Jason.decode!() 21 | 22 | locale = TestBackend.Cldr.Locale.new!(locale_name) 23 | 24 | if Cldr.known_rbnf_locale_name?(locale_name, TestBackend.Cldr) do 25 | rbnf_data = Cldr.Rbnf.for_locale!(locale) 26 | 27 | Enum.each(Map.keys(json_data), fn rule_group -> 28 | if rbnf_data[String.to_existing_atom(rule_group)] do 29 | module = 30 | "Elixir.TestBackend.Cldr.Rbnf.#{rule_group}" 31 | |> String.replace("Rules", "") 32 | |> String.to_atom() 33 | 34 | Enum.each(json_data[rule_group], fn {rule_set, tests} -> 35 | function = 36 | rule_set 37 | |> String.replace("-", "_") 38 | |> String.to_atom() 39 | 40 | name = 41 | "#{module}.#{function} for locale #{inspect(locale_name)}" 42 | |> String.replace("_", "-") 43 | 44 | fun.(name, tests, module, function, locale) 45 | end) 46 | end 47 | end) 48 | end 49 | end 50 | end 51 | end 52 | end 53 | -------------------------------------------------------------------------------- /test/support/split_format_test_data.exs: -------------------------------------------------------------------------------- 1 | defmodule Cldr.Test.Number.Split.Format do 2 | def test_data do 3 | [ 4 | {"#", 5 | %{"exponent_digits" => "", "exponent_sign" => "", "fraction" => "", "integer" => "#"}}, 6 | {"###", 7 | %{ 8 | "exponent_digits" => "", 9 | "exponent_sign" => "", 10 | "fraction" => "", 11 | "integer" => "###" 12 | }}, 13 | {"###.0", 14 | %{ 15 | "exponent_digits" => "", 16 | "exponent_sign" => "", 17 | "fraction" => "0", 18 | "integer" => "###" 19 | }}, 20 | {"#00.0", 21 | %{ 22 | "exponent_digits" => "", 23 | "exponent_sign" => "", 24 | "fraction" => "0", 25 | "integer" => "#00" 26 | }}, 27 | {"#00.0E0", 28 | %{ 29 | "exponent_digits" => "0", 30 | "exponent_sign" => "", 31 | "fraction" => "0", 32 | "integer" => "#00" 33 | }}, 34 | {"#00.0E+0", 35 | %{ 36 | "exponent_digits" => "0", 37 | "exponent_sign" => "+", 38 | "fraction" => "0", 39 | "integer" => "#00" 40 | }}, 41 | {"#00.0E-0", 42 | %{ 43 | "exponent_digits" => "0", 44 | "exponent_sign" => "-", 45 | "fraction" => "0", 46 | "integer" => "#00" 47 | }}, 48 | {"#00E-0", 49 | %{ 50 | "exponent_digits" => "0", 51 | "exponent_sign" => "-", 52 | "fraction" => "", 53 | "integer" => "#00" 54 | }} 55 | ] 56 | end 57 | end 58 | -------------------------------------------------------------------------------- /test/support/test_backend.ex: -------------------------------------------------------------------------------- 1 | defmodule TestBackend.Cldr do 2 | use Cldr, 3 | default_locale: "en", 4 | locales: :all, 5 | precompile_transliterations: [{:latn, :arab}, {:arab, :thai}, {:arab, :latn}], 6 | providers: [Cldr.Number], 7 | suppress_warnings: true 8 | end 9 | 10 | defmodule NoDoc.Cldr do 11 | use Cldr, 12 | generate_docs: false, 13 | suppress_warnings: true, 14 | default_currency_format: nil 15 | end 16 | 17 | defmodule DefaultCurrencyFormat do 18 | use Cldr, 19 | default_locale: "en", 20 | locales: ["en", "fr"], 21 | providers: [Cldr.Number], 22 | default_currency_format: :currency 23 | end 24 | 25 | defmodule DefaultAccountingFormat do 26 | use Cldr, 27 | default_locale: "en", 28 | locales: ["en", "fr"], 29 | providers: [Cldr.Number], 30 | default_currency_format: :accounting 31 | end 32 | -------------------------------------------------------------------------------- /test/support/wrapper.ex: -------------------------------------------------------------------------------- 1 | defmodule NumberWrapper do 2 | def wrapper(string, tag) do 3 | "<#{tag}>" <> string <> "<#{tag}>" 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /test/sync_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Cldr.SyncTest do 2 | use ExUnit.Case 3 | 4 | test "that we raise if no default backend" do 5 | :ok = Application.delete_env(:ex_cldr, :default_backend) 6 | 7 | assert_raise Cldr.NoDefaultBackendError, fn -> 8 | Cldr.Number.to_string(1234) 9 | end 10 | 11 | :ok = Application.put_env(:ex_cldr, :default_backend, TestBackend.Cldr) 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start(trace: "--trace" in System.argv(), timeout: 120_000) 2 | 3 | Code.require_file("test/support/number_format_test_data.exs") 4 | Code.require_file("test/support/split_format_test_data.exs") 5 | Code.require_file("test/support/rbnf_test_support.exs") 6 | --------------------------------------------------------------------------------