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 }