// Package money represents amounts as int64 minor units + a currency code. // CNY/USD = 分 (1e-2), USDT = micro (1e-6). No float, no "元 string" (v2). package money import ( "errors" "fmt" "math" "strconv" "strings" ) var ErrUnknownCurrency = errors.New("money: unknown currency") var exponents = map[string]int{ "CNY": 2, "USD": 2, "USDT": 6, } // Exponent returns the number of decimal places for a currency. func Exponent(currency string) (int, bool) { e, ok := exponents[strings.ToUpper(currency)] return e, ok } // pow10Int64 computes 10^exp as a pure int64 (no float), matching the // currency exponents above which never exceed a handful of digits. func pow10Int64(exp int) int64 { mult := int64(1) for i := 0; i < exp; i++ { mult *= 10 } return mult } // Format renders minor units as a decimal string, trailing zeros trimmed. func Format(minor int64, currency string) (string, error) { exp, ok := Exponent(currency) if !ok { return "", ErrUnknownCurrency } if exp == 0 { return strconv.FormatInt(minor, 10), nil } neg := minor < 0 if neg { minor = -minor } div := pow10Int64(exp) whole := minor / div frac := minor % div s := fmt.Sprintf("%d.%0*d", whole, exp, frac) s = strings.TrimRight(s, "0") s = strings.TrimRight(s, ".") if neg { s = "-" + s } return s, nil } // Parse converts a decimal string to minor units for the currency. It rejects // values with more fractional digits than the currency allows. func Parse(s, currency string) (int64, error) { exp, ok := Exponent(currency) if !ok { return 0, ErrUnknownCurrency } s = strings.TrimSpace(s) neg := strings.HasPrefix(s, "-") s = strings.TrimPrefix(s, "-") intPart, fracPart := s, "" if i := strings.IndexByte(s, '.'); i >= 0 { intPart, fracPart = s[:i], s[i+1:] } if len(fracPart) > exp { return 0, fmt.Errorf("money.Parse: %q exceeds %d dp for %s", s, exp, currency) } if intPart == "" && fracPart == "" { return 0, fmt.Errorf("money.Parse: empty %q", s) } whole, err := strconv.ParseInt("0"+intPart, 10, 64) if err != nil { return 0, fmt.Errorf("money.Parse int %q: %w", s, err) } fracPart += strings.Repeat("0", exp-len(fracPart)) var frac int64 if fracPart != "" { frac, err = strconv.ParseInt(fracPart, 10, 64) if err != nil { return 0, fmt.Errorf("money.Parse frac %q: %w", s, err) } } mult := pow10Int64(exp) if mult > 0 && whole > (math.MaxInt64-frac)/mult { return 0, fmt.Errorf("money.Parse: %q overflows int64 for %s", s, currency) } minor := whole*mult + frac if neg { minor = -minor } return minor, nil }