7da15c00a1
- money.go: 用纯 int64 循环乘法替代 int64(math.Pow10(exp)) 计算进制因子 - Parse: whole*mult+frac 前加溢出检查(math.MaxInt64 仅作整型常量参与比较,无 float 参与) - money_test.go: 补 Format 未知币种错误分支(errors.Is ErrUnknownCurrency)、 Parse 溢出(整数部分过大)、负数往返测试
104 lines
2.5 KiB
Go
104 lines
2.5 KiB
Go
// 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
|
|
}
|