Skip to content

v3.2.0

Latest

Choose a tag to compare

@ericmj ericmj released this 10 Oct 21:14

Enhancements

  • Make most operations faster. Compared with v3.1.2 on OTP 29, on decimals
    with up to 20 digits and exponents from -20 to 20, div/2 is about 8x
    faster, mult/2 6x, add/2 5x, round/3 4x, compare/2 2.7x and
    normalize/1 2x. On money amounts with two decimals, to_float/1 is about
    23x faster, compare/3 18x, div/2 15x, rem/2 and sqrt/1 4x,
    round/3 2-3x, add/2, sub/2, mult/2, div_int/2 and compare/2
    2-2.7x, and parsing 1.4x. from_float/1 and cast/1 of a float are about
    3.4x faster and cast/1 of an integer 3x. to_string/1 is unchanged.

  • Add the :subnormal and :clamped signals from the General Decimal
    Arithmetic spec. :subnormal is signalled for every result whose adjusted
    exponent is below emin, even an exact one, and :underflow only when
    such a result is also inexact. A zero result's exponent is kept between
    emin - precision + 1 and emax, the range of nonzero results, and
    :clamped is signalled when it has to move:
    Decimal.mult("0e-3500", "0e-3500") returns 0E-6176 instead of
    0E-7000. This also applies to the zero results of division by infinity,
    of div_int/2 and div_rem/2 with a smaller dividend, and of sqrt/1. A
    subnormal result that rounds to zero signals :clamped too.

Bug fixes

  • Fix Decimal.div/2 rounding some inexact results the wrong way. Under
    :half_even and :half_down, a result just above a tie was rounded as a
    tie, and under :ceiling, :floor and :up, a result whose first
    discarded digit was 0 was treated as exact even when later digits weren't,
    so :ceiling and :floor could return a value on the wrong side of the
    true quotient. About 5% of random divisions at the default precision were
    affected. :inexact was also missing when the remainder was a power of
    ten.

  • Fix :up rounding changing exact values. A result whose discarded digits
    were all zero was still rounded away from zero: with precision: 1 and
    rounding: :up, Decimal.add(0, Decimal.new("9.0")) returned 10
    instead of 9, and Decimal.round(Decimal.new(0), -1, :up) returned
    1E+1 instead of 0E+1.

  • Fix rounding carries leaving one digit more than the precision. Rounding
    9.99 to precision 2 returned 10.0 instead of 10, and
    Decimal.div(95, 10) at precision 1 returned 10 instead of 1E+1. This
    affected every operation that rounds to the context precision, and the
    results now match the General Decimal Arithmetic spec and Python's
    decimal.

  • Fix Decimal.sqrt/1 rounding inexact roots as if the first discarded
    digit were the whole remainder. At precision 9 under :ceiling,
    Decimal.sqrt(10) returned 3.16227766 instead of 3.16227767, and at
    precision 2 under :half_even, Decimal.sqrt("1.57") returned 1.2
    instead of 1.3. The default :half_up rounding wasn't affected. Inexact
    roots now signal :inexact as well as :rounded.

  • Fix Decimal.rem/2 and Decimal.div_rem/2 computing the remainder from a
    product rounded to the context precision, which could cancel it out:
    Decimal.rem("9999999999999999999999999999999999", "2.000000000000000000000000000000001")
    returned 0 instead of 3E-33.

  • Fix Decimal.to_float/1 returning the next float up for odd integers
    between 2^52 and 2^53, which doubles represent exactly:
    Decimal.to_float(Decimal.new(4_503_599_627_370_497)) returned
    4503599627370498.0.

  • Keep subnormal results instead of flushing them to zero. Since v3.0.0 set
    the default emin to -6143, every result with an adjusted exponent below
    it became 0 with :underflow, even an exact one:
    Decimal.div("1e-6140", 10000) returned 0 instead of 1E-6144. Such
    results are now rounded at the exponent emin - precision + 1 (-6176 by
    default) with the context's rounding, as IEEE 754 and the General Decimal
    Arithmetic spec specify, and signal :subnormal, plus :underflow when
    that rounding is inexact. A result that rounds to zero keeps that exponent
    (0E-6176). Decimal.round/3 no longer flushes an input below emin to
    zero before rounding it.

  • Fix Decimal.round/3 rounding an input with more digits than the
    precision twice, first with the context's rounding instead of mode: at
    precision 3, Decimal.round(Decimal.new("0.4995"), 1, :down) returned
    0.5 instead of 0.4.

  • Make Decimal.round/3 signal :rounded when it discards digits of a
    nonzero coefficient, and :inexact when any of them is nonzero, as the
    quantize operation of the General Decimal Arithmetic spec does.
    Decimal.round("1.25", 1) returned 1.3 with no flags.

  • Make Decimal.round/3 signal :invalid_operation and return NaN where
    quantize does: for ±Infinity, when -places is outside
    emin - precision + 1 to emax, and when the result would need more
    digits than the precision or an adjusted exponent above emax. The signal
    is trapped by default, so these calls now raise Decimal.Error. Before,
    the context rounded such results to an exponent other than -places:
    Decimal.round(Decimal.new("1e40"), 2) returned
    1.000000000000000000000000000000000E+40, and Decimal.round("1.5", 7000)
    returned 1.500000000000000000000000000000000. ±Infinity was returned
    unchanged.

  • Make Decimal.compare/3 exact. It rounded num1 ± threshold to the
    context precision, so numbers near the threshold compared wrong:
    Decimal.compare(1, "1.000000000000000000000000000000002", "1.5e-33")
    returned :eq although they differ by 2e-33. It no longer sets
    :inexact or :rounded, accepts -0 as a threshold, and raises
    Decimal.Error instead of CondClauseError for a NaN when
    :invalid_operation isn't trapped.

  • Make Decimal.compare/2 raise Decimal.Error for a NaN operand even when
    :invalid_operation isn't trapped, instead of returning the NaN. Comparing
    a NaN with ±Infinity now raises too, instead of returning :lt or :gt.

  • Make Decimal.Context.set/1, Decimal.Context.with/2 and
    Decimal.Context.update/1 raise ArgumentError for an invalid context: a
    precision that isn't a positive integer, an unknown rounding mode, an
    emax or emin that is neither an integer nor :infinity, or an emin
    above emax. A precision of 0 silently gave wrong results
    (Decimal.add(1, 1) returned 0E+1), and the other cases failed with
    FunctionClauseError in the next operation.

  • Make Decimal.to_integer/1 raise ArgumentError for NaN and ±Infinity,
    like Decimal.to_float/1, instead of FunctionClauseError.