Skip to content

v1.1.0

Latest

Choose a tag to compare

@aytechnet aytechnet released this 22 Oct 10:21
· 1 commit to main since this release

v1.1.0 — shopspring v1.6 parity: rounding, float conversion, fmt, roots

This release aligns aytechnet/decimal with shopspring/decimal v1.6.0 wherever the two disagreed, adds the methods shopspring v1.5/v1.6 introduced, and fixes two precision bugs. Every hot path stays allocation-free.

Rounding and float conversion are now checked value by value against shopspring v1.4.0 and v1.6.0 in bench/compare_round_test.go.

⚠️ Behavior changes

Review these before upgrading. Each one brings the package in line with shopspring.

  • A negative half now rounds away from zero. This applies to Round, StringFixed, DivRound and RoundCash. Before, a negative half rounded toward +∞: -2.5 gave -2 and -0.005 gave 0.00 at 2 places. Now they give -3 and -0.01, as shopspring v1.4/v1.6 and PHP's round() do. A credit note now rounds exactly like its invoice. Positive values are unchanged.

  • NewFromFloat and NewFromFloat32 keep the shortest decimal. They now convert the shortest decimal that converts back to the same float, so NewFromFloat(-178464.49) is exactly -178464.49. It used to be ~-178464.49000000009, and 97 % of two- or three-decimal amounts were affected. NewFromFloat32(54.33) is 54.33.

  • NewFromFloatWithExponent(value, exp) takes an exponent, like shopspring: -2 keeps 2 decimal places, where you used to pass 2. It rounds the exact binary value of the float half away from zero, so NewFromFloatWithExponent(0.015, -2) is 0.01, because 0.015 is stored as 0.01499999…. It returns NaN/±Inf instead of panicking.

  • Truncate with a negative precision truncates the integer part: New(5432, 0).Truncate(-2) gives 5400. It used to return the value unchanged, as shopspring v1.4 did; shopspring v1.5+ truncates.

  • %d and %#v no longer print the internal encoding. Because Decimal now implements fmt.Formatter:

    • %d prints integer decimals and fmt's bad-verb error for others, for example %!d(decimal.Decimal=3.14);
    • %#v prints Go syntax such as decimal.New(-314, -2) or decimal.Zero.

    %v, %s, %q and %x are unchanged.

✨ New

  • fmt.Formatter support:
    • %.2f rounds like StringFixed;
    • width and the -, +, and 0 flags work as for floats, for example %08.2f;
    • %#v prints Go syntax.
  • Normalize(), for API compatibility. It is a no-op here, because a Decimal already has a unique representation: 1.50 == 1.5.
  • SqrtRound(places), Root(n) and RootRound(n, places). Like Sqrt and Ln, they return a Decimal with no error, intentionally unlike shopspring's (Decimal, error):
    • an out-of-domain input yields NaN, so the call composes in an expression;
    • they are computed from float64, about 16 significant digits;
    • exact integer roots stay exact: Root(32, 5) = 2.

🐛 Fixes

  • StringFixed and BytesToFixed beyond 2^57 (≈1.44e17) showed a value 100 times too small when places > 0: 1e18 at 2 places gave 10000000000000000.0000. BytesToFixedBank with a negative places wrote extra zeros.
  • The binary float conversion (NewFromFloat64Exact, used by Sqrt, Sin, Atan…) kept only about 52 significant bits, so the 16th–17th digit was often wrong: it gave ~-178464.49000000009 where the binary value is -178464.48999999999…. It is now correct to the last digit, at the same speed: Sqrt(2) is ~1.4142135623730951 and NewFromFloat64Exact(math.Pi, true) is ~3.1415926535897931. The heuristic that cleared the low 32 bits of the mantissa as "float noise" compensated for that bug; it is removed. Without it, NewFromFloat64Exact(1+2⁻⁵², true) is ~1.0000000000000002 instead of ~1.
  • Test coverage is back to 100 %. This required removing dead code: the binary float32 conversion and the fallback of the shortest-decimal parser, which can never fail on strconv output.

⚡ Performance

Benchmarks are now run against shopspring v1.6.0. shopspring v1.5/v1.6 roughly halved its own costs, so the README headline is lower than with v1.4.0. aytechnet's own timings are unchanged, except for NewFromFloat, which pays for the shortest-decimal conversion.

aytechnet shopspring v1.6.0 Speedup
Fractional arithmetic (Add/Mul/Div/Pow) 8–39 ns, 0 alloc 44–217 ns 4×–19×
Pure-integer arithmetic 1.7–14 ns, 0 alloc 35–149 ns 11×–26×
NewFromFloat 52 ns, 0 alloc (19 ns before, binary) 65 ns, 2 allocs 1.3×
Sqrt(2) / Root(2, 3) 23 / 51 ns, 0 alloc 336 / 1567 ns ~15× / ~31×
Sin/Cos/Tan/Atan, Ln 31–57 ns, 0 alloc 1.5–3.8 µs ~40×–95× (not iso-precision)

Still intentionally different from shopspring

  • Ln, Sqrt, SqrtRound, Root, RootRound, Sin, Cos, Tan, Atan and Pow return no error. Out-of-domain inputs give NaN.
  • JSON is unquoted by default.
  • Methods around math/big (NewFromBigInt, NewFromBigFloat, BigInt, Rat…) are not provided.
  • shopspring's global settings MarshalJSONWithoutQuotes, TrimTrailingZeros, UseScientificNotation, ExpMaxIterations and MaxDecodeExponent have no equivalent.
  • A Decimal holds at most 16 fractional digits and about 17 significant digits. Beyond that, values are rounded with the loss bit set.

Upgrading

go get github.com/aytechnet/decimal@v1.1.0

Search your code for these calls:

  • NewFromFloatWithExponent(…, n) with a positive n: change it to -n.
  • Truncate(n) with a negative n.
  • %d or %#v applied to a Decimal.
  • Any test that expects a negative half to round toward zero.