mirror of
https://github.com/aclindsa/moneygo.git
synced 2024-11-16 19:40:05 -05:00
157 lines
3.8 KiB
Go
157 lines
3.8 KiB
Go
|
package models
|
||
|
|
||
|
import (
|
||
|
"encoding/json"
|
||
|
"fmt"
|
||
|
"math"
|
||
|
"math/big"
|
||
|
"strings"
|
||
|
)
|
||
|
|
||
|
type Amount struct {
|
||
|
big.Rat
|
||
|
}
|
||
|
|
||
|
type PrecisionError struct {
|
||
|
message string
|
||
|
}
|
||
|
|
||
|
func (p PrecisionError) Error() string {
|
||
|
return p.message
|
||
|
}
|
||
|
|
||
|
// Whole returns the integral portion of the Amount
|
||
|
func (amount Amount) Whole() (int64, error) {
|
||
|
var whole big.Int
|
||
|
whole.Quo(amount.Num(), amount.Denom())
|
||
|
if whole.IsInt64() {
|
||
|
return whole.Int64(), nil
|
||
|
}
|
||
|
return 0, PrecisionError{"integral portion of Amount cannot be represented as an int64"}
|
||
|
}
|
||
|
|
||
|
// Fractional returns the fractional portion of the Amount, multiplied by
|
||
|
// 10^precision
|
||
|
func (amount Amount) Fractional(precision uint64) (int64, error) {
|
||
|
if precision < amount.Precision() {
|
||
|
return 0, PrecisionError{"Fractional portion of Amount cannot be represented with the given precision"}
|
||
|
}
|
||
|
|
||
|
// Reduce the fraction to its simplest form
|
||
|
var r, gcd, d, n big.Int
|
||
|
r.Rem(amount.Num(), amount.Denom())
|
||
|
gcd.GCD(nil, nil, &r, amount.Denom())
|
||
|
if gcd.Sign() != 0 {
|
||
|
n.Quo(&r, &gcd)
|
||
|
d.Quo(amount.Denom(), &gcd)
|
||
|
} else {
|
||
|
n.Set(&r)
|
||
|
d.Set(amount.Denom())
|
||
|
}
|
||
|
|
||
|
// Figure out what we need to multiply the numerator by to get the
|
||
|
// denominator to be 10^precision
|
||
|
var prec, multiplier big.Int
|
||
|
prec.SetUint64(precision)
|
||
|
multiplier.SetInt64(10)
|
||
|
multiplier.Exp(&multiplier, &prec, nil)
|
||
|
multiplier.Quo(&multiplier, &d)
|
||
|
|
||
|
n.Mul(&n, &multiplier)
|
||
|
if n.IsInt64() {
|
||
|
return n.Int64(), nil
|
||
|
}
|
||
|
return 0, fmt.Errorf("Fractional portion of Amount does not fit in int64 with given precision")
|
||
|
}
|
||
|
|
||
|
// FromParts re-assembles an Amount from the results from previous calls to
|
||
|
// Whole and Fractional
|
||
|
func (amount *Amount) FromParts(whole, fractional int64, precision uint64) {
|
||
|
var fracnum, fracdenom, power big.Int
|
||
|
fracnum.SetInt64(fractional)
|
||
|
fracdenom.SetInt64(10)
|
||
|
power.SetUint64(precision)
|
||
|
fracdenom.Exp(&fracdenom, &power, nil)
|
||
|
|
||
|
var fracrat big.Rat
|
||
|
fracrat.SetFrac(&fracnum, &fracdenom)
|
||
|
amount.Rat.SetInt64(whole)
|
||
|
amount.Rat.Add(&amount.Rat, &fracrat)
|
||
|
}
|
||
|
|
||
|
// Round rounds the given Amount to the given precision
|
||
|
func (amount *Amount) Round(precision uint64) {
|
||
|
// This probably isn't exactly the most efficient way to do this...
|
||
|
amount.SetString(amount.FloatString(int(precision)))
|
||
|
}
|
||
|
|
||
|
func (amount Amount) String() string {
|
||
|
return amount.FloatString(int(amount.Precision()))
|
||
|
}
|
||
|
|
||
|
func (amount *Amount) UnmarshalJSON(bytes []byte) error {
|
||
|
var value string
|
||
|
if err := json.Unmarshal(bytes, &value); err != nil {
|
||
|
return err
|
||
|
}
|
||
|
value = strings.TrimSpace(value)
|
||
|
|
||
|
if _, ok := amount.SetString(value); !ok {
|
||
|
return fmt.Errorf("Failed to parse '%s' into Amount", value)
|
||
|
}
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
func (amount Amount) MarshalJSON() ([]byte, error) {
|
||
|
return json.Marshal(amount.String())
|
||
|
}
|
||
|
|
||
|
// Precision returns the minimum positive integer p such that if you multiplied
|
||
|
// this Amount by 10^p, it would become an integer
|
||
|
func (amount Amount) Precision() uint64 {
|
||
|
if amount.IsInt() || amount.Sign() == 0 {
|
||
|
return 0
|
||
|
}
|
||
|
|
||
|
// Find d, the denominator of the reduced fractional portion of 'amount'
|
||
|
var r, gcd, d big.Int
|
||
|
r.Rem(amount.Num(), amount.Denom())
|
||
|
gcd.GCD(nil, nil, &r, amount.Denom())
|
||
|
if gcd.Sign() != 0 {
|
||
|
d.Quo(amount.Denom(), &gcd)
|
||
|
} else {
|
||
|
d.Set(amount.Denom())
|
||
|
}
|
||
|
d.Abs(&d)
|
||
|
|
||
|
var power, result big.Int
|
||
|
one := big.NewInt(1)
|
||
|
ten := big.NewInt(10)
|
||
|
|
||
|
// Estimate an initial power
|
||
|
if d.IsUint64() {
|
||
|
power.SetInt64(int64(math.Log10(float64(d.Uint64()))))
|
||
|
} else {
|
||
|
|
||
|
// If the simplified denominator wasn't a uint64, its > 10^19
|
||
|
power.SetInt64(19)
|
||
|
}
|
||
|
|
||
|
// If the initial estimate was too high, bring it down
|
||
|
result.Exp(ten, &power, nil)
|
||
|
for result.Cmp(&d) > 0 {
|
||
|
power.Sub(&power, one)
|
||
|
result.Exp(ten, &power, nil)
|
||
|
}
|
||
|
// If it was too low, bring it up
|
||
|
for result.Cmp(&d) < 0 {
|
||
|
power.Add(&power, one)
|
||
|
result.Exp(ten, &power, nil)
|
||
|
}
|
||
|
|
||
|
if !power.IsUint64() {
|
||
|
panic("Unable to represent Amount's precision as a uint64")
|
||
|
}
|
||
|
return power.Uint64()
|
||
|
}
|