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) } }