mirror of
https://github.com/shopspring/decimal.git
synced 2024-11-22 20:40:48 +01:00
Add implementation of natural exponent function (Taylor, Hull-Abraham) (#229)
* Add implementation of natural exponent function (Taylor, Hull-Abraham) * Update docs * Improve docs, handle multithreading race condition in exp method
This commit is contained in:
parent
9ffd602a49
commit
46544472a5
3 changed files with 363 additions and 1 deletions
180
decimal.go
180
decimal.go
|
@ -52,6 +52,10 @@ var DivisionPrecision = 16
|
||||||
// silently lose precision.
|
// silently lose precision.
|
||||||
var MarshalJSONWithoutQuotes = false
|
var MarshalJSONWithoutQuotes = false
|
||||||
|
|
||||||
|
// ExpMaxIterations specifies the maximum number of iterations needed to calculate
|
||||||
|
// precise natural exponent value using ExpHullAbrham method.
|
||||||
|
var ExpMaxIterations = 1000
|
||||||
|
|
||||||
// Zero constant, to make computations faster.
|
// Zero constant, to make computations faster.
|
||||||
// Zero should never be compared with == or != directly, please use decimal.Equal or decimal.Cmp instead.
|
// Zero should never be compared with == or != directly, please use decimal.Equal or decimal.Cmp instead.
|
||||||
var Zero = New(0, 1)
|
var Zero = New(0, 1)
|
||||||
|
@ -64,6 +68,8 @@ var fiveInt = big.NewInt(5)
|
||||||
var tenInt = big.NewInt(10)
|
var tenInt = big.NewInt(10)
|
||||||
var twentyInt = big.NewInt(20)
|
var twentyInt = big.NewInt(20)
|
||||||
|
|
||||||
|
var factorials = []Decimal{New(1, 0)}
|
||||||
|
|
||||||
// Decimal represents a fixed-point decimal. It is immutable.
|
// Decimal represents a fixed-point decimal. It is immutable.
|
||||||
// number = value * 10 ^ exp
|
// number = value * 10 ^ exp
|
||||||
type Decimal struct {
|
type Decimal struct {
|
||||||
|
@ -633,6 +639,180 @@ func (d Decimal) Pow(d2 Decimal) Decimal {
|
||||||
return temp.Mul(temp).Div(d)
|
return temp.Mul(temp).Div(d)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ExpHullAbrham calculates the natural exponent of decimal (e to the power of d) using Hull-Abraham algorithm.
|
||||||
|
// OverallPrecision argument specifies the overall precision of the result (integer part + decimal part).
|
||||||
|
//
|
||||||
|
// ExpHullAbrham is faster than ExpTaylor for small precision values, but it is much slower for large precision values.
|
||||||
|
//
|
||||||
|
// Example:
|
||||||
|
//
|
||||||
|
// NewFromFloat(26.1).ExpHullAbrham(2).String() // output: "220000000000"
|
||||||
|
// NewFromFloat(26.1).ExpHullAbrham(20).String() // output: "216314672147.05767284"
|
||||||
|
//
|
||||||
|
func (d Decimal) ExpHullAbrham(overallPrecision uint32) (Decimal, error) {
|
||||||
|
// Algorithm based on Variable precision exponential function.
|
||||||
|
// ACM Transactions on Mathematical Software by T. E. Hull & A. Abrham.
|
||||||
|
if d.IsZero() {
|
||||||
|
return Decimal{oneInt, 0}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
currentPrecision := overallPrecision
|
||||||
|
|
||||||
|
// Algorithm does not work if currentPrecision * 23 < |x|.
|
||||||
|
// Precision is automatically increased in such cases, so the value can be calculated precisely.
|
||||||
|
// If newly calculated precision is higher than ExpMaxIterations the currentPrecision will not be changed.
|
||||||
|
f := d.Abs().InexactFloat64()
|
||||||
|
if ncp := f / 23; ncp > float64(currentPrecision) && ncp < float64(ExpMaxIterations) {
|
||||||
|
currentPrecision = uint32(math.Ceil(ncp))
|
||||||
|
}
|
||||||
|
|
||||||
|
// fail if abs(d) beyond an over/underflow threshold
|
||||||
|
overflowThreshold := New(23*int64(currentPrecision), 0)
|
||||||
|
if d.Abs().Cmp(overflowThreshold) > 0 {
|
||||||
|
return Decimal{}, fmt.Errorf("over/underflow threshold, exp(x) cannot be calculated precisely")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return 1 if abs(d) small enough; this also avoids later over/underflow
|
||||||
|
overflowThreshold2 := New(9, -int32(currentPrecision)-1)
|
||||||
|
if d.Abs().Cmp(overflowThreshold2) <= 0 {
|
||||||
|
return Decimal{oneInt, d.exp}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// t is the smallest integer >= 0 such that the corresponding abs(d/k) < 1
|
||||||
|
t := d.exp + int32(d.NumDigits()) // Add d.NumDigits because the paper assumes that d.value [0.1, 1)
|
||||||
|
|
||||||
|
if t < 0 {
|
||||||
|
t = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
k := New(1, t) // reduction factor
|
||||||
|
r := Decimal{new(big.Int).Set(d.value), d.exp - t} // reduced argument
|
||||||
|
p := int32(currentPrecision) + t + 2 // precision for calculating the sum
|
||||||
|
|
||||||
|
// Determine n, the number of therms for calculating sum
|
||||||
|
// use first Newton step (1.435p - 1.182) / log10(p/abs(r))
|
||||||
|
// for solving appropriate equation, along with directed
|
||||||
|
// roundings and simple rational bound for log10(p/abs(r))
|
||||||
|
rf := r.Abs().InexactFloat64()
|
||||||
|
pf := float64(p)
|
||||||
|
nf := math.Ceil((1.453*pf - 1.182) / math.Log10(pf/rf))
|
||||||
|
if nf > float64(ExpMaxIterations) || math.IsNaN(nf) {
|
||||||
|
return Decimal{}, fmt.Errorf("exact value cannot be calculated in <=ExpMaxIterations iterations")
|
||||||
|
}
|
||||||
|
n := int64(nf)
|
||||||
|
|
||||||
|
tmp := New(0, 0)
|
||||||
|
sum := New(1, 0)
|
||||||
|
one := New(1, 0)
|
||||||
|
for i := n - 1; i > 0; i-- {
|
||||||
|
tmp.value.SetInt64(i)
|
||||||
|
sum = sum.Mul(r.DivRound(tmp, p))
|
||||||
|
sum = sum.Add(one)
|
||||||
|
}
|
||||||
|
|
||||||
|
ki := k.IntPart()
|
||||||
|
res := New(1, 0)
|
||||||
|
for i := ki; i > 0; i-- {
|
||||||
|
res = res.Mul(sum)
|
||||||
|
}
|
||||||
|
|
||||||
|
resNumDigits := int32(res.NumDigits())
|
||||||
|
|
||||||
|
var roundDigits int32
|
||||||
|
if resNumDigits > abs(res.exp) {
|
||||||
|
roundDigits = int32(currentPrecision) - resNumDigits - res.exp
|
||||||
|
} else {
|
||||||
|
roundDigits = int32(currentPrecision)
|
||||||
|
}
|
||||||
|
|
||||||
|
res = res.Round(roundDigits)
|
||||||
|
|
||||||
|
return res, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExpTaylor calculates the natural exponent of decimal (e to the power of d) using Taylor series expansion.
|
||||||
|
// Precision argument specifies how precise the result must be (number of digits after decimal point).
|
||||||
|
// Negative precision is allowed.
|
||||||
|
//
|
||||||
|
// ExpTaylor is much faster for large precision values than ExpHullAbrham.
|
||||||
|
//
|
||||||
|
// Example:
|
||||||
|
//
|
||||||
|
// d, err := NewFromFloat(26.1).ExpTaylor(2).String()
|
||||||
|
// d.String() // output: "216314672147.06"
|
||||||
|
//
|
||||||
|
// NewFromFloat(26.1).ExpTaylor(20).String()
|
||||||
|
// d.String() // output: "216314672147.05767284062928674083"
|
||||||
|
//
|
||||||
|
// NewFromFloat(26.1).ExpTaylor(-10).String()
|
||||||
|
// d.String() // output: "220000000000"
|
||||||
|
//
|
||||||
|
func (d Decimal) ExpTaylor(precision int32) (Decimal, error) {
|
||||||
|
// Note(mwoss): Implementation can be optimized by exclusively using big.Int API only
|
||||||
|
if d.IsZero() {
|
||||||
|
return Decimal{oneInt, 0}.Round(precision), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var epsilon Decimal
|
||||||
|
var divPrecision int32
|
||||||
|
if precision < 0 {
|
||||||
|
epsilon = New(1, -1)
|
||||||
|
divPrecision = 8
|
||||||
|
} else {
|
||||||
|
epsilon = New(1, -precision-1)
|
||||||
|
divPrecision = precision + 1
|
||||||
|
}
|
||||||
|
|
||||||
|
decAbs := d.Abs()
|
||||||
|
pow := d.Abs()
|
||||||
|
factorial := New(1, 0)
|
||||||
|
|
||||||
|
result := New(1, 0)
|
||||||
|
|
||||||
|
for i := int64(1); ; {
|
||||||
|
step := pow.DivRound(factorial, divPrecision)
|
||||||
|
result = result.Add(step)
|
||||||
|
|
||||||
|
// Stop Taylor series when current step is smaller than epsilon
|
||||||
|
if step.Cmp(epsilon) < 0 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
pow = pow.Mul(decAbs)
|
||||||
|
|
||||||
|
i++
|
||||||
|
|
||||||
|
// Calculate next factorial number or retrieve cached value
|
||||||
|
if len(factorials) >= int(i) && !factorials[i-1].IsZero() {
|
||||||
|
factorial = factorials[i-1]
|
||||||
|
} else {
|
||||||
|
// To avoid any race conditions, firstly the zero value is appended to a slice to create
|
||||||
|
// a spot for newly calculated factorial. After that, the zero value is replaced by calculated
|
||||||
|
// factorial using the index notation.
|
||||||
|
factorial = factorials[i-2].Mul(New(i, 0))
|
||||||
|
factorials = append(factorials, Zero)
|
||||||
|
factorials[i-1] = factorial
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if d.Sign() < 0 {
|
||||||
|
result = New(1, 0).DivRound(result, precision+1)
|
||||||
|
}
|
||||||
|
|
||||||
|
result = result.Round(precision)
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// NumDigits returns the number of digits of the decimal coefficient (d.Value)
|
||||||
|
// Note: Current implementation is extremely slow for large decimals and/or decimals with large fractional part
|
||||||
|
func (d Decimal) NumDigits() int {
|
||||||
|
// Note(mwoss): It can be optimized, unnecessary cast of big.Int to string
|
||||||
|
if d.IsNegative() {
|
||||||
|
return len(d.value.String()) - 1
|
||||||
|
}
|
||||||
|
return len(d.value.String())
|
||||||
|
}
|
||||||
|
|
||||||
// IsInteger returns true when decimal can be represented as an integer value, otherwise, it returns false.
|
// IsInteger returns true when decimal can be represented as an integer value, otherwise, it returns false.
|
||||||
func (d Decimal) IsInteger() bool {
|
func (d Decimal) IsInteger() bool {
|
||||||
// The most typical case, all decimal with exponent higher or equal 0 can be represented as integer
|
// The most typical case, all decimal with exponent higher or equal 0 can be represented as integer
|
||||||
|
|
|
@ -224,3 +224,27 @@ func BenchmarkDecimal_NewFromString_large_number(b *testing.B) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func BenchmarkDecimal_ExpHullAbraham(b *testing.B) {
|
||||||
|
b.ResetTimer()
|
||||||
|
|
||||||
|
d := RequireFromString("30.412346346346")
|
||||||
|
|
||||||
|
b.ReportAllocs()
|
||||||
|
b.ResetTimer()
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
_, _ = d.ExpHullAbrham(10)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkDecimal_ExpTaylor(b *testing.B) {
|
||||||
|
b.ResetTimer()
|
||||||
|
|
||||||
|
d := RequireFromString("30.412346346346")
|
||||||
|
|
||||||
|
b.ReportAllocs()
|
||||||
|
b.ResetTimer()
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
_, _ = d.ExpTaylor(10)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
158
decimal_test.go
158
decimal_test.go
|
@ -2596,6 +2596,164 @@ func TestDecimal_IsInteger(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestDecimal_ExpHullAbrham(t *testing.T) {
|
||||||
|
for _, testCase := range []struct {
|
||||||
|
Dec string
|
||||||
|
OverallPrecision uint32
|
||||||
|
ExpectedDec string
|
||||||
|
}{
|
||||||
|
{"0", 1, "1"},
|
||||||
|
{"0.00", 5, "1"},
|
||||||
|
{"0.5", 5, "1.6487"},
|
||||||
|
{"0.569297", 10, "1.767024397"},
|
||||||
|
{"0.569297", 16, "1.76702439654095"},
|
||||||
|
{"0.569297", 20, "1.7670243965409496521"},
|
||||||
|
{"1.0", 0, "3"},
|
||||||
|
{"1.0", 1, "3"},
|
||||||
|
{"1.0", 5, "2.7183"},
|
||||||
|
{"1.0", 10, "2.718281828"},
|
||||||
|
{"3.0", 0, "20"},
|
||||||
|
{"3.0", 2, "20"},
|
||||||
|
{"5.26", 0, "200"},
|
||||||
|
{"5.26", 2, "190"},
|
||||||
|
{"5.26", 10, "192.4814913"},
|
||||||
|
{"5.2663117716", 2, "190"},
|
||||||
|
{"5.2663117716", 10, "193.7002327"},
|
||||||
|
{"26.1", 2, "220000000000"},
|
||||||
|
{"26.1", 10, "216314672100"},
|
||||||
|
{"26.1", 20, "216314672147.05767284"},
|
||||||
|
{"50.1591", 10, "6078834887000000000000"},
|
||||||
|
{"50.1591", 30, "6078834886623434464595.07937141"},
|
||||||
|
{"-0.5", 5, "0.60653"},
|
||||||
|
{"-0.569297", 10, "0.5659231429"},
|
||||||
|
{"-0.569297", 16, "0.565923142859576"},
|
||||||
|
{"-0.569297", 20, "0.56592314285957604443"},
|
||||||
|
{"-1.0", 1, "0.4"},
|
||||||
|
{"-1.0", 5, "0.36788"},
|
||||||
|
{"-3.0", 1, "0"},
|
||||||
|
{"-3.0", 2, "0.05"},
|
||||||
|
{"-3.0", 10, "0.0497870684"},
|
||||||
|
{"-5.26", 2, "0.01"},
|
||||||
|
{"-5.26", 10, "0.0051953047"},
|
||||||
|
{"-5.2663117716", 2, "0.01"},
|
||||||
|
{"-5.2663117716", 10, "0.0051626164"},
|
||||||
|
{"-26.1", 2, "0"},
|
||||||
|
{"-26.1", 15, "0.000000000004623"},
|
||||||
|
{"-50.1591", 10, "0"},
|
||||||
|
{"-50.1591", 30, "0.000000000000000000000164505208"},
|
||||||
|
} {
|
||||||
|
d, _ := NewFromString(testCase.Dec)
|
||||||
|
expected, _ := NewFromString(testCase.ExpectedDec)
|
||||||
|
|
||||||
|
exp, err := d.ExpHullAbrham(testCase.OverallPrecision)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if exp.Cmp(expected) != 0 {
|
||||||
|
t.Errorf("expected %s, got %s, for decimal %s", testCase.ExpectedDec, exp.String(), testCase.Dec)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDecimal_ExpTaylor(t *testing.T) {
|
||||||
|
for _, testCase := range []struct {
|
||||||
|
Dec string
|
||||||
|
Precision int32
|
||||||
|
ExpectedDec string
|
||||||
|
}{
|
||||||
|
{"0", 1, "1"},
|
||||||
|
{"0.00", 5, "1"},
|
||||||
|
{"0", -1, "0"},
|
||||||
|
{"0.5", 5, "1.64872"},
|
||||||
|
{"0.569297", 10, "1.7670243965"},
|
||||||
|
{"0.569297", 16, "1.7670243965409497"},
|
||||||
|
{"0.569297", 20, "1.76702439654094965215"},
|
||||||
|
{"1.0", 0, "3"},
|
||||||
|
{"1.0", 1, "2.7"},
|
||||||
|
{"1.0", 5, "2.71828"},
|
||||||
|
{"1.0", -1, "0"},
|
||||||
|
{"1.0", -5, "0"},
|
||||||
|
{"3.0", 1, "20.1"},
|
||||||
|
{"3.0", 2, "20.09"},
|
||||||
|
{"5.26", 0, "192"},
|
||||||
|
{"5.26", 2, "192.48"},
|
||||||
|
{"5.26", 10, "192.4814912972"},
|
||||||
|
{"5.26", -2, "200"},
|
||||||
|
{"5.2663117716", 2, "193.70"},
|
||||||
|
{"5.2663117716", 10, "193.7002326701"},
|
||||||
|
{"26.1", 2, "216314672147.06"},
|
||||||
|
{"26.1", 20, "216314672147.05767284062928674083"},
|
||||||
|
{"26.1", -2, "216314672100"},
|
||||||
|
{"26.1", -10, "220000000000"},
|
||||||
|
{"50.1591", 10, "6078834886623434464595.0793714061"},
|
||||||
|
{"-0.5", 5, "0.60653"},
|
||||||
|
{"-0.569297", 10, "0.5659231429"},
|
||||||
|
{"-0.569297", 16, "0.565923142859576"},
|
||||||
|
{"-0.569297", 20, "0.56592314285957604443"},
|
||||||
|
{"-1.0", 1, "0.4"},
|
||||||
|
{"-1.0", 5, "0.36788"},
|
||||||
|
{"-1.0", -1, "0"},
|
||||||
|
{"-1.0", -5, "0"},
|
||||||
|
{"-3.0", 1, "0.1"},
|
||||||
|
{"-3.0", 2, "0.05"},
|
||||||
|
{"-3.0", 10, "0.0497870684"},
|
||||||
|
{"-5.26", 2, "0.01"},
|
||||||
|
{"-5.26", 10, "0.0051953047"},
|
||||||
|
{"-5.26", -2, "0"},
|
||||||
|
{"-5.2663117716", 2, "0.01"},
|
||||||
|
{"-5.2663117716", 10, "0.0051626164"},
|
||||||
|
{"-26.1", 2, "0"},
|
||||||
|
{"-26.1", 15, "0.000000000004623"},
|
||||||
|
{"-26.1", -2, "0"},
|
||||||
|
{"-26.1", -10, "0"},
|
||||||
|
{"-50.1591", 10, "0"},
|
||||||
|
{"-50.1591", 30, "0.000000000000000000000164505208"},
|
||||||
|
} {
|
||||||
|
d, _ := NewFromString(testCase.Dec)
|
||||||
|
expected, _ := NewFromString(testCase.ExpectedDec)
|
||||||
|
|
||||||
|
exp, err := d.ExpTaylor(testCase.Precision)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if exp.Cmp(expected) != 0 {
|
||||||
|
t.Errorf("expected %s, got %s", testCase.ExpectedDec, exp.String())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDecimal_NumDigits(t *testing.T) {
|
||||||
|
for _, testCase := range []struct {
|
||||||
|
Dec string
|
||||||
|
ExpectedNumDigits int
|
||||||
|
}{
|
||||||
|
{"0", 1},
|
||||||
|
{"0.00", 1},
|
||||||
|
{"1.0", 2},
|
||||||
|
{"3.0", 2},
|
||||||
|
{"5.26", 3},
|
||||||
|
{"5.2663117716", 11},
|
||||||
|
{"3164836416948884.2162426426426267863", 35},
|
||||||
|
{"26.1", 3},
|
||||||
|
{"529.1591", 7},
|
||||||
|
{"-1.0", 2},
|
||||||
|
{"-3.0", 2},
|
||||||
|
{"-5.26", 3},
|
||||||
|
{"-5.2663117716", 11},
|
||||||
|
{"-26.1", 3},
|
||||||
|
} {
|
||||||
|
d, _ := NewFromString(testCase.Dec)
|
||||||
|
|
||||||
|
nums := d.NumDigits()
|
||||||
|
if nums != testCase.ExpectedNumDigits {
|
||||||
|
t.Errorf("expected %d digits for decimal %s, got %d", testCase.ExpectedNumDigits, testCase.Dec, nums)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestDecimal_Sign(t *testing.T) {
|
func TestDecimal_Sign(t *testing.T) {
|
||||||
if Zero.Sign() != 0 {
|
if Zero.Sign() != 0 {
|
||||||
t.Errorf("%q should have sign 0", Zero)
|
t.Errorf("%q should have sign 0", Zero)
|
||||||
|
|
Loading…
Reference in a new issue