130 lines
2.8 KiB
Go
130 lines
2.8 KiB
Go
package money
|
|
|
|
import (
|
|
"errors"
|
|
"testing"
|
|
)
|
|
|
|
func TestParseCentsExact(t *testing.T) {
|
|
cases := []struct {
|
|
in string
|
|
want Cents
|
|
}{
|
|
{"149.95", 14995},
|
|
{"149.9", 14990},
|
|
{"149", 14900},
|
|
{"149.", 14900},
|
|
{".95", 95},
|
|
{"0.05", 5},
|
|
{"0", 0},
|
|
{"0.00", 0},
|
|
{"-12.34", -1234},
|
|
{"-0.05", -5},
|
|
{"+7.50", 750},
|
|
{" 10.10 ", 1010},
|
|
{"1000000.00", 100000000},
|
|
// The classic float-representation trap: 0.1+0.2 etc. Parsed from the
|
|
// string these are exact; ParseFloat would have drifted.
|
|
{"0.10", 10},
|
|
{"0.20", 20},
|
|
{"0.30", 30},
|
|
}
|
|
for _, c := range cases {
|
|
got, err := ParseCents(c.in)
|
|
if err != nil {
|
|
t.Errorf("ParseCents(%q) unexpected error: %v", c.in, err)
|
|
continue
|
|
}
|
|
if got != c.want {
|
|
t.Errorf("ParseCents(%q) = %d, want %d", c.in, got, c.want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestParseCentsRejects(t *testing.T) {
|
|
bad := []string{
|
|
"",
|
|
" ",
|
|
"-",
|
|
"+",
|
|
"abc",
|
|
"12.3.4", // multiple dots
|
|
"149.955", // 3 decimal places — would silently truncate
|
|
"1.234", // 3 decimal places
|
|
"12,34", // comma not a decimal separator here
|
|
"1e3", // no scientific notation; integer-only parse
|
|
"12 34", // internal space
|
|
"$10.00", // currency symbol
|
|
}
|
|
for _, s := range bad {
|
|
if _, err := ParseCents(s); err == nil {
|
|
t.Errorf("ParseCents(%q) = nil error, want rejection", s)
|
|
} else if !errors.Is(err, ErrParse) {
|
|
t.Errorf("ParseCents(%q) error %v not wrapping ErrParse", s, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestStringRoundTrip(t *testing.T) {
|
|
cases := []struct {
|
|
c Cents
|
|
want string
|
|
}{
|
|
{14995, "149.95"},
|
|
{14900, "149.00"},
|
|
{5, "0.05"},
|
|
{50, "0.50"},
|
|
{0, "0.00"},
|
|
{-1234, "-12.34"},
|
|
{-5, "-0.05"},
|
|
{100000000, "1000000.00"},
|
|
}
|
|
for _, tc := range cases {
|
|
if got := tc.c.String(); got != tc.want {
|
|
t.Errorf("Cents(%d).String() = %q, want %q", tc.c, got, tc.want)
|
|
}
|
|
// Round-trip: String -> ParseCents -> same Cents.
|
|
back, err := ParseCents(tc.c.String())
|
|
if err != nil {
|
|
t.Errorf("round-trip ParseCents(%q): %v", tc.c.String(), err)
|
|
continue
|
|
}
|
|
if back != tc.c {
|
|
t.Errorf("round-trip Cents(%d) -> %q -> %d", tc.c, tc.c.String(), back)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestFromFloatMajor(t *testing.T) {
|
|
cases := []struct {
|
|
f float64
|
|
want Cents
|
|
}{
|
|
{149.95, 14995},
|
|
{149.0, 14900},
|
|
{0.05, 5},
|
|
{0.005, 1}, // half-away-from-zero rounds up at the boundary
|
|
{0.004, 0},
|
|
{-12.34, -1234},
|
|
{-0.005, -1},
|
|
}
|
|
for _, c := range cases {
|
|
if got := FromFloatMajor(c.f); got != c.want {
|
|
t.Errorf("FromFloatMajor(%v) = %d, want %d", c.f, got, c.want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestArithmeticStaysInteger(t *testing.T) {
|
|
// Cents arithmetic is plain integer math — associativity / exactness that
|
|
// float prices famously lack.
|
|
unit := Cents(1099) // 10.99
|
|
var total Cents
|
|
for i := 0; i < 3; i++ {
|
|
total += unit
|
|
}
|
|
if total != 3297 {
|
|
t.Errorf("3 * 10.99 = %s, want 32.97", total)
|
|
}
|
|
}
|