├── .dialyzer_ignore_warnings ├── logo.png ├── .formatter.exs ├── benchee └── rounding.exs ├── lib ├── cldr │ ├── utils │ │ ├── code.ex │ │ ├── enum.ex │ │ ├── macros.ex │ │ ├── helpers.ex │ │ ├── json.ex │ │ ├── string.ex │ │ ├── digits.ex │ │ ├── map.ex │ │ └── math.ex │ ├── decimal │ │ └── decimal.ex │ └── http │ │ └── http.ex └── cldr_utils.ex ├── test ├── macro_test.exs ├── warn_once.exs ├── math │ ├── math_int_digits_test.exs │ ├── math_sqrt_test.exs │ ├── math_coef_exponent_test.exs │ ├── math_log_test.exs │ ├── math_power_test.exs │ ├── math_rounding_test.exs │ ├── math_digits_test.exs │ └── math_test.exs ├── cldr_utils_test.exs ├── test_helper.exs ├── http_test.exs └── map_test.exs ├── LICENSE.md ├── .gitignore ├── config └── config.exs ├── README.md ├── mix.exs ├── .github └── workflows │ └── ci.yml ├── mix.lock └── CHANGELOG.md /.dialyzer_ignore_warnings: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elixir-cldr/cldr_utils/HEAD/logo.png -------------------------------------------------------------------------------- /.formatter.exs: -------------------------------------------------------------------------------- 1 | # Used by "mix format" 2 | [ 3 | inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"], 4 | locals_without_parens: [docp: 1, defparsec: 2, defparsec: 3] 5 | ] 6 | -------------------------------------------------------------------------------- /benchee/rounding.exs: -------------------------------------------------------------------------------- 1 | float = 3.14159762 2 | decimal = Decimal.new("3.14159762") 3 | Benchee.run(%{ 4 | "Float" => fn -> Cldr.Math.round(float, 2) end, 5 | "Decimal" => fn -> Decimal.round(decimal, 2) end, 6 | "Float to Decimal" => fn -> Cldr.Math.round(Decimal.from_float(float), 2) end 7 | }) -------------------------------------------------------------------------------- /lib/cldr/utils/code.ex: -------------------------------------------------------------------------------- 1 | defmodule Cldr.Code do 2 | @moduledoc false 3 | 4 | # Provides a backwards compatible version of 5 | # the deprecated `Code.ensure_compiled?/1` 6 | 7 | @doc false 8 | def ensure_compiled?(module) do 9 | case Code.ensure_compiled(module) do 10 | {:module, _} -> true 11 | {:error, _} -> false 12 | end 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /test/macro_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Support.Macro.Test do 2 | use ExUnit.Case, async: true 3 | 4 | import ExUnit.CaptureLog 5 | 6 | test "warn once" do 7 | assert capture_log(fn -> 8 | defmodule M do 9 | import Cldr.Macros 10 | warn_once(:a, "Here we are") 11 | end 12 | end) =~ "Here we are" 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /test/warn_once.exs: -------------------------------------------------------------------------------- 1 | defmodule Warn.Test do 2 | use ExUnit.Case, async: true 3 | 4 | import ExUnit.CaptureLog 5 | 6 | test "warn once" do 7 | import Cldr.Macros 8 | 9 | assert capture_log(fn -> 10 | warn_once("aaa", "this is the warning message") 11 | end) =~ "this is the warning message" 12 | 13 | assert capture_log(fn -> 14 | warn_once("aaa", "this should not appear") 15 | end) == "" 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /test/math/math_int_digits_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Math.Int.Digits.Test do 2 | use ExUnit.Case, async: true 3 | 4 | @digits [ 5 | {1, 1}, 6 | {12, 2}, 7 | {1.0, 1}, 8 | {12.0, 2}, 9 | {0.1, 0}, 10 | {0.001, 0}, 11 | {1234, 4}, 12 | {1234.5678, 4} 13 | ] 14 | 15 | Enum.each(@digits, fn {num, digits} -> 16 | test "that #{inspect(num)} has #{inspect(digits)} digits" do 17 | assert Cldr.Digits.number_of_integer_digits(unquote(num)) == unquote(digits) 18 | end 19 | end) 20 | end 21 | -------------------------------------------------------------------------------- /test/cldr_utils_test.exs: -------------------------------------------------------------------------------- 1 | defmodule CldrUtilsTest do 2 | use ExUnit.Case, async: true 3 | 4 | doctest Cldr.Utils 5 | 6 | doctest Cldr.Math 7 | doctest Cldr.Digits 8 | doctest Cldr.Helpers 9 | doctest Cldr.Map 10 | doctest Cldr.String 11 | 12 | if Code.ensure_loaded?(Cldr.Json) do 13 | test "Cldr.Json proxy" do 14 | assert %{} = Cldr.Json.decode!("{}") 15 | assert %{"foo" => 1} = Cldr.Json.decode!("{\"foo\": 1}") 16 | assert %{foo: 1} = Cldr.Json.decode!("{\"foo\": 1}", keys: :atoms) 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | # License 2 | 3 | Copyright (c) 2017-2024 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 | -------------------------------------------------------------------------------- /lib/cldr_utils.ex: -------------------------------------------------------------------------------- 1 | defmodule Cldr.Utils do 2 | @moduledoc """ 3 | CLDR Utility functions. 4 | 5 | """ 6 | 7 | @doc """ 8 | Returns the current OTP version. 9 | 10 | """ 11 | def otp_version do 12 | major = :erlang.system_info(:otp_release) |> List.to_string() 13 | vsn_file = Path.join([:code.root_dir(), "releases", major, "OTP_VERSION"]) 14 | 15 | try do 16 | {:ok, contents} = File.read(vsn_file) 17 | String.split(contents, "\n", trim: true) 18 | else 19 | [full] -> full 20 | _ -> major 21 | catch 22 | :error, _ -> major 23 | end 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start() 2 | Application.ensure_all_started(:stream_data) 3 | 4 | defmodule GenerateNumber do 5 | require ExUnitProperties 6 | 7 | def decimal do 8 | ExUnitProperties.gen all(float <- StreamData.float()) do 9 | Decimal.from_float(float) 10 | end 11 | end 12 | 13 | def float do 14 | ExUnitProperties.gen all( 15 | float <- StreamData.float(min: -999_999_999_999, max: 999_999_999_999) 16 | ) do 17 | float 18 | end 19 | end 20 | 21 | def integer do 22 | ExUnitProperties.gen all(integer <- StreamData.integer()) do 23 | integer 24 | end 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /lib/cldr/utils/enum.ex: -------------------------------------------------------------------------------- 1 | defmodule Cldr.Enum do 2 | @doc """ 3 | Very simple reduce that passes both the head and the tail 4 | to the reducing function so it has some lookahead. 5 | """ 6 | 7 | def reduce_peeking(_list, {:halt, acc}, _fun), 8 | do: {:halted, acc} 9 | 10 | def reduce_peeking(list, {:suspend, acc}, fun), 11 | do: {:suspended, acc, &reduce_peeking(list, &1, fun)} 12 | 13 | def reduce_peeking([], {:cont, acc}, _fun), 14 | do: {:done, acc} 15 | 16 | def reduce_peeking([head | tail], {:cont, acc}, fun), 17 | do: reduce_peeking(tail, fun.(head, tail, acc), fun) 18 | 19 | def reduce_peeking(list, acc, fun), 20 | do: reduce_peeking(list, {:cont, acc}, fun) |> elem(1) 21 | 22 | def combine_list([head]), 23 | do: [to_string(head)] 24 | 25 | def combine_list([head | [next | tail]]), 26 | do: [to_string(head) | combine_list(["#{head}_#{next}" | tail])] 27 | end 28 | -------------------------------------------------------------------------------- /test/math/math_sqrt_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Math.Sqrt.Test do 2 | use ExUnit.Case, async: true 3 | 4 | # Each of these is validated to return the original number 5 | # when squared 6 | @roots [ 7 | {9, 3}, 8 | {11, "3.316624790355399849114932737"}, 9 | {465, "21.56385865284782467473394180"}, 10 | {11.321, "3.364669374544845230862071572"}, 11 | {0.1, "0.3162277660168379331998893544"} 12 | ] 13 | 14 | Enum.each(@roots, fn {value, root} -> 15 | test "square root of #{inspect(value)} should be #{root}" do 16 | assert Cldr.Decimal.compare( 17 | Cldr.Math.sqrt(Decimal.new(unquote(to_string(value)))), 18 | Decimal.new(unquote(to_string(root))) 19 | ) == :eq 20 | end 21 | end) 22 | 23 | test "sqrt of a negative number raises" do 24 | assert_raise ArgumentError, ~r/bad argument in arithmetic expression/, fn -> 25 | Cldr.Math.sqrt(Decimal.new(-5)) 26 | end 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /lib/cldr/utils/macros.ex: -------------------------------------------------------------------------------- 1 | defmodule Cldr.Macros do 2 | @moduledoc false 3 | 4 | defmacro is_false(value) do 5 | quote do 6 | is_nil(unquote(value)) or unquote(value) == false 7 | end 8 | end 9 | 10 | defmacro doc_since(version) do 11 | if Version.match?(System.version(), ">= 1.7.0") do 12 | quote do 13 | @doc since: unquote(version) 14 | end 15 | end 16 | end 17 | 18 | defmacro calendar_impl do 19 | if Version.match?(System.version(), ">= 1.10.0-dev") do 20 | quote do 21 | @impl true 22 | end 23 | end 24 | end 25 | 26 | defmacro warn_once(key, message, level \\ :warning) do 27 | caller = __CALLER__.module 28 | 29 | quote do 30 | require Logger 31 | 32 | if Cldr.Helpers.get_term({unquote(caller), unquote(key)}, true) do 33 | Logger.unquote(level)(unquote(message)) 34 | Cldr.Helpers.put_term({unquote(caller), unquote(key)}, nil) 35 | end 36 | end 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # The directory Mix will write compiled artifacts to. 2 | /_build/ 3 | 4 | # If you run "mix test --cover", coverage assets end up here. 5 | /cover/ 6 | 7 | # The directory Mix downloads your dependencies sources to. 8 | /deps/ 9 | 10 | # Where third-party dependencies like ExDoc output generated docs. 11 | /doc/ 12 | 13 | # Ignore .fetch files in case you like to edit your project deps locally. 14 | /.fetch 15 | 16 | # If the VM crashes, it generates a dump, let's ignore it too. 17 | erl_crash.dump 18 | 19 | # Also ignore archive artifacts (built via "mix archive.build"). 20 | *.ez 21 | 22 | # Ignore package tarball (built via "mix hex.build"). 23 | cldr_utils-*.tar 24 | 25 | # Temporary files for e.g. tests. 26 | /tmp 27 | 28 | # The xml downloaded from Unicode. 29 | /downloads 30 | 31 | # Ignore the generated JSON files. 32 | /data 33 | 34 | # Generated Erlang source. 35 | /src/*.erl 36 | 37 | # Misc. 38 | /references 39 | *.snapshot 40 | .DS_Store 41 | .iex.exs 42 | .tool-versions 43 | mise.toml 44 | -------------------------------------------------------------------------------- /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 | # This configuration is loaded before any dependency and is restricted 6 | # to this project. If another project depends on this project, this 7 | # file won't be loaded nor affect the parent project. For this reason, 8 | # if you want to provide default values for your application for 9 | # 3rd-party users, it should be done in your "mix.exs" file. 10 | 11 | # You can configure your application as: 12 | # 13 | # config :cldr_utils, key: :value 14 | # 15 | # and access this configuration in your application as: 16 | # 17 | # Application.get_env(:cldr_utils, :key) 18 | # 19 | # You can also configure a 3rd-party app: 20 | # 21 | # config :logger, level: :info 22 | # 23 | 24 | # It is also possible to import configuration files, relative to this 25 | # directory. For example, you can emulate configuration per environment 26 | # by uncommenting the line below and defining dev.exs, test.exs and such. 27 | # Configuration from the imported file will override the ones defined 28 | # here (which is why it is important to import them last). 29 | # 30 | # import_config "#{Mix.env()}.exs" 31 | -------------------------------------------------------------------------------- /test/math/math_coef_exponent_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Math.Mantissa.Exponent.Test do 2 | use ExUnit.Case, async: true 3 | 4 | @test [ 5 | 1.23004, 6 | 12.345, 7 | 645.978, 8 | 0.00345, 9 | 0.0123, 10 | 0.1, 11 | -0.1, 12 | -1, 13 | 0.3, 14 | 0.99, 15 | 0, 16 | 0.0, 17 | 1, 18 | 5, 19 | 10, 20 | 17, 21 | 47, 22 | 107, 23 | 507, 24 | 1000, 25 | 1007, 26 | 2345, 27 | 40000 28 | ] 29 | 30 | @ten Decimal.new(10) 31 | 32 | Enum.each(@test, fn value -> 33 | test "Validate coef * 10**exponent == original number of #{inspect(value)}" do 34 | test_value = 35 | if is_float(unquote(value)) do 36 | Decimal.new(unquote(to_string(value))) 37 | else 38 | Decimal.new(unquote(Macro.escape(value))) 39 | end 40 | 41 | # Calculate the mantissa and exponent 42 | {coef, exponent} = Cldr.Math.coef_exponent(test_value) 43 | 44 | # And then recalculate the decimal value 45 | calculated_value = 46 | @ten 47 | |> Cldr.Math.power(exponent) 48 | |> Decimal.mult(coef) 49 | 50 | # And confirm we made the round trip 51 | assert Cldr.Decimal.compare(calculated_value, test_value) == :eq 52 | end 53 | end) 54 | end 55 | -------------------------------------------------------------------------------- /lib/cldr/utils/helpers.ex: -------------------------------------------------------------------------------- 1 | defmodule Cldr.Helpers do 2 | @moduledoc """ 3 | General purpose helper functions for CLDR 4 | """ 5 | 6 | @doc """ 7 | Returns a boolean indicating if a data 8 | structure is semanically empty. 9 | 10 | Applies to lists, maps and `nil` 11 | """ 12 | 13 | def empty?([]), do: true 14 | def empty?(%{} = map) when map == %{}, do: true 15 | def empty?(nil), do: true 16 | def empty?(_), do: false 17 | 18 | cond do 19 | function_exported?(:persistent_term, :get, 2) -> 20 | @doc false 21 | def get_term(key, default) do 22 | :persistent_term.get(key, default) 23 | end 24 | 25 | @doc false 26 | def put_term(key, value) do 27 | :persistent_term.put(key, value) 28 | end 29 | 30 | function_exported?(:persistent_term, :get, 1) -> 31 | @doc false 32 | def get_term(key, default) do 33 | :persistent_term.get(key) 34 | rescue 35 | ArgumentError -> 36 | default 37 | end 38 | 39 | @doc false 40 | def put_term(key, value) do 41 | :persistent_term.put(key, value) 42 | end 43 | 44 | true -> 45 | @doc false 46 | def get_term(_key, default) do 47 | default 48 | end 49 | 50 | @doc false 51 | def put_term(_key, value) do 52 | value 53 | end 54 | end 55 | end 56 | -------------------------------------------------------------------------------- /test/math/math_log_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Math.Log.Test do 2 | use ExUnit.Case, async: true 3 | 4 | @round 2 5 | @samples [ 6 | {1, 0}, 7 | {10, 2.30258509299}, 8 | {1.23004, 0.20704668918075508} 9 | ] 10 | 11 | Enum.each(@samples, fn {sample, result} -> 12 | test "that decimal log(e) is correct for #{inspect(sample)}" do 13 | calc = Cldr.Math.log(Decimal.new(unquote(to_string(sample)))) |> Decimal.round(@round) 14 | sample = Decimal.new(unquote(to_string(result))) |> Decimal.round(@round) 15 | assert Cldr.Decimal.compare(calc, sample) == :eq 16 | end 17 | end) 18 | 19 | random = 20 | for _i <- 1..500 do 21 | :rand.uniform(10000) / 10 22 | end 23 | |> Enum.uniq() 24 | 25 | @diff 0.005 26 | Enum.each(random, fn x -> 27 | test "that decimal log(e) is more or less the same as bif log(e) for #{inspect(x)}" do 28 | assert :math.log(unquote(x)) - 29 | Cldr.Math.to_float(Cldr.Math.log(Decimal.new(unquote(to_string(x))))) < 30 | @diff 31 | end 32 | end) 33 | 34 | # Testing large decimals that are beyond the precision of a float 35 | test "log Decimal.new(\"1.33333333333333333333333333333333\")" do 36 | assert Cldr.Decimal.compare( 37 | Cldr.Math.log(Decimal.new("1.33333333333333333333333333333333")), 38 | Decimal.new("0.2876820724291554672132526174") 39 | ) == :eq 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /lib/cldr/decimal/decimal.ex: -------------------------------------------------------------------------------- 1 | defmodule Cldr.Decimal do 2 | @moduledoc """ 3 | Adds a compatibility layer for functions which changed 4 | either semantics or returns types between Decimal version 5 | 1.x and 2.x. 6 | 7 | """ 8 | decimal_version = Application.ensure_all_started(:decimal) && 9 | Application.spec(:decimal, :vsn) 10 | |> List.to_string() 11 | 12 | # To cater for both Decimal 1.x and 2.x 13 | if Code.ensure_loaded?(Decimal) && function_exported?(Decimal, :normalize, 1) do 14 | def reduce(decimal) do 15 | Decimal.normalize(decimal) 16 | end 17 | else 18 | def reduce(decimal) do 19 | Decimal.reduce(decimal) 20 | end 21 | end 22 | 23 | @spec compare(Decimal.t(), Decimal.t()) :: :eq | :lt | :gt 24 | if Version.match?(decimal_version, "~> 1.6 or ~> 1.9.0-rc or ~> 1.9") do 25 | def compare(d1, d2) do 26 | Decimal.cmp(d1, d2) 27 | end 28 | else 29 | def compare(d1, d2) do 30 | Decimal.compare(d1, d2) 31 | end 32 | end 33 | 34 | if Version.match?(decimal_version, "~> 2.0") do 35 | def parse(string) do 36 | case Decimal.parse(string) do 37 | {decimal, ""} -> {decimal, ""} 38 | _other -> {:error, string} 39 | end 40 | end 41 | else 42 | def parse(string) do 43 | case Decimal.parse(string) do 44 | {:ok, decimal} -> {decimal, ""} 45 | _other -> {:error, string} 46 | end 47 | end 48 | end 49 | end 50 | -------------------------------------------------------------------------------- /lib/cldr/utils/json.ex: -------------------------------------------------------------------------------- 1 | if Code.ensure_loaded?(:json) do 2 | defmodule Cldr.Json do 3 | @moduledoc """ 4 | A wrapper for the OTP 27 :json module. 5 | 6 | It implements a `decode!/1` function that wraps 7 | `:json.decode/1` with `decode!/1` so that its 8 | compatible with the calling conventions of 9 | Elixir - which is used by `ex_cldr`. 10 | 11 | This allows configuration such as: 12 | ```elixir 13 | config :ex_cldr, 14 | json_library: Cldr.Json 15 | ``` 16 | 17 | """ 18 | @doc since: "2.27.0" 19 | 20 | @doc """ 21 | Implements a Jason-compatible decode!/1,2 function suitable 22 | for decoding CLDR json data. 23 | 24 | ### Example 25 | 26 | iex> Cldr.Json.decode!("{\\"foo\\": 1}") 27 | %{"foo" => 1} 28 | 29 | iex> Cldr.Json.decode!("{\\"foo\\": 1}", keys: :atoms) 30 | %{foo: 1} 31 | 32 | """ 33 | def decode!(string) when is_binary(string) do 34 | {json, :ok, ""} = :json.decode(string, :ok, %{null: nil}) 35 | json 36 | end 37 | 38 | def decode!(charlist) when is_list(charlist) do 39 | charlist 40 | |> :erlang.iolist_to_binary() 41 | |> decode!() 42 | end 43 | 44 | def decode!(string, [keys: :atoms]) when is_binary(string) do 45 | push = fn key, value, acc -> 46 | [{String.to_atom(key), value} | acc] 47 | end 48 | 49 | decoders = %{ 50 | null: nil, 51 | object_push: push 52 | } 53 | 54 | {json, :ok, ""} = :json.decode(string, :ok, decoders) 55 | json 56 | end 57 | 58 | def decode!(charlist, options) when is_list(charlist) do 59 | charlist 60 | |> :erlang.iolist_to_binary() 61 | |> decode!(options) 62 | end 63 | 64 | end 65 | end -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Cldr Utils 2 | 3 | ![Build status](https://github.com/elixir-cldr/cldr_utils/actions/workflows/ci.yml/badge.svg) 4 | [![Hex.pm](https://img.shields.io/hexpm/v/cldr_utils.svg)](https://hex.pm/packages/cldr_utils) 5 | [![Hex.pm](https://img.shields.io/hexpm/dw/cldr_utils.svg?)](https://hex.pm/packages/cldr_utils) 6 | [![Hex.pm](https://img.shields.io/hexpm/dt/cldr_utils.svg?)](https://hex.pm/packages/cldr_utils) 7 | [![Hex.pm](https://img.shields.io/hexpm/l/cldr_utils.svg)](https://hex.pm/packages/ex_cldr) 8 | 9 | Utility functions extracted from [Cldr](https://github.com/elixir-cldr/cldr). 10 | 11 | * Map functions for deep mapping, deep merging, transforming keys 12 | * Math functions including `mod/2` that works on floored division 13 | * Number functions for working with the number of digits, the fraction as an integer, ... 14 | * String function for underscoring (converting CamelCase to snake case) 15 | * Cldr.Json.decode!/1 to wrap OTP 27's `:json` module 16 | * Various macros 17 | 18 | ## Installation 19 | 20 | The package can be installed by adding `:cldr_utils` to your list of dependencies in `mix.exs`: 21 | 22 | ```elixir 23 | def deps do 24 | [ 25 | {:cldr_utils, "~> 2.0"} 26 | ] 27 | end 28 | ``` 29 | 30 | ## Copyright and License 31 | 32 | Copyright (c) 2017-2024 Kip Cole 33 | 34 | Licensed under the Apache License, Version 2.0 (the "License"); 35 | you may not use this file except in compliance with the License. 36 | You may obtain a copy of the License at [http://www.apache.org/licenses/LICENSE-2.0](http://www.apache.org/licenses/LICENSE-2.0) 37 | 38 | Unless required by applicable law or agreed to in writing, software 39 | distributed under the License is distributed on an "AS IS" BASIS, 40 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 41 | See the License for the specific language governing permissions and 42 | limitations under the License. 43 | -------------------------------------------------------------------------------- /test/http_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Cldr.Http.Test do 2 | use ExUnit.Case 3 | import ExUnit.CaptureLog 4 | 5 | @accept_language String.to_charlist("Accept-Language") 6 | @any String.to_charlist("*") 7 | 8 | test "Downloading an https url" do 9 | assert {:ok, _body} = Cldr.Http.get("https://google.com") 10 | end 11 | 12 | test "Downloading an https url and return headers" do 13 | assert {:ok, _headers, _body} = Cldr.Http.get_with_headers("https://google.com") 14 | end 15 | 16 | test "Downloading an unknown url" do 17 | capture_log(fn -> 18 | assert {:error, :nxdomain} = Cldr.Http.get("https://xzzzzzzzzzzzzzzzz.com") 19 | end) =~ "Failed to connect to 'xzzzzzzzzzzzzzzzz.com'" 20 | end 21 | 22 | test "Request with headers" do 23 | assert {:ok, _body} = Cldr.Http.get({"https://google.com", [{@accept_language, @any}]}) 24 | end 25 | 26 | test "Request with headers and no peer verification" do 27 | assert {:ok, _body} = Cldr.Http.get({"https://google.com", [{@accept_language, @any}]}, verify_peer: false) 28 | end 29 | 30 | test "Request with headers returning headers" do 31 | assert {:ok, _headers, _body} = Cldr.Http.get_with_headers({"https://google.com", [{@accept_language, @any}]}) 32 | end 33 | 34 | if Version.compare(System.version(), "1.14.9") == :gt do 35 | test "Request with connection timeout" do 36 | 37 | options = [connection_timeout: 2] 38 | 39 | assert capture_log(fn -> 40 | assert {:error, :connection_timeout} = 41 | Cldr.Http.get_with_headers({"https://google.com", [{@accept_language, @any}]}, options) 42 | end) =~ "Timeout connecting to" 43 | end 44 | 45 | test "Request with timeout" do 46 | options = [timeout: 2] 47 | 48 | assert capture_log(fn -> 49 | assert {:error, :timeout} = 50 | Cldr.Http.get_with_headers({"https://google.com", [{@accept_language, @any}]}, options) 51 | end) =~ "Timeout downloading from" 52 | end 53 | end 54 | end -------------------------------------------------------------------------------- /test/math/math_power_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Math.Power.Test do 2 | use ExUnit.Case, async: true 3 | 4 | @significance 15 5 | 6 | # On my machine, iterations over 12 bring bad karma 7 | @iterations 12 8 | 9 | Enum.each(-10..@iterations, fn n -> 10 | test "Confirm Cldr.Math.power(5, n) for #{inspect(n)} returns the same result as :math.pow" do 11 | p = 12 | Cldr.Math.power(5, unquote(n)) 13 | |> Cldr.Math.round_significant(@significance) 14 | 15 | q = 16 | :math.pow(5, unquote(n)) 17 | |> Cldr.Math.round_significant(@significance) 18 | 19 | assert p == q 20 | end 21 | 22 | # Decimal number, decimal power 23 | test "Confirm Decimal Cldr.Math.power(5, n) for Decimal #{inspect(n)} returns the same result as :math.pow" do 24 | p = 25 | Cldr.Math.power(Decimal.new(5), Decimal.new(unquote(n))) 26 | |> Cldr.Math.round_significant(10) 27 | |> Decimal.to_float() 28 | 29 | q = 30 | :math.pow(5, unquote(n)) 31 | |> Cldr.Math.round_significant(10) 32 | 33 | assert p == q 34 | end 35 | end) 36 | 37 | test "Short cut decimal power of 10 for a positive number" do 38 | p = Cldr.Math.power(Decimal.new(10), 2) 39 | assert Cldr.Decimal.compare(p, Decimal.new(100)) == :eq 40 | 41 | p = Cldr.Math.power(Decimal.new(10), 3) 42 | assert Cldr.Decimal.compare(p, Decimal.new(1000)) == :eq 43 | 44 | p = Cldr.Math.power(Decimal.new(10), 4) 45 | assert Cldr.Decimal.compare(p, Decimal.new(10000)) == :eq 46 | end 47 | 48 | test "Short cut decimal power of 10 for a negative number" do 49 | p = Cldr.Math.power(Decimal.new(10), -2) 50 | assert Cldr.Decimal.compare(p, Decimal.new("0.01")) == :eq 51 | 52 | p = Cldr.Math.power(Decimal.new(10), -3) 53 | assert Cldr.Decimal.compare(p, Decimal.new("0.001")) == :eq 54 | 55 | p = Cldr.Math.power(Decimal.new(10), -4) 56 | assert Cldr.Decimal.compare(p, Decimal.new("0.0001")) == :eq 57 | end 58 | 59 | test "A specific bug fix" do 60 | a = Decimal.new("0.00001232") 61 | b = Decimal.new("0.00001242") 62 | x = Decimal.sub(a, b) 63 | 64 | assert Cldr.Decimal.compare(Cldr.Math.power(x, 2), Decimal.new("0.00000000000001")) == 65 | :eq 66 | end 67 | end 68 | -------------------------------------------------------------------------------- /test/math/math_rounding_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Cldr.Math.RoundingTest do 2 | use ExUnit.Case, async: true 3 | 4 | def round(number, precision \\ 0, rounding \\ :half_up) do 5 | Cldr.Math.round(number, precision, rounding) 6 | end 7 | 8 | def ceil(number, precision \\ 0) do 9 | round(number, precision, :ceiling) 10 | end 11 | 12 | def floor(number, precision \\ 0) do 13 | round(number, precision, :floor) 14 | end 15 | 16 | test "rounding to less than the precision of the number returns 0" do 17 | assert Cldr.Math.round(1.235e-4, 3, :half_even) == 0.0 18 | end 19 | 20 | for mode <- Cldr.Math.rounding_modes() do 21 | number = 100_000.00 22 | test "rounding 100_000.00 with mode #{inspect mode}" do 23 | assert unquote(number) == Cldr.Math.round(unquote(number), 2, unquote(mode)) 24 | end 25 | end 26 | 27 | test "Simple round :half_up" do 28 | assert 1.21 == round(1.205, 2) 29 | assert 1.22 == round(1.215, 2) 30 | assert 1.23 == round(1.225, 2) 31 | assert 1.24 == round(1.235, 2) 32 | assert 1.25 == round(1.245, 2) 33 | assert 1.26 == round(1.255, 2) 34 | assert 1.27 == round(1.265, 2) 35 | assert 1.28 == round(1.275, 2) 36 | assert 1.29 == round(1.285, 2) 37 | assert 1.30 == round(1.295, 2) 38 | end 39 | 40 | test "Simple round :half_even" do 41 | assert 1.20 == round(1.205, 2, :half_even) 42 | assert 1.22 == round(1.215, 2, :half_even) 43 | assert 1.22 == round(1.225, 2, :half_even) 44 | assert 1.24 == round(1.235, 2, :half_even) 45 | assert 1.24 == round(1.245, 2, :half_even) 46 | assert 1.26 == round(1.255, 2, :half_even) 47 | assert 1.26 == round(1.265, 2, :half_even) 48 | assert 1.28 == round(1.275, 2, :half_even) 49 | assert 1.28 == round(1.285, 2, :half_even) 50 | assert 1.30 == round(1.295, 2, :half_even) 51 | end 52 | 53 | test "test ceil" do 54 | assert 1.21 == ceil(1.204, 2) 55 | assert 1.21 == ceil(1.205, 2) 56 | assert 1.21 == ceil(1.206, 2) 57 | 58 | assert 1.22 == ceil(1.214, 2) 59 | assert 1.22 == ceil(1.215, 2) 60 | assert 1.22 == ceil(1.216, 2) 61 | 62 | assert -1.20 == ceil(-1.204, 2) 63 | assert -1.20 == ceil(-1.205, 2) 64 | assert -1.20 == ceil(-1.206, 2) 65 | end 66 | 67 | test "test floor" do 68 | assert 1.20 == floor(1.204, 2) 69 | assert 1.20 == floor(1.205, 2) 70 | assert 1.20 == floor(1.206, 2) 71 | 72 | assert 1.21 == floor(1.214, 2) 73 | assert 1.21 == floor(1.215, 2) 74 | assert 1.21 == floor(1.216, 2) 75 | 76 | assert -1.21 == floor(-1.204, 2) 77 | assert -1.21 == floor(-1.205, 2) 78 | assert -1.21 == floor(-1.206, 2) 79 | end 80 | end 81 | -------------------------------------------------------------------------------- /mix.exs: -------------------------------------------------------------------------------- 1 | defmodule Cldr.Utils.MixProject do 2 | use Mix.Project 3 | 4 | @version "2.29.1" 5 | @source_url "https://github.com/elixir-cldr/cldr_utils" 6 | 7 | def project do 8 | [ 9 | app: :cldr_utils, 10 | version: @version, 11 | elixir: "~> 1.12", 12 | description: description(), 13 | start_permanent: Mix.env() == :prod, 14 | deps: deps(), 15 | package: package(), 16 | docs: docs(), 17 | test_coverage: [tool: ExCoveralls], 18 | aliases: aliases(), 19 | elixirc_paths: elixirc_paths(Mix.env()), 20 | dialyzer: [ 21 | ignore_warnings: ".dialyzer_ignore_warnings", 22 | plt_add_apps: ~w(inets decimal certifi castore)a 23 | ], 24 | compilers: Mix.compilers() 25 | ] 26 | end 27 | 28 | defp description do 29 | """ 30 | Map, Calendar, Digits, Decimal, HTTP, Macro, Math, and String helpers for ex_cldr. 31 | """ 32 | end 33 | 34 | def application do 35 | [ 36 | extra_applications: [:logger, :inets, :ssl] 37 | ] 38 | end 39 | 40 | defp deps do 41 | [ 42 | {:decimal, "~> 1.9 or ~> 2.0"}, 43 | {:castore, "~> 0.1 or ~> 1.0", optional: true}, 44 | {:certifi, "~> 2.5", optional: true}, 45 | {:ex_doc, ">= 0.0.0", optional: true, only: [:dev, :release], runtime: false}, 46 | {:stream_data, "~> 1.0", optional: true, only: :test}, 47 | {:dialyxir, "~> 1.0", optional: true, only: [:dev, :test], runtime: false}, 48 | {:benchee, "~> 1.0", optional: true, only: [:dev], runtime: false} 49 | ] 50 | end 51 | 52 | defp package do 53 | [ 54 | maintainers: ["Kip Cole"], 55 | licenses: ["Apache-2.0"], 56 | links: links(), 57 | files: [ 58 | "lib", 59 | "config", 60 | "mix.exs", 61 | "README*", 62 | "CHANGELOG*", 63 | "LICENSE*" 64 | ] 65 | ] 66 | end 67 | 68 | def links do 69 | %{ 70 | "GitHub" => @source_url, 71 | "Readme" => "#{@source_url}/blob/v#{@version}/README.md", 72 | "Changelog" => "#{@source_url}/blob/v#{@version}/CHANGELOG.md" 73 | } 74 | end 75 | 76 | def docs do 77 | [ 78 | extras: [ 79 | "CHANGELOG.md", 80 | "LICENSE.md", 81 | "README.md" 82 | ], 83 | main: "readme", 84 | source_url: @source_url, 85 | source_ref: "v#{@version}", 86 | formatters: ["html"], 87 | logo: "logo.png", 88 | skip_undefined_reference_warnings_on: ["CHANGELOG.md"] 89 | ] 90 | end 91 | 92 | defp elixirc_paths(:test), do: ["lib", "mix", "test"] 93 | defp elixirc_paths(:dev), do: ["lib", "mix", "benchee"] 94 | defp elixirc_paths(_), do: ["lib"] 95 | 96 | def aliases do 97 | [] 98 | end 99 | end 100 | -------------------------------------------------------------------------------- /lib/cldr/utils/string.ex: -------------------------------------------------------------------------------- 1 | defmodule Cldr.String do 2 | @moduledoc """ 3 | Functions that operate on a `String.t` that are not provided 4 | in the standard lib. 5 | """ 6 | 7 | @doc """ 8 | Hash a string using a polynomial rolling hash function. 9 | 10 | See https://cp-algorithms.com/string/string-hashing.html for 11 | a description of the algoithim. 12 | """ 13 | 14 | @p 99991 15 | @m trunc(1.0e9) + 9 16 | def hash(string) do 17 | {hash, _} = 18 | string 19 | |> String.to_charlist() 20 | |> Enum.reduce({0, 1}, fn char, {hash, p_pow} -> 21 | hash = rem(hash + char * p_pow, @m) 22 | p_pow = rem(p_pow * @p, @m) 23 | {hash, p_pow} 24 | end) 25 | 26 | hash 27 | end 28 | 29 | @doc """ 30 | Replaces "-" with "_" in a string. 31 | 32 | ## Examples 33 | 34 | iex> Cldr.String.to_underscore("this-one") 35 | "this_one" 36 | 37 | """ 38 | def to_underscore(string) when is_binary(string) do 39 | String.replace(string, "-", "_") 40 | end 41 | 42 | @doc """ 43 | This is the code of Macro.underscore with modifications: 44 | 45 | The change is to cater for strings in the format: 46 | 47 | This_That 48 | 49 | which in Macro.underscore gets formatted as: 50 | 51 | this__that (note the double underscore) 52 | 53 | when we actually want: 54 | 55 | that_that 56 | 57 | """ 58 | def underscore(atom) when is_atom(atom) do 59 | "Elixir." <> rest = Atom.to_string(atom) 60 | underscore(rest) 61 | end 62 | 63 | def underscore(<>) do 64 | <> <> do_underscore(t, h) 65 | end 66 | 67 | def underscore("") do 68 | "" 69 | end 70 | 71 | # h is upper case, next char is not uppercase, or a _ or . => and prev != _ 72 | defp do_underscore(<>, prev) 73 | when h >= ?A and h <= ?Z and not (t >= ?A and t <= ?Z) and t != ?. and t != ?_ and 74 | prev != ?_ do 75 | <> <> do_underscore(rest, t) 76 | end 77 | 78 | # h is uppercase, previous was not uppercase or _ 79 | defp do_underscore(<>, prev) 80 | when h >= ?A and h <= ?Z and not (prev >= ?A and prev <= ?Z) and prev != ?_ do 81 | <> <> do_underscore(t, h) 82 | end 83 | 84 | # h is . 85 | defp do_underscore(<>, _) do 86 | <> <> underscore(t) 87 | end 88 | 89 | # Any other char 90 | defp do_underscore(<>, _) do 91 | <> <> do_underscore(t, h) 92 | end 93 | 94 | defp do_underscore(<<>>, _) do 95 | <<>> 96 | end 97 | 98 | def to_upper_char(char) when char >= ?a and char <= ?z, do: char - 32 99 | def to_upper_char(char), do: char 100 | 101 | def to_lower_char(char) when char >= ?A and char <= ?Z, do: char + 32 102 | def to_lower_char(char), do: char 103 | end 104 | -------------------------------------------------------------------------------- /test/math/math_digits_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Cldr.Math.DigitsTest do 2 | use ExUnit.Case, async: true 3 | 4 | test "test to_digits(zero)" do 5 | assert {[0], 1, 1} == Cldr.Digits.to_digits(0.0) 6 | end 7 | 8 | test "test to_digits(one)" do 9 | assert {[1], 1, 1} == Cldr.Digits.to_digits(1.0) 10 | end 11 | 12 | test "test to_digits(negative one)" do 13 | assert {[1], 1, -1} == Cldr.Digits.to_digits(-1.0) 14 | end 15 | 16 | test "test to_digits(small denormalized number)" do 17 | # 4.94065645841246544177e-324 18 | <> = <<0, 0, 0, 0, 0, 0, 0, 1>> 19 | 20 | assert {[4, 9, 4, 0, 6, 5, 6, 4, 5, 8, 4, 1, 2, 4, 6, 5, 4], -323, 1} == 21 | Cldr.Digits.to_digits(small_denorm) 22 | end 23 | 24 | test "test to_digits(large denormalized number)" do 25 | # 2.22507385850720088902e-308 26 | <> = <<0, 15, 255, 255, 255, 255, 255, 255>> 27 | 28 | assert {[2, 2, 2, 5, 0, 7, 3, 8, 5, 8, 5, 0, 7, 2, 0, 1], -307, 1} == 29 | Cldr.Digits.to_digits(large_denorm) 30 | end 31 | 32 | test "test to_digits(small normalized number)" do 33 | # 2.22507385850720138309e-308 34 | <> = <<0, 16, 0, 0, 0, 0, 0, 0>> 35 | 36 | assert {[2, 2, 2, 5, 0, 7, 3, 8, 5, 8, 5, 0, 7, 2, 0, 1, 4], -307, 1} == 37 | Cldr.Digits.to_digits(small_norm) 38 | end 39 | 40 | test "test to_digits(large normalized number)" do 41 | # 1.79769313486231570815e+308 42 | <> = <<127, 239, 255, 255, 255, 255, 255, 255>> 43 | 44 | assert {[1, 7, 9, 7, 6, 9, 3, 1, 3, 4, 8, 6, 2, 3, 1, 5, 7], 309, 1} == 45 | Cldr.Digits.to_digits(large_norm) 46 | end 47 | 48 | ############################################################################ 49 | # test frexp/1 50 | # 51 | 52 | test "test frexp(zero)" do 53 | assert {0.0, 0} == Cldr.Digits.frexp(0.0) 54 | end 55 | 56 | test "test frexp(one)" do 57 | assert {0.5, 1} == Cldr.Digits.frexp(1.0) 58 | end 59 | 60 | test "test frexp(negative one)" do 61 | assert {-0.5, 1} == Cldr.Digits.frexp(-1.0) 62 | end 63 | 64 | test "test frexp(small denormalized number)" do 65 | # 4.94065645841246544177e-324 66 | <> = <<0, 0, 0, 0, 0, 0, 0, 1>> 67 | assert {0.5, -1073} == Cldr.Digits.frexp(small_denorm) 68 | end 69 | 70 | test "test frexp(large denormalized number)" do 71 | # 2.22507385850720088902e-308 72 | <> = <<0, 15, 255, 255, 255, 255, 255, 255>> 73 | assert {0.99999999999999978, -1022} == Cldr.Digits.frexp(large_denorm) 74 | end 75 | 76 | test "test frexp(small normalized number)" do 77 | # 2.22507385850720138309e-308 78 | <> = <<0, 16, 0, 0, 0, 0, 0, 0>> 79 | assert {0.5, -1021} == Cldr.Digits.frexp(small_norm) 80 | end 81 | 82 | test "test frexp(large normalized number)" do 83 | # 1.79769313486231570815e+308 84 | <> = <<127, 239, 255, 255, 255, 255, 255, 255>> 85 | assert {0.99999999999999989, 1024} == Cldr.Digits.frexp(large_norm) 86 | end 87 | end 88 | -------------------------------------------------------------------------------- /test/map_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Support.Map.Test do 2 | use ExUnit.Case, async: true 3 | 4 | test "that map keys are underscored" do 5 | assert Cldr.Map.underscore_keys(%{"thisKey" => "value"}) == %{"this_key" => "value"} 6 | end 7 | 8 | test "that map keys are atomised" do 9 | assert Cldr.Map.atomize_keys(%{"thisKey" => "value"}) == %{thisKey: "value"} 10 | end 11 | 12 | test "that nested map keys are atomised" do 13 | test_map = %{ 14 | "key" => %{ 15 | "nested" => "value" 16 | } 17 | } 18 | 19 | test_result = %{ 20 | key: %{ 21 | nested: "value" 22 | } 23 | } 24 | 25 | assert Cldr.Map.atomize_keys(test_map) == test_result 26 | end 27 | 28 | test "that interizing negative integer keys works" do 29 | map = %{"-1" => "something"} 30 | result = %{-1 => "something"} 31 | 32 | assert Cldr.Map.integerize_keys(map) == result 33 | end 34 | 35 | test "atomize keys with only option" do 36 | test_map = %{ 37 | "key" => %{ 38 | "nested" => "value" 39 | } 40 | } 41 | 42 | test_result = %{ 43 | "key" => %{ 44 | nested: "value" 45 | } 46 | } 47 | 48 | assert Cldr.Map.atomize_keys(test_map, only: "nested") == test_result 49 | assert Cldr.Map.atomize_keys(test_map, only: ["nested"]) == test_result 50 | assert Cldr.Map.atomize_keys(test_map, only: &(elem(&1, 0) == "nested")) == test_result 51 | 52 | assert Cldr.Map.atomize_keys(test_map, except: "key") == test_result 53 | assert Cldr.Map.atomize_keys(test_map, except: ["key"]) == test_result 54 | assert Cldr.Map.atomize_keys(test_map, except: &(elem(&1, 0) == "key")) == test_result 55 | end 56 | 57 | test "atomizing values when the value is a list" do 58 | assert Cldr.Map.atomize_values(%{"key" => ["a", "b", "c"]}) == %{"key" => [:a, :b, :c]} 59 | end 60 | 61 | test "deep_map with levels" do 62 | test_map = %{ 63 | "key" => %{ 64 | "nested" => "value" 65 | } 66 | } 67 | 68 | test_result = %{ 69 | key: %{ 70 | "nested" => "value" 71 | } 72 | } 73 | 74 | assert Cldr.Map.atomize_keys(test_map, level: 1..1) == test_result 75 | end 76 | 77 | test "deep_map with :skip" do 78 | test_map = %{ 79 | "key" => %{ 80 | "nested" => %{ 81 | "some" => "value" 82 | } 83 | } 84 | } 85 | 86 | test_result = %{ 87 | key: %{ 88 | :nested => %{ 89 | "some" => "value" 90 | } 91 | } 92 | } 93 | 94 | assert Cldr.Map.atomize_keys(test_map, skip: "nested") == test_result 95 | end 96 | 97 | test "deep_map with :reject" do 98 | test_map = %{ 99 | "key" => %{ 100 | "nested" => "value" 101 | } 102 | } 103 | 104 | test_result = %{ 105 | key: %{} 106 | } 107 | 108 | assert Cldr.Map.atomize_keys(test_map, reject: "nested") == test_result 109 | end 110 | 111 | test "Cldr.Map.extract_strings/2" do 112 | assert Cldr.Map.extract_strings(%{a: "string", b: :atom, c: "Another string"}) |> Enum.sort() == 113 | ["Another string", "string"] 114 | 115 | assert Cldr.Map.extract_strings(%{a: "string", b: %{c: "Another string"}}) |> Enum.sort() == 116 | ["Another string", "string"] 117 | 118 | assert Cldr.Map.extract_strings(%{a: "string", b: [:c, "Another string"]}) |> Enum.sort() == 119 | ["Another string", "string"] 120 | end 121 | 122 | end 123 | -------------------------------------------------------------------------------- /.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: ['27.0'] # Define the OTP version [required] 44 | elixir: ['1.17.2'] # Define the elixir version [required] 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 | -------------------------------------------------------------------------------- /mix.lock: -------------------------------------------------------------------------------- 1 | %{ 2 | "benchee": {:hex, :benchee, "1.4.0", "9f1f96a30ac80bab94faad644b39a9031d5632e517416a8ab0a6b0ac4df124ce", [: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", "299cd10dd8ce51c9ea3ddb74bb150f93d25e968f93e4c1fa31698a8e4fa5d715"}, 3 | "castore": {:hex, :castore, "1.0.15", "8aa930c890fe18b6fe0a0cff27b27d0d4d231867897bd23ea772dee561f032a3", [:mix], [], "hexpm", "96ce4c69d7d5d7a0761420ef743e2f4096253931a3ba69e5ff8ef1844fe446d3"}, 4 | "certifi": {:hex, :certifi, "2.15.0", "0e6e882fcdaaa0a5a9f2b3db55b1394dba07e8d6d9bcad08318fb604c6839712", [:rebar3], [], "hexpm", "b147ed22ce71d72eafdad94f055165c1c182f61a2ff49df28bcc71d1d5b94a60"}, 5 | "cldr_utils": {:hex, :cldr_utils, "2.13.1", "e066dfb426b638751f1b6f39dd3398f10d5f16e049317bf435193ce8f77e5b2a", [:mix], [{:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}], "hexpm", "19092aa029e518af24463af1798d6efac1b85126501f32b3b5a8de0fcd2d2249"}, 6 | "decimal": {:hex, :decimal, "2.3.0", "3ad6255aa77b4a3c4f818171b12d237500e63525c2fd056699967a3e7ea20f62", [:mix], [], "hexpm", "a4d66355cb29cb47c3cf30e71329e58361cfcb37c34235ef3bf1d7bf3773aeac"}, 7 | "deep_merge": {:hex, :deep_merge, "1.0.0", "b4aa1a0d1acac393bdf38b2291af38cb1d4a52806cf7a4906f718e1feb5ee961", [:mix], [], "hexpm", "ce708e5f094b9cd4e8f2be4f00d2f4250c4095be93f8cd6d018c753894885430"}, 8 | "dialyxir": {:hex, :dialyxir, "1.4.6", "7cca478334bf8307e968664343cbdb432ee95b4b68a9cba95bdabb0ad5bdfd9a", [:mix], [{:erlex, ">= 0.2.7", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "8cf5615c5cd4c2da6c501faae642839c8405b49f8aa057ad4ae401cb808ef64d"}, 9 | "earmark": {:hex, :earmark, "1.4.3", "364ca2e9710f6bff494117dbbd53880d84bebb692dafc3a78eb50aa3183f2bfd", [:mix], [], "hexpm", "8cf8a291ebf1c7b9539e3cddb19e9cef066c2441b1640f13c34c1d3cfc825fec"}, 10 | "earmark_parser": {:hex, :earmark_parser, "1.4.44", "f20830dd6b5c77afe2b063777ddbbff09f9759396500cdbe7523efd58d7a339c", [:mix], [], "hexpm", "4778ac752b4701a5599215f7030989c989ffdc4f6df457c5f36938cc2d2a2750"}, 11 | "erlex": {:hex, :erlex, "0.2.7", "810e8725f96ab74d17aac676e748627a07bc87eb950d2b83acd29dc047a30595", [:mix], [], "hexpm", "3ed95f79d1a844c3f6bf0cea61e0d5612a42ce56da9c03f01df538685365efb0"}, 12 | "ex_doc": {:hex, :ex_doc, "0.38.4", "ab48dff7a8af84226bf23baddcdda329f467255d924380a0cf0cee97bb9a9ede", [:mix], [{:earmark_parser, "~> 1.4.44", [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", "f7b62346408a83911c2580154e35613eb314e0278aeea72ed7fedef9c1f165b2"}, 13 | "makeup": {:hex, :makeup, "1.2.1", "e90ac1c65589ef354378def3ba19d401e739ee7ee06fb47f94c687016e3713d1", [:mix], [{:nimble_parsec, "~> 1.4", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "d36484867b0bae0fea568d10131197a4c2e47056a6fbe84922bf6ba71c8d17ce"}, 14 | "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"}, 15 | "makeup_erlang": {:hex, :makeup_erlang, "1.0.2", "03e1804074b3aa64d5fad7aa64601ed0fb395337b982d9bcf04029d68d51b6a7", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "af33ff7ef368d5893e4a267933e7744e46ce3cf1f61e2dccf53a111ed3aa3727"}, 16 | "nimble_parsec": {:hex, :nimble_parsec, "1.4.2", "8efba0122db06df95bfaa78f791344a89352ba04baedd3849593bfce4d0dc1c6", [:mix], [], "hexpm", "4b21398942dda052b403bbe1da991ccd03a053668d147d53fb8c4e0efe09c973"}, 17 | "parse_trans": {:hex, :parse_trans, "3.3.1", "16328ab840cc09919bd10dab29e431da3af9e9e7e7e6f0089dd5a2d2820011d8", [:rebar3], [], "hexpm", "07cd9577885f56362d414e8c4c4e6bdf10d43a8767abb92d24cbe8b24c54888b"}, 18 | "statistex": {:hex, :statistex, "1.1.0", "7fec1eb2f580a0d2c1a05ed27396a084ab064a40cfc84246dbfb0c72a5c761e5", [:mix], [], "hexpm", "f5950ea26ad43246ba2cce54324ac394a4e7408fdcf98b8e230f503a0cba9cf5"}, 19 | "stream_data": {:hex, :stream_data, "1.2.0", "58dd3f9e88afe27dc38bef26fce0c84a9e7a96772b2925c7b32cd2435697a52b", [:mix], [], "hexpm", "eb5c546ee3466920314643edf68943a5b14b32d1da9fe01698dc92b73f89a9ed"}, 20 | } 21 | -------------------------------------------------------------------------------- /test/math/math_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Math.Test do 2 | use ExUnit.Case, async: true 3 | 4 | alias Cldr.Math 5 | alias Cldr.Digits 6 | 7 | use ExUnitProperties 8 | 9 | property "check rounding for decimals is the same as Decimal.round/3" do 10 | check all(decimal <- GenerateNumber.decimal(), max_runs: 1_000) do 11 | assert Decimal.round(decimal, 0, :half_up) == Cldr.Math.round(decimal, 0, :half_up) 12 | end 13 | end 14 | 15 | property "check rounding to zero places for floats is the same as Kernel.round/1" do 16 | check all(float <- GenerateNumber.float(), max_runs: 1_000) do 17 | assert Kernel.round(float) == Cldr.Math.round(float, 0, :half_up) 18 | end 19 | end 20 | 21 | property "check rounding to zero places for floats is the same as Float.round/1" do 22 | check all(float <- GenerateNumber.float(), max_runs: 1_000) do 23 | assert Float.round(float) == Cldr.Math.round(float, 0, :half_up) 24 | end 25 | end 26 | 27 | test "integer number of digits for a decimal integer" do 28 | decimal = Decimal.new(1234) 29 | assert Digits.number_of_integer_digits(decimal) == 4 30 | end 31 | 32 | test "rounding floats" do 33 | assert Cldr.Math.round(1.401, 2, :half_even) == 1.4 34 | assert Cldr.Math.round(1.404, 2, :half_even) == 1.4 35 | assert Cldr.Math.round(1.405, 2, :half_even) == 1.4 36 | assert Cldr.Math.round(1.406, 2, :half_even) == 1.41 37 | end 38 | 39 | test "rounding floats with zero decimals" do 40 | assert Cldr.Math.round(3.6000000000000085) == 4.0 41 | end 42 | 43 | test "rounding a float < 0 with zero decimals" do 44 | assert Cldr.Math.round(0.959999999999809) == 1.0 45 | end 46 | 47 | test "integer number of digits for a decimal fixnum" do 48 | decimal = Decimal.from_float(1234.5678) 49 | assert Digits.number_of_integer_digits(decimal) == 4 50 | end 51 | 52 | test "rounding decimal number" do 53 | decimal = Decimal.new("0.1111") |> Cldr.Math.round() 54 | assert Decimal.equal?(decimal, Decimal.new(0)) 55 | assert decimal.sign == 1 56 | end 57 | 58 | @decimals [Decimal.new("0.9876"), Decimal.new("1.9876"), Decimal.new("0.4"), Decimal.new("0.6")] 59 | @places [0, 1, 2, 3] 60 | @rounding [:half_even, :floor, :ceiling, :half_up, :half_down] 61 | for d <- @decimals, p <- @places, r <- @rounding do 62 | test "default rounding is the same as Decimal.round for #{inspect(d)}, places: #{inspect(p)}, mode: #{ 63 | inspect(r) 64 | }" do 65 | assert Cldr.Math.round(unquote(Macro.escape(d)), unquote(p), unquote(r)) == 66 | Decimal.round(unquote(Macro.escape(d)), unquote(p), unquote(r)) 67 | end 68 | end 69 | 70 | if Code.ensure_loaded?(Decimal) && function_exported?(Decimal, :normalize, 1) do 71 | test "round significant digits for a decimal integer" do 72 | decimal = Decimal.new(1234) 73 | assert Math.round_significant(decimal, 2) == Decimal.normalize(Decimal.new(1200)) 74 | end 75 | 76 | test "round significant digits for a decimal" do 77 | decimal = Decimal.from_float(1234.45) 78 | assert Math.round_significant(decimal, 4) == Decimal.normalize(Decimal.new(1234)) 79 | end 80 | 81 | test "round significant digits for a decimal to 5 digits" do 82 | decimal = Decimal.from_float(1234.45) 83 | assert Math.round_significant(decimal, 5) == Decimal.normalize(Decimal.from_float(1234.5)) 84 | end 85 | else 86 | test "round significant digits for a decimal integer" do 87 | decimal = Decimal.new(1234) 88 | assert Math.round_significant(decimal, 2) == Decimal.reduce(Decimal.new(1200)) 89 | end 90 | 91 | test "round significant digits for a decimal" do 92 | decimal = Decimal.from_float(1234.45) 93 | assert Math.round_significant(decimal, 4) == Decimal.reduce(Decimal.new(1234)) 94 | end 95 | 96 | test "round significant digits for a decimal to 5 digits" do 97 | decimal = Decimal.from_float(1234.45) 98 | assert Math.round_significant(decimal, 5) == Decimal.reduce(Decimal.from_float(1234.5)) 99 | end 100 | end 101 | 102 | test "power of 0 == 1" do 103 | assert Math.power(Decimal.new(123), 0) == Decimal.new(1) 104 | end 105 | 106 | test "power of decimal where n > 1" do 107 | assert Math.power(Decimal.new(12), 3) == Decimal.new(1728) 108 | end 109 | 110 | test "power of decimal where n < 0" do 111 | assert Math.power(Decimal.new(4), -2) == Decimal.from_float(0.0625) 112 | end 113 | 114 | test "power of decimal where number < 0" do 115 | assert Math.power(Decimal.new(-4), 2) == Decimal.new(16) 116 | end 117 | 118 | test "power of integer when n = 0" do 119 | assert Math.power(3, 0) === 1 120 | end 121 | 122 | test "power of float when n == 0" do 123 | assert Math.power(3.0, 0) === 1.0 124 | end 125 | 126 | test "power of integer when n < 1" do 127 | assert Math.power(4, -2) == 0.0625 128 | end 129 | 130 | test "amod returns the divisor when it the remainder would be zero and test that dividend is one less" do 131 | {div, amod} = Cldr.Math.div_amod(24, 12) 132 | assert amod == 12 133 | assert div == 1 134 | end 135 | 136 | test "amod returns the zero for the remainder" do 137 | {div, mod} = Cldr.Math.div_mod(24, 12) 138 | assert mod == 0 139 | assert div == 2 140 | end 141 | end 142 | -------------------------------------------------------------------------------- /lib/cldr/http/http.ex: -------------------------------------------------------------------------------- 1 | defmodule Cldr.Http do 2 | @moduledoc """ 3 | Supports securely downloading https content. 4 | 5 | """ 6 | 7 | @cldr_unsafe_https "CLDR_UNSAFE_HTTPS" 8 | @cldr_default_timeout "120000" 9 | @cldr_default_connection_timeout "60000" 10 | 11 | @doc """ 12 | Securely download https content from 13 | a URL. 14 | 15 | This function uses the built-in `:httpc` 16 | client but enables certificate verification 17 | which is not enabled by `:httc` by default. 18 | 19 | See also https://erlef.github.io/security-wg/secure_coding_and_deployment_hardening/ssl 20 | 21 | ### Arguments 22 | 23 | * `url` is a binary URL or a `{url, list_of_headers}` tuple. If 24 | provided the headers are a list of `{'header_name', 'header_value'}` 25 | tuples. Note that the name and value are both charlists, not 26 | strings. 27 | 28 | * `options` is a keyword list of options. 29 | 30 | ### Options 31 | 32 | * `:verify_peer` is a boolean value indicating 33 | if peer verification should be done for this request. 34 | The default is `true` in which case the default 35 | `:ssl` options follow the [erlef guidelines](https://erlef.github.io/security-wg/secure_coding_and_deployment_hardening/ssl) 36 | noted above. 37 | 38 | * `:timeout` is the number of milliseconds available 39 | for the request to complete. The default is 40 | #{inspect @cldr_default_timeout}. This option may also be 41 | set with the `CLDR_HTTP_TIMEOUT` environment variable. 42 | 43 | * `:connection_timeout` is the number of milliseconds 44 | available for the a connection to be estabklished to 45 | the remote host. The default is #{inspect @cldr_default_connection_timeout}. 46 | This option may also be set with the 47 | `CLDR_HTTP_CONNECTION_TIMEOUT` environment variable. 48 | 49 | ### Returns 50 | 51 | * `{:ok, body}` if the return is successful. 52 | 53 | * `{:not_modified, headers}` if the request would result in 54 | returning the same results as one matching an etag. 55 | 56 | * `{:error, error}` if the download is 57 | unsuccessful. An error will also be logged 58 | in these cases. 59 | 60 | ### Unsafe HTTPS 61 | 62 | If the environment variable `CLDR_UNSAFE_HTTPS` is 63 | set to anything other than `FALSE`, `false`, `nil` 64 | or `NIL` then no peer verification of certificates 65 | is performed. Setting this variable is not recommended 66 | but may be required is where peer verification for 67 | unidentified reasons. Please [open an issue](https://github.com/elixir-cldr/cldr/issues) 68 | if this occurs. 69 | 70 | ### Certificate stores 71 | 72 | In order to keep dependencies to a minimum, 73 | `get/1` attempts to locate an already installed 74 | certificate store. It will try to locate a 75 | store in the following order which is intended 76 | to satisfy most host systems. The certificate 77 | store is expected to be a path name on the 78 | host system. 79 | 80 | ```elixir 81 | # A certificate store configured by the 82 | # developer 83 | Application.get_env(:ex_cldr, :cacertfile) 84 | 85 | # Populated if hex package `CAStore` is configured 86 | CAStore.file_path() 87 | 88 | # Populated if hex package `certfi` is configured 89 | :certifi.cacertfile() 90 | 91 | # Debian/Ubuntu/Gentoo etc. 92 | "/etc/ssl/certs/ca-certificates.crt", 93 | 94 | # Fedora/RHEL 6 95 | "/etc/pki/tls/certs/ca-bundle.crt", 96 | 97 | # OpenSUSE 98 | "/etc/ssl/ca-bundle.pem", 99 | 100 | # OpenELEC 101 | "/etc/pki/tls/cacert.pem", 102 | 103 | # CentOS/RHEL 7 104 | "/etc/pki/ca-trust/extracted/pem/tls-ca-bundle.pem", 105 | 106 | # Open SSL on MacOS 107 | "/usr/local/etc/openssl/cert.pem", 108 | 109 | # MacOS & Alpine Linux 110 | "/etc/ssl/cert.pem" 111 | ``` 112 | 113 | """ 114 | @spec get(String.t | {String.t, list()}, options :: Keyword.t) :: 115 | {:ok, binary} | {:not_modified, any()} | {:error, any} 116 | 117 | def get(url, options \\ []) 118 | 119 | def get(url, options) when is_binary(url) and is_list(options) do 120 | case get_with_headers(url, options) do 121 | {:ok, _headers, body} -> {:ok, body} 122 | other -> other 123 | end 124 | end 125 | 126 | def get({url, headers}, options) when is_binary(url) and is_list(headers) and is_list(options) do 127 | case get_with_headers({url, headers}, options) do 128 | {:ok, _headers, body} -> {:ok, body} 129 | other -> other 130 | end 131 | end 132 | 133 | @doc """ 134 | Securely download https content from 135 | a URL. 136 | 137 | This function uses the built-in `:httpc` 138 | client but enables certificate verification 139 | which is not enabled by `:httc` by default. 140 | 141 | See also https://erlef.github.io/security-wg/secure_coding_and_deployment_hardening/ssl 142 | 143 | ### Arguments 144 | 145 | * `url` is a binary URL or a `{url, list_of_headers}` tuple. If 146 | provided the headers are a list of `{'header_name', 'header_value'}` 147 | tuples. Note that the name and value are both charlists, not 148 | strings. 149 | 150 | * `options` is a keyword list of options. 151 | 152 | ### Options 153 | 154 | * `:verify_peer` is a boolean value indicating 155 | if peer verification should be done for this request. 156 | The default is `true` in which case the default 157 | `:ssl` options follow the [erlef guidelines](https://erlef.github.io/security-wg/secure_coding_and_deployment_hardening/ssl) 158 | noted above. 159 | 160 | * `:timeout` is the number of milliseconds available 161 | for the request to complete. The default is 162 | #{inspect @cldr_default_timeout}. This option may also be 163 | set with the `CLDR_HTTP_TIMEOUT` environment variable. 164 | 165 | * `:connection_timeout` is the number of milliseconds 166 | available for the a connection to be estabklished to 167 | the remote host. The default is #{inspect @cldr_default_connection_timeout}. 168 | This option may also be set with the 169 | `CLDR_HTTP_CONNECTION_TIMEOUT` environment variable. 170 | 171 | * `:https_proxy` is the URL of an https proxy to be used. The 172 | default is `nil`. 173 | 174 | ### Returns 175 | 176 | * `{:ok, body, headers}` if the return is successful. 177 | 178 | * `{:not_modified, headers}` if the request would result in 179 | returning the same results as one matching an etag. 180 | 181 | * `{:error, error}` if the download is 182 | unsuccessful. An error will also be logged 183 | in these cases. 184 | 185 | ### Unsafe HTTPS 186 | 187 | If the environment variable `CLDR_UNSAFE_HTTPS` is 188 | set to anything other than `FALSE`, `false`, `nil` 189 | or `NIL` then no peer verification of certificates 190 | is performed. Setting this variable is not recommended 191 | but may be required is where peer verification for 192 | unidentified reasons. Please [open an issue](https://github.com/elixir-cldr/cldr/issues) 193 | if this occurs. 194 | 195 | ### Https Proxy 196 | 197 | `Cldr.Http.get/2` will look for a proxy URL in the following 198 | locales in the order presented: 199 | 200 | * `options[:https_proxy]` 201 | * `ex_cldr` compile-time configuration under the 202 | key `:ex_cldr[:https_proxy]` 203 | * The environment variable `HTTPS_PROXY` 204 | * The environment variable `https_proxy` 205 | 206 | ### Certificate stores 207 | 208 | In order to keep dependencies to a minimum, 209 | `get/1` attempts to locate an already installed 210 | certificate store. It will try to locate a 211 | store in the following order which is intended 212 | to satisfy most host systems. The certificate 213 | store is expected to be a path name on the 214 | host system. 215 | 216 | ```elixir 217 | # A certificate store configured by the 218 | # developer 219 | Application.get_env(:ex_cldr, :cacertfile) 220 | 221 | # Populated if hex package `CAStore` is configured 222 | CAStore.file_path() 223 | 224 | # Populated if hex package `certfi` is configured 225 | :certifi.cacertfile() 226 | 227 | # Debian/Ubuntu/Gentoo etc. 228 | "/etc/ssl/certs/ca-certificates.crt", 229 | 230 | # Fedora/RHEL 6 231 | "/etc/pki/tls/certs/ca-bundle.crt", 232 | 233 | # OpenSUSE 234 | "/etc/ssl/ca-bundle.pem", 235 | 236 | # OpenELEC 237 | "/etc/pki/tls/cacert.pem", 238 | 239 | # CentOS/RHEL 7 240 | "/etc/pki/ca-trust/extracted/pem/tls-ca-bundle.pem", 241 | 242 | # Open SSL on MacOS 243 | "/usr/local/etc/openssl/cert.pem", 244 | 245 | # MacOS & Alpine Linux 246 | "/etc/ssl/cert.pem" 247 | ``` 248 | 249 | """ 250 | @doc since: "2.21.0" 251 | 252 | @spec get_with_headers(String.t | {String.t, list()}, options :: Keyword.t) :: 253 | {:ok, list(), binary} | {:not_modified, any()} | {:error, any} 254 | 255 | def get_with_headers(request, options \\ []) 256 | 257 | def get_with_headers(url, options) when is_binary(url) do 258 | get_with_headers({url, []}, options) 259 | end 260 | 261 | def get_with_headers({url, headers}, options) when is_binary(url) and is_list(headers) and is_list(options) do 262 | require Logger 263 | 264 | hostname = String.to_charlist(URI.parse(url).host) 265 | url = String.to_charlist(url) 266 | http_options = http_opts(hostname, options) 267 | https_proxy = https_proxy(options) 268 | ip_family = :inet6fb4 269 | 270 | if https_proxy do 271 | case URI.parse(https_proxy) do 272 | %{host: host, port: port} when is_binary(host) and is_integer(port) -> 273 | :ok = :httpc.set_options(https_proxy: {{String.to_charlist(host), port}, []}, ipfamily: ip_family) 274 | _other -> 275 | Logger.bare_log(:warning, "https_proxy was set to an invalid value. Found #{inspect https_proxy}.") 276 | end 277 | else 278 | :ok = :httpc.set_options(ipfamily: ip_family) 279 | end 280 | 281 | case :httpc.request(:get, {url, headers}, http_options, []) do 282 | {:ok, {{_version, 200, _}, headers, body}} -> 283 | {:ok, headers, body} 284 | 285 | {:ok, {{_version, 304, _}, headers, _body}} -> 286 | {:not_modified, headers} 287 | 288 | {_, {{_version, code, message}, _headers, _body}} -> 289 | Logger.bare_log( 290 | :error, 291 | "Failed to download #{url}. " <> 292 | "HTTP Error: (#{code}) #{inspect(message)}" 293 | ) 294 | 295 | {:error, code} 296 | 297 | {:error, {:failed_connect, [{:to_address, {host, _port}}, {:inet6, _, _}, {_, _, :timeout}]}} -> 298 | Logger.bare_log( 299 | :error, 300 | "Timeout connecting to #{inspect(host)} to download #{inspect url}. " <> 301 | "Connection time exceeded #{http_options[:connect_timeout]}ms." 302 | ) 303 | 304 | {:error, :connection_timeout} 305 | 306 | {:error, {:failed_connect, [{:to_address, {host, _port}}, {:inet6, _, _}, {_, _, :nxdomain}]}} -> 307 | Logger.bare_log( 308 | :error, 309 | "Failed to resolve host #{inspect(host)} to download #{inspect url}" 310 | ) 311 | 312 | {:error, :nxdomain} 313 | 314 | {:error, {other}} -> 315 | Logger.bare_log( 316 | :error, 317 | "Failed to download #{inspect url}. Error #{inspect other}" 318 | ) 319 | 320 | {:error, other} 321 | 322 | {:error, :timeout} -> 323 | Logger.bare_log( 324 | :error, 325 | "Timeout downloading from #{inspect url}. " <> 326 | "Request exceeded #{http_options[:timeout]}ms." 327 | ) 328 | {:error, :timeout} 329 | end 330 | end 331 | 332 | @static_certificate_locations [ 333 | # Debian/Ubuntu/Gentoo etc. 334 | "/etc/ssl/certs/ca-certificates.crt", 335 | 336 | # Fedora/RHEL 6 337 | "/etc/pki/tls/certs/ca-bundle.crt", 338 | 339 | # OpenSUSE 340 | "/etc/ssl/ca-bundle.pem", 341 | 342 | # OpenELEC 343 | "/etc/pki/tls/cacert.pem", 344 | 345 | # CentOS/RHEL 7 346 | "/etc/pki/ca-trust/extracted/pem/tls-ca-bundle.pem", 347 | 348 | # Open SSL on MacOS 349 | "/usr/local/etc/openssl/cert.pem", 350 | 351 | # MacOS & Alpine Linux 352 | "/etc/ssl/cert.pem" 353 | ] 354 | 355 | defp dynamic_certificate_locations do 356 | [ 357 | # Configured cacertfile 358 | Application.get_env(:ex_cldr, :cacertfile), 359 | 360 | # Populated if hex package CAStore is configured 361 | if(Code.ensure_loaded?(CAStore), do: apply(CAStore, :file_path, [])), 362 | 363 | # Populated if hex package certfi is configured 364 | if(Code.ensure_loaded?(:certifi), do: apply(:certifi, :cacertfile, []) |> List.to_string()) 365 | ] 366 | |> Enum.reject(&is_nil/1) 367 | end 368 | 369 | def certificate_locations() do 370 | dynamic_certificate_locations() ++ @static_certificate_locations 371 | end 372 | 373 | @doc false 374 | defp certificate_store do 375 | certificate_locations() 376 | |> Enum.find(&File.exists?/1) 377 | |> raise_if_no_cacertfile! 378 | |> :erlang.binary_to_list() 379 | end 380 | 381 | defp raise_if_no_cacertfile!(nil) do 382 | raise RuntimeError, """ 383 | No certificate trust store was found. 384 | Tried looking for: #{inspect(certificate_locations())} 385 | 386 | A certificate trust store is required in 387 | order to download locales for your configuration. 388 | 389 | Since ex_cldr could not detect a system 390 | installed certificate trust store one of the 391 | following actions may be taken: 392 | 393 | 1. Install the hex package `castore`. It will 394 | be automatically detected after recompilation. 395 | 396 | 2. Install the hex package `certifi`. It will 397 | be automatically detected after recomilation. 398 | 399 | 3. Specify the location of a certificate trust store 400 | by configuring it in `config.exs` or `runtime.exs`: 401 | 402 | config :ex_cldr, 403 | cacertfile: "/path/to/cacertfile", 404 | ... 405 | 406 | """ 407 | end 408 | 409 | defp raise_if_no_cacertfile!(file) do 410 | file 411 | end 412 | 413 | defp http_opts(hostname, options) do 414 | default_timeout = 415 | "CLDR_HTTP_TIMEOUT" 416 | |> System.get_env(@cldr_default_timeout) 417 | |> String.to_integer() 418 | 419 | default_connection_timeout = 420 | "CLDR_HTTP_CONNECTION_TIMEOUT" 421 | |> System.get_env(@cldr_default_connection_timeout) 422 | |> String.to_integer() 423 | 424 | verify_peer? = Keyword.get(options, :verify_peer, true) 425 | ssl_options = https_ssl_opts(hostname, verify_peer?) 426 | timeout = Keyword.get(options, :timeout, default_timeout) 427 | connection_timeout = Keyword.get(options, :connection_timeout, default_connection_timeout) 428 | 429 | [timeout: timeout, connect_timeout: connection_timeout, ssl: ssl_options] 430 | end 431 | 432 | defp https_ssl_opts(hostname, verify_peer?) do 433 | if secure_ssl?() and verify_peer? do 434 | [ 435 | verify: :verify_peer, 436 | cacertfile: certificate_store(), 437 | depth: 4, 438 | ciphers: preferred_ciphers(), 439 | versions: protocol_versions(), 440 | eccs: preferred_eccs(), 441 | reuse_sessions: true, 442 | server_name_indication: hostname, 443 | secure_renegotiate: true, 444 | customize_hostname_check: [ 445 | match_fun: :public_key.pkix_verify_hostname_match_fun(:https) 446 | ] 447 | ] 448 | else 449 | [ 450 | verify: :verify_none, 451 | server_name_indication: hostname, 452 | secure_renegotiate: true, 453 | reuse_sessions: true, 454 | versions: protocol_versions(), 455 | ciphers: preferred_ciphers(), 456 | versions: protocol_versions(), 457 | ] 458 | end 459 | end 460 | 461 | defp preferred_ciphers do 462 | preferred_ciphers = 463 | [ 464 | # Cipher suites (TLS 1.3): TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256 465 | %{cipher: :aes_128_gcm, key_exchange: :any, mac: :aead, prf: :sha256}, 466 | %{cipher: :aes_256_gcm, key_exchange: :any, mac: :aead, prf: :sha384}, 467 | %{cipher: :chacha20_poly1305, key_exchange: :any, mac: :aead, prf: :sha256}, 468 | # Cipher suites (TLS 1.2): ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256: 469 | # ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305: 470 | # ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384 471 | %{cipher: :aes_128_gcm, key_exchange: :ecdhe_ecdsa, mac: :aead, prf: :sha256}, 472 | %{cipher: :aes_128_gcm, key_exchange: :ecdhe_rsa, mac: :aead, prf: :sha256}, 473 | %{cipher: :aes_256_gcm, key_exchange: :ecdh_ecdsa, mac: :aead, prf: :sha384}, 474 | %{cipher: :aes_256_gcm, key_exchange: :ecdh_rsa, mac: :aead, prf: :sha384}, 475 | %{cipher: :chacha20_poly1305, key_exchange: :ecdhe_ecdsa, mac: :aead, prf: :sha256}, 476 | %{cipher: :chacha20_poly1305, key_exchange: :ecdhe_rsa, mac: :aead, prf: :sha256}, 477 | %{cipher: :aes_128_gcm, key_exchange: :dhe_rsa, mac: :aead, prf: :sha256}, 478 | %{cipher: :aes_256_gcm, key_exchange: :dhe_rsa, mac: :aead, prf: :sha384} 479 | ] 480 | 481 | :ssl.filter_cipher_suites(preferred_ciphers, []) 482 | end 483 | 484 | defp protocol_versions do 485 | if otp_version() < 25 do 486 | [:"tlsv1.2"] 487 | else 488 | [:"tlsv1.2", :"tlsv1.3"] 489 | end 490 | end 491 | 492 | defp preferred_eccs do 493 | # TLS curves: X25519, prime256v1, secp384r1 494 | preferred_eccs = [:secp256r1, :secp384r1] 495 | :ssl.eccs() -- (:ssl.eccs() -- preferred_eccs) 496 | end 497 | 498 | defp secure_ssl? do 499 | case System.get_env(@cldr_unsafe_https) do 500 | nil -> true 501 | "FALSE" -> false 502 | "false" -> false 503 | "nil" -> false 504 | "NIL" -> false 505 | _other -> true 506 | end 507 | end 508 | 509 | defp https_proxy(options) do 510 | options[:https_proxy] || 511 | Application.get_env(:ex_cldr, :https_proxy) || 512 | System.get_env("HTTPS_PROXY") || 513 | System.get_env("https_proxy") 514 | end 515 | 516 | def otp_version do 517 | :erlang.system_info(:otp_release) |> List.to_integer 518 | end 519 | end 520 | -------------------------------------------------------------------------------- /lib/cldr/utils/digits.ex: -------------------------------------------------------------------------------- 1 | defmodule Cldr.Digits do 2 | @moduledoc """ 3 | Abstract representation of number (integer, float, Decimal) in tuple form 4 | and functions for transformations on number parts. 5 | 6 | Representing a number as a list of its digits, and integer representing 7 | where the decimal point is placed and an integer representing the sign 8 | of the number allow more efficient transforms on the various parts of 9 | the number as happens during the formatting of a number for string output. 10 | """ 11 | 12 | import Bitwise 13 | import Cldr.Math, only: [power_of_10: 1] 14 | require Integer 15 | alias Cldr.Math 16 | 17 | @typedoc """ 18 | Defines a number in a tuple form of three parts: 19 | 20 | * A list of digits (0..9) representing the number 21 | 22 | * A digit representing the place of the decimal points 23 | in the number 24 | 25 | * a `1` or `-1` representing the sign of the number 26 | 27 | A number in integer, float or Decimal form can be converted 28 | to digit form with `Digits.to_digits/1`. 29 | 30 | The digits can be converted back to normal form with 31 | `Cldr.Digits.to_integer/1`, `Cldr.Digits.to_float/1` and 32 | `Cldr.Digits.to_decimal/1`. 33 | """ 34 | @type t :: {[0..9, ...], non_neg_integer, 1 | -1} 35 | 36 | @two52 bsl(1, 52) 37 | @two53 bsl(1, 53) 38 | @float_bias 1022 39 | @min_e -1074 40 | 41 | @doc """ 42 | Returns the fractional part of an integer, float or Decimal as an integer. 43 | 44 | * `number` can be either a float, Decimal or integer although an integer has 45 | no fraction part and will therefore always return 0. 46 | 47 | ## Examples 48 | 49 | iex> Cldr.Digits.fraction_as_integer(123.456) 50 | 456 51 | 52 | iex> Cldr.Digits.fraction_as_integer(Decimal.new("123.456")) 53 | 456 54 | 55 | iex> Cldr.Digits.fraction_as_integer(1999) 56 | 0 57 | 58 | """ 59 | @spec fraction_as_integer(Math.number_or_decimal() | {list, list, 1 | -1}) :: integer 60 | def fraction_as_integer({_integer, fraction, _sign}) 61 | when is_list(fraction) do 62 | Integer.undigits(fraction) 63 | end 64 | 65 | def fraction_as_integer({_integer, [], _sign}) do 66 | 0 67 | end 68 | 69 | def fraction_as_integer(number) do 70 | number 71 | |> to_tuple 72 | |> fraction_as_integer 73 | end 74 | 75 | def fraction_as_integer(number, rounding) do 76 | number = Float.round(number, rounding) 77 | fraction_as_integer(number) 78 | end 79 | 80 | @doc """ 81 | Returns the number of decimal digits in a number 82 | (integer, float, Decimal) 83 | 84 | ## Options 85 | 86 | * `number` is an integer, float or `Decimal` 87 | or a list (which is assumed to contain digits). 88 | 89 | ## Examples 90 | 91 | iex> Cldr.Digits.number_of_digits(1234) 92 | 4 93 | 94 | iex> Cldr.Digits.number_of_digits(Decimal.new("123456789")) 95 | 9 96 | 97 | iex> Cldr.Digits.number_of_digits(1234.456) 98 | 7 99 | 100 | iex> Cldr.Digits.number_of_digits(1234.56789098765) 101 | 15 102 | 103 | iex> Cldr.Digits.number_of_digits(~c"12345") 104 | 5 105 | 106 | """ 107 | @spec number_of_digits( 108 | Math.number_or_decimal() 109 | | list() 110 | | {[integer(), ...], integer | [integer(), ...], -1 | 1} 111 | ) :: integer 112 | 113 | def number_of_digits(%Decimal{} = number) do 114 | number 115 | |> to_digits 116 | |> number_of_digits 117 | end 118 | 119 | def number_of_digits(number) when is_number(number) do 120 | number 121 | |> to_digits 122 | |> number_of_digits 123 | end 124 | 125 | def number_of_digits(list) when is_list(list) do 126 | length(list) 127 | end 128 | 129 | def number_of_digits({integer, place, _sign}) 130 | when is_list(integer) and is_integer(place) do 131 | length(integer) 132 | end 133 | 134 | @doc """ 135 | Returns the number of decimal digits in the integer 136 | part of a number. 137 | 138 | ## Options 139 | 140 | * `number` is an integer, float or `Decimal` or 141 | a list (which is assumed to contain digits). 142 | 143 | ## Examples 144 | 145 | iex> Cldr.Digits.number_of_integer_digits(1234) 146 | 4 147 | 148 | iex> Cldr.Digits.number_of_integer_digits(Decimal.new("123456789")) 149 | 9 150 | 151 | iex> Cldr.Digits.number_of_integer_digits(1234.456) 152 | 4 153 | 154 | iex> Cldr.Digits.number_of_integer_digits(~c"12345") 155 | 5 156 | 157 | """ 158 | @spec number_of_integer_digits( 159 | Math.number_or_decimal() 160 | | list() 161 | | {[integer(), ...], integer | [integer(), ...], -1 | 1} 162 | ) :: integer 163 | def number_of_integer_digits(%Decimal{} = number) do 164 | number 165 | |> to_digits 166 | |> number_of_integer_digits 167 | end 168 | 169 | def number_of_integer_digits(number) when is_number(number) do 170 | number 171 | |> to_digits 172 | |> number_of_integer_digits 173 | end 174 | 175 | # A decomposed integer might be charlist or a list of integers 176 | # since for certain transforms this is more efficient. Note 177 | # that we are not checking if the list elements are actually 178 | # digits. 179 | def number_of_integer_digits(list) when is_list(list) do 180 | length(list) 181 | end 182 | 183 | # For a tuple returned by `Digits.to_digits/1` 184 | def number_of_integer_digits({integer, place, _sign}) 185 | when is_list(integer) and is_integer(place) and place <= 0 do 186 | 0 187 | end 188 | 189 | def number_of_integer_digits({integer, place, _sign}) 190 | when is_list(integer) and is_integer(place) do 191 | place 192 | end 193 | 194 | # For a tuple returned by `Digits.to_tuple/1` 195 | def number_of_integer_digits({[], _fraction, _sign}) do 196 | 0 197 | end 198 | 199 | def number_of_integer_digits({integer, fraction, _sign}) 200 | when is_list(integer) and is_list(fraction) do 201 | number_of_integer_digits(integer) 202 | end 203 | 204 | @doc """ 205 | Remove trailing zeroes from the integer part of a number 206 | and returns the integer part without trailing zeros. 207 | 208 | * `number` is an integer, float or Decimal. 209 | 210 | ## Examples 211 | 212 | iex> Cldr.Digits.remove_trailing_zeros(1234000) 213 | 1234 214 | 215 | """ 216 | @spec remove_trailing_zeros(Math.number_or_decimal() | [integer(), ...]) :: 217 | integer | [integer(), ...] 218 | def remove_trailing_zeros(0) do 219 | 0 220 | end 221 | 222 | def remove_trailing_zeros(number) when is_number(number) do 223 | {integer_digits, _fraction_digits, sign} = to_tuple(number) 224 | removed = remove_trailing_zeros(integer_digits) 225 | to_integer({removed, length(removed), sign}) 226 | end 227 | 228 | def remove_trailing_zeros(%Decimal{} = number) do 229 | {integer_digits, _fraction_digits, sign} = to_tuple(number) 230 | removed = remove_trailing_zeros(integer_digits) 231 | to_integer({removed, length(removed), sign}) 232 | end 233 | 234 | # Filters either a charlist or a list of integers. 235 | def remove_trailing_zeros(number) when is_list(number) do 236 | Enum.take_while(number, fn c -> 237 | (c >= ?1 and c <= ?9) or c > 0 238 | end) 239 | end 240 | 241 | @doc """ 242 | Returns the number of leading zeros in a 243 | Decimal fraction. 244 | 245 | * `number` is an integer, float or Decimal 246 | 247 | Returns the number of leading zeros in the fractional 248 | part of a number. 249 | 250 | ## Examples 251 | 252 | iex> Cldr.Digits.number_of_leading_zeros(Decimal.new("0.0001")) 253 | 3 254 | 255 | """ 256 | @spec number_of_leading_zeros(Math.number_or_decimal() | [integer(), ...]) :: integer 257 | def number_of_leading_zeros(%Decimal{} = number) do 258 | {_integer_digits, fraction_digits, _sign} = to_tuple(number) 259 | number_of_leading_zeros(fraction_digits) 260 | end 261 | 262 | def number_of_leading_zeros(number) when is_number(number) do 263 | {_integer_digits, fraction_digits, _sign} = to_tuple(number) 264 | number_of_leading_zeros(fraction_digits) 265 | end 266 | 267 | def number_of_leading_zeros(number) when is_list(number) do 268 | Enum.take_while(number, fn c -> c == ?0 or c == 0 end) 269 | |> length 270 | end 271 | 272 | @doc """ 273 | Returns the number of trailing zeros in an 274 | integer number. 275 | 276 | * `number` is an integer. 277 | 278 | Returns the number of trailing zeros in the fractional 279 | part of an integer. 280 | 281 | ## Examples 282 | 283 | iex> Cldr.Digits.number_of_trailing_zeros(123000) 284 | 3 285 | 286 | """ 287 | def number_of_trailing_zeros(number) when is_integer(number) do 288 | {integer_digits, _fraction_digits, _sign} = to_tuple(number) 289 | number_of_trailing_zeros(integer_digits) 290 | end 291 | 292 | def number_of_trailing_zeros(number) when is_list(number) do 293 | number 294 | |> Enum.reverse 295 | |> Enum.take_while(fn c -> c == ?0 or c == 0 end) 296 | |> length 297 | end 298 | 299 | @doc """ 300 | Converts given number to a list representation. 301 | 302 | Given an IEEE 754 float, computes the shortest, correctly rounded list of 303 | digits that converts back to the same Double value when read back with 304 | String.to_float/1. Implements the algorithm from "Printing Floating-Point 305 | Numbers Quickly and Accurately" in Proceedings of the SIGPLAN '96 Conference 306 | on Programming Language Design and Implementation. 307 | 308 | Returns a tuple comprising a charlist for the integer part, 309 | a charlist for the fractional part and an integer for the sign. 310 | """ 311 | 312 | # Code extracted from: 313 | # https://github.com/ewildgoose/elixir-float_pp/blob/master/lib/float_pp/digits.ex, 314 | # which is licensed under http://www.apache.org/licenses/LICENSE-2.0 315 | 316 | @spec to_tuple(Decimal.t() | number) :: {list(), list(), integer} 317 | def to_tuple(number) do 318 | {mantissa, exp, sign} = to_digits(number) 319 | 320 | mantissa = 321 | cond do 322 | # Need to right fill with zeros 323 | exp > length(mantissa) -> 324 | mantissa ++ :lists.duplicate(exp - length(mantissa), 0) 325 | 326 | # Need to left fill with zeros 327 | exp < 0 -> 328 | :lists.duplicate(abs(exp), 0) ++ mantissa 329 | 330 | true -> 331 | mantissa 332 | end 333 | 334 | cond do 335 | # Its an integer 336 | exp == length(mantissa) -> 337 | {mantissa, [], sign} 338 | 339 | # It's a fraction with no integer part 340 | exp <= 0 -> 341 | {[], mantissa, sign} 342 | 343 | # It's a fraction 344 | exp > 0 and exp < length(mantissa) -> 345 | {integer, fraction} = :lists.split(exp, mantissa) 346 | {integer, fraction, sign} 347 | end 348 | end 349 | 350 | @doc """ 351 | Computes a iodata list of the digits of the given IEEE 754 floating point number, 352 | together with the location of the decimal point as {digits, place, positive}. 353 | 354 | A "compact" representation is returned, so there may be fewer digits returned 355 | than the decimal point location. 356 | """ 357 | def to_digits(float_0) when float_0 == 0.0, do: {[0], 1, 1} 358 | def to_digits(0), do: {[0], 1, 1} 359 | 360 | def to_digits(float) when is_float(float) do 361 | # Find mantissa and exponent from IEEE-754 packed notation 362 | {frac, exp} = frexp(float) 363 | 364 | # Scale fraction to integer (and adjust mantissa to compensate) 365 | frac = trunc(abs(frac) * @two53) 366 | exp = exp - 53 367 | 368 | # Compute digits 369 | flonum(float, frac, exp) 370 | end 371 | 372 | if Code.ensure_loaded?(Decimal) and function_exported?(Decimal, :normalize, 1) do 373 | def to_digits(%Decimal{} = number) do 374 | %Decimal{coef: coef, exp: exp, sign: sign} = Decimal.normalize(number) 375 | {digits, _place, _sign} = to_digits(coef) 376 | {digits, length(digits) + exp, sign} 377 | end 378 | else 379 | def to_digits(%Decimal{} = number) do 380 | %Decimal{coef: coef, exp: exp, sign: sign} = Decimal.reduce(number) 381 | {digits, _place, _sign} = to_digits(coef) 382 | {digits, length(digits) + exp, sign} 383 | end 384 | end 385 | 386 | def to_digits(integer) when is_integer(integer) when integer >= 0 do 387 | digits = Integer.digits(integer) 388 | {digits, length(digits), 1} 389 | end 390 | 391 | def to_digits(integer) when is_integer(integer) do 392 | digits = Integer.digits(integer) 393 | {digits, length(digits), -1} 394 | end 395 | 396 | @doc """ 397 | Takes a list of digits and coverts them back to a number of the same 398 | type as `number`. 399 | """ 400 | def to_number(digits, number) when is_integer(number), do: to_integer(digits) 401 | def to_number(digits, number) when is_float(number), do: to_float(digits) 402 | def to_number(digits, %Decimal{}), do: to_decimal(digits) 403 | 404 | def to_number(digits, :integer), do: to_integer(digits) 405 | def to_number(digits, :float), do: to_float(digits) 406 | def to_number(digits, :decimal), do: to_decimal(digits) 407 | 408 | def to_integer({digits, place, sign}) do 409 | {int_digits, _fraction_digits} = Enum.split(digits, place) 410 | Integer.undigits(int_digits) * sign 411 | end 412 | 413 | def to_float({[0], _place, _sign}) do 414 | 0.0 415 | end 416 | 417 | def to_float({digits, place, sign}) when length(digits) >= place do 418 | Integer.undigits(digits) / power_of_10(length(digits) - place) * sign 419 | end 420 | 421 | def to_float({digits, place, sign}) do 422 | Integer.undigits(digits) * power_of_10(place - length(digits)) * sign * 1.0 423 | end 424 | 425 | def to_decimal({digits, place, sign}) do 426 | %Decimal{coef: Integer.undigits(digits), exp: place - length(digits), sign: sign} 427 | end 428 | 429 | ############################################################################ 430 | # The following functions are Elixir translations of the original paper: 431 | # "Printing Floating-Point Numbers Quickly and Accurately" 432 | # http://www.cs.tufts.edu/~nr/cs257/archive/florian-loitsch/printf.pdf 433 | # See the paper for further explanation 434 | 435 | _ = """ 436 | Set initial values {r, s, m+, m-} 437 | based on table 1 from FP-Printing paper 438 | Assumes frac is scaled to integer (and exponent scaled appropriately) 439 | """ 440 | 441 | defp flonum(float, frac, exp) do 442 | round = Integer.is_even(frac) 443 | 444 | if exp >= 0 do 445 | b_exp = bsl(1, exp) 446 | 447 | if frac !== @two52 do 448 | scale(frac * b_exp * 2, 2, b_exp, b_exp, round, round, float) 449 | else 450 | scale(frac * b_exp * 4, 4, b_exp * 2, b_exp, round, round, float) 451 | end 452 | else 453 | if exp === @min_e or frac !== @two52 do 454 | scale(frac * 2, bsl(1, 1 - exp), 1, 1, round, round, float) 455 | else 456 | scale(frac * 4, bsl(1, 2 - exp), 2, 1, round, round, float) 457 | end 458 | end 459 | end 460 | 461 | @log_0_approx -60 462 | def scale(r, s, m_plus, m_minus, low_ok, high_ok, float) do 463 | # TODO: Benchmark removing the log10 and using the approximation given in original paper? 464 | est = 465 | if float == 0 do 466 | @log_0_approx 467 | else 468 | trunc(Float.ceil(:math.log10(abs(float)) - 1.0e-10)) 469 | end 470 | 471 | if est >= 0 do 472 | fixup(r, s * power_of_10(est), m_plus, m_minus, est, low_ok, high_ok, float) 473 | else 474 | scale = power_of_10(-est) 475 | fixup(r * scale, s, m_plus * scale, m_minus * scale, est, low_ok, high_ok, float) 476 | end 477 | end 478 | 479 | def fixup(r, s, m_plus, m_minus, k, low_ok, high_ok, float) do 480 | too_low = if high_ok, do: r + m_plus >= s, else: r + m_plus > s 481 | 482 | if too_low do 483 | {generate(r, s, m_plus, m_minus, low_ok, high_ok), k + 1, sign(float)} 484 | else 485 | {generate(r * 10, s, m_plus * 10, m_minus * 10, low_ok, high_ok), k, sign(float)} 486 | end 487 | end 488 | 489 | defp generate(r, s, m_plus, m_minus, low_ok, high_ok) do 490 | d = div(r, s) 491 | r = rem(r, s) 492 | 493 | tc1 = if low_ok, do: r <= m_minus, else: r < m_minus 494 | tc2 = if high_ok, do: r + m_plus >= s, else: r + m_plus > s 495 | 496 | if not tc1 do 497 | if not tc2 do 498 | [d | generate(r * 10, s, m_plus * 10, m_minus * 10, low_ok, high_ok)] 499 | else 500 | [d + 1] 501 | end 502 | else 503 | if not tc2 do 504 | [d] 505 | else 506 | if r * 2 < s do 507 | [d] 508 | else 509 | [d + 1] 510 | end 511 | end 512 | end 513 | end 514 | 515 | ############################################################################ 516 | # Utility functions 517 | 518 | # FIXME: We don't handle +/-inf and NaN inputs. Not believed to be an issue in 519 | # Elixir, but beware future-self reading this... 520 | 521 | # The frexp() function is as per the clib function with the same name. It breaks 522 | # the floating-point number value into a normalized fraction and an integral 523 | # power of 2. 524 | # 525 | # Returns {frac, exp}, where the magnitude of frac is in the interval 526 | # [1/2, 1) or 0, and value = frac*(2^exp). 527 | 528 | @doc false 529 | def frexp(value) do 530 | <> = <> 531 | frexp(sign, frac, exp) 532 | end 533 | 534 | def frexp(_Sign, 0, 0) do 535 | {0.0, 0} 536 | end 537 | 538 | # Handle denormalised values 539 | def frexp(sign, frac, 0) do 540 | exp = bitwise_length(frac) 541 | <> = <> 542 | {f, -@float_bias - 52 + exp} 543 | end 544 | 545 | # Handle normalised values 546 | def frexp(sign, frac, exp) do 547 | <> = <> 548 | {f, exp - @float_bias} 549 | end 550 | 551 | _ = """ 552 | Return the number of significant bits needed to store the given number 553 | """ 554 | 555 | defp bitwise_length(value) do 556 | bitwise_length(value, 0) 557 | end 558 | 559 | defp bitwise_length(0, n), do: n 560 | defp bitwise_length(value, n), do: bitwise_length(bsr(value, 1), n + 1) 561 | 562 | defp sign(float) when float < 0, do: -1 563 | defp sign(_float), do: 1 564 | end 565 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | **Cldr Utils from version 2.27.0 requires Elixir 1.12 or later** 4 | 5 | ## Cldr Utils version 2.29.1 6 | 7 | This is the changelog for Cldr Utils v2.29.1 released on November 1st, 2025. For older changelogs please consult the release tag on [GitHub](https://github.com/elixir-cldr/cldr_utils/tags) 8 | 9 | ### Bug Fixes 10 | 11 | * Fix range warning in `Cldr.Math.float_to_ratio/2`. 12 | 13 | ## Cldr Utils version 2.29.0 14 | 15 | This is the changelog for Cldr Utils v2.29.0 released on October 9th, 2025. For older changelogs please consult the release tag on [GitHub](https://github.com/elixir-cldr/cldr_utils/tags) 16 | 17 | ### Enhancements 18 | 19 | * Adds `Cldr.Math.float_to_ratio/2`. This function supports formatting numbers as fractions in the upcoming [CLDR 48](https://cldr.unicode.org/downloads/cldr-48) and the relevant `ex_cldr` version. Note that decimals are not currently supported. 20 | 21 | ## Cldr Utils version 2.28.3 22 | 23 | This is the changelog for Cldr Utils v2.28.3 released on April 28th, 2025. For older changelogs please consult the release tag on [GitHub](https://github.com/elixir-cldr/cldr_utils/tags) 24 | 25 | ### Bug Fixes 26 | 27 | * Set `ip_family: :inet6fb4` in `Cldr.Http` to support both ipv6 and ipv4 for downloading assets. Thanks to @edolnx for the report. Closes #8. 28 | 29 | ## Cldr Utils version 2.28.2 30 | 31 | This is the changelog for Cldr Utils v2.28.2 released on September 6th, 2024. For older changelogs please consult the release tag on [GitHub](https://github.com/elixir-cldr/cldr_utils/tags) 32 | 33 | ### Bug Fixes 34 | 35 | * Use `:erlang.iolist_to_binary/1` instead of `List.to_string/1` since the later won't handle some unicode correctly. 36 | 37 | ## Cldr Utils version 2.28.1 38 | 39 | This is the changelog for Cldr Utils v2.28.1 released on August 14th, 2024. For older changelogs please consult the release tag on [GitHub](https://github.com/elixir-cldr/cldr_utils/tags) 40 | 41 | ### Bug Fixes 42 | 43 | * Fix `Cldr.Json.decode!/1` when calling with a charlist instead of a binary. 44 | 45 | ## Cldr Utils version 2.28.0 46 | 47 | This is the changelog for Cldr Utils v2.28.0 released on July 10th, 2024. For older changelogs please consult the release tag on [GitHub](https://github.com/elixir-cldr/cldr_utils/tags) 48 | 49 | ### Bug Fixes 50 | 51 | * Fix `Cldr.Json.decode!/1` to return only the decoded JSON. 52 | 53 | ### Enhancements 54 | 55 | * Add `Cldr.Json.decode!/2` that implements the `keys: :atoms` option from `Jason`. 56 | 57 | ## Cldr Utils version 2.27.0 58 | 59 | This is the changelog for Cldr Utils v2.27.0 released on June 23rd, 2024. For older changelogs please consult the release tag on [GitHub](https://github.com/elixir-cldr/cldr_utils/tags) 60 | 61 | ### Minimum Elixir version 62 | 63 | * `cldr_utils` version 2.27.0 and later requires Elixir 1.12 or later. 64 | 65 | ### Enhancements 66 | 67 | * Adds `Cldr.Json.decode!/1` that delegates to `:json.decode/1`. This allows `Cldr.Json` to be configured as a `json_library` in `ex_cldr` for OTP versions 27 and later. For example: 68 | 69 | ```elixir 70 | config :ex_cldr, 71 | json_library: Cldr.Json 72 | ``` 73 | 74 | * Refactor some tests so they work on older Elixir versions without `sigil_c`. 75 | 76 | ## Cldr Utils version 2.26.0 77 | 78 | This is the changelog for Cldr Utils v2.25.0 released on May 28th, 2024. For older changelogs please consult the release tag on [GitHub](https://github.com/elixir-cldr/cldr_utils/tags) 79 | 80 | ### Bug Fixes 81 | 82 | * Fix warnings on Elixir 1.17. This primarily relates to charlists constants now required to use `sigil_c` to avoid warnings. As a result, tests will only work on Elixir 1.16 and later even though support for the library is for Elixir 1.11 and later. 83 | 84 | ## Cldr Utils version 2.25.0 85 | 86 | This is the changelog for Cldr Utils v2.25.0 released on March 20th, 2024. For older changelogs please consult the release tag on [GitHub](https://github.com/elixir-cldr/cldr_utils/tags) 87 | 88 | ### Bug Fixes 89 | 90 | * Fix `Cldr.Math.pow/2` when the exponent is in the range 0 < n < 1. 91 | 92 | ### Enhancements 93 | 94 | * Adds `Cldr.Math.mult/2`, `Cldr.Math.div/2`, `Cldr.Math.add/2` and `Cldr.Math.sub/2` to operate on integers, floats and Decimals. 95 | 96 | ## Cldr Utils version 2.24.2 97 | 98 | This is the changelog for Cldr Utils v2.24.2 released on November 2nd, 2023. For older changelogs please consult the release tag on [GitHub](https://github.com/elixir-cldr/cldr_utils/tags) 99 | 100 | ### Bug Fixes 101 | 102 | * Fix deprecation warnings for Elixir 1.16. 103 | 104 | ## Cldr Utils version 2.24.1 105 | 106 | This is the changelog for Cldr Utils v2.24.1 released on June 17th, 2023. For older changelogs please consult the release tag on [GitHub](https://github.com/elixir-cldr/cldr_utils/tags) 107 | 108 | **Cldr Utils now requires Elixir 1.11 or later** 109 | 110 | ### Bug Fixes 111 | 112 | * Resolve host certificate stores at runtime, not compile time. Thanks to @joshk for the PR. Closes #7. 113 | 114 | ## Cldr Utils version 2.24.0 115 | 116 | This is the changelog for Cldr Utils v2.24.0 released on May 22nd, 2023. For older changelogs please consult the release tag on [GitHub](https://github.com/elixir-cldr/cldr_utils/tags) 117 | 118 | **Cldr Utils now requires Elixir 1.11 or later** 119 | 120 | ### Enhancements 121 | 122 | * Adds `Cldr.Utils.otp_version/0` to return the OTP version as a string. Copied with thanks and appreciation from the `Hex` source. 123 | 124 | ## Cldr Utils version 2.23.1 125 | 126 | This is the changelog for Cldr Utils v2.23.0 released on May 4th, 2023. For older changelogs please consult the release tag on [GitHub](https://github.com/elixir-cldr/cldr_utils/tags) 127 | 128 | **Cldr Utils now requires Elixir 1.11 or later** 129 | 130 | ### Bug Fixes 131 | 132 | * Make parsing `HTTP_PROXY` values more resilient to invalid URLs. 133 | 134 | ## Cldr Utils version 2.23.0 135 | 136 | This is the changelog for Cldr Utils v2.23.0 released on May 4th, 2023. For older changelogs please consult the release tag on [GitHub](https://github.com/elixir-cldr/cldr_utils/tags) 137 | 138 | **Cldr Utils now requires Elixir 1.11 or later** 139 | 140 | ### Enhancements 141 | 142 | * Adds support for https proxy for `Cldr.Http.get/2`. The proxy can be specified as an option to to `Cldr.Http.get/2`, as a configuration option under the `:ex_cldr[:https_proxy]` key, or from the environment variables `HTTPS_PROXY` or `https_proxy`. Thanks to @d-led for the PR and issue. 143 | 144 | ## Cldr Utils version 2.22.0 145 | 146 | This is the changelog for Cldr Utils v2.22.0 released on March 25th, 2023. For older changelogs please consult the release tag on [GitHub](https://github.com/elixir-cldr/cldr_utils/tags) 147 | 148 | **Cldr Utils now requires Elixir 1.11 or later** 149 | 150 | ### Enhancements 151 | 152 | * Adds `:timeout` and `:connection_timeout` options to `Cldr.Http.get/2`. The defaults are `[timeout: 60_000, connection_timeout: 120_000]`. The environment variables `CLDR_HTTP_TIMEOUT` and `CLDR_HTTPS_CONNECTION_TIMEOUT` can also be used to set the timeouts. The prededence is `options[:timeout] -> environment variable -> default.` 153 | 154 | ## Cldr Utils version 2.21.0 155 | 156 | This is the changelog for Cldr Utils v2.21.0 released on January 27th, 2023. For older changelogs please consult the release tag on [GitHub](https://github.com/elixir-cldr/cldr_utils/tags) 157 | 158 | **Cldr Utils now requires Elixir 1.11 or later** 159 | 160 | ### Enhancements 161 | 162 | * Add `:verify_peer` as an option to `Cldr.Http.get/1` and `Cldr.Http.get_with_headers/1` 163 | 164 | ## Cldr Utils version 2.20.0 165 | 166 | This is the changelog for Cldr Utils v2.20.0 released on January 27th, 2023. For older changelogs please consult the release tag on [GitHub](https://github.com/elixir-cldr/cldr_utils/tags) 167 | 168 | **Cldr Utils now requires Elixir 1.11 or later** 169 | 170 | ### Enhancements 171 | 172 | * Adds `Cldr.Http.get_with_headers/1` that will return the headers from the response as well as the body. 173 | 174 | * Support headers when sending a request with `Cldr.Http.get/1` and `Cldr.Http.get_with_headers/1` 175 | 176 | * Add `:verify_peer` as an option to `Cldr.Http.get/1` and `Cldr.Http.get_with_headers/1` 177 | 178 | ## Cldr Utils version 2.19.2 179 | 180 | This is the changelog for Cldr Utils v2.19.2 released on January 25th, 2023. For older changelogs please consult the release tag on [GitHub](https://github.com/elixir-cldr/cldr_utils/tags) 181 | 182 | **Cldr Utils now requires Elixir 1.11 or later** 183 | 184 | ### Bug Fixes 185 | 186 | * Relaxes the requirement for the optional [castore](https://hex.pm/packages/castore) library. Thanks to @maennchen for the PR. Closes #6. 187 | 188 | ## Cldr Utils version 2.19.1 189 | 190 | This is the changelog for Cldr Utils v2.19.1 released on August 23rd, 2022. For older changelogs please consult the release tag on [GitHub](https://github.com/elixir-cldr/cldr_utils/tags) 191 | 192 | **Cldr Utils now requires Elixir 1.11 or later** 193 | 194 | ### Bug Fixes 195 | 196 | * Use only TLS 1.2 on OTP versions less than 25. 197 | 198 | ## Cldr Utils version 2.19.0 199 | 200 | This is the changelog for Cldr Utils v2.19.0 released on August 22nd, 2022. For older changelogs please consult the release tag on [GitHub](https://github.com/elixir-cldr/cldr_utils/tags) 201 | 202 | **Cldr Utils now requires Elixir 1.11 or later** 203 | 204 | ### Enhancements 205 | 206 | * Sets SNI option for SSL connections 207 | 208 | * Supports `CLDR_UNSAFE_HTTPS` environment variable option which, if set to anything other than `FALSE`, `false`, `nil` or `NIL` will not perform peer verification for HTTPS requests. This may be used in circumstances where peer verification is failing but if generally not recommended. 209 | 210 | ## Cldr Utils version 2.18.0 211 | 212 | This is the changelog for Cldr Utils v2.18.0 released on July 31st, 2022. For older changelogs please consult the release tag on [GitHub](https://github.com/elixir-cldr/cldr_utils/tags) 213 | 214 | **Cldr Utils now requires Elixir 1.11 or later** 215 | 216 | ### Bug Fixes 217 | 218 | * Fix deprecation warnings for Elixir 1.14. 219 | 220 | ## Cldr Utils version 2.17.2 221 | 222 | This is the changelog for Cldr Utils v2.17.2 released on May 8th, 2022. For older changelogs please consult the release tag on [GitHub](https://github.com/elixir-cldr/cldr_utils/tags) 223 | 224 | ### Bug Fixes 225 | 226 | * Harden the SSL options for `Cldr.Http.get/1` in line with the recommendations at https://erlef.github.io/security-wg/secure_coding_and_deployment_hardening/ssl 227 | 228 | ## Cldr Utils version 2.17.1 229 | 230 | This is the changelog for Cldr Utils v2.17.1 released on February 21st, 2022. For older changelogs please consult the release tag on [GitHub](https://github.com/elixir-cldr/cldr_utils/tags) 231 | 232 | ### Bug Fixes 233 | 234 | * Fix `Cldr.Map.invert/2` to use `Enum.map/2` not `Enum.flat_map/2` 235 | 236 | ## Cldr Utils version 2.17.0 237 | 238 | This is the changelog for Cldr Utils v2.17.0 released on October 27th, 2021. For older changelogs please consult the release tag on [GitHub](https://github.com/elixir-cldr/cldr_utils/tags) 239 | 240 | ### Enhancements 241 | 242 | * Add `:duplicates` option to `Cldr.Map.invert/2` to determine how to handle duplicate values after inversion. The options are: 243 | 244 | * `nil` or `false` which is the default and means only one value is kept. `Map.new/1` is used meanng the selected value is non-deterministic. 245 | * `:keep` meaning duplicate values are returned in a list 246 | * `:shortest` means the shortest duplicate is kept. This operates on string or atom values. 247 | * `:longest` means the shortest duplicate is kept. This operates on string or atom values. 248 | 249 | ### Bug Fixes 250 | 251 | * Don't attempt to convert calendar era dates to iso days - do that when required in `ex_cldr_calendars` 252 | 253 | * Remove `Cldr.Calendar.Conversion` module which is not required 254 | 255 | * Fix `Cldr.Map.deep_map/3` so that the `:filter` option is propogated correctly when `:only/:except` is also specified. 256 | 257 | ## Cldr Utils version 2.17.0-rc.0 258 | 259 | This is the changelog for Cldr Utils v2.17.0 released on October 5th, 2021. For older changelogs please consult the release tag on [GitHub](https://github.com/elixir-cldr/cldr_utils/tags) 260 | 261 | ### Bug Fixes 262 | 263 | * Don't attempt to convert calendar era dates to iso days - do that when required in `ex_cldr_calendars` 264 | 265 | * Remove `Cldr.Calendar.Conversion` module which is not required 266 | 267 | * Fix `Cldr.Map.deep_map/3` so that the `:filter` option is propogated correctly when `:only/:except` is also specified. 268 | 269 | ### Enhancements 270 | 271 | * Add `:duplicates` option to `Cldr.Map.invert/2` to determine how to handle duplicate values after inversion. The options are: 272 | 273 | * `nil` or `false` which is the default and means only one value is kept. `Map.new/1` is used meanng the selected value is non-deterministic. 274 | * `:keep` meaning duplicate values are returned in a list 275 | * `:shortest` means the shortest duplicate is kept. This operates on string or atom values. 276 | * `:longest` means the shortest duplicate is kept. This operates on string or atom values. 277 | 278 | ## Cldr Utils version 2.16.0 279 | 280 | This is the changelog for Cldr Utils v2.16.0 released on June 11th, 2021. For older changelogs please consult the release tag on [GitHub](https://github.com/elixir-cldr/cldr_utils/tags) 281 | 282 | ### Enhancements 283 | 284 | * Add `Cldr.Map.extract_strings/2` 285 | 286 | * Make resolver a parameter to `Cldr.Map.deep_merge/3` 287 | 288 | * Make resolver a parameter to `Cldr.Map.merge_map_list/2` 289 | 290 | * Add `Cldr.Map.prune/2` that prunes (deletes) branches from a (possibly deeply nested) map 291 | 292 | * Add `Cldr.Map.invert/1` that inverts the `{key, value}` of a map to be `{value, key}` and if `value` is a list, one new map entry for each element of `value` will be created (mapped to `key`) 293 | 294 | ## Cldr Utils version 2.15.1 295 | 296 | This is the changelog for Cldr Utils v2.15.1 released on March 16th, 2021. For older changelogs please consult the release tag on [GitHub](https://github.com/elixir-cldr/cldr_utils/tags) 297 | 298 | ### Bug Fixes 299 | 300 | * Fix `Cldr.Digit.to_number/2` for floats. Thanks for the report from @jlauemoeller. Fixes #15. 301 | 302 | ## Cldr Utils version 2.15.0 303 | 304 | This is the changelog for Cldr Utils v2.15.0 released on March 5th, 2021. For older changelogs please consult the release tag on [GitHub](https://github.com/elixir-cldr/cldr_utils/tags) 305 | 306 | ### Enhancements 307 | 308 | * Adds the options `:filter`, `:reject` and `:skip` to `Cldr.Map.deep_map/3` that work on entire branches of a map. 309 | 310 | ## Cldr Utils version 2.14.1 311 | 312 | This is the changelog for Cldr Utils v2.14.1 released on February 17th, 2021. For older changelogs please consult the release tag on [GitHub](https://github.com/elixir-cldr/cldr_utils/tags) 313 | 314 | ### Bug Fixes 315 | 316 | * Merge the fixes from cldr_utils version 2.13.3 for `Cldr.Math.power/2` 317 | 318 | ## Cldr Utils version 2.14.0 319 | 320 | This is the changelog for Cldr Utils v2.14.0 released on November 7th, 2020. For older changelogs please consult the release tag on [GitHub](https://github.com/elixir-cldr/cldr_utils/tags) 321 | 322 | ### Enhancements 323 | 324 | * Adds `Cldr.Http.get/1` to download from `https` URLs using `:httpc` but with certificate vertification enabled (it is not enabled by default in the `:httpc` module). 325 | 326 | ## Cldr Utils version 2.13.3 327 | 328 | This is the changelog for Cldr Utils v2.13.3 released on February 17th, 2021. For older changelogs please consult the release tag on [GitHub](https://github.com/elixir-cldr/cldr_utils/tags) 329 | 330 | ### Bug fixes 331 | 332 | * Fix `Cldr.Math.power/2` when both arguments are Decimal and the power is negative. 333 | 334 | * Update the docs for `Cldr.Math.round_significant/2` to note that rounding floats to significant digits cannot always return the expected precision since floats cannot represent all decimal numbers correctly. 335 | 336 | ## Cldr Utils version 2.13.2 337 | 338 | This is the changelog for Cldr Utils v2.13.2 released on October 20th, 2020. For older changelogs please consult the release tag on [GitHub](https://github.com/elixir-cldr/cldr_utils/tags) 339 | 340 | ### Bug fixes 341 | 342 | * Fix unused variable warning on OTP versions that do not include `:persistent_term` module. Thanks to @kianmeng. 343 | 344 | ## Cldr Utils version 2.13.1 345 | 346 | This is the changelog for Cldr Utils v2.13.1 released on September 30th, 2020. For older changelogs please consult the release tag on [GitHub](https://github.com/elixir-cldr/cldr_utils/tags) 347 | 348 | ### Enhancements 349 | 350 | * Add `Cldr.Decimal.parse/1` as a compatibiity layer for Decimal 1.x and 2.x 351 | 352 | ## Cldr Utils version 2.12.0 353 | 354 | This is the changelog for Cldr Utils v2.12.0 released on September 29th, 2020. For older changelogs please consult the release tag on [GitHub](https://github.com/elixir-cldr/cldr_utils/tags) 355 | 356 | ### Enhancements 357 | 358 | * Add `Cldr.Digit.number_of_trailing_zeros/1` to calculate the number of trailing zeros in an integer 359 | 360 | ## Cldr Utils version 2.11.0 361 | 362 | This is the changelog for Cldr Utils v2.11.0 released on September 25th, 2020. For older changelogs please consult the release tag on [GitHub](https://github.com/elixir-cldr/cldr_utils/tags) 363 | 364 | ### Enhancements 365 | 366 | * Provides `Cldr.Decimal.reduce/1` as a compatibility shim for Decimal 1.x and 2.x 367 | 368 | * Provides `Cldr.Decimal.compare/2` as a compatibility shim for Decimal 1.x and 2.x 369 | 370 | ## Cldr Utils version 2.10.0 371 | 372 | This is the changelog for Cldr Utils v2.10.0 released on September 8th, 2020. For older changelogs please consult the release tag on [GitHub](https://github.com/elixir-cldr/cldr_utils/tags) 373 | 374 | ### Enhancements 375 | 376 | * Supports `Decimal` 1.6 or greater or `Decimal` 2.x or later 377 | 378 | ## Cldr Utils version 2.9.1 379 | 380 | This is the changelog for Cldr Utils v2.9.1 released on May 3rd, 2020. For older changelogs please consult the release tag on [GitHub](https://github.com/elixir-cldr/cldr_utils/tags) 381 | 382 | ### Bug Fixes 383 | 384 | * Fix compatibility with `ex_cldr` releases up to 2.13.0. Thanks to @hl for the report. Fixes #3. 385 | 386 | ## Cldr Utils version 2.9.0 387 | 388 | This is the changelog for Cldr Utils v2.9.0 released on May 2nd, 2020. For older changelogs please consult the release tag on [GitHub](https://github.com/elixir-cldr/cldr_utils/tags) 389 | 390 | ### Enhancements 391 | 392 | * Add `:level`, `:only` and `:except` options to `Cldr.Map.deep_map/3` and refactor functions that use it 393 | 394 | * Add `Cldr.Enum.reduce_peeking/3` that is a simple reduce function that also passed the tail of the enum being reduced to enable a simple form of lookahead 395 | 396 | * Refactor `Cldr.Math.round/2` implementation for floating point numbers that improves efficiency by about 100% since it avoids round trip conversion to `Decimal` 397 | 398 | ## Cldr Utils version 2.8.0 399 | 400 | This is the changelog for Cldr Utils v2.8.0 released on February 14th, 2020. For older changelogs please consult the release tag on [GitHub](https://github.com/elixir-cldr/cldr_utils/tags) 401 | 402 | ### Enhancements 403 | 404 | * Be more resilient of the availability of `:persistent_term` given that `get/2`, `get/1` and `:persistent_term` itself are available on different OTP releases. Thanks to @halostatue. Closes #2. 405 | 406 | ## Cldr Utils version 2.7.0 407 | 408 | This is the changelog for Cldr Utils v2.7.0 released on January 31st, 2020. For older changelogs please consult the release tag on [GitHub](https://github.com/elixir-cldr/cldr_utils/tags) 409 | 410 | ### Enhancements 411 | 412 | * Add `Cldr.String.to_underscore/1` that replaces "-" with "_" 413 | 414 | ## Cldr Utils version 2.6.0 415 | 416 | This is the changelog for Cldr Utils v2.6.0 released on January 21st, 2020. For older changelogs please consult the release tag on [GitHub](https://github.com/elixir-cldr/cldr_utils/tags) 417 | 418 | ### Enhancements 419 | 420 | * Support `Decimal` versions `~> 1.6 or 1.9 or 2.0`. Version 1.9 deprecates `Decimal.compare/2` in favour of `Decimal.cmp/2`. The upcoming `Decimal` version 2.0 deprecates `Decimal.cmp/2` in favour of a new implementation of `Decimal.compare/2` that conforms to Elixir norms and is required to support `Enum.sort/2` correctly. This version of `cldr_utils` detects the relevant version and adapts accordingly at compile time. 421 | 422 | ## Cldr Utils version 2.5.0 423 | 424 | This is the changelog for Cldr Utils v2.5.0 released on October 22nd, 2019. For older changelogs please consult the release tag on [GitHub](https://github.com/elixir-cldr/cldr_utils/tags) 425 | 426 | ### Enhancements 427 | 428 | * Add `Cldr.Macros.warn_once/3` to log a warning, but only once for a given key 429 | 430 | ## Cldr Utils version 2.4.0 431 | 432 | This is the changelog for Cldr Utils v2.4.0 released on August 23rd, 2019. For older changelogs please consult the release tag on [GitHub](https://github.com/elixir-cldr/cldr_utils/tags) 433 | 434 | ### Enhancements 435 | 436 | * Add `Cldr.String.hash/1` to implement a polynomial rolling hash function 437 | 438 | ## Cldr Utils version 2.3.0 439 | 440 | This is the changelog for Cldr Utils v2.3.0 released on June 15th, 2019. For older changelogs please consult the release tag on [GitHub](https://github.com/elixir-cldr/cldr_utils/tags) 441 | 442 | ### Enhancements 443 | 444 | * Adds `doc_since/1` and `calendar_impl/0` to support conditional compilation based upon Elixir versions 445 | 446 | ## Cldr Utils version 2.2.0 447 | 448 | This is the changelog for Cldr Utils v2.2.0 released on March 25th, 2019. For older changelogs please consult the release tag on [GitHub](https://github.com/elixir-cldr/cldr_utils/tags) 449 | 450 | ### Enhancements 451 | 452 | * Add `Cldr.Math.div_amod/2` 453 | 454 | ## Cldr Utils version 2.1.0 455 | 456 | This is the changelog for Cldr Utils v2.1.0 released on March 10th, 2019. For older changelogs please consult the release tag on [GitHub](https://github.com/elixir-cldr/cldr_utils/tags) 457 | 458 | ### Bug Fixes 459 | 460 | * `Cldr.Map.integerize_keys/1` now properly processes negative integer keys. Minor version change to make it easier to peg versions in upstream packages. 461 | 462 | ## Cldr Utils version 2.0.5 463 | 464 | This is the changelog for Cldr Utils v2.0.4 released on Jnauary 3rd, 2018. For older changelogs please consult the release tag on [GitHub](https://github.com/elixir-cldr/cldr_utils/tags) 465 | 466 | ### Bug Fixes 467 | 468 | * Fixes `Cldr.Math.round/3` for floats when rounding is > 0 digits 469 | 470 | ## Cldr Utils version 2.0.4 471 | 472 | This is the changelog for Cldr Utils v2.0.4 released on Decmber 15th, 2018. For older changelogs please consult the release tag on [GitHub](https://github.com/elixir-cldr/cldr_utils/tags) 473 | 474 | ### Bug Fixes 475 | 476 | * Fixes `Cldr.Math.round/3` to be compatible with `Decimal.round/3` and `Kernel.round/1` 477 | 478 | ## Cldr Utils version 2.0.3 479 | 480 | This is the changelog for Cldr Utils v2.0.3 released on Decmber 8th, 2018. For older changelogs please consult the release tag on [GitHub](https://github.com/elixir-cldr/cldr_utils/tags) 481 | 482 | ### Bug Fixes 483 | 484 | * Fixed an error in `Cldr.Math.round/3` for `Decimal` numbers where the value being rounded is < 1 but greater than 0 whereby the sign was being returned as `true` instead of `1`. 485 | 486 | ## Cldr Utils version 2.0.2 487 | 488 | This is the changelog for Cldr Utils v2.0.2 released on November 23rd, 2018. For older changelogs please consult the release tag on [GitHub](https://github.com/elixir-cldr/cldr_utils/tags) 489 | 490 | ### Enhancements 491 | 492 | * Replace *additional* deprecated `Decimal.new/1` with `Decimal.from_float/1` where required 493 | 494 | ## Cldr Utils version 2.0.1 495 | 496 | This is the changelog for Cldr Utils v2.0.1 released on November 23rd, 2018. For older changelogs please consult the release tag on [GitHub](https://github.com/elixir-cldr/cldr_utils/tags) 497 | 498 | ### Enhancements 499 | 500 | * Replace deprecated `Decimal.new/1` with `Decimal.from_float/1` where required 501 | 502 | ## Cldr Utils version 2.0.0 503 | 504 | This is the changelog for Cldr Utils v2.0.0 released on October 29th, 2018. For older changelogs please consult the release tag on [GitHub](https://github.com/elixir-cldr/cldr_utils/tags) 505 | 506 | ### Enhancements 507 | 508 | * Initial release extracted from [ex_cldr](https://hex.pm/packages/ex_cldr) 509 | -------------------------------------------------------------------------------- /lib/cldr/utils/map.ex: -------------------------------------------------------------------------------- 1 | defmodule Cldr.Map do 2 | @moduledoc """ 3 | Functions for transforming maps, keys and values. 4 | """ 5 | 6 | @doc """ 7 | Returns am argument unchanged. 8 | 9 | Useful when a `noop` function is required. 10 | """ 11 | def identity(x), do: x 12 | 13 | @default_deep_map_options [level: 1..1_000_000, filter: [], reject: [], skip: [], only: [], except: []] 14 | @starting_level 1 15 | @max_level 1_000_000 16 | 17 | @doc """ 18 | Recursively traverse a map and invoke a function 19 | that transforms the mapfor each key/value pair. 20 | 21 | ## Arguments 22 | 23 | * `map` is any `t:map/0` 24 | 25 | * `function` is a `1-arity` function or function reference that 26 | is called for each key/value pair of the provided map. It can 27 | also be a 2-tuple of the form `{key_function, value_function}` 28 | 29 | * In the case where `function` is a single function it will be 30 | called with the 2-tuple argument `{key, value}` 31 | 32 | * In the case where function is of the form `{key_function, value_function}` 33 | the `key_function` will be called with the argument `key` and the value 34 | function will be called with the argument `value` 35 | 36 | * `options` is a keyword list of options. The default is 37 | `#{inspect @default_deep_map_options}` 38 | 39 | ## Options 40 | 41 | * `:level` indicates the starting (and optionally ending) levels of 42 | the map at which the `function` is executed. This can 43 | be an integer representing one `level` or a range 44 | indicating a range of levels. The default is `1..#{@max_level}` 45 | 46 | * `:only` is a term or list of terms or a `check function`. If it is a term 47 | or list of terms, the `function` is only called if the `key` of the 48 | map is equal to the term or in the list of terms. If `:only` is a 49 | `check function` then the `check function` is passed the `{k, v}` of 50 | the current branch in the `map`. It is expected to return a `truthy` 51 | value that if `true` signals that the argument `function` will be executed. 52 | 53 | * `:except` is a term or list of terms or a `check function`. If it is a term 54 | or list of terms, the `function` is only called if the `key` of the 55 | map is not equal to the term or not in the list of terms. If `:except` is a 56 | `check function` then the `check function` is passed the `{k, v}` of 57 | the current branch in the `map`. It is expected to return a `truthy` 58 | value that if `true` signals that the argument `function` will not be executed. 59 | 60 | * `:filter` is a term or list of terms or a `check function`. If the 61 | `key` currently being processed equals the term (or is in the list of 62 | terms, or the `check_function` returns a truthy value) then this branch 63 | of the map is processed by `function` and its output is included in the result. 64 | 65 | * `:reject` is a term or list of terms or a `check function`. If the 66 | `key` currently being processed equals the term (or is in the list of 67 | terms, or the `check_function` returns a truthy value) then this branch 68 | of the map is omitted from the mapped output. 69 | 70 | * `:skip` is a term or list of terms or a `check function`. If the 71 | `key` currently being processed equals the term (or is in the list of 72 | terms, or the `check_function` returns a truthy value) then this branch 73 | of the map is *not* processed by `function` but it is included in 74 | the mapped result. 75 | 76 | ## Notes 77 | 78 | * `:only` and `:except` operate on individual keys whereas `:filter` 79 | and `:filter` and `:reject` operator on *entire branches* of a map 80 | 81 | * If both the options `:only` and `:except` are provided then the `function` 82 | is called only when a `term` meets both criteria. That means that `:except` 83 | has priority over `:only`. 84 | 85 | * If both the options `:filter` and `:reject` are provided then `:reject` 86 | has priority over `:filter`. 87 | 88 | ## Returns 89 | 90 | * The `map` transformed by the recursive application of 91 | `function` 92 | 93 | ## Examples 94 | 95 | iex> map = %{a: :a, b: %{c: :c}} 96 | iex> fun = fn 97 | ...> {k, v} when is_atom(k) -> {Atom.to_string(k), v} 98 | ...> other -> other 99 | ...> end 100 | iex> Cldr.Map.deep_map map, fun 101 | %{"a" => :a, "b" => %{"c" => :c}} 102 | iex> map = %{a: :a, b: %{c: :c}} 103 | iex> Cldr.Map.deep_map map, fun, only: :c 104 | %{a: :a, b: %{"c" => :c}} 105 | iex> Cldr.Map.deep_map map, fun, except: [:a, :b] 106 | %{a: :a, b: %{"c" => :c}} 107 | iex> Cldr.Map.deep_map map, fun, level: 2 108 | %{a: :a, b: %{"c" => :c}} 109 | 110 | """ 111 | @spec deep_map( 112 | map() | list(), 113 | function :: function() | {function(), function()}, 114 | options :: list() 115 | ) :: 116 | map() | list() 117 | 118 | def deep_map(map, function, options \\ @default_deep_map_options) 119 | 120 | # Don't deep map structs since they have atom keys anyway and they 121 | # also don't support enumerable 122 | def deep_map(%_struct{} = map, _function, _options) when is_map(map) do 123 | map 124 | end 125 | 126 | def deep_map(map_or_list, function, options) 127 | when (is_map(map_or_list) or is_list(map_or_list)) and is_list(options) do 128 | options = validate_options(function, options) 129 | deep_map(map_or_list, function, options, @starting_level) 130 | end 131 | 132 | defp deep_map(nil, _fun, _options, _level) do 133 | nil 134 | end 135 | 136 | # If the level is greater than the return 137 | # just return the map or list 138 | defp deep_map(map_or_list, _function, %{level: %{last: last}}, level) when level > last do 139 | map_or_list 140 | end 141 | 142 | # If the level is less than the range then keep recursing 143 | # without executing the function 144 | defp deep_map(map, function, %{level: %{first: first}} = options, level) 145 | when is_map(map) and level < first do 146 | Enum.map(map, fn 147 | {k, v} when is_map(v) or is_list(v) -> 148 | {k, deep_map(v, function, options, level + 1)} 149 | 150 | {k, v} -> 151 | {k, v} 152 | end) 153 | |> Map.new() 154 | end 155 | 156 | # Here we are in range so we conditionally execute the function 157 | # if the options for `:only` and `:except` are matched 158 | defp deep_map(map, function, options, level) when is_map(map) and is_function(function) do 159 | Enum.reduce(map, [], fn 160 | {k, v}, acc when is_map(v) or is_list(v) -> 161 | case process_type({k, v}, options) do 162 | :continue -> 163 | v = deep_map(v, function, Map.put(options, :filtering, true), level + 1) 164 | [{k, v} | acc] 165 | :process -> 166 | v = deep_map(v, function, Map.put(options, :filtering, true), level + 1) 167 | [function.({k, v}) | acc] 168 | :except -> 169 | v = deep_map(v, function, options, level + 1) 170 | [{k, v} | acc] 171 | :skip -> 172 | [function.({k, v}) | acc] 173 | :reject -> 174 | acc 175 | end 176 | 177 | {k, v}, acc -> 178 | case process_type({k, v}, options) do 179 | :continue -> 180 | [{k, v} | acc] 181 | :process -> 182 | [function.({k, v}) | acc] 183 | :except -> 184 | [{k, v} | acc] 185 | :skip -> 186 | [function.({k, v}) | acc] 187 | :reject -> 188 | acc 189 | end 190 | end) 191 | |> Map.new() 192 | end 193 | 194 | defp deep_map(map, {key_function, value_function}, options, level) when is_map(map) do 195 | Enum.reduce(map, [], fn 196 | {k, v}, acc when is_map(v) or is_list(v) -> 197 | case process_type({k, v}, options) do 198 | :continue -> 199 | v = deep_map(v, {key_function, value_function}, Map.put(options, :filtering, true), level + 1) 200 | [{k, v} | acc] 201 | :process -> 202 | v = deep_map(v, {key_function, value_function}, Map.put(options, :filtering, true), level + 1) 203 | [{key_function.(k), value_function.(v)} | acc] 204 | :except -> 205 | v = deep_map(v, {key_function, value_function}, options, level + 1) 206 | [{k, v} | acc] 207 | :skip -> 208 | [{key_function.(k), v} | acc] 209 | :reject -> 210 | acc 211 | end 212 | 213 | {k, v}, acc -> 214 | case process_type({k, v}, options) do 215 | :continue -> 216 | [{k, v} | acc] 217 | :process -> 218 | [{key_function.(k), value_function.(v)} | acc] 219 | :except -> 220 | [{k, v} | acc] 221 | :skip -> 222 | [{key_function.(k), v} | acc] 223 | :reject -> 224 | acc 225 | end 226 | end) 227 | |> Map.new() 228 | end 229 | 230 | defp deep_map([], _function, _options, _level) do 231 | [] 232 | end 233 | 234 | defp deep_map([head | rest], function, options, level) do 235 | case process_type(head, options) do 236 | :continue -> 237 | [head | deep_map(rest, function, Map.put(options, :filtering, true), level + 1)] 238 | :process -> 239 | [deep_map(head, function, Map.put(options, :filtering, true), level + 1) | 240 | deep_map(rest, function, options, level + 1)] 241 | :except -> 242 | [deep_map(head, function, options, level + 1) | 243 | deep_map(rest, function, options, level + 1)] 244 | :skip -> 245 | [head | deep_map(rest, function, options, level + 1)] 246 | :reject -> 247 | deep_map(rest, function, options, level + 1) 248 | end 249 | end 250 | 251 | defp deep_map(value, function, options, _level) when is_function(function) do 252 | case process_type(value, options) do 253 | :continue -> 254 | value 255 | :process -> 256 | function.(value) 257 | :except -> 258 | value 259 | :skip -> 260 | value 261 | :reject -> 262 | nil 263 | end 264 | end 265 | 266 | defp deep_map(value, {_key_function, value_function}, options, _level) do 267 | case process_type(value, options) do 268 | :continue -> 269 | value 270 | :process -> 271 | value_function.(value) 272 | :skip -> 273 | value 274 | :reject -> 275 | nil 276 | end 277 | end 278 | 279 | @doc """ 280 | Transforms a `map`'s `String.t` keys to `atom()` keys. 281 | 282 | ## Arguments 283 | 284 | * `map` is any `t:map/0` 285 | 286 | * `options` is a keyword list of options passed 287 | to `deep_map/3`. One additional option apples 288 | to this function directly: 289 | 290 | * `:only_existing` which is set to `true` will 291 | only convert the binary value to an atom if the atom 292 | already exists. The default is `false`. 293 | 294 | ## Example 295 | 296 | iex> Cldr.Map.atomize_keys %{"a" => %{"b" => %{1 => "c"}}} 297 | %{a: %{b: %{1 => "c"}}} 298 | 299 | """ 300 | @default_atomize_options [only_existing: false] 301 | def atomize_keys(map, options \\ []) 302 | 303 | def atomize_keys(map, options) when is_map(map) or is_list(map) do 304 | options = @default_atomize_options ++ options 305 | map_options = Map.new(options) 306 | 307 | deep_map(map, &atomize_key(&1, map_options), options) 308 | end 309 | 310 | def atomize_keys({k, value}, options) when is_map(value) or is_list(value) do 311 | options = @default_atomize_options ++ options 312 | map_options = Map.new(options) 313 | 314 | {atomize_key(k, map_options), deep_map(value, &atomize_key(&1, map_options), options)} 315 | end 316 | 317 | # For compatibility with older 318 | # versions of ex_cldr that expect this 319 | # behaviour 320 | 321 | # TODO Fix Cldr.Config.get_locale to not make this assumption 322 | def atomize_keys(other, _options) do 323 | other 324 | end 325 | 326 | @doc """ 327 | Transforms a `map`'s `String.t` values to `atom()` values. 328 | 329 | ## Arguments 330 | 331 | * `map` is any `t:map/0` 332 | 333 | * `options` is a keyword list of options passed 334 | to `deep_map/3`. One additional option apples 335 | to this function directly: 336 | 337 | * `:only_existing` which is set to `true` will 338 | only convert the binary value to an atom if the atom 339 | already exists. The default is `false`. 340 | 341 | ## Examples 342 | 343 | iex> Cldr.Map.atomize_values %{"a" => %{"b" => %{1 => "c"}}} 344 | %{"a" => %{"b" => %{1 => :c}}} 345 | 346 | """ 347 | def atomize_values(map, options \\ [only_existing: false]) 348 | 349 | def atomize_values(map, options) when is_map(map) or is_list(map) do 350 | options = @default_atomize_options ++ options 351 | map_options = Map.new(options) 352 | 353 | deep_map(map, &atomize_value(&1, map_options), options) 354 | end 355 | 356 | def atomize_values({k, value}, options) when is_map(value) or is_list(value) do 357 | options = @default_atomize_options ++ options 358 | map_options = Map.new(options) 359 | 360 | {k, deep_map(value, &atomize_value(&1, map_options), options)} 361 | end 362 | 363 | @doc """ 364 | Transforms a `map`'s `String.t` keys to `Integer.t` keys. 365 | 366 | ## Arguments 367 | 368 | * `map` is any `t:map/0` 369 | 370 | * `options` is a keyword list of options passed 371 | to `deep_map/3` 372 | 373 | The map key is converted to an `integer` from 374 | either an `atom` or `String.t` only when the 375 | key is comprised of `integer` digits. 376 | 377 | Keys which cannot be converted to an `integer` 378 | are returned unchanged. 379 | 380 | ## Example 381 | 382 | iex> Cldr.Map.integerize_keys %{a: %{"1" => "value"}} 383 | %{a: %{1 => "value"}} 384 | 385 | """ 386 | def integerize_keys(map, options \\ []) 387 | 388 | def integerize_keys(map, options) when is_map(map) or is_list(map) do 389 | deep_map(map, &integerize_key/1, options) 390 | end 391 | 392 | def integerize_keys({k, value}, options) when is_map(value) or is_list(value) do 393 | {integerize_key(k), deep_map(value, &integerize_key/1, options)} 394 | end 395 | 396 | @doc """ 397 | Transforms a `map`'s `String.t` values to `Integer.t` values. 398 | 399 | ## Arguments 400 | 401 | * `map` is any `t:map/0` 402 | 403 | * `options` is a keyword list of options passed 404 | to `deep_map/3` 405 | 406 | The map value is converted to an `integer` from 407 | either an `atom` or `String.t` only when the 408 | value is comprised of `integer` digits. 409 | 410 | Keys which cannot be converted to an integer 411 | are returned unchanged. 412 | 413 | ## Example 414 | 415 | iex> Cldr.Map.integerize_values %{a: %{b: "1"}} 416 | %{a: %{b: 1}} 417 | 418 | """ 419 | def integerize_values(map, options \\ []) do 420 | deep_map(map, &integerize_value/1, options) 421 | end 422 | 423 | @doc """ 424 | Transforms a `map`'s `String.t` keys to `Float.t` values. 425 | 426 | ## Arguments 427 | 428 | * `map` is any `t:map/0` 429 | 430 | * `options` is a keyword list of options passed 431 | to `deep_map/3` 432 | 433 | The map key is converted to a `float` from 434 | a `String.t` only when the key is comprised of 435 | a valid float form. 436 | 437 | Keys which cannot be converted to a `float` 438 | are returned unchanged. 439 | 440 | ## Examples 441 | 442 | iex> Cldr.Map.floatize_keys %{a: %{"1.0" => "value"}} 443 | %{a: %{1.0 => "value"}} 444 | 445 | iex> Cldr.Map.floatize_keys %{a: %{"1" => "value"}} 446 | %{a: %{1.0 => "value"}} 447 | 448 | """ 449 | def floatize_keys(map, options \\ []) 450 | 451 | def floatize_keys(map, options) when is_map(map) or is_list(map) do 452 | deep_map(map, &floatize_key/1, options) 453 | end 454 | 455 | def floatize_keys({k, value}, options) when is_map(value) or is_list(value) do 456 | {floatize_key(k), deep_map(value, &floatize_key/1, options)} 457 | end 458 | 459 | @doc """ 460 | Transforms a `map`'s `String.t` values to `Float.t` values. 461 | 462 | ## Arguments 463 | 464 | * `map` is any `t:map/0` 465 | 466 | * `options` is a keyword list of options passed 467 | to `deep_map/3` 468 | 469 | The map value is converted to a `float` from 470 | a `String.t` only when the 471 | value is comprised of a valid float form. 472 | 473 | Values which cannot be converted to a `float` 474 | are returned unchanged. 475 | 476 | ## Examples 477 | 478 | iex> Cldr.Map.floatize_values %{a: %{b: "1.0"}} 479 | %{a: %{b: 1.0}} 480 | 481 | iex> Cldr.Map.floatize_values %{a: %{b: "1"}} 482 | %{a: %{b: 1.0}} 483 | 484 | """ 485 | def floatize_values(map, options \\ []) do 486 | deep_map(map, &floatize_value/1, options) 487 | end 488 | 489 | @doc """ 490 | Transforms a `map`'s `atom()` keys to `String.t` keys. 491 | 492 | ## Arguments 493 | 494 | * `map` is any `t:map/0` 495 | 496 | * `options` is a keyword list of options passed 497 | to `deep_map/3` 498 | 499 | ## Example 500 | 501 | iex> Cldr.Map.stringify_keys %{a: %{"1" => "value"}} 502 | %{"a" => %{"1" => "value"}} 503 | 504 | """ 505 | def stringify_keys(map, options \\ []) 506 | 507 | def stringify_keys(map, options) when is_map(map) or is_list(map) do 508 | deep_map(map, &stringify_key/1, options) 509 | end 510 | 511 | def stringify_keys({k, value}, options) when is_map(value) or is_list(value) do 512 | {stringify_key(k), deep_map(value, &stringify_key/1, options)} 513 | end 514 | 515 | @doc """ 516 | Transforms a `map`'s `atom()` keys to `String.t` keys. 517 | 518 | ## Arguments 519 | 520 | * `map` is any `t:map/0` 521 | 522 | * `options` is a keyword list of options passed 523 | to `deep_map/3` 524 | 525 | ## Example 526 | 527 | iex> Cldr.Map.stringify_values %{a: %{"1" => :value}} 528 | %{a: %{"1" => "value"}} 529 | 530 | """ 531 | def stringify_values(map, options \\ []) do 532 | deep_map(map, &stringify_value/1, options) 533 | end 534 | 535 | @doc """ 536 | Convert map `String.t` keys from `camelCase` to `snake_case` 537 | 538 | * `map` is any `t:map/0` 539 | 540 | * `options` is a keyword list of options passed 541 | to `deep_map/3` 542 | 543 | ## Example 544 | 545 | iex> Cldr.Map.underscore_keys %{"a" => %{"thisOne" => "value"}} 546 | %{"a" => %{"this_one" => "value"}} 547 | 548 | """ 549 | def underscore_keys(map, options \\ []) 550 | 551 | def underscore_keys(map, options) when is_map(map) or is_nil(map) do 552 | deep_map(map, &underscore_key/1, options) 553 | end 554 | 555 | def underscore_keys({k, value}, options) when is_map(value) or is_list(value) do 556 | {underscore_key(k), deep_map(value, &underscore_key/1, options)} 557 | end 558 | 559 | @doc """ 560 | Rename map keys from `from` to `to` 561 | 562 | * `map` is any `t:map/0` 563 | 564 | * `from` is any value map key 565 | 566 | * `to` is any valid map key 567 | 568 | * `options` is a keyword list of options passed 569 | to `deep_map/3` 570 | 571 | ## Example 572 | 573 | iex> Cldr.Map.rename_keys %{"a" => %{"this_one" => "value"}}, "this_one", "that_one" 574 | %{"a" => %{"that_one" => "value"}} 575 | 576 | """ 577 | def rename_keys(map, from, to, options \\ []) when is_map(map) or is_list(map) do 578 | renamer = fn 579 | {^from, v} -> {to, v} 580 | other -> other 581 | end 582 | 583 | deep_map(map, renamer, options) 584 | end 585 | 586 | @doc """ 587 | Convert a camelCase string or atom to a snake_case 588 | 589 | * `string` is a `String.t` or `atom()` to be 590 | transformed 591 | 592 | This is the code of Macro.underscore with modifications. 593 | The change is to cater for strings in the format: 594 | 595 | This_That 596 | 597 | which in Macro.underscore gets formatted as 598 | 599 | this__that (note the double underscore) 600 | 601 | when we actually want 602 | 603 | that_that 604 | 605 | ## Examples 606 | 607 | iex> Cldr.Map.underscore "thisThat" 608 | "this_that" 609 | 610 | iex> Cldr.Map.underscore "This_That" 611 | "this_that" 612 | 613 | """ 614 | @spec underscore(string :: String.t() | atom()) :: String.t() 615 | def underscore(<>) do 616 | <> <> do_underscore(t, h) 617 | end 618 | 619 | def underscore(other) do 620 | other 621 | end 622 | 623 | # h is upper case, next char is not uppercase, or a _ or . => and prev != _ 624 | defp do_underscore(<>, prev) 625 | when h >= ?A and h <= ?Z and not (t >= ?A and t <= ?Z) and t != ?. and t != ?_ and t != ?- and 626 | prev != ?_ do 627 | <> <> do_underscore(rest, t) 628 | end 629 | 630 | # h is uppercase, previous was not uppercase or _ 631 | defp do_underscore(<>, prev) 632 | when h >= ?A and h <= ?Z and not (prev >= ?A and prev <= ?Z) and prev != ?_ do 633 | <> <> do_underscore(t, h) 634 | end 635 | 636 | # h is dash "-" -> replace with underscore "_" 637 | defp do_underscore(<>, _) do 638 | <> <> underscore(t) 639 | end 640 | 641 | # h is . 642 | defp do_underscore(<>, _) do 643 | <> <> underscore(t) 644 | end 645 | 646 | # Any other char 647 | defp do_underscore(<>, _) do 648 | <> <> do_underscore(t, h) 649 | end 650 | 651 | defp do_underscore(<<>>, _) do 652 | <<>> 653 | end 654 | 655 | defp to_lower_char(char) when char == ?-, do: ?_ 656 | defp to_lower_char(char) when char >= ?A and char <= ?Z, do: char + 32 657 | defp to_lower_char(char), do: char 658 | 659 | @doc """ 660 | Removes any leading underscores from `map` 661 | `String.t` keys. 662 | 663 | * `map` is any `t:map/0` 664 | 665 | * `options` is a keyword list of options passed 666 | to `deep_map/3` 667 | 668 | ## Examples 669 | 670 | iex> Cldr.Map.remove_leading_underscores %{"a" => %{"_b" => "b"}} 671 | %{"a" => %{"b" => "b"}} 672 | 673 | """ 674 | def remove_leading_underscores(map, options \\ []) do 675 | remover = fn 676 | {k, v} when is_binary(k) -> {String.trim_leading(k, "_"), v} 677 | other -> other 678 | end 679 | 680 | deep_map(map, remover, options) 681 | end 682 | 683 | @doc """ 684 | Returns the result of deep merging a list of maps 685 | 686 | ## Examples 687 | 688 | iex> Cldr.Map.merge_map_list [%{a: "a", b: "b"}, %{c: "c", d: "d"}] 689 | %{a: "a", b: "b", c: "c", d: "d"} 690 | 691 | """ 692 | def merge_map_list(list, resolver \\ &standard_deep_resolver/3) 693 | 694 | def merge_map_list([h | []], _resolver) do 695 | h 696 | end 697 | 698 | def merge_map_list([h | t], resolver) do 699 | deep_merge(h, merge_map_list(t, resolver), resolver) 700 | end 701 | 702 | def merge_map_list([], _resolver) do 703 | [] 704 | end 705 | 706 | @doc """ 707 | Deep merge two maps 708 | 709 | * `left` is any `t:map/0` 710 | 711 | * `right` is any `t:map/0` 712 | 713 | ## Examples 714 | 715 | iex> Cldr.Map.deep_merge %{a: "a", b: "b"}, %{c: "c", d: "d"} 716 | %{a: "a", b: "b", c: "c", d: "d"} 717 | 718 | iex> Cldr.Map.deep_merge %{a: "a", b: "b"}, %{c: "c", d: "d", a: "aa"} 719 | %{a: "aa", b: "b", c: "c", d: "d"} 720 | 721 | """ 722 | def deep_merge(left, right, resolver \\ &standard_deep_resolver/3) when is_map(left) and is_map(right) do 723 | Map.merge(left, right, resolver) 724 | end 725 | 726 | # Key exists in both maps, and both values are maps as well. 727 | # These can be merged recursively. 728 | defp standard_deep_resolver(_key, left, right) when is_map(left) and is_map(right) do 729 | deep_merge(left, right, &standard_deep_resolver/3) 730 | end 731 | 732 | # Key exists in both maps, but at least one of the values is 733 | # NOT a map. We fall back to standard merge behavior, preferring 734 | # the value on the right. 735 | defp standard_deep_resolver(_key, _left, right) do 736 | right 737 | end 738 | 739 | def combine_list_resolver(_key, left, right) when is_list(left) and is_list(right) do 740 | left ++ right 741 | end 742 | 743 | @doc """ 744 | Delete all members of a map that have a 745 | key in the list of keys 746 | 747 | ## Examples 748 | 749 | iex> Cldr.Map.delete_in %{a: "a", b: "b"}, [:a] 750 | %{b: "b"} 751 | 752 | """ 753 | def delete_in(%{} = map, keys) when is_list(keys) do 754 | Enum.reject(map, fn {k, _v} -> k in keys end) 755 | |> Enum.map(fn {k, v} -> {k, delete_in(v, keys)} end) 756 | |> Map.new() 757 | end 758 | 759 | def delete_in(map, keys) when is_list(map) and is_binary(keys) do 760 | delete_in(map, [keys]) 761 | end 762 | 763 | def delete_in(map, keys) when is_list(map) do 764 | Enum.reject(map, fn {k, _v} -> k in keys end) 765 | |> Enum.map(fn {k, v} -> {k, delete_in(v, keys)} end) 766 | end 767 | 768 | def delete_in(%{} = map, keys) when is_binary(keys) do 769 | delete_in(map, [keys]) 770 | end 771 | 772 | def delete_in(other, _keys) do 773 | other 774 | end 775 | 776 | def from_keyword([] = list) do 777 | Map.new(list) 778 | end 779 | 780 | def from_keyword([{key, _value} | _rest] = keyword_list) when is_atom(key) do 781 | Map.new(keyword_list) 782 | end 783 | 784 | @doc """ 785 | Extract strings from a map or list 786 | 787 | Recursively process the map or list 788 | and extract string values from maps 789 | and string elements from lists 790 | 791 | """ 792 | def extract_strings(map_or_list, options \\ []) 793 | 794 | def extract_strings([], _options) do 795 | [] 796 | end 797 | 798 | def extract_strings(map, _options) when is_map(map) do 799 | Enum.reduce(map, [], fn 800 | {_k, v}, acc when is_binary(v) -> [v | acc] 801 | {_k, v}, acc when is_map(v) -> [extract_strings(v) | acc] 802 | {_k, v}, acc when is_list(v) -> [extract_strings(v) | acc] 803 | _other, acc -> acc 804 | end) 805 | |> List.flatten 806 | end 807 | 808 | def extract_strings(list, _options) when is_list(list) do 809 | Enum.reduce(list, [], fn 810 | v, acc when is_binary(v) -> [v | acc] 811 | v, acc when is_map(v) -> extract_strings(v, acc) 812 | v, acc when is_list(v) -> extract_strings(v, acc) 813 | _other, acc -> acc 814 | end) 815 | |> List.flatten 816 | end 817 | 818 | @doc """ 819 | Prune a potentially deeply nested map of some of 820 | its branches 821 | 822 | """ 823 | def prune(map, fun) when is_map(map) and is_function(fun, 1) do 824 | deep_map(map, &(&1), reject: fun) 825 | end 826 | 827 | @doc """ 828 | Invert a map 829 | 830 | Requires that the map is a simple map of 831 | keys and a list of values or a single 832 | non-map value 833 | 834 | ## Options 835 | 836 | * `:duplicates` which determines how duplicate values 837 | are handled: 838 | * `nil` or `false` which is the default and means only 839 | one value is kept. `Map.new/1` is used meanng the 840 | selected value is non-deterministic. 841 | * `:keep` meaning duplicate values are returned in a list 842 | * `:shortest` means the shortest duplicate is kept. 843 | This operates on string or atom values. 844 | * `:longest` means the shortest duplicate is kept. 845 | This operates on string or atom values. 846 | 847 | """ 848 | def invert(map, options \\ []) 849 | 850 | def invert(map, options) when is_map(map) do 851 | map 852 | |> Enum.map(fn 853 | {k, v} when is_list(v) -> Enum.map(v, fn vv -> {vv, k} end) 854 | {k, v} when not is_map(v) -> {v, k} 855 | end) 856 | |> List.flatten 857 | |> process_duplicates(options[:duplicates]) 858 | end 859 | 860 | defp process_duplicates(list, keep) when is_nil(keep) or keep == false do 861 | Map.new(list) 862 | end 863 | 864 | defp process_duplicates(list, :keep) do 865 | list 866 | |> Enum.group_by(&elem(&1, 0), &elem(&1, 1)) 867 | end 868 | 869 | defp process_duplicates(list, :shortest) do 870 | list 871 | |> process_duplicates(:keep) 872 | |> Enum.map(fn {k, v} -> {k, shortest(v)} end) 873 | |> Map.new() 874 | end 875 | 876 | defp process_duplicates(list, :longest) do 877 | list 878 | |> process_duplicates(:keep) 879 | |> Enum.map(fn {k, v} -> {k, longest(v)} end) 880 | |> Map.new() 881 | end 882 | 883 | defp shortest(list) when is_list(list) do 884 | Enum.min_by(list, &len/1) 885 | end 886 | 887 | defp longest(list) when is_list(list) do 888 | Enum.max_by(list, &len/1) 889 | end 890 | 891 | defp len(e) when is_binary(e) do 892 | String.length(e) 893 | end 894 | 895 | defp len(e) when is_atom(e) do 896 | e 897 | |> Atom.to_string 898 | |> len() 899 | end 900 | 901 | # 902 | # Helpers 903 | # 904 | 905 | defp validate_options(function, options) when is_function(function) do 906 | validate_options(options) 907 | end 908 | 909 | defp validate_options({key_function, value_function}, options) 910 | when is_function(key_function) and is_function(value_function) do 911 | validate_options(options) 912 | end 913 | 914 | defp validate_options(function, _options) do 915 | raise ArgumentError, 916 | "function parameter must be a function or a 2-tuple " <> 917 | "consisting of a key_function and a value_function. Found #{inspect(function)}" 918 | end 919 | 920 | defp validate_options(options) do 921 | @default_deep_map_options 922 | |> Keyword.merge(options) 923 | |> Map.new() 924 | |> Map.update!(:level, fn 925 | level when is_integer(level) -> 926 | level..level 927 | 928 | %Range{} = level -> 929 | level 930 | 931 | other -> 932 | raise ArgumentError, ":level must be an integer or a range. Found #{inspect(other)}" 933 | end) 934 | end 935 | 936 | defp atomize_key({k, v}, %{only_existing: true}) when is_binary(k) do 937 | {String.to_existing_atom(k), v} 938 | rescue 939 | ArgumentError -> {k, v} 940 | end 941 | 942 | defp atomize_key({k, v}, %{only_existing: false}) when is_binary(k) do 943 | {String.to_atom(k), v} 944 | end 945 | 946 | defp atomize_key(other, _options) do 947 | other 948 | end 949 | 950 | defp atomize_value({k, v}, %{only_existing: true}) when is_binary(v) do 951 | {k, String.to_existing_atom(v)} 952 | rescue 953 | ArgumentError -> {k, v} 954 | end 955 | 956 | defp atomize_value({k, v}, %{only_existing: false}) when is_binary(v) do 957 | {k, String.to_atom(v)} 958 | end 959 | 960 | defp atomize_value(v, %{only_existing: false}) when is_binary(v) do 961 | String.to_atom(v) 962 | end 963 | 964 | defp atomize_value(other, _options) do 965 | other 966 | end 967 | 968 | defp integerize_key({k, v}) when is_binary(k) do 969 | case Integer.parse(k) do 970 | {integer, ""} -> {integer, v} 971 | _other -> {k, v} 972 | end 973 | end 974 | 975 | defp integerize_key(other) do 976 | other 977 | end 978 | 979 | defp integerize_value({k, v}) when is_binary(v) do 980 | case Integer.parse(v) do 981 | {integer, ""} -> {k, integer} 982 | _other -> {k, v} 983 | end 984 | end 985 | 986 | defp integerize_value(other) do 987 | other 988 | end 989 | 990 | defp floatize_key({k, v}) when is_binary(k) do 991 | case Float.parse(k) do 992 | {float, ""} -> {float, v} 993 | _other -> {k, v} 994 | end 995 | end 996 | 997 | defp floatize_key(other) do 998 | other 999 | end 1000 | 1001 | defp floatize_value({k, v}) when is_binary(v) do 1002 | case Float.parse(v) do 1003 | {float, ""} -> {k, float} 1004 | _other -> {k, v} 1005 | end 1006 | end 1007 | 1008 | defp floatize_value(other) do 1009 | other 1010 | end 1011 | 1012 | defp stringify_key({k, v}) when is_atom(k), do: {Atom.to_string(k), v} 1013 | defp stringify_key(other), do: other 1014 | 1015 | defp stringify_value({k, v}) when is_atom(v), do: {k, Atom.to_string(v)} 1016 | defp stringify_value(other) when is_atom(other), do: Atom.to_string(other) 1017 | defp stringify_value(other), do: other 1018 | 1019 | defp underscore_key({k, v}) when is_binary(k), do: {underscore(k), v} 1020 | defp underscore_key(other), do: other 1021 | 1022 | # process_element?/2 determines whether the 1023 | # calling function should apply to a given 1024 | # value 1025 | 1026 | def process_type(x, options) do 1027 | filter? = filter?(x, options) # |> IO.inspect(label: "Filter: #{inspect x}") 1028 | reject? = reject?(x, options) # |> IO.inspect(label: "Reject: #{inspect x}") 1029 | # IO.inspect only?(x, options), label: "Only: #{inspect x}" 1030 | # IO.inspect except?(x, options), label: "Except: #{inspect x}" 1031 | # IO.inspect skip?(x, options), label: "Skip: #{inspect x}" 1032 | 1033 | cond do 1034 | reject? -> :reject 1035 | skip?(x, options) -> :skip 1036 | filter? && only?(x, options) && !except?(x, options) -> :process 1037 | filter? -> :continue 1038 | true -> :except 1039 | end 1040 | # |> IO.inspect(label: inspect(x)) 1041 | end 1042 | 1043 | # Keep this branch but don't process it 1044 | defp skip?(x, %{skip: skip}) when is_function(skip) do 1045 | skip.(x) 1046 | end 1047 | 1048 | defp skip?({k, _v}, %{skip: skip}) when is_list(skip) do 1049 | k in skip 1050 | end 1051 | 1052 | defp skip?({k, _v}, %{skip: skip}) do 1053 | k == skip 1054 | end 1055 | 1056 | defp skip?(k, %{skip: skip}) when is_list(skip) do 1057 | k in skip 1058 | end 1059 | 1060 | defp skip?(k, %{skip: skip}) do 1061 | k == skip 1062 | end 1063 | 1064 | # Keep this branch is the result 1065 | defp filter?(x, %{filter: filter}) when is_function(filter) do 1066 | filter.(x) 1067 | end 1068 | 1069 | defp filter?(_x, %{filtering: true}) do 1070 | true 1071 | end 1072 | 1073 | defp filter?(_x, %{filter: []}) do 1074 | true 1075 | end 1076 | 1077 | defp filter?({k, _v}, %{filter: filter}) when is_list(filter) do 1078 | k in filter 1079 | end 1080 | 1081 | defp filter?({k, _v}, %{filter: filter}) do 1082 | k == filter 1083 | end 1084 | 1085 | defp filter?(k, %{filter: filter}) when is_list(filter) do 1086 | k in filter 1087 | end 1088 | 1089 | defp filter?(k, %{filter: filter}) do 1090 | k == filter 1091 | end 1092 | 1093 | # Don't include this branch is the result 1094 | defp reject?(x, %{reject: reject}) when is_function(reject) do 1095 | reject.(x) 1096 | end 1097 | 1098 | defp reject?({k, _v}, %{reject: reject}) when is_list(reject) do 1099 | k in reject 1100 | end 1101 | 1102 | defp reject?({k, _v}, %{reject: reject}) do 1103 | k == reject 1104 | end 1105 | 1106 | defp reject?(k, %{reject: reject}) when is_list(reject) do 1107 | k in reject 1108 | end 1109 | 1110 | defp reject?(k, %{reject: reject}) do 1111 | k == reject 1112 | end 1113 | 1114 | # Process this item 1115 | defp only?(x, %{only: only}) when is_function(only) do 1116 | only.(x) 1117 | end 1118 | 1119 | defp only?(_x, %{only: []}) do 1120 | true 1121 | end 1122 | 1123 | defp only?({k, _v}, %{only: only}) when is_list(only) do 1124 | k in only 1125 | end 1126 | 1127 | defp only?({k, _v}, %{only: only}) do 1128 | k == only 1129 | end 1130 | 1131 | defp only?(k, %{only: only}) when is_list(only) do 1132 | k in only 1133 | end 1134 | 1135 | defp only?(k, %{only: only}) do 1136 | k == only 1137 | end 1138 | 1139 | # Don;t process this item 1140 | defp except?(x, %{except: except}) when is_function(except) do 1141 | except.(x) 1142 | end 1143 | 1144 | defp except?({k, _v}, %{except: except}) when is_list(except) do 1145 | k in except 1146 | end 1147 | 1148 | defp except?({k, _v}, %{except: except}) do 1149 | k == except 1150 | end 1151 | 1152 | defp except?(k, %{except: except}) when is_list(except) do 1153 | k in except 1154 | end 1155 | 1156 | defp except?(k, %{except: except}) do 1157 | k == except 1158 | end 1159 | 1160 | end 1161 | -------------------------------------------------------------------------------- /lib/cldr/utils/math.ex: -------------------------------------------------------------------------------- 1 | defmodule Cldr.Math do 2 | @moduledoc """ 3 | Math helper functions for number formatting. 4 | """ 5 | 6 | import Kernel, except: [div: 2] 7 | alias Cldr.Digits 8 | require Integer 9 | 10 | @type rounding :: 11 | :down 12 | | :half_up 13 | | :half_even 14 | | :ceiling 15 | | :floor 16 | | :half_down 17 | | :up 18 | 19 | @type number_or_decimal :: number | %Decimal{} 20 | @type normalised_decimal :: {%Decimal{}, integer} 21 | @default_rounding 3 22 | @default_rounding_mode :half_even 23 | @zero Decimal.new(0) 24 | @one Decimal.new(1) 25 | @two Decimal.new(2) 26 | @ten Decimal.new(10) 27 | 28 | @doc """ 29 | Adds two numbers together. 30 | 31 | The numbers can be integers, floats or Decimals. 32 | The type of the return will be Decimal if the 33 | either of the arguments is a Decimal. 34 | 35 | If both arguments are integers, the result will 36 | be an integer. If either of the arguments is a float, 37 | the result will be a float. 38 | 39 | """ 40 | @doc since: "2.25.0" 41 | def add(%Decimal{} = num_1, %Decimal{} = num_2) do 42 | Decimal.add(num_1, num_2) 43 | end 44 | 45 | def add(%Decimal{} = num_1, num_2) when is_integer(num_2) do 46 | Decimal.add(num_1, num_2) 47 | end 48 | 49 | def add(%Decimal{} = num_1, num_2) when is_float(num_2) do 50 | Decimal.add(num_1, Decimal.from_float(num_2)) 51 | end 52 | 53 | def add(num_1, %Decimal{} = num_2) when is_integer(num_1) do 54 | Decimal.add(num_1, num_2) 55 | end 56 | 57 | def add(num_1, %Decimal{} = num_2) when is_float(num_1) do 58 | Decimal.add(Decimal.from_float(num_1), num_2) 59 | end 60 | 61 | def add(num_1, num_2) when is_number(num_1) and is_number(num_2) do 62 | num_1 + num_2 63 | end 64 | 65 | @doc """ 66 | Subtracts one number from another. 67 | 68 | The numbers can be integers, floats or Decimals. 69 | The type of the return will be Decimal if the 70 | either of the arguments is a Decimal. 71 | 72 | If both arguments are integers, the result will 73 | be an integer. If either of the arguments is a float, 74 | the result will be a float. 75 | 76 | """ 77 | @doc since: "2.25.0" 78 | def sub(%Decimal{} = num_1, %Decimal{} = num_2) do 79 | Decimal.sub(num_1, num_2) 80 | end 81 | 82 | def sub(%Decimal{} = num_1, num_2) when is_integer(num_2) do 83 | Decimal.sub(num_1, num_2) 84 | end 85 | 86 | def sub(%Decimal{} = num_1, num_2) when is_float(num_2) do 87 | Decimal.sub(num_1, Decimal.from_float(num_2)) 88 | end 89 | 90 | def sub(num_1, %Decimal{} = num_2) when is_integer(num_1) do 91 | Decimal.sub(num_1, num_2) 92 | end 93 | 94 | def sub(num_1, %Decimal{} = num_2) when is_float(num_1) do 95 | Decimal.sub(Decimal.from_float(num_1), num_2) 96 | end 97 | 98 | def sub(num_1, num_2) when is_number(num_1) and is_number(num_2) do 99 | num_1 - num_2 100 | end 101 | 102 | @doc """ 103 | Multiplies two numbers together. 104 | 105 | The numbers can be integers, floats or Decimals. 106 | The type of the return will be Decimal if the 107 | either of the arguments is a Decimal. 108 | 109 | If both arguments are integers, the result will 110 | be an integer. If either of the arguments is a float, 111 | the result will be a float. 112 | 113 | """ 114 | @doc since: "2.25.0" 115 | def mult(%Decimal{} = num_1, %Decimal{} = num_2) do 116 | Decimal.mult(num_1, num_2) 117 | end 118 | 119 | def mult(%Decimal{} = num_1, num_2) when is_integer(num_2) do 120 | Decimal.mult(num_1, num_2) 121 | end 122 | 123 | def mult(%Decimal{} = num_1, num_2) when is_float(num_2) do 124 | Decimal.mult(num_1, Decimal.from_float(num_2)) 125 | end 126 | 127 | def mult(num_1, %Decimal{} = num_2) when is_integer(num_1) do 128 | Decimal.mult(num_1, num_2) 129 | end 130 | 131 | def mult(num_1, %Decimal{} = num_2) when is_float(num_1) do 132 | Decimal.mult(Decimal.from_float(num_1), num_2) 133 | end 134 | 135 | def mult(num_1, num_2) when is_number(num_1) and is_number(num_2) do 136 | num_1 * num_2 137 | end 138 | 139 | @doc """ 140 | Divides one number by the other. 141 | 142 | The numbers can be integers, floats or Decimals. 143 | The type of the return will be Decimal if the 144 | either of the arguments is a Decimal. 145 | 146 | If both arguments are numbers, the resulting type 147 | will be a be a Decimal. 148 | 149 | """ 150 | @doc since: "2.25.0" 151 | def div(%Decimal{} = num_1, %Decimal{} = num_2) do 152 | Decimal.div(num_1, num_2) 153 | end 154 | 155 | def div(%Decimal{} = num_1, num_2) when is_integer(num_2) do 156 | Decimal.div(num_1, num_2) 157 | end 158 | 159 | def div(%Decimal{} = num_1, num_2) when is_float(num_2) do 160 | Decimal.div(num_1, Decimal.from_float(num_2)) 161 | end 162 | 163 | def div(num_1, %Decimal{} = num_2) when is_integer(num_1) do 164 | Decimal.div(num_1, num_2) 165 | end 166 | 167 | def div(num_1, %Decimal{} = num_2) when is_float(num_1) do 168 | Decimal.div(Decimal.from_float(num_1), num_2) 169 | end 170 | 171 | def div(num_1, num_2) when is_number(num_1) and is_number(num_2) do 172 | Decimal.from_float(num_1 / num_2) 173 | end 174 | 175 | 176 | # Decimal.integer? only on 2.x but we support 1.x 177 | # so we have to check the hard way 178 | 179 | def maybe_integer(%Decimal{} = a) do 180 | Decimal.to_integer(a) 181 | rescue 182 | FunctionClauseError -> 183 | a 184 | ArgumentError -> 185 | a 186 | end 187 | 188 | def maybe_integer(a) when is_float(a) do 189 | case trunc(a) do 190 | b when a == b -> b 191 | _b -> a 192 | end 193 | end 194 | 195 | def maybe_integer(a) when is_integer(a) do 196 | a 197 | end 198 | 199 | @doc false 200 | @deprecated "Use Cldr.Decimal.compare/2" 201 | def decimal_compare(d1, d2) do 202 | Cldr.Decimal.compare(d1, d2) 203 | end 204 | 205 | @doc """ 206 | Returns the default number of rounding digits. 207 | """ 208 | @spec default_rounding :: integer 209 | def default_rounding do 210 | @default_rounding 211 | end 212 | 213 | @doc """ 214 | Returns the default rounding mode for rounding operations. 215 | """ 216 | @spec default_rounding_mode :: atom 217 | def default_rounding_mode do 218 | @default_rounding_mode 219 | end 220 | 221 | @doc """ 222 | Check if a `number` is within a `range`. 223 | 224 | * `number` is either an integer or a float. 225 | 226 | When an integer, the comparison is made using the standard Elixir `in` 227 | operator. 228 | 229 | When `number` is a float the comparison is made using the `>=` and `<=` 230 | operators on the range endpoints. Note the comparison for a float is only for 231 | floats that have no fractional part. If a float has a fractional part then 232 | `within` returns `false`. 233 | 234 | *Since this function is only provided to support plural rules, the float 235 | comparison is only useful if the float has no fractional part.* 236 | 237 | ## Examples 238 | 239 | iex> Cldr.Math.within(2.0, 1..3) 240 | true 241 | 242 | iex> Cldr.Math.within(2.1, 1..3) 243 | false 244 | 245 | """ 246 | @spec within(number, integer | Range.t()) :: boolean 247 | def within(number, range) when is_integer(number) do 248 | number in range 249 | end 250 | 251 | # When checking if a decimal is in a range it is only 252 | # valid if there are no decimal places 253 | def within(number, %{first: first, last: last}) when is_float(number) do 254 | number == trunc(number) && number >= first && number <= last 255 | end 256 | 257 | @doc """ 258 | Calculates the modulo of a number (integer, float or Decimal). 259 | 260 | Note that this function uses `floored division` whereas the builtin `rem` 261 | function uses `truncated division`. See `Decimal.rem/2` if you want a 262 | `truncated division` function for Decimals that will return the same value as 263 | the BIF `rem/2` but in Decimal form. 264 | 265 | See [Wikipedia](https://en.wikipedia.org/wiki/Modulo_operation) for an 266 | explanation of the difference. 267 | 268 | ## Examples 269 | 270 | iex> Cldr.Math.mod(1234.0, 5) 271 | 4.0 272 | 273 | iex> Cldr.Math.mod(Decimal.new("1234.456"), 5) 274 | Decimal.new("4.456") 275 | 276 | iex> Cldr.Math.mod(Decimal.new("123.456"), Decimal.new("3.4")) 277 | Decimal.new("1.056") 278 | 279 | iex> Cldr.Math.mod Decimal.new("123.456"), 3.4 280 | Decimal.new("1.056") 281 | 282 | """ 283 | @spec mod(number_or_decimal, number_or_decimal) :: number_or_decimal 284 | 285 | def mod(number, modulus) when is_float(number) and is_number(modulus) do 286 | number - Float.floor(number / modulus) * modulus 287 | end 288 | 289 | def mod(number, modulus) when is_integer(number) and is_integer(modulus) do 290 | modulo = 291 | number 292 | |> Integer.floor_div(modulus) 293 | |> Kernel.*(modulus) 294 | 295 | number - modulo 296 | end 297 | 298 | def mod(number, modulus) when is_integer(number) and is_number(modulus) do 299 | modulo = 300 | number 301 | |> Kernel./(modulus) 302 | |> Float.floor() 303 | |> Kernel.*(modulus) 304 | 305 | number - modulo 306 | end 307 | 308 | def mod(%Decimal{} = number, %Decimal{} = modulus) do 309 | modulo = 310 | number 311 | |> Decimal.div(modulus) 312 | |> Decimal.round(0, :floor) 313 | |> Decimal.mult(modulus) 314 | 315 | Decimal.sub(number, modulo) 316 | end 317 | 318 | def mod(%Decimal{} = number, modulus) when is_integer(modulus) do 319 | mod(number, Decimal.new(modulus)) 320 | end 321 | 322 | def mod(%Decimal{} = number, modulus) when is_float(modulus) do 323 | mod(number, Decimal.from_float(modulus)) 324 | end 325 | 326 | @doc """ 327 | Returns the adjusted modulus of `x` and `y`. 328 | """ 329 | @spec amod(number_or_decimal, number_or_decimal) :: number_or_decimal 330 | def amod(x, y) do 331 | case mod = mod(x, y) do 332 | %Decimal{} = decimal_mod -> 333 | if Cldr.Decimal.compare(decimal_mod, @zero) == :eq, do: y, else: mod 334 | 335 | _ -> 336 | if mod == 0, do: y, else: mod 337 | end 338 | end 339 | 340 | @doc """ 341 | Returns the remainder and dividend of two integers. 342 | """ 343 | @spec div_mod(integer, integer) :: {integer, integer} 344 | def div_mod(int1, int2) when is_integer(int1) and is_integer(int2) do 345 | div = Kernel.div(int1, int2) 346 | mod = int1 - div * int2 347 | {div, mod} 348 | end 349 | 350 | @doc """ 351 | Returns the adjusted remainder and dividend of two 352 | integers. 353 | 354 | This version will return the divisor if the remainder 355 | would otherwise be zero. 356 | 357 | """ 358 | @spec div_amod(integer, integer) :: {integer, integer} 359 | def div_amod(int1, int2) when is_integer(int1) and is_integer(int2) do 360 | {div, mod} = div_mod(int1, int2) 361 | 362 | if mod == 0 do 363 | {div - 1, int2} 364 | else 365 | {div, mod} 366 | end 367 | end 368 | 369 | @doc """ 370 | Convert a Decimal to a float 371 | 372 | * `decimal` must be a Decimal 373 | 374 | This is very likely to lose precision - lots of numbers won't 375 | make the round trip conversion. Use with care. Actually, better 376 | not to use it at all. 377 | 378 | """ 379 | @spec to_float(%Decimal{}) :: float 380 | def to_float(%Decimal{sign: sign, coef: coef, exp: exp}) do 381 | sign * coef * 1.0 * power_of_10(exp) 382 | end 383 | 384 | @doc """ 385 | Rounds a number to a specified number of significant digits. 386 | 387 | This is not the same as rounding fractional digits which is performed 388 | by `Decimal.round/2` and `Float.round` 389 | 390 | * `number` is a float, integer or Decimal 391 | 392 | * `n` is the number of significant digits to which the `number` should be 393 | rounded 394 | 395 | ## Examples 396 | 397 | iex> Cldr.Math.round_significant(3.14159, 3) 398 | 3.14 399 | 400 | iex> Cldr.Math.round_significant(10.3554, 1) 401 | 10.0 402 | 403 | iex> Cldr.Math.round_significant(0.00035, 1) 404 | 0.0004 405 | 406 | iex> Cldr.Math.round_significant(Decimal.from_float(3.342742283480345e27), 7) 407 | Decimal.new("3.342742E+27") 408 | 409 | ## Notes about precision 410 | 411 | Since floats cannot accurately represent all decimal 412 | numbers, so rounding to significant digits for a float cannot 413 | always return the expected results. For example: 414 | 415 | => Cldr.Math.round_significant(3.342742283480345e27, 7) 416 | Expected result: 3.342742e27 417 | Actual result: 3.3427420000000003e27 418 | 419 | Use of `Decimal` numbers avoids this issue: 420 | 421 | => Cldr.Math.round_significant(Decimal.from_float(3.342742283480345e27), 7) 422 | Expected result: #Decimal<3.342742E+27> 423 | Actual result: #Decimal<3.342742E+27> 424 | 425 | ## More on significant digits 426 | 427 | * 3.14159 has six significant digits (all the numbers give you useful 428 | information) 429 | 430 | * 1000 has one significant digit (only the 1 is interesting; you don't know 431 | anything for sure about the hundreds, tens, or units places; the zeroes may 432 | just be placeholders; they may have rounded something off to get this value) 433 | 434 | * 1000.0 has five significant digits (the ".0" tells us something interesting 435 | about the presumed accuracy of the measurement being made: that the 436 | measurement is accurate to the tenths place, but that there happen to be 437 | zero tenths) 438 | 439 | * 0.00035 has two significant digits (only the 3 and 5 tell us something; the 440 | other zeroes are placeholders, only providing information about relative 441 | size) 442 | 443 | * 0.000350 has three significant digits (that last zero tells us that the 444 | measurement was made accurate to that last digit, which just happened to 445 | have a value of zero) 446 | 447 | * 1006 has four significant digits (the 1 and 6 are interesting, and we have 448 | to count the zeroes, because they're between the two interesting numbers) 449 | 450 | * 560 has two significant digits (the last zero is just a placeholder) 451 | 452 | * 560.0 has four significant digits (the zero in the tenths place means that 453 | the measurement was made accurate to the tenths place, and that there just 454 | happen to be zero tenths; the 5 and 6 give useful information, and the 455 | other zero is between significant digits, and must therefore also be 456 | counted) 457 | 458 | Many thanks to [Stackoverflow](http://stackoverflow.com/questions/202302/rounding-to-an-arbitrary-number-of-significant-digits) 459 | 460 | """ 461 | @spec round_significant(number_or_decimal, integer) :: number_or_decimal 462 | def round_significant(number, n) when is_number(number) and n <= 0 do 463 | number 464 | end 465 | 466 | def round_significant(number, n) when is_number(number) and n > 0 do 467 | sign = if number < 0, do: -1, else: 1 468 | number = abs(number) 469 | d = Float.ceil(:math.log10(number)) 470 | power = n - d 471 | 472 | magnitude = :math.pow(10,power) 473 | rounded = Float.round(number * magnitude) / magnitude 474 | 475 | sign * 476 | if is_integer(number) do 477 | trunc(rounded) 478 | else 479 | rounded 480 | end 481 | end 482 | 483 | if Code.ensure_loaded?(Decimal) and function_exported?(Decimal, :negate, 1) do 484 | def round_significant(%Decimal{sign: sign} = number, n) when sign < 0 and n > 0 do 485 | round_significant(Decimal.abs(number), n) 486 | |> Decimal.negate() 487 | end 488 | else 489 | def round_significant(%Decimal{sign: sign} = number, n) when sign < 0 and n > 0 do 490 | round_significant(Decimal.abs(number), n) 491 | |> Decimal.minus() 492 | end 493 | end 494 | 495 | def round_significant(%Decimal{sign: sign} = number, n) when sign > 0 and n > 0 do 496 | d = 497 | number 498 | |> log10 499 | |> Decimal.round(0, :ceiling) 500 | 501 | power = 502 | n 503 | |> Decimal.new() 504 | |> Decimal.sub(d) 505 | |> Decimal.to_integer 506 | 507 | magnitude = power(@ten, power) 508 | 509 | number 510 | |> Decimal.mult(magnitude) 511 | |> Decimal.round(0) 512 | |> Decimal.div(magnitude) 513 | end 514 | 515 | @doc """ 516 | Return the natural log of a number. 517 | 518 | * `number` is an integer, a float or a Decimal 519 | 520 | * For integer and float it calls the BIF `:math.log10/1` function. 521 | 522 | * For Decimal the log is rolled by hand. 523 | 524 | ## Examples 525 | 526 | iex> Cldr.Math.log(123) 527 | 4.812184355372417 528 | 529 | iex> Cldr.Math.log(Decimal.new(9000)) 530 | Decimal.new("9.103886231350952380952380952") 531 | 532 | """ 533 | def log(number) when is_number(number) do 534 | :math.log(number) 535 | end 536 | 537 | @ln10 Decimal.from_float(2.30258509299) 538 | def log(%Decimal{} = number) do 539 | {mantissa, exp} = coef_exponent(number) 540 | exp = Decimal.new(exp) 541 | ln1 = Decimal.mult(exp, @ln10) 542 | 543 | sqrt_mantissa = sqrt(mantissa) 544 | y = Decimal.div(Decimal.sub(sqrt_mantissa, @one), Decimal.add(sqrt_mantissa, @one)) 545 | 546 | ln2 = 547 | y 548 | |> log_polynomial([3, 5, 7]) 549 | |> Decimal.add(y) 550 | |> Decimal.mult(@two) 551 | 552 | Decimal.add(Decimal.mult(@two, ln2), ln1) 553 | end 554 | 555 | defp log_polynomial(%Decimal{} = value, iterations) do 556 | Enum.reduce(iterations, @zero, fn i, acc -> 557 | i = Decimal.new(i) 558 | 559 | value 560 | |> power(i) 561 | |> Decimal.div(i) 562 | |> Decimal.add(acc) 563 | end) 564 | end 565 | 566 | @doc """ 567 | Return the log10 of a number. 568 | 569 | * `number` is an integer, a float or a Decimal 570 | 571 | * For integer and float it calls the BIF `:math.log10/1` function. 572 | 573 | * For `Decimal`, `log10` is is rolled by hand using the identify `log10(x) = 574 | ln(x) / ln(10)` 575 | 576 | ## Examples 577 | 578 | iex> Cldr.Math.log10(100) 579 | 2.0 580 | 581 | iex> Cldr.Math.log10(123) 582 | 2.089905111439398 583 | 584 | iex> Cldr.Math.log10(Decimal.new(9000)) 585 | Decimal.new("3.953767554157656512064441441") 586 | 587 | """ 588 | @spec log10(number_or_decimal) :: number_or_decimal 589 | def log10(number) when is_number(number) do 590 | :math.log10(number) 591 | end 592 | 593 | def log10(%Decimal{} = number) do 594 | Decimal.div(log(number), @ln10) 595 | end 596 | 597 | @doc """ 598 | Raises a number to a integer power. 599 | 600 | Raises a number to a power using the the binary method. There is one 601 | exception for Decimal numbers that raise `10` to some power. In this case the 602 | power is calculated by shifting the Decimal exponent which is quite efficient. 603 | 604 | For further reading see 605 | [this article](http://videlalvaro.github.io/2014/03/the-power-algorithm.html) 606 | 607 | > This function works only with integer exponents! 608 | 609 | ## Examples 610 | 611 | iex> Cldr.Math.power(10, 2) 612 | 100 613 | 614 | iex> Cldr.Math.power(10, 3) 615 | 1000 616 | 617 | iex> Cldr.Math.power(10, 4) 618 | 10000 619 | 620 | iex> Cldr.Math.power(2, 10) 621 | 1024 622 | 623 | """ 624 | 625 | # Decimal number and decimal n 626 | @spec power(number_or_decimal, number_or_decimal) :: number_or_decimal 627 | def power(%Decimal{} = _number, %Decimal{coef: n}) when n == 0 do 628 | @one 629 | end 630 | 631 | def power(%Decimal{} = number, %Decimal{sign: sign} = n) when sign < 1 do 632 | Decimal.div(@one, do_power(number, Decimal.abs(n), mod(Decimal.abs(n), @two))) 633 | end 634 | 635 | def power(%Decimal{} = number, %Decimal{coef: n}) when n == 1 do 636 | number 637 | end 638 | 639 | def power(%Decimal{} = number, %Decimal{} = n) do 640 | do_power(number, n, mod(n, @two)) 641 | end 642 | 643 | # Decimal number and integer/float n 644 | def power(%Decimal{} = _number, n) when n == 0 do 645 | @one 646 | end 647 | 648 | def power(%Decimal{} = number, n) when n == 1 do 649 | number 650 | end 651 | 652 | def power(%Decimal{} = number, n) when n > 1 do 653 | do_power(number, n, mod(n, 2)) 654 | end 655 | 656 | def power(%Decimal{} = number, n) when n < 0 do 657 | Decimal.div(@one, do_power(number, abs(n), mod(abs(n), 2))) 658 | end 659 | 660 | # n is between 0 and 1 661 | def power(%Decimal{} = number, n) do 662 | do_power(number, n, mod(number, n)) 663 | end 664 | 665 | # For integers and floats 666 | def power(number, n) when n == 0 do 667 | if is_integer(number), do: 1, else: 1.0 668 | end 669 | 670 | def power(number, n) when n == 1 do 671 | number 672 | end 673 | 674 | def power(number, n) when n > 1 do 675 | do_power(number, n, mod(n, 2)) 676 | end 677 | 678 | def power(number, n) when n < 1 do 679 | 1 / do_power(number, abs(n), mod(abs(n), 2)) 680 | end 681 | 682 | # Decimal number and decimal n 683 | defp do_power(%Decimal{} = number, %Decimal{coef: coef}, %Decimal{coef: mod}) 684 | when mod == 0 and coef == 2 do 685 | Decimal.mult(number, number) 686 | end 687 | 688 | defp do_power(%Decimal{} = number, %Decimal{coef: coef} = n, %Decimal{coef: mod}) 689 | when mod == 0 and coef != 2 do 690 | power(power(number, Decimal.div(n, @two)), @two) 691 | end 692 | 693 | defp do_power(%Decimal{} = number, %Decimal{} = n, _mod) do 694 | Decimal.mult(number, power(number, Decimal.sub(n, @one))) 695 | end 696 | 697 | # Decimal number but integer n 698 | defp do_power(%Decimal{} = number, 1, 1) do 699 | number 700 | end 701 | 702 | defp do_power(%Decimal{} = number, n, mod) 703 | when is_number(n) and mod == 0 and n == 2 do 704 | Decimal.mult(number, number) 705 | end 706 | 707 | defp do_power(%Decimal{} = number, n, mod) 708 | when is_number(n) and mod == 0 and n != 2 do 709 | power(power(number, n / 2), 2) 710 | end 711 | 712 | defp do_power(%Decimal{} = number, n, _mod) 713 | when is_number(n) and n > 1 do 714 | Decimal.mult(number, power(number, n - 1)) 715 | end 716 | 717 | # Escape hatch for when the exponent < 1 718 | defp do_power(%Decimal{} = number, n, _mod) when n < 1 do 719 | number 720 | |> Decimal.to_float() 721 | |> :math.pow(n) 722 | end 723 | 724 | # integer/float number and integer/float n 725 | defp do_power(number, n, mod) 726 | when is_number(n) and mod == 0 and n == 2 do 727 | number * number 728 | end 729 | 730 | defp do_power(number, n, mod) 731 | when is_number(n) and mod == 0 and n != 2 do 732 | power(power(number, n / 2), 2) 733 | end 734 | 735 | defp do_power(number, n, _mod) when is_number(number) and is_number(n) do 736 | if Kernel.round(n) != n do 737 | :math.pow(number, n) 738 | else 739 | number * power(number, n - 1) 740 | end 741 | end 742 | 743 | # Precompute powers of 10 up to 10^326 744 | # FIXME: duplicating existing function in Float, which only goes up to 15. 745 | @doc false 746 | Enum.reduce(0..326, 1, fn x, acc -> 747 | def power_of_10(unquote(x)), do: unquote(acc) 748 | acc * 10 749 | end) 750 | 751 | def power_of_10(n) when n < 0 do 752 | 1 / power_of_10(abs(n)) 753 | end 754 | 755 | @doc """ 756 | Raises one number to an exponent. 757 | 758 | """ 759 | defdelegate pow(n, m), to: __MODULE__, as: :power 760 | 761 | @doc """ 762 | Returns a tuple representing a number in a normalized form with 763 | the mantissa in the range `0 < m < 10` and a base 10 exponent. 764 | 765 | * `number` is an integer, float or Decimal 766 | 767 | ## Examples 768 | 769 | Cldr.Math.coef_exponent(Decimal.new(1.23004)) 770 | {Decimal.new("1.23004"), 0} 771 | 772 | Cldr.Math.coef_exponent(Decimal.new(465)) 773 | {Decimal.new("4.65"), 2} 774 | 775 | Cldr.Math.coef_exponent(Decimal.new(-46.543)) 776 | {Decimal.new("-4.6543"), 1} 777 | 778 | """ 779 | 780 | # An integer should be returned as a float mantissa 781 | @spec coef_exponent(number_or_decimal) :: {number_or_decimal, integer} 782 | def coef_exponent(number) when is_integer(number) do 783 | {mantissa_digits, exponent} = coef_exponent_digits(number) 784 | {Digits.to_float(mantissa_digits), exponent} 785 | end 786 | 787 | # All other numbers are returned as the same type as the parameter 788 | def coef_exponent(number) do 789 | {mantissa_digits, exponent} = coef_exponent_digits(number) 790 | {Digits.to_number(mantissa_digits, number), exponent} 791 | end 792 | 793 | @doc """ 794 | Returns a tuple representing a number in a normalized form with 795 | the mantissa in the range `0 < m < 10` and a base 10 exponent. 796 | 797 | The mantissa is represented as tuple of the form `Digits.t`. 798 | 799 | * `number` is an integer, float or Decimal 800 | 801 | ## Examples 802 | 803 | Cldr.Math.coef_exponent_digits(Decimal.new(1.23004)) 804 | {{[1, 2, 3, 0], 1, 1}, 0} 805 | 806 | Cldr.Math.coef_exponent_digits(Decimal.new(465)) 807 | {{[4, 6, 5], 1, 1}, -1} 808 | 809 | Cldr.Math.coef_exponent_digits(Decimal.new(-46.543)) 810 | {{[4, 6, 5, 4], 1, -1}, 1} 811 | 812 | """ 813 | @spec coef_exponent_digits(number_or_decimal) :: {Digits.t(), integer()} 814 | def coef_exponent_digits(number) do 815 | {digits, place, sign} = Digits.to_digits(number) 816 | {{digits, 1, sign}, place - 1} 817 | end 818 | 819 | @doc """ 820 | Calculates the square root of a Decimal number using Newton's method. 821 | 822 | * `number` is an integer, float or Decimal. For integer and float, 823 | `sqrt` is delegated to the erlang `:math` module. 824 | 825 | We convert the Decimal to a float and take its 826 | `:math.sqrt` only to get an initial estimate. 827 | The means typically we are only two iterations from 828 | a solution so the slight hack improves performance 829 | without sacrificing precision. 830 | 831 | ## Examples 832 | 833 | iex> Cldr.Math.sqrt(Decimal.new(9)) 834 | Decimal.new("3.0") 835 | 836 | iex> Cldr.Math.sqrt(Decimal.new("9.869")) 837 | Decimal.new("3.141496458696078173887197038") 838 | 839 | """ 840 | @precision 0.0001 841 | @decimal_precision Decimal.from_float(@precision) 842 | def sqrt(number, precision \\ @precision) 843 | 844 | def sqrt(%Decimal{sign: sign} = number, _precision) 845 | when sign == -1 do 846 | raise ArgumentError, "bad argument in arithmetic expression #{inspect(number)}" 847 | end 848 | 849 | # Get an initial estimate of the sqrt by using the built in `:math.sqrt` 850 | # function. This means typically its only two iterations to get the default 851 | # the sqrt at the specified precision. 852 | def sqrt(%Decimal{} = number, precision) 853 | when is_number(precision) do 854 | initial_estimate = 855 | number 856 | |> to_float 857 | |> :math.sqrt() 858 | |> Decimal.from_float() 859 | 860 | decimal_precision = 861 | if is_integer(precision) do 862 | Decimal.new(precision) 863 | else 864 | Decimal.from_float(precision) 865 | end 866 | 867 | do_sqrt(number, initial_estimate, @decimal_precision, decimal_precision) 868 | end 869 | 870 | def sqrt(number, _precision) do 871 | :math.sqrt(number) 872 | end 873 | 874 | defp do_sqrt( 875 | %Decimal{} = number, 876 | %Decimal{} = estimate, 877 | %Decimal{} = old_estimate, 878 | %Decimal{} = precision 879 | ) do 880 | diff = 881 | estimate 882 | |> Decimal.sub(old_estimate) 883 | |> Decimal.abs() 884 | 885 | if Cldr.Decimal.compare(diff, old_estimate) == :lt || Cldr.Decimal.compare(diff, old_estimate) == :eq do 886 | estimate 887 | else 888 | Decimal.div(number, Decimal.mult(@two, estimate)) 889 | 890 | new_estimate = 891 | Decimal.add( 892 | Decimal.div(estimate, @two), 893 | Decimal.div(number, Decimal.mult(@two, estimate)) 894 | ) 895 | 896 | do_sqrt(number, new_estimate, estimate, precision) 897 | end 898 | end 899 | 900 | @doc """ 901 | Calculate the nth root of a number. 902 | 903 | * `number` is an integer or a Decimal 904 | 905 | * `nth` is a positive integer 906 | 907 | ## Examples 908 | 909 | iex> Cldr.Math.root Decimal.new(8), 3 910 | Decimal.new("2.0") 911 | 912 | iex> Cldr.Math.root Decimal.new(16), 4 913 | Decimal.new("2.0") 914 | 915 | iex> Cldr.Math.root Decimal.new(27), 3 916 | Decimal.new("3.0") 917 | 918 | """ 919 | def root(%Decimal{} = number, nth) when is_integer(nth) and nth > 0 do 920 | guess = 921 | number 922 | |> to_float() 923 | |> :math.pow(1 / nth) 924 | |> Decimal.from_float() 925 | 926 | do_root(number, Decimal.new(nth), guess) 927 | end 928 | 929 | def root(number, nth) when is_number(number) and is_integer(nth) and nth > 0 do 930 | guess = :math.pow(number, 1 / nth) 931 | do_root(number, nth, guess) 932 | end 933 | 934 | @root_precision 0.0001 935 | defp do_root(number, nth, root) when is_number(number) do 936 | delta = 1 / nth * (number / :math.pow(root, nth - 1)) - root 937 | 938 | if delta > @root_precision do 939 | do_root(number, nth, root + delta) 940 | else 941 | root 942 | end 943 | end 944 | 945 | @decimal_root_precision Decimal.from_float(@root_precision) 946 | defp do_root(%Decimal{} = number, %Decimal{} = nth, %Decimal{} = root) do 947 | d1 = Decimal.div(@one, nth) 948 | d2 = Decimal.div(number, power(root, Decimal.sub(nth, @one))) 949 | d3 = Decimal.sub(d2, root) 950 | delta = Decimal.mult(d1, d3) 951 | 952 | if Cldr.Decimal.compare(delta, @decimal_root_precision) == :gt do 953 | do_root(number, nth, Decimal.add(root, delta)) 954 | else 955 | root 956 | end 957 | end 958 | 959 | @rounding_modes [:down, :up, :ceiling, :floor, :half_even, :half_up, :half_down] 960 | @doc false 961 | def rounding_modes do 962 | @rounding_modes 963 | end 964 | 965 | # Originally adapted from https://github.com/ewildgoose/elixir-float_pp 966 | # Thanks for making this like @ewildgoose 967 | 968 | @doc """ 969 | Round a number to an arbitrary precision using one of several rounding algorithms. 970 | 971 | Rounding algorithms are based on the definitions given in IEEE 754, but also 972 | include 2 additional options (effectively the complementary versions): 973 | 974 | ## Arguments 975 | 976 | * `number` is a `float`, `integer` or `Decimal` 977 | 978 | * `places` is an integer number of places to round to 979 | 980 | * `mode` is the rounding mode to be applied. The 981 | default is `:half_even` 982 | 983 | ## Rounding algorithms 984 | 985 | Directed roundings: 986 | 987 | * `:down` - Round towards 0 (truncate), eg 10.9 rounds to 10.0 988 | 989 | * `:up` - Round away from 0, eg 10.1 rounds to 11.0. (Non IEEE algorithm) 990 | 991 | * `:ceiling` - Round toward +∞ - Also known as rounding up or ceiling 992 | 993 | * `:floor` - Round toward -∞ - Also known as rounding down or floor 994 | 995 | Round to nearest: 996 | 997 | * `:half_even` - Round to nearest value, but in a tiebreak, round towards the 998 | nearest value with an even (zero) least significant bit, which occurs 50% 999 | of the time. This is the default for IEEE binary floating-point and the recommended 1000 | value for decimal. 1001 | 1002 | * `:half_up` - Round to nearest value, but in a tiebreak, round away from 0. 1003 | This is the default algorithm for Erlang's Kernel.round/2 1004 | 1005 | * `:half_down` - Round to nearest value, but in a tiebreak, round towards 0 1006 | (Non IEEE algorithm) 1007 | 1008 | ## Notes 1009 | 1010 | * When the `number` is a `Decimal`, the results are identical 1011 | to `Decimal.round/3` (delegates to `Decimal` in these cases) 1012 | 1013 | * When the `number` is a `float`, `places` is `0` and `mode` 1014 | is `:half_up` then the result is the same as `Kernel.trunc/1` 1015 | 1016 | * The results of rounding for `floats` may not return the same 1017 | result as `Float.round/2`. `Float.round/2` operates on the 1018 | binary representation. This implementation operates on 1019 | a decimal representation. 1020 | 1021 | """ 1022 | def round(number, places \\ 0, mode \\ :half_even) 1023 | 1024 | def round(%Decimal{} = number, places, mode) do 1025 | Decimal.round(number, places, mode) 1026 | end 1027 | 1028 | def round(number, places, mode) when is_integer(number) do 1029 | number 1030 | |> Decimal.new() 1031 | |> Decimal.round(places, mode) 1032 | |> Decimal.to_integer() 1033 | end 1034 | 1035 | def round(number, places, mode) when is_float(number) do 1036 | number 1037 | |> Digits.to_digits() 1038 | |> round_digits(%{decimals: places, rounding: mode}) 1039 | |> Digits.to_number(number) 1040 | end 1041 | 1042 | @doc false 1043 | def round_scientific(number, places, mode) when is_float(number) do 1044 | number 1045 | |> Digits.to_digits() 1046 | |> round_digits(%{scientific: places, rounding: mode}) 1047 | |> Digits.to_number(number) 1048 | end 1049 | 1050 | # The next function heads operate on decomposed numbers returned 1051 | # by Digits.to_digits. 1052 | 1053 | # scientific/decimal rounding are the same, we are just varying which 1054 | # digit we start counting from to find our rounding point 1055 | defp round_digits(digits_t, options) 1056 | 1057 | # Passing true for decimal places avoids rounding and uses whatever is necessary 1058 | defp round_digits(digits_t, %{scientific: true}), do: digits_t 1059 | defp round_digits(digits_t, %{decimals: true}), do: digits_t 1060 | 1061 | # rounded away all the decimals... return 0 1062 | # NOTE THESE IMPLY THAT ANY NUMBER LESS THAN ZERO THAT SHOULD ROUND TO 1 1063 | # WILL RETURN 0 which is not what we want! 1064 | 1065 | # defp round_digits(_, %{scientific: dp}) when dp <= 0, do: {[0], 1, 1} 1066 | # defp round_digits({_, place, _}, %{decimals: dp}) when dp + place <= 0, do: {[0], 1, 1} 1067 | 1068 | defp round_digits({_, place, _}, %{decimals: dp}) when dp + place <= 0 and place < 0 do 1069 | {[0], 1, 1} 1070 | end 1071 | 1072 | defp round_digits({_, place, _} = digits_t, %{decimals: dp} = options) when dp + place <= 0 do 1073 | # IO.inspect dp + place, label: "Round at" 1074 | {digits, place, sign} = do_round(digits_t, dp, options) 1075 | {List.flatten(digits), place, sign} 1076 | end 1077 | 1078 | defp round_digits(digits_t = {_, place, _}, options = %{decimals: dp}) do 1079 | {digits, place, sign} = do_round(digits_t, dp + place - 1, options) 1080 | {List.flatten(digits), place, sign} 1081 | end 1082 | 1083 | defp round_digits(digits_t, options = %{scientific: dp}) do 1084 | {digits, place, sign} = do_round(digits_t, dp, options) 1085 | {List.flatten(digits), place, sign} 1086 | end 1087 | 1088 | defp do_round({digits, place, sign}, round_at, %{rounding: rounding}) do 1089 | case Enum.split(digits, round_at) do 1090 | {l, [least_sig | [tie | rest]]} -> 1091 | # IO.inspect {l, [least_sig | [tie | rest]]}, label: "Case 1" 1092 | case do_incr(l, least_sig, increment?(sign == 1, least_sig, tie, rest, rounding)) do 1093 | [:rollover | digits] -> {digits, place + 1, sign} 1094 | digits -> {digits, place, sign} 1095 | end 1096 | 1097 | {[] = l, [least_sig | []]} -> 1098 | # IO.inspect {l, [least_sig | []]}, label: "Case 2" 1099 | case do_incr(l, least_sig, increment?(sign == 1, least_sig, 0, [], rounding)) do 1100 | [:rollover | digits] -> {digits, place + 1, sign} 1101 | digits -> {digits, place, sign} 1102 | end 1103 | 1104 | {l, [least_sig | []]} -> 1105 | # IO.inspect {l, [least_sig | []]}, label: "Case 4" 1106 | {[l, least_sig], place, sign} 1107 | 1108 | {l, []} -> 1109 | # IO.inspect {l, []}, label: "Case 3" 1110 | {l, place, sign} 1111 | end 1112 | end 1113 | 1114 | @doc """ 1115 | Converts a float to a rational number {numerator, denominator}. 1116 | 1117 | Note this code is adapted from that generated by Claude.ai. There 1118 | is definitely room for improvement and optimisation. 1119 | 1120 | ### Arguments 1121 | 1122 | * `x` is any float. 1123 | 1124 | * `options` is a keyword list of options. 1125 | 1126 | ### Options 1127 | 1128 | - `:max_iterations` - Maximum number of continued fraction terms (default: 20) 1129 | 1130 | - `:epsilon` - Tolerance for float comparisons (default: 1.0e-10) 1131 | 1132 | - `:max_denominator` - Maximum allowed denominator (default: nil meaning no limited) 1133 | 1134 | ##$ Examples 1135 | 1136 | iex> Cldr.Math.float_to_ratio(0.75) 1137 | {3, 4} 1138 | 1139 | iex> Cldr.Math.float_to_ratio(3.14159, max_iterations: 5) 1140 | {9208, 2931} 1141 | 1142 | iex> Cldr.Math.float_to_ratio(3.14159, max_denominator: 10) 1143 | {22, 7} 1144 | 1145 | iex> Cldr.Math.float_to_ratio(1.42, max_denominator: 10) 1146 | {10, 7} 1147 | 1148 | """ 1149 | def float_to_ratio(x, opts \\ []) do 1150 | max_iterations = Keyword.get(opts, :max_iterations, 20) 1151 | epsilon = Keyword.get(opts, :epsilon, 1.0e-10) 1152 | max_denominator = Keyword.get(opts, :max_denominator) 1153 | 1154 | continued_fraction = continued_fraction(x, max_iterations, epsilon) 1155 | 1156 | if max_denominator do 1157 | convergents_with_limit(continued_fraction, max_denominator) 1158 | else 1159 | convergents(continued_fraction) 1160 | end 1161 | |> Enum.min_by(&approximation_error(x, &1)) 1162 | end 1163 | 1164 | # Generates convergents, stopping when denominator exceeds the limit. 1165 | # Also includes semi-convergents (intermediate fractions) for better approximations. 1166 | defp convergents_with_limit([], _max_denom), do: [] 1167 | defp convergents_with_limit([a0], _max_denom), do: [{a0, 1}] 1168 | 1169 | defp convergents_with_limit([a0 | rest], max_denom) do 1170 | do_convergents_with_limit(rest, {a0, 1}, {1, 0}, max_denom, [{a0, 1}]) 1171 | end 1172 | 1173 | defp do_convergents_with_limit([], _curr, _prev, _max_denom, acc), do: acc 1174 | 1175 | defp do_convergents_with_limit([a | rest], {p_curr, q_curr}, {p_prev, q_prev}, max_denom, acc) do 1176 | p_next = a * p_curr + p_prev 1177 | q_next = a * q_curr + q_prev 1178 | 1179 | if q_next > max_denom do 1180 | # If next convergent exceeds limit, try semi-convergents 1181 | semi_convergents = generate_semi_convergents( 1182 | {p_prev, q_prev}, 1183 | {p_curr, q_curr}, 1184 | a, 1185 | max_denom 1186 | ) 1187 | Enum.reverse(semi_convergents) ++ acc 1188 | else 1189 | # Next convergent is within limit, continue 1190 | new_acc = [{p_next, q_next} | acc] 1191 | do_convergents_with_limit( 1192 | rest, 1193 | {p_next, q_next}, 1194 | {p_curr, q_curr}, 1195 | max_denom, 1196 | new_acc 1197 | ) 1198 | end 1199 | end 1200 | 1201 | # Generates semi-convergents between two consecutive convergents. 1202 | # Semi-convergents provide better approximations when the next full convergent 1203 | # would exceed the denominator limit. 1204 | defp generate_semi_convergents({p_prev, q_prev}, {p_curr, q_curr}, a, max_denom) do 1205 | # Maximum k such that k * q_curr + q_prev <= max_denom 1206 | max_k = div(max_denom - q_prev, q_curr) 1207 | 1208 | # Generate all semi-convergents from k=1 to min(max_k, a-1) 1209 | 1..min(max_k, a - 1)//1 1210 | |> Enum.map(fn k -> 1211 | {k * p_curr + p_prev, k * q_curr + q_prev} 1212 | end) 1213 | |> Enum.filter(fn {_n, d} -> d <= max_denom end) 1214 | end 1215 | 1216 | # Generates the continued fraction representation of a number. 1217 | # Returns a list of coefficients [a0, a1, a2, ...]. 1218 | defp continued_fraction(x, max_iterations, epsilon) do 1219 | do_continued_fraction(x, max_iterations, epsilon, []) 1220 | end 1221 | 1222 | defp do_continued_fraction(_x, 0, _epsilon, acc), do: Enum.reverse(acc) 1223 | 1224 | defp do_continued_fraction(x, _n, epsilon, acc) when abs(x) < epsilon do 1225 | Enum.reverse(acc) 1226 | end 1227 | 1228 | defp do_continued_fraction(x, n, epsilon, acc) do 1229 | a = floor(x) 1230 | frac = x - a 1231 | 1232 | if abs(frac) < epsilon do 1233 | Enum.reverse([a | acc]) 1234 | else 1235 | do_continued_fraction(1.0 / frac, n - 1, epsilon, [a | acc]) 1236 | end 1237 | end 1238 | 1239 | # Calculates convergents (rational approximations) from continued fraction coefficients. 1240 | # Returns a list of {numerator, denominator} tuples. 1241 | def convergents([]), do: [] 1242 | def convergents([a0]), do: [{a0, 1}] 1243 | def convergents([a0, a1 | rest]) do 1244 | initial = [{a0, 1}, {a0 * a1 + 1, a1}] 1245 | 1246 | rest 1247 | |> Enum.with_index(2) 1248 | |> Enum.reduce(initial, fn {a, _idx}, [prev, curr | _] = acc -> 1249 | {p_prev, q_prev} = prev 1250 | {p_curr, q_curr} = curr 1251 | 1252 | p_next = a * p_curr + p_prev 1253 | q_next = a * q_curr + q_prev 1254 | 1255 | [{p_curr, q_curr}, {p_next, q_next} | acc] 1256 | end) 1257 | end 1258 | 1259 | @doc """ 1260 | Calculates the error between the original float and the rational approximation. 1261 | 1262 | ### Arguments 1263 | 1264 | * `float` is the original float 1265 | 1266 | * `{numerator, denominator}` is the approximate ratio for the float 1267 | 1268 | """ 1269 | def approximation_error(original, {num, denom}) do 1270 | abs(original - num / denom) 1271 | end 1272 | 1273 | # 1274 | # Helper functions for round/2-3 1275 | # 1276 | defp do_incr(l, least_sig, false), do: [l, least_sig] 1277 | defp do_incr(l, least_sig, true) when least_sig < 9, do: [l, least_sig + 1] 1278 | # else need to cascade the increment 1279 | defp do_incr(l, 9, true) do 1280 | l 1281 | |> Enum.reverse() 1282 | |> cascade_incr 1283 | |> Enum.reverse([0]) 1284 | end 1285 | 1286 | # cascade an increment of decimal digits which could be rolling over 9 -> 0 1287 | defp cascade_incr([9 | rest]), do: [0 | cascade_incr(rest)] 1288 | defp cascade_incr([d | rest]), do: [d + 1 | rest] 1289 | defp cascade_incr([]), do: [1, :rollover] 1290 | 1291 | @spec increment?(boolean, non_neg_integer | nil, non_neg_integer | nil, list(), atom()) :: 1292 | boolean 1293 | defp increment?(positive, least_sig, tie, rest, round) 1294 | 1295 | # Directed rounding towards 0 (truncate) 1296 | defp increment?(_, _ls, _tie, _, :down), do: false 1297 | # Directed rounding away from 0 (non IEEE option) 1298 | defp increment?(_, _ls, nil, _, :up), do: false 1299 | defp increment?(_, _ls, _tie, _, :up), do: true 1300 | 1301 | # Directed rounding towards +∞ (rounding up / ceiling) 1302 | defp increment?(true, _ls, tie, _, :ceiling) when tie != nil, do: true 1303 | defp increment?(_, _ls, _tie, _, :ceiling), do: false 1304 | 1305 | # Directed rounding towards -∞ (rounding down / floor) 1306 | defp increment?(false, _ls, tie, _, :floor) when tie != nil, do: true 1307 | defp increment?(_, _ls, _tie, _, :floor), do: false 1308 | 1309 | # Round to nearest - tiebreaks by rounding to even 1310 | # Default IEEE rounding, recommended default for decimal 1311 | defp increment?(_, ls, 5, [], :half_even) when Integer.is_even(ls), do: false 1312 | defp increment?(_, _ls, tie, _rest, :half_even) when tie >= 5, do: true 1313 | defp increment?(_, _ls, _tie, _rest, :half_even), do: false 1314 | 1315 | # Round to nearest - tiebreaks by rounding away from zero (same as Elixir Kernel.round) 1316 | defp increment?(_, _ls, tie, _rest, :half_up) when tie >= 5, do: true 1317 | defp increment?(_, _ls, _tie, _rest, :half_up), do: false 1318 | 1319 | # Round to nearest - tiebreaks by rounding towards zero (non IEEE option) 1320 | defp increment?(_, _ls, 5, [], :half_down), do: false 1321 | defp increment?(_, _ls, tie, _rest, :half_down) when tie >= 5, do: true 1322 | defp increment?(_, _ls, _tie, _rest, :half_down), do: false 1323 | end 1324 | --------------------------------------------------------------------------------