116 lines
2.8 KiB
Go
116 lines
2.8 KiB
Go
// 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
|
|
}
|