all repos

clerk @ 66c8add

missing tooling for ledger/hledger

clerk/internal/decimal/decimal.go (view raw)

Oleksandr Smirnov Oleksandr Smirnov
olexsmir@gmail.com
remove decimal dependency, 14 days ago
1
package decimal
2
3
import (
4
	"fmt"
5
	"math/big"
6
	"strings"
7
)
8
9
type Decimal struct {
10
	scale int
11
	coeff *big.Int
12
}
13
14
func FromInt(v int64) Decimal {
15
	if v == 0 {
16
		return Decimal{}
17
	}
18
	return Decimal{coeff: big.NewInt(v)}
19
}
20
21
func FromString(s string) (Decimal, error) {
22
	original := s
23
	if s == "" {
24
		return Decimal{}, fmt.Errorf("can't convert %s to decimal", original)
25
	}
26
27
	sign := 1
28
	if s[0] == '+' || s[0] == '-' {
29
		if s[0] == '-' {
30
			sign = -1
31
		}
32
		s = s[1:]
33
	}
34
35
	if s == "" {
36
		return Decimal{}, fmt.Errorf("can't convert %s to decimal", original)
37
	}
38
39
	firstDot := strings.IndexByte(s, '.')
40
	lastDot := strings.LastIndexByte(s, '.')
41
	if firstDot != lastDot {
42
		return Decimal{}, fmt.Errorf("can't convert %s to decimal", original)
43
	}
44
45
	intPart := s
46
	fracPart := ""
47
	if firstDot >= 0 {
48
		intPart = s[:firstDot]
49
		fracPart = s[firstDot+1:]
50
	}
51
52
	if len(intPart)+len(fracPart) == 0 {
53
		return Decimal{}, fmt.Errorf("can't convert %s to decimal", original)
54
	}
55
56
	for i := 0; i < len(intPart); i++ {
57
		if intPart[i] < '0' || intPart[i] > '9' {
58
			return Decimal{}, fmt.Errorf("can't convert %s to decimal", original)
59
		}
60
	}
61
	for i := 0; i < len(fracPart); i++ {
62
		if fracPart[i] < '0' || fracPart[i] > '9' {
63
			return Decimal{}, fmt.Errorf("can't convert %s to decimal", original)
64
		}
65
	}
66
67
	coeffDigits := intPart + fracPart
68
	coeff := new(big.Int)
69
	if _, ok := coeff.SetString(coeffDigits, 10); !ok {
70
		return Decimal{}, fmt.Errorf("can't convert %s to decimal", original)
71
	}
72
	if sign < 0 && coeff.Sign() != 0 {
73
		coeff.Neg(coeff)
74
	}
75
76
	out := Decimal{coeff: coeff, scale: len(fracPart)}
77
	return out.normalized(), nil
78
}
79
80
func (d Decimal) String() string {
81
	if d.coeff == nil || d.coeff.Sign() == 0 {
82
		return "0"
83
	}
84
85
	abs := new(big.Int).Set(d.coeff)
86
	sign := ""
87
	if abs.Sign() < 0 {
88
		sign = "-"
89
		abs.Abs(abs)
90
	}
91
92
	digits := abs.String()
93
	if d.scale == 0 {
94
		return sign + digits
95
	}
96
97
	if len(digits) <= d.scale {
98
		digits = strings.Repeat("0", d.scale-len(digits)+1) + digits
99
	}
100
	split := len(digits) - d.scale
101
	return sign + digits[:split] + "." + digits[split:]
102
}
103
104
func (d Decimal) Neg() Decimal {
105
	if d.coeff == nil || d.coeff.Sign() == 0 {
106
		return Decimal{}
107
	}
108
	return Decimal{coeff: new(big.Int).Neg(d.coeff), scale: d.scale}
109
}
110
111
func (d Decimal) Sub(other Decimal) Decimal { return d.Add(other.Neg()) }
112
func (d Decimal) Add(other Decimal) Decimal {
113
	a, b, scale := align(d, other)
114
	sum := new(big.Int).Add(a, b)
115
	return Decimal{coeff: sum, scale: scale}.normalized()
116
}
117
118
func (d Decimal) Mul(other Decimal) Decimal {
119
	if d.IsZero() || other.IsZero() {
120
		return Decimal{}
121
	}
122
	product := new(big.Int).Mul(d.coeffOrZero(), other.coeffOrZero())
123
	return Decimal{coeff: product, scale: d.scale + other.scale}.normalized()
124
}
125
126
func (d Decimal) Cmp(other Decimal) int {
127
	a, b, _ := align(d, other)
128
	return a.Cmp(b)
129
}
130
131
func (d Decimal) Equal(other Decimal) bool {
132
	return d.Cmp(other) == 0
133
}
134
135
func (d Decimal) IsZero() bool {
136
	return d.coeff == nil || d.coeff.Sign() == 0
137
}
138
139
func align(a, b Decimal) (aCoeff *big.Int, bCoeff *big.Int, scale int) {
140
	scale = max(b.scale, a.scale)
141
142
	aCoeff = a.coeffOrZero()
143
	bCoeff = b.coeffOrZero()
144
	if delta := scale - a.scale; delta > 0 {
145
		aCoeff.Mul(aCoeff, pow10(delta))
146
	}
147
	if delta := scale - b.scale; delta > 0 {
148
		bCoeff.Mul(bCoeff, pow10(delta))
149
	}
150
	return aCoeff, bCoeff, scale
151
}
152
153
func (d Decimal) coeffOrZero() *big.Int {
154
	if d.coeff == nil {
155
		return new(big.Int)
156
	}
157
	return new(big.Int).Set(d.coeff)
158
}
159
160
func (d Decimal) normalized() Decimal {
161
	if d.coeff == nil || d.coeff.Sign() == 0 {
162
		return Decimal{}
163
	}
164
	if d.scale == 0 {
165
		return Decimal{coeff: new(big.Int).Set(d.coeff)}
166
	}
167
168
	sign := d.coeff.Sign()
169
	abs := new(big.Int).Abs(d.coeff)
170
	ten := big.NewInt(10)
171
	rem := new(big.Int)
172
	for d.scale > 0 {
173
		quotient, _ := new(big.Int).QuoRem(abs, ten, rem)
174
		if rem.Sign() != 0 {
175
			break
176
		}
177
		abs = quotient
178
		d.scale--
179
	}
180
181
	if sign < 0 {
182
		abs.Neg(abs)
183
	}
184
	return Decimal{coeff: abs, scale: d.scale}
185
}
186
187
func pow10(n int) *big.Int {
188
	if n <= 0 {
189
		return big.NewInt(1)
190
	}
191
	return new(big.Int).Exp(big.NewInt(10), big.NewInt(int64(n)), nil)
192
}