all repos

clerk @ 0c4384e

missing tooling for ledger/hledger
7 files changed, 283 insertions(+), 12 deletions(-)
remove decimal dependency
Author: Oleksandr Smirnov olexsmir@gmail.com
Committed at: 2026-05-23 17:52:23 +0300
Authored at: 2026-05-15 20:26:45 +0300
Change ID: kxoxmsqzqutkoypnyvomnuutxlqmrlpr
Parent: 7136c82
M go.mod
···
        4
        4
         

      
        5
        5
         tool golang.org/x/tools/cmd/stringer

      
        6
        6
         

      
        7
        
        -require github.com/shopspring/decimal v1.4.0

      
        8
        
        -

      
        9
        7
         require (

      
        10
        8
         	golang.org/x/mod v0.36.0 // indirect

      
        11
        9
         	golang.org/x/sync v0.20.0 // indirect

      
M go.sum
···
        1
        1
         github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=

      
        2
        2
         github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=

      
        3
        
        -github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k=

      
        4
        
        -github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME=

      
        5
        3
         golang.org/x/mod v0.36.0 h1:JJjpVx6myfUsUdAzZuOSTTmRE0PfZeNWzzvKrP7amb4=

      
        6
        4
         golang.org/x/mod v0.36.0/go.mod h1:moc6ELqsWcOw5Ef3xVprK5ul/MvtVvkIXLziUOICjUQ=

      
        7
        5
         golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=

      
A internal/decimal/decimal.go
···
        
        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
        +}

      
A internal/decimal/decimal_test.go
···
        
        1
        +package decimal

      
        
        2
        +

      
        
        3
        +import "testing"

      
        
        4
        +

      
        
        5
        +func TestNewFromString(t *testing.T) {

      
        
        6
        +	tests := []struct{ in, want string }{

      
        
        7
        +		{"10.00", "10"},

      
        
        8
        +		{"150.60", "150.6"},

      
        
        9
        +		{".33", "0.33"},

      
        
        10
        +		{"1.", "1"},

      
        
        11
        +		{"0001.2300", "1.23"},

      
        
        12
        +		{"0", "0"},

      
        
        13
        +		{"0.00", "0"},

      
        
        14
        +		{"-0.00", "0"},

      
        
        15
        +		{"-20", "-20"},

      
        
        16
        +		{"-.75", "-0.75"},

      
        
        17
        +	}

      
        
        18
        +	for _, tt := range tests {

      
        
        19
        +		got, err := FromString(tt.in)

      
        
        20
        +		if err != nil {

      
        
        21
        +			t.Fatalf("NewFromString(%q) unexpected error: %v", tt.in, err)

      
        
        22
        +		}

      
        
        23
        +		if got.String() != tt.want {

      
        
        24
        +			t.Fatalf("NewFromString(%q).String() = %q, want %q", tt.in, got.String(), tt.want)

      
        
        25
        +		}

      
        
        26
        +	}

      
        
        27
        +}

      
        
        28
        +

      
        
        29
        +func TestNewFromStringInvalid(t *testing.T) {

      
        
        30
        +	tests := []string{

      
        
        31
        +		"",

      
        
        32
        +		".",

      
        
        33
        +		"+",

      
        
        34
        +		"-",

      
        
        35
        +		"1_000.00",

      
        
        36
        +		"1,000.00",

      
        
        37
        +		"1..0",

      
        
        38
        +		"a1",

      
        
        39
        +		"1a",

      
        
        40
        +	}

      
        
        41
        +	for _, in := range tests {

      
        
        42
        +		if _, err := FromString(in); err == nil {

      
        
        43
        +			t.Fatalf("NewFromString(%q) expected error", in)

      
        
        44
        +		}

      
        
        45
        +	}

      
        
        46
        +}

      
        
        47
        +

      
        
        48
        +func TestArithmetic(t *testing.T) {

      
        
        49
        +	a, _ := FromString("1.20")

      
        
        50
        +	b, _ := FromString(".30")

      
        
        51
        +

      
        
        52
        +	if got := a.Add(b).String(); got != "1.5" {

      
        
        53
        +		t.Fatalf("a+b = %q, want %q", got, "1.5")

      
        
        54
        +	}

      
        
        55
        +	if got := a.Sub(b).String(); got != "0.9" {

      
        
        56
        +		t.Fatalf("a-b = %q, want %q", got, "0.9")

      
        
        57
        +	}

      
        
        58
        +	if got := a.Mul(b).String(); got != "0.36" {

      
        
        59
        +		t.Fatalf("a*b = %q, want %q", got, "0.36")

      
        
        60
        +	}

      
        
        61
        +}

      
        
        62
        +

      
        
        63
        +func TestCmpAndNeg(t *testing.T) {

      
        
        64
        +	a, _ := FromString("1.0")

      
        
        65
        +	b, _ := FromString("1")

      
        
        66
        +	c, _ := FromString("1.01")

      
        
        67
        +

      
        
        68
        +	if a.Cmp(b) != 0 {

      
        
        69
        +		t.Fatalf("expected %q and %q to be equal", a.String(), b.String())

      
        
        70
        +	}

      
        
        71
        +	if b.Cmp(c) >= 0 {

      
        
        72
        +		t.Fatalf("expected %q to be less than %q", b.String(), c.String())

      
        
        73
        +	}

      
        
        74
        +	if got := b.Neg().String(); got != "-1" {

      
        
        75
        +		t.Fatalf("Neg(1) = %q, want %q", got, "-1")

      
        
        76
        +	}

      
        
        77
        +	if got := (Decimal{}).Neg().String(); got != "0" {

      
        
        78
        +		t.Fatalf("Neg(0) = %q, want %q", got, "0")

      
        
        79
        +	}

      
        
        80
        +}

      
