Repository navigation
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,DivRoundandRoundCash. Before, a negative half rounded toward +∞:-2.5gave-2and-0.005gave0.00at 2 places. Now they give-3and-0.01, as shopspring v1.4/v1.6 and PHP'sround()do. A credit note now rounds exactly like its invoice. Positive values are unchanged. -
NewFromFloatandNewFromFloat32keep the shortest decimal. They now convert the shortest decimal that converts back to the same float, soNewFromFloat(-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)is54.33. -
NewFromFloatWithExponent(value, exp)takes an exponent, like shopspring:-2keeps 2 decimal places, where you used to pass2. It rounds the exact binary value of the float half away from zero, soNewFromFloatWithExponent(0.015, -2)is0.01, because 0.015 is stored as 0.01499999…. It returnsNaN/±Infinstead of panicking. -
Truncatewith a negative precision truncates the integer part:New(5432, 0).Truncate(-2)gives5400. It used to return the value unchanged, as shopspring v1.4 did; shopspring v1.5+ truncates. -
%dand%#vno longer print the internal encoding. BecauseDecimalnow implementsfmt.Formatter:%dprints integer decimals and fmt's bad-verb error for others, for example%!d(decimal.Decimal=3.14);%#vprints Go syntax such asdecimal.New(-314, -2)ordecimal.Zero.
%v,%s,%qand%xare unchanged.
✨ New
fmt.Formattersupport:%.2frounds likeStringFixed;- width and the
-,+,and0flags work as for floats, for example%08.2f; %#vprints Go syntax.
Normalize(), for API compatibility. It is a no-op here, because aDecimalalready has a unique representation:1.50 == 1.5.SqrtRound(places),Root(n)andRootRound(n, places). LikeSqrtandLn, they return aDecimalwith 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.
- an out-of-domain input yields
🐛 Fixes
StringFixedandBytesToFixedbeyond 2^57 (≈1.44e17) showed a value 100 times too small when places > 0:1e18at 2 places gave10000000000000000.0000.BytesToFixedBankwith a negativeplaceswrote extra zeros.- The binary float conversion (
NewFromFloat64Exact, used bySqrt,Sin,Atan…) kept only about 52 significant bits, so the 16th–17th digit was often wrong: it gave~-178464.49000000009where the binary value is-178464.48999999999…. It is now correct to the last digit, at the same speed:Sqrt(2)is~1.4142135623730951andNewFromFloat64Exact(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.0000000000000002instead of~1. - Test coverage is back to 100 %. This required removing dead code: the binary
float32conversion and the fallback of the shortest-decimal parser, which can never fail onstrconvoutput.
⚡ 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,AtanandPowreturn no error. Out-of-domain inputs giveNaN.- JSON is unquoted by default.
- Methods around
math/big(NewFromBigInt,NewFromBigFloat,BigInt,Rat…) are not provided. - shopspring's global settings
MarshalJSONWithoutQuotes,TrimTrailingZeros,UseScientificNotation,ExpMaxIterationsandMaxDecodeExponenthave no equivalent. - A
Decimalholds 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 positiven: change it to-n.Truncate(n)with a negativen.%dor%#vapplied to aDecimal.- Any test that expects a negative half to round toward zero.