all repos

clerk @ a45934c3c506c8bb27d2528b0dd23b74e1852482

missing tooling for ledger/hledger

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

Oleksandr Smirnov Oleksandr Smirnov
olexsmir@gmail.com
decimal: add .Abs() and .Div(), 1 month 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
// StringFixed returns a string representation with exactly places digits
105
// after the decimal point. Pads with zeros or truncates as needed.
106
// decSep and thousandsSep control formatting; zero values mean no custom separator.
107
func (d Decimal) StringFixed(places int, decSep, thousandsSep byte) string {
108
	var sb strings.Builder
109
	sb.Grow(32)
110
	d.WriteFixed(&sb, places, decSep, thousandsSep)
111
	return sb.String()
112
}
113
114
// WriteFixed writes a string representation with exactly places digits
115
// after the decimal point directly into sb. Pads with zeros or truncates
116
// as needed. decSep and thousandsSep control formatting; zero values mean
117
// no custom separator.
118
func (d Decimal) WriteFixed(sb *strings.Builder, places int, decSep, thousandsSep byte) {
119
	if d.IsZero() {
120
		sb.WriteByte('0')
121
		if places > 0 {
122
			if decSep != 0 {
123
				sb.WriteByte(decSep)
124
			} else {
125
				sb.WriteByte('.')
126
			}
127
			for range places {
128
				sb.WriteByte('0')
129
			}
130
		}
131
		return
132
	}
133
134
	var digitBuf [128]byte
135
	digits := d.coeff.Append(digitBuf[:0], 10)
136
137
	sign := false
138
	if len(digits) > 0 && digits[0] == '-' {
139
		sign = true
140
		digits = digits[1:]
141
	}
142
143
	intLen := len(digits) - d.scale
144
145
	if sign {
146
		sb.WriteByte('-')
147
	}
148
149
	// Integer part
150
	if intLen <= 0 {
151
		sb.WriteByte('0')
152
	} else {
153
		for i := range intLen {
154
			if thousandsSep != 0 && i > 0 && (intLen-i)%3 == 0 {
155
				sb.WriteByte(thousandsSep)
156
			}
157
			sb.WriteByte(digits[i])
158
		}
159
	}
160
161
	// Fractional part
162
	if places > 0 {
163
		if decSep != 0 {
164
			sb.WriteByte(decSep)
165
		} else {
166
			sb.WriteByte('.')
167
		}
168
169
		written := 0
170
		if intLen > 0 {
171
			n := min(d.scale, places)
172
			for i := range n {
173
				sb.WriteByte(digits[intLen+i])
174
				written++
175
			}
176
		} else {
177
			leadingZeros := -intLen
178
			for ; written < leadingZeros && written < places; written++ {
179
				sb.WriteByte('0')
180
			}
181
			for i := 0; i < len(digits) && written < places; i++ {
182
				sb.WriteByte(digits[i])
183
				written++
184
			}
185
		}
186
		for ; written < places; written++ {
187
			sb.WriteByte('0')
188
		}
189
	}
190
}
191
192
func (d Decimal) Abs() Decimal {
193
	if d.coeff == nil || d.coeff.Sign() == 0 {
194
		return Decimal{}
195
	}
196
	if d.coeff.Sign() > 0 {
197
		return Decimal{coeff: new(big.Int).Set(d.coeff), scale: d.scale}
198
	}
199
	return Decimal{coeff: new(big.Int).Neg(d.coeff), scale: d.scale}
200
}
201
202
func (d Decimal) Neg() Decimal {
203
	if d.coeff == nil || d.coeff.Sign() == 0 {
204
		return Decimal{}
205
	}
206
	return Decimal{coeff: new(big.Int).Neg(d.coeff), scale: d.scale}
207
}
208
209
func (d Decimal) Sub(other Decimal) Decimal { return d.Add(other.Neg()) }
210
func (d Decimal) Add(other Decimal) Decimal {
211
	a, b, scale := align(d, other)
212
	sum := new(big.Int).Add(a, b)
213
	return Decimal{coeff: sum, scale: scale}.normalized()
214
}
215
216
func (d Decimal) Mul(other Decimal) Decimal {
217
	if d.IsZero() || other.IsZero() {
218
		return Decimal{}
219
	}
220
	product := new(big.Int).Mul(d.coeffOrZero(), other.coeffOrZero())
221
	return Decimal{coeff: product, scale: d.scale + other.scale}.normalized()
222
}
223
224
func (d Decimal) Div(other Decimal) Decimal {
225
	if other.IsZero() {
226
		panic("decimal: division by zero")
227
	}
228
	if d.IsZero() {
229
		return Decimal{}
230
	}
231
232
	scale := max(d.scale, other.scale) + 10
233
234
	dCoeff := d.coeffOrZero()
235
	oCoeff := other.coeffOrZero()
236
237
	shift := scale + other.scale - d.scale
238
	if shift > 0 {
239
		dCoeff = new(big.Int).Mul(dCoeff, pow10(shift))
240
	} else if shift < 0 {
241
		oCoeff = new(big.Int).Mul(oCoeff, pow10(-shift))
242
	}
243
244
	quo := new(big.Int).Quo(dCoeff, oCoeff)
245
	return Decimal{coeff: quo, scale: scale}.normalized()
246
}
247
248
func (d Decimal) Cmp(other Decimal) int {
249
	a, b, _ := align(d, other)
250
	return a.Cmp(b)
251
}
252
253
func (d Decimal) Equal(other Decimal) bool {
254
	return d.Cmp(other) == 0
255
}
256
257
func (d Decimal) IsZero() bool {
258
	return d.coeff == nil || d.coeff.Sign() == 0
259
}
260
261
func align(a, b Decimal) (aCoeff *big.Int, bCoeff *big.Int, scale int) {
262
	scale = max(b.scale, a.scale)
263
264
	aCoeff = a.coeffOrZero()
265
	bCoeff = b.coeffOrZero()
266
	if delta := scale - a.scale; delta > 0 {
267
		aCoeff.Mul(aCoeff, pow10(delta))
268
	}
269
	if delta := scale - b.scale; delta > 0 {
270
		bCoeff.Mul(bCoeff, pow10(delta))
271
	}
272
	return aCoeff, bCoeff, scale
273
}
274
275
func (d Decimal) coeffOrZero() *big.Int {
276
	if d.coeff == nil {
277
		return new(big.Int)
278
	}
279
	return new(big.Int).Set(d.coeff)
280
}
281
282
func (d Decimal) normalized() Decimal {
283
	if d.coeff == nil || d.coeff.Sign() == 0 {
284
		return Decimal{}
285
	}
286
	if d.scale == 0 {
287
		return Decimal{coeff: new(big.Int).Set(d.coeff)}
288
	}
289
290
	sign := d.coeff.Sign()
291
	abs := new(big.Int).Abs(d.coeff)
292
	ten := big.NewInt(10)
293
	rem := new(big.Int)
294
	for d.scale > 0 {
295
		quotient, _ := new(big.Int).QuoRem(abs, ten, rem)
296
		if rem.Sign() != 0 {
297
			break
298
		}
299
		abs = quotient
300
		d.scale--
301
	}
302
303
	if sign < 0 {
304
		abs.Neg(abs)
305
	}
306
	return Decimal{coeff: abs, scale: d.scale}
307
}
308
309
func pow10(n int) *big.Int {
310
	if n <= 0 {
311
		return big.NewInt(1)
312
	}
313
	return new(big.Int).Exp(big.NewInt(10), big.NewInt(int64(n)), nil)
314
}