Feel.Runtime.Number (feel v0.1.0)

Copy Markdown View Source

Arithmetic on FEEL numbers: decimal128, with the scale rules of DMN 1.6 (Tables 58 and 60).

A number is a Decimal with a coefficient of at most 34 digits and a last digit whose exponent is at least -6176, with its first digit at exponent 6144 or lower (see Feel.Value.Number). Every result here is rounded to that format, half to even, and keeps as much scale as fits:

  • add/2, subtract/2: the scale of the operand that has more of it, so .25 + .2 is 0.45.
  • multiply/2: the scales added, so .10 * 30.00 is 3.0000.
  • divide/2: the exact quotient when it has at most 34 digits, with the scale closest to the difference of the scales that does not lose digits, so 3 / 2 is 1.5 and 1.01 / 2 is 0.505; otherwise the quotient rounded to 34 digits, so 1 / 3 is 0.3333333333333333333333333333333333. The specification's "s ≤ s1 - s2" cannot hold for 3 / 2; this is the rule of IEEE 754 decimal128 division, which its own examples follow (decision D-59).
  • power/2: one power rounded once, not a product rounded at each step. An integer exponent gives the exact power, with the scale of the product (and of the quotient, for a negative one), so 5.5 ** 2 is 30.25 and 5 ** -2 is 0.04; any other exponent gives the value exp(y * ln x) with the zeros of its fraction removed, so 2.25 ** 0.5 is 1.5.

A result outside the range of the format has no value in FEEL, and null takes its place: these functions return {:error, :overflow}. There is no infinity and no "not a number". A division by zero is {:error, :division_by_zero}, and so is a zero to a negative power. A negative base to a power with a fraction has no real value: {:error, :out_of_domain}. An operand that is not a FEEL number (too many digits, or out of range) is {:error, :invalid_operand}; it can only come from outside, since no result here is one.

A result is outside the range if its value is (D-59), after the rounding to 34 digits. 1E-6176 * 1.0 is the coefficient 10 at -6177, and the number 1E-6176; zeros below -6176 are dropped, and a digit other than zero there (1.5E-6176, 1E-6177, the 34 digits of 1E-6170 / 3) is an overflow. The upper end goes by the first digit in the same way, so 1E+6144 is a number although its coefficient is 1 at an exponent above 6111.

There is no negative zero: a result of zero is positive.

The functions use no process state and no Decimal context, so the precision does not depend on who calls them. compare/2 is exact, with no rounding.

Summary

Functions

Adds two numbers.

Compares two numbers by value, exactly: 1 and 1.000 are equal, and numbers that differ in their 34th digit are not.

The number of decimal digits of a non-negative integer.

Divides the first number by the second.

A guard for a coefficient and an exponent that make a number whatever the digits are: a coefficient of at most 34 digits and an exponent from -6176 to 6111, which is a test of integers. A number can have an exponent above 6111 and be one (1E+6144), and valid?/1 says; so a clause that has this guard needs another for the rest.

Multiplies two numbers.

Negates a number. Zero stays positive.

Raises the first number to the power of the second, as one correctly rounded result and not as repeated multiplication (see Feel.Runtime.Number.Power).

Subtracts the second number from the first.

Returns true for a finite Decimal that is a FEEL number: a coefficient of at most 34 digits, a last digit not below exponent -6176, a first digit not above exponent 6144.

Types

error()

@type error() :: :overflow | :division_by_zero | :invalid_operand | :out_of_domain

result()

@type result() :: {:ok, t()} | {:error, error()}

t()

@type t() :: Decimal.t()

Functions

add(a, b)

@spec add(t(), t()) :: result()

Adds two numbers.

iex> {:ok, sum} = Feel.Runtime.Number.add(Decimal.new("0.25"), Decimal.new("0.2"))
iex> Decimal.to_string(sum)
"0.45"

compare(a, b)

@spec compare(t(), t()) :: :lt | :eq | :gt

Compares two numbers by value, exactly: 1 and 1.000 are equal, and numbers that differ in their 34th digit are not.

iex> Feel.Runtime.Number.compare(Decimal.new("1"), Decimal.new("1.000"))
:eq
iex> Feel.Runtime.Number.compare(Decimal.new("-2"), Decimal.new("1"))
:lt

