Ejemplo
github.com/shopspring/decimal v1.4.0: Hold money in shopspring/decimal without losing cents to Div, ==, or String()
Muestra verificada para golang github.com/shopspring/decimal v1.4.0: Hold money in shopspring/decimal without losing cents to Div, ==, or String(). El…
sha256:84206d243236399467879a1efbd7f91542f9c90b1d044a06e8611694204998bd
Esta red ofrece una sola cosa: una muestra que compila. La ejecutó en un sandbox y guardó el recibo firmado. No califica ni garantiza nada: si el mismo código compila donde estás no es algo que haya medido.
Cuántas claves de firma distintas presentaron un recibo de contrato aprobado. Una es solo el autor; más de una significa que alguien más también lo compiló. Una clave se genera sola y no tiene identidad registrada detrás, así que cuenta claves, no personas.
MIT-0
Evidencia de ejecución
El entorno declarado y las ejecuciones firmadas se muestran por separado, para que veas exactamente qué ejecutó esta muestra y dónde.
- Base de evidencia
- Contrato firmado aprobado
- Recibos de verificación
- 3
- Claves de firma que lo compilaron
- 3
Entorno declarado
go 1.26 linux x64 go 1.26 go go
Entornos de las ejecuciones de verificación
| Entorno | Contrato | Etapas | Ejecución |
|---|---|---|---|
| go 1.26 · linux alpine/x64 · docker ed25519:a2ec939a4c60e243 | PASS | compile:SKIPPED · contract:PASS · load:PASS · resolve:PASS CONTAINER_RUN · golang@1 |
2026-08-14 |
| go 1.26 · linux alpine/x64 · docker ed25519:2175b912ea1c23b1 | PASS | compile:SKIPPED · contract:PASS · load:PASS · resolve:PASS CONTAINER_RUN · golang@1 |
2026-08-18 |
| go 1.26 · linux alpine/x64 · docker ed25519:c1973797be207ac4 | PASS | compile:SKIPPED · contract:PASS · load:PASS · resolve:PASS CONTAINER_RUN · golang@1golang:1.26-alpine@sha256:28d89ee9cc0f… |
2026-09-07 |
Caso
HOW- Objetivo
- Hold money in shopspring/decimal without losing cents to Div, ==, or String()
- Paquetes
- Símbolos
-
- decimal.NewFromString
- decimal.NewFromFloat
- decimal.DivisionPrecision
- Decimal.Equal
- Decimal.QuoRem
- Decimal.RoundBank
- Decimal.StringFixed
- Entorno
- go 1.26
- Creado
- 2026-08-14T08:53:07Z
Contrato
- assert NewFromFloat(0.1).String() is "0.1" and Equal to NewFromString("0.1"), so the constructor is not where 0.1 goes wrong
- assert the loss happens in float64 before the constructor, including digits float64 cannot hold at all
- assert Go folds untyped constant 0.1+0.2 exactly, so the demo only misbehaves once the operands are variables
- assert Add and Mul are exact where the float64 equivalents give 7.000000000000001 and 3.3000000000000003
- assert Mul adds the exponents of its operands while Add and Sub keep the finer of the two, and the Sum identity decimal.Zero is New(0,1) rather than New(0,0)
- assert DivisionPrecision defaults to 16 and is a mutable global, so (1/3)*3 is 0.9999999999999999 and not 1
- assert Div rounds rather than truncates, so 2/3 ends in a 7, and DivRound takes the precision as an argument
- assert QuoRem returns the exact remainder and splitting 10.00 three ways adds back up to 10.00, while a total finer than the split scale pays out a cent more
- assert Equal compares values while == compares the *big.Int pointer and the scale, so two parses of "1.0" are not ==
- assert the zero value is a usable 0 that is Equal to decimal.Zero but not == to New(0,0)
- assert Cmp orders numerically and drops into slices.SortFunc as a method expression
- assert Round is half away from zero and RoundBank is half to even, that they agree wherever the digit ahead of the half is odd, and that the disagreement moves a four-line invoice total by two cents
- assert rounding cents through float64 obeys neither rule, because 1.005 falls below the half and 8.045 lands exactly on it
- assert String() trims trailing zeros while the exponent stays -2, and StringFixed / StringFixedBank are the money formats
- assert NewFromString rejects "1,005" instead of guessing a number
Archivos
- csx.json
- go.mod
- go.sum
- src/money.go
- test/main.go
Código fuente
{"case":{"caseId":"case:sha256:ac0e3e9916aa9e523025c824240192b634984f5d970c43eff44d57abb9a699e2","constraints":{"runtime":"go"},"contract":["assert NewFromFloat(0.1).String() is \"0.1\" and Equal to NewFromString(\"0.1\"), so the constructor is not where 0.1 goes wrong","assert the loss happens in float64 before the constructor, including digits float64 cannot hold at all","assert Go folds untyped constant 0.1+0.2 exactly, so the demo only misbehaves once the operands are variables","assert Add and Mul are exact where the float64 equivalents give 7.000000000000001 and 3.3000000000000003","assert Mul adds the exponents of its operands while Add and Sub keep the finer of the two, and the Sum identity decimal.Zero is New(0,1) rather than New(0,0)","assert DivisionPrecision defaults to 16 and is a mutable global, so (1/3)*3 is 0.9999999999999999 and not 1","assert Div rounds rather than truncates, so 2/3 ends in a 7, and DivRound takes the precision as an argument","assert QuoRem returns the exact remainder and splitting 10.00 three ways adds back up to 10.00, while a total finer than the split scale pays out a cent more","assert Equal compares values while == compares the *big.Int pointer and the scale, so two parses of \"1.0\" are not ==","assert the zero value is a usable 0 that is Equal to decimal.Zero but not == to New(0,0)","assert Cmp orders numerically and drops into slices.SortFunc as a method expression","assert Round is half away from zero and RoundBank is half to even, that they agree wherever the digit ahead of the half is odd, and that the disagreement moves a four-line invoice total by two cents","assert rounding cents through float64 obeys neither rule, because 1.005 falls below the half and 8.045 lands exactly on it","assert String() trims trailing zeros while the exponent stays -2, and StringFixed / StringFixedBank are the money formats","assert NewFromString rejects \"1,005\" instead of guessing a number"],"goal":"Hold money in shopspring/decimal without losing cents to Div, ==, or String()","kind":"HOW","packages":["pkg:golang/github.com/shopspring/decimal@1.4.0"],"schemaVersion":1,"symbols":["decimal.NewFromString","decimal.NewFromFloat","decimal.DivisionPrecision","Decimal.Equal","Decimal.QuoRem","Decimal.RoundBank","Decimal.StringFixed"]},"contractCommand":["go","run","./test"],"environment":{"arch":"x64","ecosystem":"golang","executionContext":"go","language":"go","os":"linux","packageManager":"go","runtime":"go","runtimeVersion":"1.26","schemaVersion":1},"license":"MIT-0","packages":["pkg:golang/github.com/shopspring/decimal@1.4.0"],"schemaVersion":1,"symbols":["decimal.NewFromString","decimal.NewFromFloat","decimal.DivisionPrecision","Decimal.Equal","Decimal.QuoRem","Decimal.RoundBank","Decimal.StringFixed"],"verifierAdapter":"golang@1"}
module codesamplex.dev/sample/goshopspringdecimal
go 1.24
require github.com/shopspring/decimal v1.4.0
github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k=
github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME=
// Package money keeps amounts in shopspring/decimal and pins down the four
// places the library does not behave the way the advice around it says.
//
// NewFromFloat is not the catastrophe it is reputed to be. It converts via
// the shortest decimal string that round-trips back to the same float64, so
// NewFromFloat(0.1).String() is "0.1" and it compares Equal to
// NewFromString("0.1"). What actually destroys money is one step earlier:
// arithmetic performed in float64 before the constructor is already wrong,
// and NewFromFloat then records that wrong value faithfully. Parse the
// string you received and never let an amount exist as a float64.
//
// Add, Sub and Mul are exact and round nothing away, though the scale rule
// differs between them: Mul adds the exponents of its operands, while Add
// and Sub keep the finer of the two. Div is the exception, and it is not a
// rounding you can ignore: Div is DivRound at the package-level
// DivisionPrecision, a mutable global that defaults to 16, so 1/3 comes
// back as 0.3333333333333333 and multiplying that by 3 gives
// 0.9999999999999999 rather than 1. The cut is a round, not a truncation —
// 2/3 ends in a 7 — which means it can round money up as well as down.
// Amounts that have to be divided are divided with QuoRem, which hands back
// the exact remainder so the leftover cents can be distributed instead of
// disappearing.
//
// Decimal is a comparable struct (a *big.Int plus an int32 exponent), so ==
// compiles and is never what you want: it compares the pointer and the
// scale, not the value. Equal compares numbers, Cmp orders them, and Cmp
// returns an int, so it drops into slices.SortFunc as a method expression.
//
// String() is not a money format, and not for the reason people expect: it
// trims trailing zeros in the fraction, so an amount parsed from "1.00"
// prints as "1" even though its exponent is still -2. StringFixed(places)
// is the money format, and it rounds — half away from zero, with
// StringFixedBank as the half-to-even twin. Pick Round or RoundBank once
// for the whole system. They only ever disagree on an exact half, and not
// on all of those either: half to even rounds an odd digit up, the same
// direction half away from zero goes, so the two agree on 2.015 and part
// company on 1.005.
package money
import "github.com/shopspring/decimal"
// Parse is the only constructor a money path should use. NewFromString
// rejects junk instead of guessing, and the scale of the input survives.
func Parse(s string) (decimal.Decimal, error) {
return decimal.NewFromString(s)
}
// MustParse is for literals that are part of the program, not input.
func MustParse(s string) decimal.Decimal {
d, err := Parse(s)
if err != nil {
panic(err)
}
return d
}
// Line is a priced quantity. Both fields are Decimal because the moment
// either one is a float64 the total is a float64 answer wearing a Decimal
// type.
type Line struct {
Unit decimal.Decimal
Qty decimal.Decimal
}
// Total is exact: Mul adds the scales of the operands and rounds nothing.
func (l Line) Total() decimal.Decimal {
return l.Unit.Mul(l.Qty)
}
// RoundedTotal rounds every amount to places and then sums, which is the
// order an invoice uses — a per-line figure is printed, so a per-line
// rounding is what the customer can add up. bank selects half-to-even
// instead of half-away-from-zero. The two disagree only on the lines whose
// digit ahead of the half is even — half of the half-cent lines, not all
// of them — and that is already enough to move a four-line total by two
// cents.
func RoundedTotal(amounts []decimal.Decimal, places int32, bank bool) decimal.Decimal {
total := decimal.Zero
for _, a := range amounts {
if bank {
total = total.Add(a.RoundBank(places))
} else {
total = total.Add(a.Round(places))
}
}
return total
}
// Split divides total into n shares carried to places decimal places.
//
// This is the answer to "Div is not exact". QuoRem returns a quotient at a
// chosen scale plus the exact remainder, so the cents that do not divide
// are handed out one per share from the front rather than being rounded
// into nothing (or, worse, into an extra cent that has to come from
// somewhere).
//
// The shares add back up to total exactly only when total is already at
// places scale, which is the precondition to enforce upstream: a finer
// total has no share to put its tail in, so 10.005 split three ways at two
// places pays out 10.01 — the leftover half cent leaves as a whole one.
// Written for a non-negative total and n >= 1.
func Split(total decimal.Decimal, n int, places int32) []decimal.Decimal {
share, remainder := total.QuoRem(decimal.NewFromInt(int64(n)), places)
unit := decimal.New(1, -places)
shares := make([]decimal.Decimal, n)
for i := range shares {
shares[i] = share
}
for i := 0; remainder.IsPositive() && i < n; i++ {
shares[i] = shares[i].Add(unit)
remainder = remainder.Sub(unit)
}
return shares
}
// Sum is the loop worth writing once: decimal.Zero is a usable identity and
// Add never rounds, so the result keeps the most decimal places any amount
// in the list had. The identity is New(0, 1) rather than New(0, 0), which
// only ever shows in the exponent of an empty sum, because Add takes the
// finer of the two scales.
func Sum(amounts []decimal.Decimal) decimal.Decimal {
total := decimal.Zero
for _, a := range amounts {
total = total.Add(a)
}
return total
}
package main
import (
"fmt"
"math"
"os"
"slices"
"strings"
"github.com/shopspring/decimal"
"codesamplex.dev/sample/goshopspringdecimal/src"
)
func main() {
// ---- constructors -------------------------------------------------
// The reputation of NewFromFloat says this prints the full binary
// expansion of 0.1. It does not: the conversion goes through the
// shortest decimal string that round-trips back to the same float64.
eq("NewFromFloat(0.1).String()", decimal.NewFromFloat(0.1).String(), "0.1")
ok(decimal.NewFromFloat(0.1).Equal(money.MustParse("0.1")),
"NewFromFloat(0.1) should Equal NewFromString(\"0.1\")")
// Where NewFromFloat does lose is where float64 already lost, before
// the call. Both operands must be variables: Go evaluates untyped
// constant arithmetic exactly, so the famous 0.1+0.2 demo does not
// even reproduce if you write it as literals.
var a, b float64 = 0.1, 0.2
const folded = 0.1 + 0.2
ok(float64(folded) == 0.3, "constant 0.1+0.2 should fold to exactly float64(0.3)")
ok(a+b != 0.3, "float64 0.1+0.2 should not equal 0.3")
eq("float64 a+b", fmt.Sprint(a+b), "0.30000000000000004")
eq("NewFromFloat(a+b).String()", decimal.NewFromFloat(a+b).String(), "0.30000000000000004")
eq(`Parse("0.1").Add(Parse("0.2"))`,
money.MustParse("0.1").Add(money.MustParse("0.2")).String(), "0.3")
// And where the value needs more precision than float64 has, the digits
// are gone by the time the constructor sees them.
var long float64 = 1.000000000000000000000001
eq("NewFromFloat(1.000000000000000000000001)", decimal.NewFromFloat(long).String(), "1")
eq(`Parse("1.000000000000000000000001")`,
money.MustParse("1.000000000000000000000001").String(),
"1.000000000000000000000001")
// ---- Add and Mul are exact ---------------------------------------
var price float64 = 0.07
eq("float64 0.07*100", fmt.Sprint(price*100), "7.000000000000001")
eq(`Parse("0.07").Mul(Parse("100"))`,
money.MustParse("0.07").Mul(money.MustParse("100")).String(), "7")
line := money.Line{Unit: money.MustParse("1.10"), Qty: money.MustParse("3")}
eq("Line.Total()", line.Total().String(), "3.3")
eq("Line.Total().StringFixed(2)", line.Total().StringFixed(2), "3.30")
var unit float64 = 1.10
eq("float64 1.10*3", fmt.Sprint(unit*3), "3.3000000000000003")
// Exact keeps the scale as well as the digits, but not by the same rule
// for every operator: Mul adds the exponents, Add and Sub keep the finer
// of the two. String() hides all of it, so read Exponent().
ok(line.Total().Exponent() == -2,
"1.10*3 exponent = %d, want -2 (-2 + 0)", line.Total().Exponent())
fine := money.MustParse("1.10").Mul(money.MustParse("0.005"))
ok(fine.Exponent() == -5, "1.10*0.005 exponent = %d, want -5 (-2 + -3)", fine.Exponent())
sum := money.MustParse("1.10").Add(money.MustParse("2"))
ok(sum.Exponent() == -2, "1.10+2 exponent = %d, want -2", sum.Exponent())
diff := money.MustParse("1.10").Sub(money.MustParse("2.0005"))
ok(diff.Exponent() == -4, "1.10-2.0005 exponent = %d, want -4", diff.Exponent())
// The identity Sum starts from is New(0, 1), not New(0, 0). Add takes
// the finer scale, so the 1 is invisible except on an empty sum.
ok(decimal.Zero.Exponent() == 1,
"decimal.Zero exponent = %d, want 1", decimal.Zero.Exponent())
mixed := money.Sum([]decimal.Decimal{money.MustParse("1.005"), money.MustParse("2")})
ok(mixed.Exponent() == -3, "Sum([1.005, 2]) exponent = %d, want -3", mixed.Exponent())
eq("Sum(nil)", money.Sum(nil).String(), "0")
ok(money.Sum(nil).Exponent() == 1,
"Sum(nil) exponent = %d, want 1", money.Sum(nil).Exponent())
// ---- Div is not exact --------------------------------------------
// Div is DivRound at a package-level global. Any dependency in the
// process can change it, and nothing about the call site says so.
ok(decimal.DivisionPrecision == 16,
"DivisionPrecision = %d, want 16", decimal.DivisionPrecision)
one, three := money.MustParse("1"), money.MustParse("3")
third := one.Div(three)
eq("1/3", third.String(), "0.3333333333333333")
eq("(1/3)*3", third.Mul(three).String(), "0.9999999999999999")
ok(!third.Mul(three).Equal(one), "(1/3)*3 must not equal 1")
// Not truncated — rounded. 2/3 comes back with a 7 on the end.
eq("2/3", money.MustParse("2").Div(three).String(), "0.6666666666666667")
// A division that terminates inside the precision budget is exact.
eq("1/8", one.Div(money.MustParse("8")).String(), "0.125")
saved := decimal.DivisionPrecision
decimal.DivisionPrecision = 4
eq("1/3 with DivisionPrecision=4", one.Div(three).String(), "0.3333")
decimal.DivisionPrecision = saved
eq("1/3 after restoring the global", one.Div(three).String(), "0.3333333333333333")
// DivRound takes the precision as an argument instead of reading the
// global, and rounds half away from zero.
eq("1.DivRound(3, 2)", one.DivRound(three, 2).String(), "0.33")
eq("2.DivRound(3, 2)", money.MustParse("2").DivRound(three, 2).String(), "0.67")
// QuoRem is the one that keeps the money: quotient at a chosen scale
// plus the exact remainder, and the two reconstruct the input.
q, r := money.MustParse("10").QuoRem(three, 2)
eq("10.QuoRem(3, 2) quotient", q.String(), "3.33")
eq("10.QuoRem(3, 2) remainder", r.String(), "0.01")
ok(q.Mul(three).Add(r).Equal(money.MustParse("10")),
"q*3+r should reconstruct 10, got %s", q.Mul(three).Add(r))
shares := money.Split(money.MustParse("10.00"), 3, 2)
got := make([]string, len(shares))
for i, s := range shares {
got[i] = s.StringFixed(2)
}
eq("Split(10.00, 3)", strings.Join(got, " "), "3.34 3.33 3.33")
eq("Split total", money.Sum(shares).StringFixed(2), "10.00")
// The shares only reconstruct a total that is already at places scale.
// A finer total has nowhere to put its tail: the leftover half cent is
// handed out as a whole cent and the payout is a cent over.
eq("Split(10.005, 3) total",
money.Sum(money.Split(money.MustParse("10.005"), 3, 2)).StringFixed(2), "10.01")
// ---- Equal versus == ---------------------------------------------
// Decimal is a *big.Int plus an int32 exponent, so == compiles and
// compares the representation: the pointer and the scale.
oneScale1 := money.MustParse("1.0")
oneScale2 := money.MustParse("1.00")
ok(oneScale1.Equal(oneScale2), "1.0 should Equal 1.00")
ok(oneScale1 != oneScale2, "1.0 == 1.00 must be false: the scales differ")
ok(oneScale1.Exponent() == -1 && oneScale2.Exponent() == -2,
"exponents = %d, %d; want -1, -2", oneScale1.Exponent(), oneScale2.Exponent())
// The sharper half: even the same text twice is not ==, because each
// parse allocates its own big.Int and == compares that pointer.
again := money.MustParse("1.0")
ok(oneScale1.Equal(again), `two parses of "1.0" should Equal`)
ok(oneScale1 != again, `two parses of "1.0" must not be ==`)
// A copy shares the pointer, so == holds for a copy of an allocated
// value and for nothing else except the nil-pointer zero value below.
// That it works at all for copies is what makes the bug survive review.
copied := oneScale1
ok(copied == oneScale1, "a copy of a Decimal should be == to it")
// The zero value is a usable 0 (nil big.Int, exponent 0), which is why
// a Decimal field in a struct needs no constructor — and why it is
// still not == to a constructed zero.
var zero decimal.Decimal
eq("zero value String()", zero.String(), "0")
ok(zero.Equal(decimal.Zero), "the zero value should Equal decimal.Zero")
ok(zero == decimal.Decimal{}, "two zero values should be ==")
ok(decimal.New(0, 0) != decimal.Decimal{},
"New(0,0) allocates, so it must not be == to the zero value")
// ---- Cmp for ordering --------------------------------------------
ok(money.MustParse("2.50").Cmp(money.MustParse("2.5")) == 0,
"2.50 and 2.5 should Cmp equal")
ok(money.MustParse("2.50").Cmp(money.MustParse("10")) == -1,
"2.50 should sort below 10 even though the strings say otherwise")
ok(money.MustParse("-3").Cmp(decimal.Zero) == -1, "-3 should Cmp below zero")
// Cmp already has the shape slices.SortFunc wants, so a method
// expression is the whole comparator.
amounts := []decimal.Decimal{
money.MustParse("10"), money.MustParse("2.5"),
money.MustParse("-3"), money.MustParse("0.75"),
}
slices.SortFunc(amounts, decimal.Decimal.Cmp)
sorted := make([]string, len(amounts))
for i, d := range amounts {
sorted[i] = d.String()
}
eq("sorted with Cmp", strings.Join(sorted, " "), "-3 0.75 2.5 10")
// ---- Round versus RoundBank --------------------------------------
// Round is half away from zero, RoundBank is half to even. The pair can
// only disagree on an exact half, and prices quoted in half cents put an
// invoice on that boundary constantly.
eq("0.5 Round(0)", money.MustParse("0.5").Round(0).String(), "1")
eq("0.5 RoundBank(0)", money.MustParse("0.5").RoundBank(0).String(), "0")
eq("1.5 RoundBank(0)", money.MustParse("1.5").RoundBank(0).String(), "2")
eq("2.5 Round(0)", money.MustParse("2.5").Round(0).String(), "3")
eq("2.5 RoundBank(0)", money.MustParse("2.5").RoundBank(0).String(), "2")
eq("-0.5 Round(0)", money.MustParse("-0.5").Round(0).String(), "-1")
eq("-0.5 RoundBank(0)", money.MustParse("-0.5").RoundBank(0).String(), "0")
// The two rules agree more often than the folklore suggests: half to
// even rounds an odd digit up, which is the direction half away from
// zero always goes. So they part company only where the digit ahead of
// the half is even.
eq("2.015 Round(2)", money.MustParse("2.015").Round(2).StringFixed(2), "2.02")
eq("2.015 RoundBank(2)", money.MustParse("2.015").RoundBank(2).StringFixed(2), "2.02")
eq("3.025 Round(2)", money.MustParse("3.025").Round(2).StringFixed(2), "3.03")
eq("3.025 RoundBank(2)", money.MustParse("3.025").RoundBank(2).StringFixed(2), "3.02")
// Four half-cent lines, two of which round identically under both rules,
// and the choice still moves the invoice total by two cents. The
// banker's total also happens to match the unrounded sum here, which is
// the reason accountants ask for it.
lines := []decimal.Decimal{
money.MustParse("1.005"), money.MustParse("2.015"),
money.MustParse("3.025"), money.MustParse("4.035"),
}
eq("exact sum", money.Sum(lines).StringFixed(2), "10.08")
eq("total with Round", money.RoundedTotal(lines, 2, false).StringFixed(2), "10.10")
eq("total with RoundBank", money.RoundedTotal(lines, 2, true).StringFixed(2), "10.08")
// The float64 version of "round to cents" implements no rounding rule at
// all — it implements whatever the representation error left behind.
// math.Round is half away from zero, the same rule as Round, but 1.005
// is below the half by the time it has been multiplied by 100, so it
// rounds down and lands on the banker's answer for a reason that has
// nothing to do with banker's rounding.
var half float64 = 1.005
eq("float64 1.005*100", fmt.Sprint(half*100), "100.49999999999999")
eq("math.Round(1.005*100)/100", fmt.Sprint(math.Round(half*100)/100), "1")
eq("1.005 RoundBank(2)", money.MustParse("1.005").RoundBank(2).StringFixed(2), "1.00")
// Change the value and the coincidence goes the other way: 8.045 does
// land exactly on the half in float64, so math.Round rounds it up and
// disagrees with RoundBank instead. Neither result was a policy anyone
// picked.
var other float64 = 8.045
eq("float64 8.045*100", fmt.Sprint(other*100), "804.5")
eq("math.Round(8.045*100)/100", fmt.Sprint(math.Round(other*100)/100), "8.05")
eq("8.045 Round(2)", money.MustParse("8.045").Round(2).StringFixed(2), "8.05")
eq("8.045 RoundBank(2)", money.MustParse("8.045").RoundBank(2).StringFixed(2), "8.04")
// ---- String versus StringFixed -----------------------------------
// String() drops trailing zeros in the fraction, so it is not a money
// format: "1.00" comes back as "1" while the scale is still -2 inside.
eq(`Parse("1.00").String()`, money.MustParse("1.00").String(), "1")
ok(money.MustParse("1.00").Exponent() == -2,
"the scale survives even though String() hides it: %d",
money.MustParse("1.00").Exponent())
eq(`Parse("1.00").StringFixed(2)`, money.MustParse("1.00").StringFixed(2), "1.00")
eq(`Parse("1.5").StringFixed(2)`, money.MustParse("1.5").StringFixed(2), "1.50")
eq(`Parse("2").StringFixed(2)`, money.MustParse("2").StringFixed(2), "2.00")
// StringFixed rounds rather than truncating, and it rounds half away
// from zero; StringFixedBank is the half-to-even twin.
eq(`Parse("1.005").StringFixed(2)`, money.MustParse("1.005").StringFixed(2), "1.01")
eq(`Parse("1.005").StringFixedBank(2)`, money.MustParse("1.005").StringFixedBank(2), "1.00")
// Parse rejects junk instead of guessing a zero.
if _, err := money.Parse("1,005"); err == nil {
fail(`Parse("1,005") should fail`)
}
report()
}
// The checks collect instead of exiting on the first mismatch: every line
// here is a measurement, and one wrong hypothesis should not hide the rest.
var failures []string
func fail(format string, args ...any) {
failures = append(failures, fmt.Sprintf(format, args...))
}
func eq(label, got, want string) {
if got != want {
fail("%s = %q, want %q", label, got, want)
}
}
func ok(cond bool, format string, args ...any) {
if !cond {
fail(format, args...)
}
}
func report() {
if len(failures) > 0 {
for _, f := range failures {
fmt.Fprintln(os.Stderr, f)
}
os.Exit(1)
}
fmt.Println("contract ok")
}