yumi-golang/internal/model/decimal24_2.go
2026-07-18 15:32:58 +08:00

170 lines
4.1 KiB
Go

package model
import (
"database/sql/driver"
"encoding/json"
"fmt"
"math/big"
"strings"
)
// Decimal24_2 用规范十进制文本承载 MySQL DECIMAL(24,2),避免金额经过 float64 丢精度。
// JSON 输出字符串以保护 22 位整数精度;数据库读写通过 Scanner/Valuer 完成。
type Decimal24_2 string
func ParseDecimal24_2(value string) (Decimal24_2, error) {
value = strings.TrimSpace(value)
if value == "" {
return "", fmt.Errorf("decimal is blank")
}
negative := false
if value[0] == '-' {
negative, value = true, value[1:]
} else if value[0] == '+' {
return "", fmt.Errorf("decimal plus sign is not supported")
}
if value == "" || strings.ContainsAny(value, "eE") {
return "", fmt.Errorf("invalid decimal")
}
parts := strings.Split(value, ".")
if len(parts) > 2 || parts[0] == "" {
return "", fmt.Errorf("invalid decimal")
}
for _, char := range parts[0] {
if char < '0' || char > '9' {
return "", fmt.Errorf("invalid decimal")
}
}
integer := strings.TrimLeft(parts[0], "0")
if integer == "" {
integer = "0"
}
fraction := ""
if len(parts) == 2 {
fraction = parts[1]
for _, char := range fraction {
if char < '0' || char > '9' {
return "", fmt.Errorf("invalid decimal")
}
}
// BigDecimal.stripTrailingZeros 后 scale<=2 的值均可接受,例如 1.2300。
if len(fraction) > 2 && strings.Trim(fraction[2:], "0") != "" {
return "", fmt.Errorf("decimal scale exceeds 2")
}
}
if len(integer) > 22 {
return "", fmt.Errorf("decimal integer digits exceed 22")
}
if len(fraction) > 2 {
fraction = fraction[:2]
}
fraction += strings.Repeat("0", 2-len(fraction))
if integer == "0" && fraction == "00" {
negative = false
}
prefix := ""
if negative {
prefix = "-"
}
return Decimal24_2(prefix + integer + "." + fraction), nil
}
func (d *Decimal24_2) UnmarshalJSON(data []byte) error {
raw := strings.TrimSpace(string(data))
if raw == "" || raw == "null" || raw == `""` {
*d = DecimalZero()
return nil
}
if strings.HasPrefix(raw, `"`) && strings.HasSuffix(raw, `"`) {
var value string
if err := json.Unmarshal(data, &value); err != nil {
return err
}
raw = value
}
parsed, err := ParseDecimal24_2(raw)
if err != nil {
return err
}
*d = parsed
return nil
}
func (d Decimal24_2) MarshalJSON() ([]byte, error) { return json.Marshal(d.String()) }
func (d *Decimal24_2) Scan(value any) error {
var raw string
switch typed := value.(type) {
case nil:
raw = "0"
case []byte:
raw = string(typed)
case string:
raw = typed
case int64:
raw = fmt.Sprintf("%d", typed)
default:
raw = fmt.Sprint(typed)
}
parsed, err := ParseDecimal24_2(raw)
if err != nil {
return err
}
*d = parsed
return nil
}
func (d Decimal24_2) Value() (driver.Value, error) { return d.String(), nil }
func (d Decimal24_2) String() string {
if strings.TrimSpace(string(d)) == "" {
return "0.00"
}
return string(d)
}
func DecimalZero() Decimal24_2 { return Decimal24_2("0.00") }
func (d Decimal24_2) Positive() bool { return d.cents().Sign() > 0 }
func (d Decimal24_2) Compare(other Decimal24_2) int { return d.cents().Cmp(other.cents()) }
func (d Decimal24_2) MultiplyInt(multiplier int64) (Decimal24_2, error) {
if multiplier < 0 {
return "", fmt.Errorf("decimal multiplier must not be negative")
}
value := new(big.Int).Mul(d.cents(), big.NewInt(multiplier))
if len(new(big.Int).Abs(new(big.Int).Set(value)).String()) > 24 {
return "", fmt.Errorf("decimal exceeds DECIMAL(24,2)")
}
return decimalFromCents(value), nil
}
func (d Decimal24_2) cents() *big.Int {
raw := d.String()
negative := strings.HasPrefix(raw, "-")
raw = strings.TrimPrefix(raw, "-")
digits := strings.ReplaceAll(raw, ".", "")
value, ok := new(big.Int).SetString(digits, 10)
if !ok {
return new(big.Int)
}
if negative {
value.Neg(value)
}
return value
}
func decimalFromCents(value *big.Int) Decimal24_2 {
negative := value.Sign() < 0
abs := new(big.Int).Abs(new(big.Int).Set(value)).String()
if len(abs) < 3 {
abs = strings.Repeat("0", 3-len(abs)) + abs
}
result := abs[:len(abs)-2] + "." + abs[len(abs)-2:]
if negative && result != "0.00" {
result = "-" + result
}
return Decimal24_2(result)
}