feat(gl): add immutable postgres ledger storage
This commit is contained in:
@@ -0,0 +1,115 @@
|
||||
// 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
|
||||
}
|
||||
Reference in New Issue
Block a user