all repos

clerk @ 23d0a0ddf451ff6ac76c6dd2ffda2cd7d4fe34fc

missing tooling for ledger/hledger

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

Oleksandr Smirnov Oleksandr Smirnov
olexsmir@gmail.com
fix typos and grammar, 9 days ago
1
package decimal
2
3
import (
4
	"fmt"
5
	"math"
6
	"math/big"
7
	"strconv"
8
	"strings"
9
)
10
11
type Decimal struct {
12
	v     int64
13
	scale int
14
	big   *big.Int
15
}
16
17
func FromInt(v int64) Decimal {
18
	if v == 0 {
19
		return Decimal{}
20
	}
21
	return Decimal{v: v}
22
}
23
24
func FromString(s string) (Decimal, error) {
25
	original := s
26
	if s == "" {
27
		return Decimal{}, badDecimal(original)
28
	}
29
30
	neg := false
31
	if s[0] == '+' || s[0] == '-' {
32
		neg = s[0] == '-'
33
		s = s[1:]
34
	}
35
36
	exp := 0
37
	if e := strings.IndexAny(s, "eE"); e >= 0 {
38
		n, err := strconv.Atoi(s[e+1:])
39
		if err != nil {
40
			return Decimal{}, badDecimal(original)
41
		}
42
		// bounded so pow10 can't be asked to materialize an astronomically
43
		// large number (e.g. 1E1000000000000)
44
		const maxExp = 10000
45
		if n > maxExp || n < -maxExp {
46
			return Decimal{}, badDecimal(original)
47
		}
48
		exp = n
49
		s = s[:e]
50
	}
51
52
	intPart, fracPart := s, ""
53
	if before, after, ok := strings.Cut(s, "."); ok {
54
		if strings.IndexByte(after, '.') >= 0 {
55
			return Decimal{}, badDecimal(original)
56
		}
57
		intPart, fracPart = before, after
58
	}
59
60
	scale := len(fracPart) - exp
61
	if scale >= 0 {
62
		if v, ok := parseDigits(intPart + fracPart); ok {
63
			if neg {
64
				v = -v
65
			}
66
			return Decimal{v: v, scale: scale}.fastNormalized(), nil
67
		}
68
	}
69
70
	digits := intPart + fracPart
71
	if neg {
72
		digits = "-" + digits
73
	}
74
	coeff, ok := new(big.Int).SetString(digits, 10)
75
	if !ok {
76
		return Decimal{}, badDecimal(original)
77
	}
78
	if scale < 0 {
79
		coeff.Mul(coeff, pow10(-scale))
80
		scale = 0
81
	}
82
	return Decimal{big: coeff, scale: scale}.normalized(), nil
83
}
84
85
func badDecimal(s string) error { return fmt.Errorf("can't convert %s to decimal", s) }
86
87
func parseDigits(s string) (int64, bool) {
88
	var v int64
89
	for i := 0; i < len(s); i++ {
90
		c := s[i]
91
		if c < '0' || c > '9' {
92
			return 0, false
93
		}
94
		d := int64(c - '0')
95
		if v > (math.MaxInt64-d)/10 {
96
			return 0, false
97
		}
98
		v = v*10 + d
99
	}
100
	return v, len(s) > 0
101
}
102
103
func (d Decimal) fastNormalized() Decimal {
104
	v, scale := d.v, d.scale
105
	for v != 0 && scale > 0 && v%10 == 0 {
106
		v /= 10
107
		scale--
108
	}
109
	if v == 0 {
110
		return Decimal{}
111
	}
112
	return Decimal{v: v, scale: scale}
113
}
114
115
func (d Decimal) String() string {
116
	if d.big != nil {
117
		return bigString(d.big, d.scale)
118
	}
119
	if d.v == 0 {
120
		return "0"
121
	}
122
	digits := strconv.FormatInt(d.v, 10)
123
	sign := ""
124
	if digits[0] == '-' {
125
		sign, digits = "-", digits[1:]
126
	}
127
	if d.scale == 0 {
128
		return sign + digits
129
	}
130
	if len(digits) <= d.scale {
131
		digits = strings.Repeat("0", d.scale-len(digits)+1) + digits
132
	}
133
	split := len(digits) - d.scale
134
	return sign + digits[:split] + "." + digits[split:]
135
}
136
137
func bigString(coeff *big.Int, scale int) string {
138
	if coeff.Sign() == 0 {
139
		return "0"
140
	}
141
	abs := new(big.Int).Set(coeff)
142
	sign := ""
143
	if abs.Sign() < 0 {
144
		sign = "-"
145
		abs.Abs(abs)
146
	}
147
	digits := abs.String()
148
	if scale == 0 {
149
		return sign + digits
150
	}
151
	if len(digits) <= scale {
152
		digits = strings.Repeat("0", scale-len(digits)+1) + digits
153
	}
154
	split := len(digits) - scale
155
	return sign + digits[:split] + "." + digits[split:]
156
}
157
158
// StringFixed returns a string representation with exactly places digits
159
// after the decimal point. Pads with zeros or truncates as needed.
160
// decSep and thousandsSep control formatting; zero values mean no custom separator.
161
func (d Decimal) StringFixed(places int, decSep, thousandsSep byte) string {
162
	var sb strings.Builder
163
	sb.Grow(32)
164
	d.WriteFixed(&sb, places, decSep, thousandsSep)
165
	return sb.String()
166
}
167
168
// WriteFixed is [Decimal.StringFixed] writing into sb instead of returning a string.
169
func (d Decimal) WriteFixed(sb *strings.Builder, places int, decSep, thousandsSep byte) {
170
	if d.IsZero() {
171
		sb.WriteByte('0')
172
		if places > 0 {
173
			if decSep != 0 {
174
				sb.WriteByte(decSep)
175
			} else {
176
				sb.WriteByte('.')
177
			}
178
			for range places {
179
				sb.WriteByte('0')
180
			}
181
		}
182
		return
183
	}
184
185
	var digitBuf [128]byte
186
	var digits []byte
187
	if d.big != nil {
188
		digits = d.big.Append(digitBuf[:0], 10)
189
	} else {
190
		digits = strconv.AppendInt(digitBuf[:0], d.v, 10)
191
	}
192
193
	sign := false
194
	if len(digits) > 0 && digits[0] == '-' {
195
		sign = true
196
		digits = digits[1:]
197
	}
198
199
	intLen := len(digits) - d.scale
200
201
	if sign {
202
		sb.WriteByte('-')
203
	}
204
205
	// Integer part
206
	if intLen <= 0 {
207
		sb.WriteByte('0')
208
	} else {
209
		for i := range intLen {
210
			if thousandsSep != 0 && i > 0 && (intLen-i)%3 == 0 {
211
				sb.WriteByte(thousandsSep)
212
			}
213
			sb.WriteByte(digits[i])
214
		}
215
	}
216
217
	// Fractional part
218
	if places > 0 {
219
		if decSep != 0 {
220
			sb.WriteByte(decSep)
221
		} else {
222
			sb.WriteByte('.')
223
		}
224
225
		written := 0
226
		if intLen > 0 {
227
			n := min(d.scale, places)
228
			for i := range n {
229
				sb.WriteByte(digits[intLen+i])
230
				written++
231
			}
232
		} else {
233
			leadingZeros := -intLen
234
			for ; written < leadingZeros && written < places; written++ {
235
				sb.WriteByte('0')
236
			}
237
			for i := 0; i < len(digits) && written < places; i++ {
238
				sb.WriteByte(digits[i])
239
				written++
240
			}
241
		}
242
		for ; written < places; written++ {
243
			sb.WriteByte('0')
244
		}
245
	}
246
}
247
248
func (d Decimal) Abs() Decimal {
249
	if d.big != nil {
250
		if d.big.Sign() == 0 {
251
			return Decimal{}
252
		}
253
		if d.big.Sign() > 0 {
254
			return Decimal{big: new(big.Int).Set(d.big), scale: d.scale}
255
		}
256
		return Decimal{big: new(big.Int).Neg(d.big), scale: d.scale}
257
	}
258
	if d.v == 0 {
259
		return Decimal{}
260
	}
261
	if d.v < 0 {
262
		return Decimal{v: -d.v, scale: d.scale}
263
	}
264
	return Decimal{v: d.v, scale: d.scale}
265
}
266
267
func (d Decimal) Neg() Decimal {
268
	if d.big != nil {
269
		if d.big.Sign() == 0 {
270
			return Decimal{}
271
		}
272
		return Decimal{big: new(big.Int).Neg(d.big), scale: d.scale}
273
	}
274
	if d.v == 0 {
275
		return Decimal{}
276
	}
277
	return Decimal{v: -d.v, scale: d.scale}
278
}
279
280
func (d Decimal) Sub(other Decimal) Decimal { return d.Add(other.Neg()) }
281
func (d Decimal) Add(other Decimal) Decimal {
282
	if d.big == nil && other.big == nil {
283
		if r, ok := fastAdd(d, other); ok {
284
			return r
285
		}
286
	}
287
	a, b, scale := align(d, other)
288
	sum := new(big.Int).Add(a, b)
289
	return Decimal{big: sum, scale: scale}.normalized()
290
}
291
292
func fastAdd(a, b Decimal) (Decimal, bool) {
293
	scale := max(a.scale, b.scale)
294
	av, ok1 := scaleBy(a.v, scale-a.scale)
295
	bv, ok2 := scaleBy(b.v, scale-b.scale)
296
	if !ok1 || !ok2 {
297
		return Decimal{}, false
298
	}
299
	sum := av + bv
300
	if (av > 0 && bv > 0 && sum < 0) || (av < 0 && bv < 0 && sum >= 0) {
301
		return Decimal{}, false
302
	}
303
	return Decimal{v: sum, scale: scale}.fastNormalized(), true
304
}
305
306
func (d Decimal) Mul(other Decimal) Decimal {
307
	if d.IsZero() || other.IsZero() {
308
		return Decimal{}
309
	}
310
	product := new(big.Int).Mul(d.coeffBig(), other.coeffBig())
311
	return Decimal{big: product, scale: d.scale + other.scale}.normalized()
312
}
313
314
func (d Decimal) Div(other Decimal) Decimal {
315
	if other.IsZero() {
316
		panic("decimal: division by zero")
317
	}
318
	if d.IsZero() {
319
		return Decimal{}
320
	}
321
322
	scale := max(d.scale, other.scale) + 10
323
324
	dCoeff := d.coeffBig()
325
	oCoeff := other.coeffBig()
326
327
	shift := scale + other.scale - d.scale
328
	if shift > 0 {
329
		dCoeff = new(big.Int).Mul(dCoeff, pow10(shift))
330
	} else if shift < 0 {
331
		oCoeff = new(big.Int).Mul(oCoeff, pow10(-shift))
332
	}
333
334
	quo := new(big.Int).Quo(dCoeff, oCoeff)
335
	return Decimal{big: quo, scale: scale}.normalized()
336
}
337
338
func (d Decimal) Cmp(other Decimal) int {
339
	if d.big == nil && other.big == nil {
340
		if r, ok := fastCmp(d, other); ok {
341
			return r
342
		}
343
	}
344
	a, b, _ := align(d, other)
345
	return a.Cmp(b)
346
}
347
348
func fastCmp(a, b Decimal) (int, bool) {
349
	scale := max(a.scale, b.scale)
350
	av, ok1 := scaleBy(a.v, scale-a.scale)
351
	bv, ok2 := scaleBy(b.v, scale-b.scale)
352
	if !ok1 || !ok2 {
353
		return 0, false
354
	}
355
	switch {
356
	case av < bv:
357
		return -1, true
358
	case av > bv:
359
		return 1, true
360
	}
361
	return 0, true
362
}
363
364
func (d Decimal) Equal(other Decimal) bool {
365
	return d.Cmp(other) == 0
366
}
367
368
func (d Decimal) IsZero() bool {
369
	if d.big != nil {
370
		return d.big.Sign() == 0
371
	}
372
	return d.v == 0
373
}
374
375
func (d Decimal) coeffBig() *big.Int {
376
	if d.big != nil {
377
		return new(big.Int).Set(d.big)
378
	}
379
	return big.NewInt(d.v)
380
}
381
382
// normalized strips trailing zero digits from a big coefficient and demotes
383
// to the fast lane when the result fits in an int64.
384
func (d Decimal) normalized() Decimal {
385
	if d.big == nil || d.big.Sign() == 0 {
386
		return Decimal{}
387
	}
388
389
	sign := d.big.Sign()
390
	abs := new(big.Int).Abs(d.big)
391
	ten := big.NewInt(10)
392
	rem := new(big.Int)
393
	scale := d.scale
394
	for scale > 0 {
395
		quotient, _ := new(big.Int).QuoRem(abs, ten, rem)
396
		if rem.Sign() != 0 {
397
			break
398
		}
399
		abs = quotient
400
		scale--
401
	}
402
403
	if sign < 0 {
404
		abs.Neg(abs)
405
	}
406
	if abs.IsInt64() {
407
		return Decimal{v: abs.Int64(), scale: scale}
408
	}
409
	return Decimal{big: abs, scale: scale}
410
}
411
412
func align(a, b Decimal) (aCoeff *big.Int, bCoeff *big.Int, scale int) {
413
	scale = max(b.scale, a.scale)
414
415
	aCoeff = a.coeffBig()
416
	bCoeff = b.coeffBig()
417
	if delta := scale - a.scale; delta > 0 {
418
		aCoeff.Mul(aCoeff, pow10(delta))
419
	}
420
	if delta := scale - b.scale; delta > 0 {
421
		bCoeff.Mul(bCoeff, pow10(delta))
422
	}
423
	return aCoeff, bCoeff, scale
424
}
425
426
func pow10(n int) *big.Int {
427
	if n <= 0 {
428
		return big.NewInt(1)
429
	}
430
	return new(big.Int).Exp(big.NewInt(10), big.NewInt(int64(n)), nil)
431
}
432
433
// pow10i64 holds 10^n for n up to 18 (10^18 fits in an int64).
434
var pow10i64 = [...]int64{
435
	1, 10, 100, 1_000, 10_000, 100_000, 1_000_000, 10_000_000, 100_000_000,
436
	1_000_000_000, 10_000_000_000, 100_000_000_000, 1_000_000_000_000,
437
	10_000_000_000_000, 100_000_000_000_000, 1_000_000_000_000_000,
438
	10_000_000_000_000_000, 100_000_000_000_000_000, 1_000_000_000_000_000_000,
439
}
440
441
// scaleBy returns v·10^n; ok is false when the product overflows int64.
442
func scaleBy(v int64, n int) (int64, bool) {
443
	if n <= 0 {
444
		return v, true
445
	}
446
	if n >= len(pow10i64) {
447
		return 0, false
448
	}
449
	p := pow10i64[n]
450
	r := v * p
451
	return r, v == 0 || r/v == p
452
}