service backend
This commit is contained in:
129
api/pkg/decimal/money.go
Normal file
129
api/pkg/decimal/money.go
Normal file
@@ -0,0 +1,129 @@
|
||||
package decimal
|
||||
|
||||
import (
|
||||
"math/big"
|
||||
|
||||
"github.com/tech/sendico/pkg/merrors"
|
||||
)
|
||||
|
||||
// Money represents a monetary amount with currency and precision handling
|
||||
type Money struct {
|
||||
Amount *big.Rat
|
||||
Currency string
|
||||
Scale uint32 // decimal places for this currency
|
||||
}
|
||||
|
||||
// NewMoney creates a new Money value from a decimal string
|
||||
func NewMoney(amount, currency string, scale uint32) (*Money, error) {
|
||||
rat, err := RatFromString(amount)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &Money{
|
||||
Amount: rat,
|
||||
Currency: currency,
|
||||
Scale: scale,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Add adds two money values (must have same currency)
|
||||
func (m *Money) Add(other *Money) (*Money, error) {
|
||||
if m.Currency != other.Currency {
|
||||
return nil, merrors.InvalidArgument("decimal: currency mismatch: " + m.Currency + " != " + other.Currency)
|
||||
}
|
||||
return &Money{
|
||||
Amount: AddRat(m.Amount, other.Amount),
|
||||
Currency: m.Currency,
|
||||
Scale: m.Scale,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Sub subtracts two money values (must have same currency)
|
||||
func (m *Money) Sub(other *Money) (*Money, error) {
|
||||
if m.Currency != other.Currency {
|
||||
return nil, merrors.InvalidArgument("decimal: currency mismatch: " + m.Currency + " != " + other.Currency)
|
||||
}
|
||||
return &Money{
|
||||
Amount: SubRat(m.Amount, other.Amount),
|
||||
Currency: m.Currency,
|
||||
Scale: m.Scale,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Mul multiplies money by a rational number (for fees, percentages, etc.)
|
||||
func (m *Money) Mul(factor *big.Rat) *Money {
|
||||
return &Money{
|
||||
Amount: MulRat(m.Amount, factor),
|
||||
Currency: m.Currency,
|
||||
Scale: m.Scale,
|
||||
}
|
||||
}
|
||||
|
||||
// Div divides money by a rational number
|
||||
func (m *Money) Div(divisor *big.Rat) (*Money, error) {
|
||||
result, err := DivRat(m.Amount, divisor)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &Money{
|
||||
Amount: result,
|
||||
Currency: m.Currency,
|
||||
Scale: m.Scale,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Negate returns the negative of this money value
|
||||
func (m *Money) Negate() *Money {
|
||||
return &Money{
|
||||
Amount: NegRat(m.Amount),
|
||||
Currency: m.Currency,
|
||||
Scale: m.Scale,
|
||||
}
|
||||
}
|
||||
|
||||
// Round rounds the money to its scale using the specified rounding mode
|
||||
func (m *Money) Round(mode RoundingMode) (*Money, error) {
|
||||
rounded, err := RoundRatToScale(m.Amount, m.Scale, mode)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &Money{
|
||||
Amount: rounded,
|
||||
Currency: m.Currency,
|
||||
Scale: m.Scale,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// String returns the formatted money value
|
||||
func (m *Money) String() string {
|
||||
return FormatRat(m.Amount, m.Scale) + " " + m.Currency
|
||||
}
|
||||
|
||||
// StringAmount returns just the formatted amount
|
||||
func (m *Money) StringAmount() string {
|
||||
return FormatRat(m.Amount, m.Scale)
|
||||
}
|
||||
|
||||
// IsZero checks if the money amount is zero
|
||||
func (m *Money) IsZero() bool {
|
||||
return IsZero(m.Amount)
|
||||
}
|
||||
|
||||
// IsPositive checks if the money amount is positive
|
||||
func (m *Money) IsPositive() bool {
|
||||
return IsPositive(m.Amount)
|
||||
}
|
||||
|
||||
// IsNegative checks if the money amount is negative
|
||||
func (m *Money) IsNegative() bool {
|
||||
return IsNegative(m.Amount)
|
||||
}
|
||||
|
||||
// Cmp compares two money values (must have same currency)
|
||||
// Returns -1 if m < other, 0 if m == other, 1 if m > other
|
||||
func (m *Money) Cmp(other *Money) (int, error) {
|
||||
if m.Currency != other.Currency {
|
||||
return 0, merrors.InvalidArgument("decimal: currency mismatch: " + m.Currency + " != " + other.Currency)
|
||||
}
|
||||
return CmpRat(m.Amount, other.Amount), nil
|
||||
}
|
||||
161
api/pkg/decimal/rational.go
Normal file
161
api/pkg/decimal/rational.go
Normal file
@@ -0,0 +1,161 @@
|
||||
package decimal
|
||||
|
||||
import (
|
||||
"math/big"
|
||||
"strings"
|
||||
|
||||
"github.com/tech/sendico/pkg/merrors"
|
||||
)
|
||||
|
||||
// RatFromString parses a decimal string into a big.Rat
|
||||
// Supports standard decimal notation like "123.45", "-0.001", etc.
|
||||
func RatFromString(value string) (*big.Rat, error) {
|
||||
if strings.TrimSpace(value) == "" {
|
||||
return nil, merrors.InvalidArgument("decimal: empty value")
|
||||
}
|
||||
r := new(big.Rat)
|
||||
if _, ok := r.SetString(value); !ok {
|
||||
return nil, merrors.InvalidArgument("decimal: invalid decimal value: " + value)
|
||||
}
|
||||
return r, nil
|
||||
}
|
||||
|
||||
// MulRat multiplies two rational numbers
|
||||
func MulRat(a, b *big.Rat) *big.Rat {
|
||||
return new(big.Rat).Mul(a, b)
|
||||
}
|
||||
|
||||
// DivRat divides two rational numbers
|
||||
func DivRat(a, b *big.Rat) (*big.Rat, error) {
|
||||
if b.Sign() == 0 {
|
||||
return nil, merrors.InvalidArgument("decimal: division by zero")
|
||||
}
|
||||
return new(big.Rat).Quo(a, b), nil
|
||||
}
|
||||
|
||||
// AddRat adds two rational numbers
|
||||
func AddRat(a, b *big.Rat) *big.Rat {
|
||||
return new(big.Rat).Add(a, b)
|
||||
}
|
||||
|
||||
// SubRat subtracts two rational numbers (a - b)
|
||||
func SubRat(a, b *big.Rat) *big.Rat {
|
||||
return new(big.Rat).Sub(a, b)
|
||||
}
|
||||
|
||||
// NegRat negates a rational number
|
||||
func NegRat(a *big.Rat) *big.Rat {
|
||||
return new(big.Rat).Neg(a)
|
||||
}
|
||||
|
||||
// RoundRatToScale rounds a rational number to a specific number of decimal places
|
||||
// using the specified rounding mode
|
||||
func RoundRatToScale(value *big.Rat, scale uint32, mode RoundingMode) (*big.Rat, error) {
|
||||
scaleFactor := new(big.Int).Exp(big.NewInt(10), big.NewInt(int64(scale)), nil)
|
||||
numerator := new(big.Int).Mul(new(big.Int).Set(value.Num()), scaleFactor)
|
||||
denominator := value.Denom()
|
||||
|
||||
quotient := new(big.Int)
|
||||
remainder := new(big.Int)
|
||||
quotient.QuoRem(numerator, denominator, remainder)
|
||||
|
||||
// No remainder, already exact
|
||||
if remainder.Sign() == 0 {
|
||||
return new(big.Rat).SetFrac(quotient, scaleFactor), nil
|
||||
}
|
||||
|
||||
sign := quotient.Sign()
|
||||
absQuotient := new(big.Int).Abs(new(big.Int).Set(quotient))
|
||||
absRemainder := new(big.Int).Abs(remainder)
|
||||
absDenominator := new(big.Int).Abs(denominator)
|
||||
|
||||
doubledRemainder := new(big.Int).Mul(absRemainder, big.NewInt(2))
|
||||
cmp := doubledRemainder.Cmp(absDenominator)
|
||||
|
||||
shouldIncrement := false
|
||||
|
||||
switch mode {
|
||||
case RoundingModeDown:
|
||||
shouldIncrement = false
|
||||
case RoundingModeHalfUp:
|
||||
if cmp >= 0 {
|
||||
shouldIncrement = true
|
||||
}
|
||||
case RoundingModeHalfEven, RoundingModeUnspecified:
|
||||
if cmp > 0 {
|
||||
shouldIncrement = true
|
||||
} else if cmp == 0 {
|
||||
// Tie: round to even
|
||||
if absQuotient.Bit(0) == 1 {
|
||||
shouldIncrement = true
|
||||
}
|
||||
}
|
||||
default:
|
||||
// Default to HALF_EVEN
|
||||
if cmp > 0 {
|
||||
shouldIncrement = true
|
||||
} else if cmp == 0 {
|
||||
if absQuotient.Bit(0) == 1 {
|
||||
shouldIncrement = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if shouldIncrement {
|
||||
if sign < 0 {
|
||||
absQuotient.Add(absQuotient, big.NewInt(1))
|
||||
quotient = absQuotient.Neg(absQuotient)
|
||||
} else {
|
||||
absQuotient.Add(absQuotient, big.NewInt(1))
|
||||
quotient = absQuotient
|
||||
}
|
||||
}
|
||||
|
||||
return new(big.Rat).SetFrac(quotient, scaleFactor), nil
|
||||
}
|
||||
|
||||
// FormatRat formats a rational number as a decimal string with the specified scale
|
||||
func FormatRat(r *big.Rat, scale uint32) string {
|
||||
sign := ""
|
||||
if r.Sign() < 0 {
|
||||
sign = "-"
|
||||
}
|
||||
|
||||
absRat := new(big.Rat).Abs(r)
|
||||
scaleFactor := new(big.Int).Exp(big.NewInt(10), big.NewInt(int64(scale)), nil)
|
||||
numerator := new(big.Int).Mul(absRat.Num(), scaleFactor)
|
||||
numerator.Quo(numerator, absRat.Denom())
|
||||
|
||||
intStr := numerator.String()
|
||||
if scale == 0 {
|
||||
return sign + intStr
|
||||
}
|
||||
|
||||
if len(intStr) <= int(scale) {
|
||||
intStr = strings.Repeat("0", int(scale)-len(intStr)+1) + intStr
|
||||
}
|
||||
|
||||
pointPos := len(intStr) - int(scale)
|
||||
return sign + intStr[:pointPos] + "." + intStr[pointPos:]
|
||||
}
|
||||
|
||||
// CmpRat compares two rational numbers
|
||||
// Returns -1 if a < b, 0 if a == b, 1 if a > b
|
||||
func CmpRat(a, b *big.Rat) int {
|
||||
return a.Cmp(b)
|
||||
}
|
||||
|
||||
// IsZero checks if a rational number is zero
|
||||
func IsZero(r *big.Rat) bool {
|
||||
return r.Sign() == 0
|
||||
}
|
||||
|
||||
// IsPositive checks if a rational number is positive
|
||||
func IsPositive(r *big.Rat) bool {
|
||||
return r.Sign() > 0
|
||||
}
|
||||
|
||||
// IsNegative checks if a rational number is negative
|
||||
func IsNegative(r *big.Rat) bool {
|
||||
return r.Sign() < 0
|
||||
}
|
||||
29
api/pkg/decimal/rounding.go
Normal file
29
api/pkg/decimal/rounding.go
Normal file
@@ -0,0 +1,29 @@
|
||||
package decimal
|
||||
|
||||
// RoundingMode specifies how to round decimal values
|
||||
type RoundingMode int
|
||||
|
||||
const (
|
||||
// RoundingModeUnspecified defaults to HALF_EVEN
|
||||
RoundingModeUnspecified RoundingMode = iota
|
||||
// RoundingModeHalfEven rounds to nearest, ties to even (banker's rounding)
|
||||
RoundingModeHalfEven
|
||||
// RoundingModeHalfUp rounds to nearest, ties away from zero
|
||||
RoundingModeHalfUp
|
||||
// RoundingModeDown rounds toward zero (truncate)
|
||||
RoundingModeDown
|
||||
)
|
||||
|
||||
// String returns the string representation of the rounding mode
|
||||
func (r RoundingMode) String() string {
|
||||
switch r {
|
||||
case RoundingModeHalfEven:
|
||||
return "HALF_EVEN"
|
||||
case RoundingModeHalfUp:
|
||||
return "HALF_UP"
|
||||
case RoundingModeDown:
|
||||
return "DOWN"
|
||||
default:
|
||||
return "HALF_EVEN"
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user