all repos

clerk @ 6fdb9097048e212574439fb0da84d0c94aa7e01b

missing tooling for ledger/hledger

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

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