digit_count(integer)

@spec digit_count(non_neg_integer()) :: pos_integer()

The number of decimal digits of a non-negative integer.

The coefficient of a number has at most 34, but the integer that two numbers far apart in exponent are added in (1E+6000 + 1 is written at the exponent of the second) has thousands, and writing one out as text to count them cost most of the time of an analysis of sums whose operands are not bounded (D-59). Up to 64 digits the count is found by comparing with the powers of ten: from the smallest for an integer that is a single word (almost every one: 5 ns for it where writing it out took 20), and by halving the range of counts for a larger one (90 ns where writing it out took 195, D-59). Above 64 digits the count is found from the size of the integer in bits, which gives a count that is never above the right one (log10(2) is more than 0.30102), and then made exact against the powers of ten, one or two steps.

iex> Feel.Runtime.Number.digit_count(0)
1
iex> Feel.Runtime.Number.digit_count(999)
3
iex> Feel.Runtime.Number.digit_count(1000)
4
iex> Feel.Runtime.Number.digit_count(Integer.pow(10, 6000))
6001
iex> Feel.Runtime.Number.digit_count(Integer.pow(10, 6000) - 1)
6000

divide(a, b)

@spec divide(t(), t()) :: result()

Divides the first number by the second.

iex> {:ok, quotient} = Feel.Runtime.Number.divide(Decimal.new(3), Decimal.new(2))
iex> Decimal.to_string(quotient)
"1.5"
iex> {:ok, quotient} = Feel.Runtime.Number.divide(Decimal.new(1), Decimal.new(3))
iex> Decimal.to_string(quotient)
"0.3333333333333333333333333333333333"
iex> Feel.Runtime.Number.divide(Decimal.new(1), Decimal.new(0))
{:error, :division_by_zero}

is_plain_number(coef, exp)

(macro)

A guard for a coefficient and an exponent that make a number whatever the digits are: a coefficient of at most 34 digits and an exponent from -6176 to 6111, which is a test of integers. A number can have an exponent above 6111 and be one (1E+6144), and valid?/1 says; so a clause that has this guard needs another for the rest.

multiply(a, b)

@spec multiply(t(), t()) :: result()

Multiplies two numbers.

iex> {:ok, product} = Feel.Runtime.Number.multiply(Decimal.new("0.10"), Decimal.new("30.00"))
iex> Decimal.to_string(product)
"3.0000"

negate(number)

@spec negate(t()) :: result()

Negates a number. Zero stays positive.

iex> {:ok, negated} = Feel.Runtime.Number.negate(Decimal.new("1.50"))
iex> Decimal.to_string(negated)
"-1.50"

power(base, exponent)

@spec power(t(), t()) :: result()

Raises the first number to the power of the second, as one correctly rounded result and not as repeated multiplication (see Feel.Runtime.Number.Power).

iex> {:ok, power} = Feel.Runtime.Number.power(Decimal.new("5.5"), Decimal.new(2))
iex> Decimal.to_string(power, :normal)
"30.25"
iex> {:ok, power} = Feel.Runtime.Number.power(Decimal.new(5), Decimal.new(-2))
iex> Decimal.to_string(power, :normal)
"0.04"
iex> Feel.Runtime.Number.power(Decimal.new(0), Decimal.new(-1))
{:error, :division_by_zero}
iex> Feel.Runtime.Number.power(Decimal.new(-2), Decimal.new("0.5"))
{:error, :out_of_domain}

subtract(a, b)

@spec subtract(t(), t()) :: result()

Subtracts the second number from the first.

iex> {:ok, difference} = Feel.Runtime.Number.subtract(Decimal.new("1.50"), Decimal.new("1.5"))
iex> Decimal.to_string(difference)
"0.00"

valid?(arg1)

@spec valid?(term()) :: boolean()

Returns true for a finite Decimal that is a FEEL number: a coefficient of at most 34 digits, a last digit not below exponent -6176, a first digit not above exponent 6144.

iex> Feel.Runtime.Number.valid?(Decimal.new("1.5"))
true
iex> Feel.Runtime.Number.valid?(Decimal.new(1, 1, 6145))
false
iex> Feel.Runtime.Number.valid?(Decimal.new("NaN"))
false