all repos

clerk @ 73861df16eec040593c420cff6248a994e80f30f

missing tooling for ledger/hledger
5 files changed, 46 insertions(+), 23 deletions(-)
journal: split "EUR123" into commodity+quantity, fix quoted commodities in postings
Author: Oleksandr Smirnov olexsmir@gmail.com
Committed at: 2026-06-22 18:00:13 +0300
Authored at: 2026-06-16 23:05:03 +0300
Change ID: uozptzskqwsvzqwnvymwqyxrknrmlvwp
Parent: ecc31e9
M journal/lexer/lexer.go
···
        576
        576
         	}

      
        577
        577
         

      
        578
        578
         	if unicode.IsLetter(l.ch) {

      
        579
        
        -		for unicode.IsLetter(l.ch) || unicode.IsDigit(l.ch) {

      
        
        579
        +		for unicode.IsLetter(l.ch) {

      
        580
        580
         			l.advance()

      
        581
        581
         		}

      
        582
        582
         		return token.Token{Type: token.COMMODITYMARK, Literal: string(l.input[s.offset:l.pos]), Span: l.span(s)}

      
M journal/parser/parser.go
···
        378
        378
         	var format *ast.Amount

      
        379
        379
         

      
        380
        380
         	switch p.cur.Type {

      
        381
        
        -	case token.TEXT, token.INT, token.DECIMAL:

      
        382
        
        -		format = p.parseAmount()

      
        383
        
        -		commodity = format.Commodity

      
        384
        
        -	case token.COMMODITYMARK:

      
        
        381
        +	case token.COMMODITYMARK, token.TEXT, token.STRING:

      
        385
        382
         		commodity = p.cur.Literal

      
        
        383
        +		if p.got(token.STRING) {

      
        
        384
        +			commodity = unquote(commodity)

      
        
        385
        +		}

      
        386
        386
         		p.advance()

      
        387
        387
         		hadSpace := p.got(token.WHITESPACE)

      
        388
        388
         		p.skipWhitespace()

      ···
        392
        392
         			format.CommodityPos = ast.CommodityBefore

      
        393
        393
         			format.HasSpace = hadSpace

      
        394
        394
         		}

      
        
        395
        +	case token.INT, token.DECIMAL:

      
        
        396
        +		format = p.parseAmount()

      
        
        397
        +		commodity = format.Commodity

      
        395
        398
         	default:

      
        396
        399
         		p.errorf("expected commodity name or amount, got %s", p.cur.Type)

      
        397
        400
         	}

      ···
        770
        773
         	switch p.cur.Type {

      
        771
        774
         	default:

      
        772
        775
         		return false

      
        773
        
        -	case token.COMMODITYMARK, token.INT, token.DECIMAL, token.MINUS, token.PLUS, token.PARENEXPR:

      
        
        776
        +	case token.COMMODITYMARK, token.STRING, token.INT, token.DECIMAL, token.MINUS, token.PLUS, token.PARENEXPR:

      
        774
        777
         		return true

      
        775
        778
         	case token.TEXT:

      
        776
        779
         		return len(p.cur.Literal) > 0 && p.cur.Literal[0] >= '0' && p.cur.Literal[0] <= '9'

      ···
        784
        787
         		Span:        p.span(s),

      
        785
        788
         	}

      
        786
        789
         

      
        787
        
        -	// commodity before quantity: $10.00

      
        788
        
        -	if p.got(token.COMMODITYMARK) {

      
        789
        
        -		amt.Commodity = p.cur.Literal

      
        
        790
        +	// commodity before quantity: $10.00, eur 10.00

      
        
        791
        +	if p.got(token.COMMODITYMARK) || p.got(token.TEXT) || p.got(token.STRING) {

      
        
        792
        +		amt.Commodity = unquote(p.cur.Literal)

      
        790
        793
         		amt.CommodityPos = ast.CommodityBefore

      
        791
        794
         		p.advance()

      
        792
        795
         		if p.got(token.WHITESPACE) {

      ···
        811
        814
         			p.advance()

      
        812
        815
         		}

      
        813
        816
         

      
        814
        
        -		// commodity before quantity: -$120:

      
        815
        
        -		if p.got(token.COMMODITYMARK) {

      
        816
        
        -			amt.Commodity = p.cur.Literal

      
        
        817
        +		// commodity before quantity: -$120, -eur 120:

      
        
        818
        +		if p.got(token.COMMODITYMARK) || p.got(token.TEXT) || p.got(token.STRING) {

      
        
        819
        +			amt.Commodity = unquote(p.cur.Literal)

      
        817
        820
         			amt.CommodityPos = ast.CommodityBefore

      
        818
        821
         			p.advance()

      
        819
        822
         			if p.got(token.WHITESPACE) {

      ···
        824
        827
         

      
        825
        828
         		p.parseQuantityInto(amt)

      
        826
        829
         

      
        827
        
        -		// commodity after quantity: 10.00 UAH (only if not set)

      
        
        830
        +		// commodity after quantity: 10.00 UAH, 10.00 "EUR" (only if not set)

      
        828
        831
         		if amt.Commodity == "" {

      
        829
        832
         			switch p.cur.Type {

      
        830
        833
         			case token.WHITESPACE:

      
        831
        834
         				p.skipWhitespace()

      
        832
        
        -				if p.got(token.COMMODITYMARK) || p.got(token.TEXT) {

      
        
        835
        +				if p.got(token.COMMODITYMARK) || p.got(token.TEXT) || p.got(token.STRING) {

      
        833
        836
         					amt.HasSpace = true

      
        834
        
        -					amt.Commodity = p.cur.Literal

      
        
        837
        +					amt.Commodity = unquote(p.cur.Literal)

      
        835
        838
         					amt.CommodityPos = ast.CommodityAfter

      
        836
        839
         					p.advance()

      
        837
        840
         				}

      
        838
        
        -			case token.COMMODITYMARK, token.TEXT:

      
        839
        
        -				amt.Commodity = p.cur.Literal

      
        
        841
        +			case token.COMMODITYMARK, token.TEXT, token.STRING:

      
        
        842
        +				amt.Commodity = unquote(p.cur.Literal)

      
        840
        843
         				amt.CommodityPos = ast.CommodityAfter

      
        841
        844
         				p.advance()

      
        842
        845
         			}

      ···
        930
        933
         	// optional amount - after two spaces

      
        931
        934
         	if p.got(token.WHITESPACE) {

      
        932
        935
         		p.skipWhitespace()

      
        933
        
        -		if p.got(token.STRING) { // e.g "Plans: Wildthorn Mail" 1 @ 125s

      
        934
        
        -			p.advance()

      
        935
        
        -			p.skipWhitespace()

      
        936
        
        -		}

      
        937
        936
         		if p.isAmountStart() || p.got(token.STAR) {

      
        938
        937
         			posting.Amount = p.parseAmountWithOptExpr()

      
        939
        938
         		}

      
M journal/parser/parser_test.go
···
        111
        111
             assets:cash  150.60 HRK

      
        112
        112
         `},

      
        113
        113
         		{"transaction with cost and assertion", `2026-05-11 testies

      
        114
        
        -	expenses:atm  20.00 UAH @ 1 USD = 0 UAH

      
        
        114
        +	expenses:atm  UAH 20.00 @ 1 USD = 0 UAH

      
        115
        115
         	assets:bank

      
        116
        116
         `},

      
        117
        117
         		{"transaction with posting", `2024-01-01 groceries

      ···
        123
        123
             expenses:food  £5.00

      
        124
        124
             expenses:food  ₹700.00

      
        125
        125
             assets:checking

      
        
        126
        +`},

      
        
        127
        +		{"transaction with quoted commodity", `2025-06-16 groceries

      
        
        128
        +    expenses:food  10.00 "EUR"

      
        
        129
        +    assets:cash

      
        126
        130
         `},

      
        127
        131
         		{"transaction with strange commodity symbols", `2024-01-01 groceries

      
        128
        132
         2026-05-20

      
M journal/parser/testdata/transaction_with_cost_and_assertion.golden
···
        9
        9
               Amount j:2:16-2:16

      
        10
        10
                 Quantity: 20

      
        11
        11
                 Commodity: "UAH"

      
        12
        
        -        CommodityPos: After

      
        
        12
        +        CommodityPos: Before

      
        13
        13
                 HasSpace: true

      
        14
        14
                 Precision: 2

      
        15
        15
                 Decimal: "."

      
A journal/parser/testdata/transaction_with_quoted_commodity.golden
···
        
        1
        +Journal

      
        
        2
        +  Transaction j:1:1-4:1

      
        
        3
        +    Date: 2025-06-16

      
        
        4
        +    Payee: "groceries" j:1:12-2:0

      
        
        5
        +    Posting j:2:1-3:1

      
        
        6
        +      Account j:2:5-2:18

      
        
        7
        +        SubAccount: "expenses" j:2:5-2:13

      
        
        8
        +        SubAccount: "food" j:2:14-2:18

      
        
        9
        +      Amount j:2:20-2:20

      
        
        10
        +        Quantity: 10

      
        
        11
        +        Commodity: "EUR"

      
        
        12
        +        CommodityPos: After

      
        
        13
        +        HasSpace: true

      
        
        14
        +        Precision: 2

      
        
        15
        +        Decimal: "."

      
        
        16
        +    Posting j:3:1-4:1

      
        
        17
        +      Account j:3:5-4:0

      
        
        18
        +        SubAccount: "assets" j:3:5-3:11

      
        
        19
        +        SubAccount: "cash" j:3:12-4:0

      
        
        20
        +      Amount: <elided>