defmodule ApaNumber do @moduledoc """ APA : Arbitrary Precision Arithmetic - internal helper functions - ApaNumber. """ @parse_digit_memory_speed_border Application.get_env( :apa, :parse_digit_memory_speed_border, 42 ) @doc """ Parses a binary (number string) into an ApaNumber tuple. It works with signs, leading and trailing zeros and additional chars will be ignored. If successful, returns a tuple in the form of `{integer_value, exponent}`: ApaNumber.parse("+0003.00e+00000 Dollar") {3, 0} When the binary cannot be parsed, the atom `:error` will be returned. The limit only depends on the internal integers - because of Elixir "unlimited" integers I would say "arbitrary". ## Examples iex> ApaNumber.parse("0003") {3, 0} iex> ApaNumber.parse("+0003") {3, 0} iex> ApaNumber.parse("-0003") {-3, 0} iex> ApaNumber.parse("-0000120.1200") {-12012, -2} iex> ApaNumber.parse("-0000120.1200") {-12012, -2} iex> ApaNumber.parse("-03 Euro") {-3, 0} iex> ApaNumber.parse("-0003e-2") {-3, -2} iex> ApaNumber.parse("-3e-0002") {-3, -2} iex> ApaNumber.parse("3e-12") {3, -12} iex> ApaNumber.parse("+0003e+12") {3, 12} iex> ApaNumber.parse("+0003e+00000") {3, 0} iex> ApaNumber.parse("+0003.00e+00000 Dollar") {3, 0} """ @spec parse(binary) :: {integer(), integer()} | :error def parse("+" <> rest) when byte_size(rest) + 1 > @parse_digit_memory_speed_border do parse_unsigned_decimal(rest) end def parse("-" <> rest) when byte_size(rest) + 1 > @parse_digit_memory_speed_border do case parse_unsigned_decimal(rest) do {int_value, exp} -> {int_value * -1, exp} :error -> :error end end def parse(binary) when byte_size(binary) > @parse_digit_memory_speed_border and is_binary(binary) do parse_unsigned_decimal(binary) end def parse("+" <> rest) do parse_unsigned(rest) end def parse("-" <> rest) do case parse_unsigned(rest) do {int_value, exp} -> {int_value * -1, exp} :error -> :error end end def parse(binary) when is_binary(binary) do parse_unsigned(binary) end ################################################################################################### # Decimal like version for improve speed in case of bigger strings # at the cost of more memory consumption - see @parse_digit_memory_speed_border in docs ################################################################################################### defp parse_unsigned_decimal(bin) do {int, rest} = parse_digits_decimal(bin) {float, rest} = parse_float_decimal(rest) {exp, _rest} = parse_exp_decimal(rest) if int == [] and float == [] do :error else int = if int == [], do: '0', else: int exp = if exp == [], do: '0', else: exp {List.to_integer(int ++ float), List.to_integer(exp) - Kernel.length(float)} end end defp parse_float_decimal("." <> rest), do: parse_digits_decimal(rest) defp parse_float_decimal(bin), do: {[], bin} defp parse_exp_decimal(<>) when e in [?e, ?E] do case rest do <> when sign in [?+, ?-] -> {digits, rest} = parse_digits_decimal(rest) {[sign | digits], rest} _ -> parse_digits_decimal(rest) end end defp parse_exp_decimal(bin), do: {[], bin} defp parse_digits_decimal(bin), do: parse_digits_decimal(bin, []) defp parse_digits_decimal(<>, acc) when digit in ?0..?9 do parse_digits_decimal(rest, [digit | acc]) end defp parse_digits_decimal(rest, acc) do {:lists.reverse(acc), rest} end ################################################################################################### # Apa version - with more speed for less then @parse_digit_memory_speed_border digits an much less memory consumption # see @parse_digit_memory_speed_border in docs ################################################################################################### defp parse_unsigned(bin) do {int, _int_len, int_trailing_zeros, int_rest} = parse_digits(bin) {float, float_len, float_trailing_zeros, float_rest} = parse_float(int_rest) {exp, _exp_rest} = parse_exp(float_rest) parse_unsigned(int, int_trailing_zeros, float, float_len, float_trailing_zeros, exp) end defp parse_unsigned(int, _int_trailing_zeros, _float, _float_len, _float_trailing_zeros, _exp) when int == :error do :error end defp parse_unsigned(int, int_trailing_zeros, float, _float_len, _float_trailing_zeros, exp) when (exp == :error and float == :error) or (exp == :error and float == 0) do parse_unsigned_integer(int, int_trailing_zeros) end defp parse_unsigned(int, _int_trailing_zeros, float, float_len, float_trailing_zeros, exp) when exp == :error do parse_unsigned_float(int, float, float_len, float_trailing_zeros) end defp parse_unsigned(int, int_trailing_zeros, float, _float_len, _float_trailing_zeros, exp) when float == :error or float == 0 do parse_unsigned_integer(int, int_trailing_zeros, exp) end defp parse_unsigned(int, _int_trailing_zeros, float, float_len, float_trailing_zeros, exp) do parse_unsigned_float(int, float, float_len, float_trailing_zeros, exp) end ### defp parse_unsigned_integer(int, int_trailing_zeros) do int = div(int, ApaNumber.pow10(int_trailing_zeros)) {int, int_trailing_zeros} end defp parse_unsigned_integer(int, int_trailing_zeros, exp) do int = div(int, ApaNumber.pow10(int_trailing_zeros)) {int, int_trailing_zeros + exp} end ### defp parse_unsigned_float(int, float, float_len, float_trailing_zeros) do int_value = int * ApaNumber.pow10(float_len - float_trailing_zeros) + float float_exp = float_trailing_zeros - float_len {int_value, float_exp} end defp parse_unsigned_float(int, float, float_len, float_trailing_zeros, exp) do float = div(float, ApaNumber.pow10(float_trailing_zeros)) int_value = int * ApaNumber.pow10(float_len - float_trailing_zeros) + float float_exp = float_trailing_zeros - float_len {int_value, float_exp + exp} end defp parse_float("." <> rest), do: parse_digits(rest) defp parse_float(bin), do: {:error, 0, 0, bin} defp parse_exp(<>) when e in [?e, ?E] and sign == ?- do {exp, _exp_len, _exp_trailing_zeros, exp_rest} = parse_digits(rest) {-1 * exp, exp_rest} end defp parse_exp(<>) when e in [?e, ?E] and sign == ?+ do {exp, _exp_len, _exp_trailing_zeros, exp_rest} = parse_digits(rest) {exp, exp_rest} end defp parse_exp(<>) when e in [?e, ?E] do {exp, _exp_len, _exp_trailing_zeros, exp_rest} = parse_digits(rest) {exp, exp_rest} end defp parse_exp(bin) do {0, bin} end defp parse_digits(<>) when digit in ?0..?9 do parse_digits(rest, digit - 48, 0, 1) end defp parse_digits(rest), do: {:error, 0, 0, rest} defp parse_digits(<>, acc, trailing_zeros, len) when digit in ?0..?9 do trailing_zeros = if digit == 48 and acc > 0, do: trailing_zeros + 1, else: 0 # unbelievable but mult by 10 is so time expensive here !!! # there is a difference of 280 K in 5 sec between + and * # 404.86 K in 5 sec (benchee) # parse_digits(rest, acc + 10 + (digit - 48), trailing_zeros, len + 1) # 123.90 K in 5 sec (benchee) parse_digits(rest, acc * 10 + (digit - 48), trailing_zeros, len + 1) end defp parse_digits(rest, acc, trailing_zeros, len), do: {acc, len, trailing_zeros, rest} ################################################################################################### @doc """ Creates a string from an ApaNumber tuple. ## Examples iex> ApaNumber.to_string({3, 0}, -1, -1) "3" """ @spec to_string({integer(), integer()}, integer(), integer()) :: binary | :error def to_string({0, _exp}, precision, scale) do to_string_integer({0, 0}, precision, scale) end def to_string({int_value, exp}, precision, scale) when exp >= 0 do to_string_integer({int_value, exp}, precision, scale) end def to_string({int_value, exp}, precision, scale) when exp < 0 and int_value < 0 do "-" <> to_string_decimals({int_value * -1, exp}, precision, scale) end def to_string({int_value, exp}, precision, scale) when exp < 0 do to_string_decimals({int_value, exp}, precision, scale) end ########## to_string_integer exp >= 0 defp to_string_integer({int_value, exp}, precision, scale) when precision <= 0 do to_string_integer({int_value, exp}, scale) end defp to_string_integer({int_value, exp}, precision, scale) when precision > 0 do len = int_value |> Kernel.abs() |> Integer.to_string() |> Kernel.byte_size() diff = len - precision if diff > 0 do int_value = div(int_value, ApaNumber.pow10(diff)) to_string_integer({int_value, exp + diff}, scale) else to_string_integer({int_value, exp}, scale) end end defp to_string_integer({int_value, exp}, scale) do shifted_int = int_value * ApaNumber.pow10(exp) Integer.to_string(shifted_int) |> scale_up_integer(scale) end defp scale_up_integer(int_string, scale) when scale <= 0 do int_string end defp scale_up_integer(int_string, scale) when scale > 0 do scale_zeros = String.duplicate("0", scale) "#{int_string}.#{scale_zeros}" end ########## to_string_decimals exp < 0 defp to_string_decimals({int_value, exp}, precision, scale) when precision <= 0 do to_string_decimals({int_value, exp}, scale) end defp to_string_decimals({int_value, exp}, precision, scale) when precision > 0 do len = int_value |> Kernel.abs() |> Integer.to_string() |> Kernel.byte_size() diff = len - precision if diff > 0 do fac = ApaNumber.pow10(diff) shifted_int = div(int_value, fac) * fac to_string_decimals({shifted_int, exp}, scale) else to_string_decimals({int_value, exp}, scale) end end defp to_string_decimals({int_value, exp}, scale) when scale == 0 do int_value |> div(ApaNumber.pow10(Kernel.abs(exp))) |> Integer.to_string() ## faster and less mem then: # |> Integer.to_charlist() # |> IO.iodata_to_binary() end defp to_string_decimals({int_value, exp}, scale) when scale < 0 do int_value |> Integer.to_charlist() |> list_and_length() |> to_string_decimals_list(exp) |> IO.iodata_to_binary() end defp to_string_decimals({int_value, exp}, scale) when scale + exp >= 0 do zeros = scale + exp (int_value * ApaNumber.pow10(zeros)) |> Integer.to_charlist() |> list_and_length() |> to_string_decimals_list(-scale) |> IO.iodata_to_binary() end defp to_string_decimals({int_value, exp}, scale) when scale + exp < 0 do shrink = Kernel.abs(scale + exp) int_value |> div(ApaNumber.pow10(shrink)) |> Integer.to_charlist() |> list_and_length() |> to_string_decimals_list(-scale) |> IO.iodata_to_binary() end ### defp to_string_decimals_list({list, list_len}, exp) when list_len + exp > 0 do List.insert_at(list, list_len + exp, ?.) end defp to_string_decimals_list({list, list_len}, exp) do # Todo: check for optimizing here because String.duplicate is 5.49x faster!!! '0.' ++ :lists.duplicate(-(list_len + exp), ?0) ++ list end defp list_and_length(list) do {list, Kernel.length(list)} end @doc """ This is based on Decimal version of pow10 (cool!!! - is much faster then my version). Extended with :error guard for < 0 - in case that ever happen. ## Examples iex> ApaNumber.pow10(3) 1000 iex> ApaNumber.pow10(0) 1 """ @spec pow10(non_neg_integer()) :: non_neg_integer() | :error def pow10(num) when num < 0, do: :error @spec base10?(non_neg_integer()) :: boolean() | :error def base10?(num) when num < 0, do: :error pow10_max = Enum.reduce(0..104, 1, fn int, acc -> def pow10(unquote(int)), do: unquote(acc) def base10?(unquote(acc)), do: true acc * 10 end) def pow10(num) when num > 104, do: pow10(104) * pow10(num - 104) def base10?(num) when num >= unquote(pow10_max) do if Kernel.rem(num, unquote(pow10_max)) == 0 do base10?(Kernel.div(num, unquote(pow10_max))) else false end end def base10?(_num), do: false end