M journal/ast/dump.go
···
        307
        307
         	fmt.Fprintf(b, "CommodityDirective %s\n", c.Span)

      
        308
        308
         	indent(b, depth+1)

      
        309
        309
         	fmt.Fprintf(b, "Commodity: %q\n", c.Commodity)

      
        310
        
        -	dumpAmount(b, &c.Format, depth+1)

      
        
        310
        +	if c.Format.QuantityFmt.Decimal != 0 {

      
        
        311
        +		dumpAmount(b, &c.Format, depth+1)

      
        
        312
        +	}

      
        311
        313
         	dumpOptComment(b, c.Comment, depth+1)

      
        312
        314
         }

      
        313
        315
         

      
M journal/ast/entries.go
···
        1
        1
         package ast

      
        2
        2
         

      
        3
        3
         import (

      
        4
        
        -	"github.com/shopspring/decimal"

      
        5
        
        -

      
        
        4
        +	"olexsmir.xyz/clerk/internal/decimal"

      
        6
        5
         	"olexsmir.xyz/clerk/journal/token"

      
        7
        6
         )

      
        8
        7
         

      
M journal/parser/parser.go
···
        5
        5
         	"strconv"

      
        6
        6
         	"strings"

      
        7
        7
         

      
        8
        
        -	"github.com/shopspring/decimal"

      
        9
        
        -

      
        
        8
        +	"olexsmir.xyz/clerk/internal/decimal"

      
        10
        9
         	"olexsmir.xyz/clerk/journal/ast"

      
        11
        10
         	"olexsmir.xyz/clerk/journal/lexer"

      
        12
        11
         	"olexsmir.xyz/clerk/journal/token"

      ···
        364
        363
         	comment := p.parseOptInlineComment()

      
        365
        364
         	p.expectNewline()

      
        366
        365
         

      
        367
        
        -	return &ast.CommodityDirective{

      
        
        366
        +	cd := &ast.CommodityDirective{

      
        368
        367
         		Commodity: commodity,

      
        369
        
        -		Format:    *format,

      
        370
        368
         		Comment:   comment,

      
        371
        369
         		Span:      p.span(s),

      
        372
        370
         	}

      
        
        371
        +	if format != nil {

      
        
        372
        +		cd.Format = *format

      
        
        373
        +	}

      
        
        374
        +	return cd

      
        373
        375
         }

      
        374
        376
         

      
        375
        377
         func (p *Parser) parseIncludeDirective() *ast.IncludeDirective {

      ···
        1083
        1085
         	// remove thousands separators, replace decimal mark with '.'

      
        1084
        1086
         	normalized := normalizeLiteral(lit, amt.QuantityFmt.Thousands, amt.QuantityFmt.Decimal)

      
        1085
        1087
         

      
        1086
        
        -	q, err := decimal.NewFromString(normalized)

      
        
        1088
        +	q, err := decimal.FromString(normalized)

      
        1087
        1089
         	if err != nil {

      
        1088
        1090
         		p.errorf("invalid quantity %q: %v", lit, err)

      
        1089
        1091
         		return