feat(v2): money 包 int64 最小单位 + 币种码(CNY/USD/USDT)
This commit is contained in:
@@ -0,0 +1,89 @@
|
||||
// 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
|
||||
}
|
||||
|
||||
// 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 := int64(math.Pow10(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)
|
||||
}
|
||||
}
|
||||
minor := whole*int64(math.Pow10(exp)) + frac
|
||||
if neg {
|
||||
minor = -minor
|
||||
}
|
||||
return minor, nil
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
package money_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/wangjia/pay/internal/money"
|
||||
)
|
||||
|
||||
func TestParseFormatRoundTrip(t *testing.T) {
|
||||
cases := []struct {
|
||||
s, cur string
|
||||
minor int64
|
||||
}{
|
||||
{"12.3417", "USDT", 12341700},
|
||||
{"0.01", "CNY", 1},
|
||||
{"29.99", "USD", 2999},
|
||||
{"199.99", "CNY", 19999},
|
||||
{"1", "USDT", 1000000},
|
||||
}
|
||||
for _, c := range cases {
|
||||
got, err := money.Parse(c.s, c.cur)
|
||||
if err != nil || got != c.minor {
|
||||
t.Fatalf("Parse(%q,%s)=%d,%v want %d", c.s, c.cur, got, err, c.minor)
|
||||
}
|
||||
back, err := money.Format(c.minor, c.cur)
|
||||
if err != nil {
|
||||
t.Fatalf("Format(%d,%s): %v", c.minor, c.cur, err)
|
||||
}
|
||||
// round-trip 回同一 minor(去尾零后可能字符串不同,再 Parse 校验)
|
||||
again, _ := money.Parse(back, c.cur)
|
||||
if again != c.minor {
|
||||
t.Fatalf("round-trip %d → %q → %d", c.minor, back, again)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseErrors(t *testing.T) {
|
||||
if _, err := money.Parse("1.00", "JPYX"); err == nil {
|
||||
t.Fatal("未知币种应报错")
|
||||
}
|
||||
if _, err := money.Parse("0.001", "CNY"); err == nil {
|
||||
t.Fatal("超精度(CNY 3 位小数)应报错")
|
||||
}
|
||||
if _, err := money.Parse("abc", "CNY"); err == nil {
|
||||
t.Fatal("非数字应报错")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user