// Package ledger contains GL's framework-independent financial model. package ledger import ( "fmt" "math/big" "strings" "unicode" ) const ( AmountPrecision = 38 AmountScale = 18 ) var amountFactor = new(big.Int).Exp(big.NewInt(10), big.NewInt(AmountScale), nil) // Amount is an exact fixed-point decimal with PostgreSQL numeric(38,18) // semantics. Its zero value is a valid zero amount. type Amount struct { units big.Int } func ParseAmount(value string) (Amount, error) { if value == "" { return Amount{}, fmt.Errorf("amount is required") } negative := value[0] == '-' unsigned := value if negative { unsigned = value[1:] } if unsigned == "" || strings.HasPrefix(unsigned, "+") { return Amount{}, fmt.Errorf("invalid amount %q", value) } parts := strings.Split(unsigned, ".") if len(parts) > 2 || parts[0] == "" { return Amount{}, fmt.Errorf("invalid amount %q", value) } for _, part := range parts { if part == "" || strings.IndexFunc(part, func(r rune) bool { return !unicode.IsDigit(r) }) >= 0 { return Amount{}, fmt.Errorf("invalid amount %q", value) } } if len(parts[0]) > 1 && parts[0][0] == '0' { return Amount{}, fmt.Errorf("amount must not contain leading zeroes") } if len(strings.TrimLeft(parts[0], "0")) > AmountPrecision-AmountScale { return Amount{}, fmt.Errorf("amount exceeds integer precision %d", AmountPrecision-AmountScale) } fraction := "" if len(parts) == 2 { fraction = parts[1] if len(fraction) > AmountScale { return Amount{}, fmt.Errorf("amount exceeds scale %d", AmountScale) } } digits := strings.TrimLeft(parts[0]+fraction, "0") if len(digits) > AmountPrecision { return Amount{}, fmt.Errorf("amount exceeds precision %d", AmountPrecision) } whole := new(big.Int) whole.SetString(parts[0], 10) units := new(big.Int).Mul(whole, amountFactor) if fraction != "" { padded := fraction + strings.Repeat("0", AmountScale-len(fraction)) fractional := new(big.Int) fractional.SetString(padded, 10) units.Add(units, fractional) } if negative { units.Neg(units) } return Amount{units: *units}, nil } func (a Amount) IsZero() bool { return a.units.Sign() == 0 } func (a Amount) Add(other Amount) Amount { var result big.Int result.Add(&a.units, &other.units) return Amount{units: result} } func (a Amount) Negate() Amount { var result big.Int result.Neg(&a.units) return Amount{units: result} } func (a Amount) String() string { if a.units.Sign() == 0 { return "0" } abs := new(big.Int).Abs(new(big.Int).Set(&a.units)) whole, fraction := new(big.Int), new(big.Int) whole.QuoRem(abs, amountFactor, fraction) value := whole.String() if fraction.Sign() != 0 { fractionText := fmt.Sprintf("%018s", fraction.String()) value += "." + strings.TrimRight(fractionText, "0") } if a.units.Sign() < 0 { return "-" + value } return value }