all repos

clerk @ f58ca0d53763fda8f02b50cee150a3d30918fd1d

missing tooling for ledger/hledger
1 files changed, 143 insertions(+), 246 deletions(-)
parser: remove reduntant code
Author: Oleksandr Smirnov olexsmir@gmail.com
Committed at: 2026-08-04 14:36:36 +0300
Authored at: 2026-08-04 13:26:56 +0300
Change ID: zoyqtnzpwslwspvwkvprzsmolkwsyqss
Parent: d216565
M journal/parser/parser.go
···
        45
        45
         	return f

      
        46
        46
         }

      
        47
        47
         

      
        48
        
        -func isDirectiveKeyword(t token.Type) bool {

      
        49
        
        -	switch t {

      
        50
        
        -	case token.COMMENTKW, token.ACCOUNT, token.COMMODITY, token.INCLUDE,

      
        51
        
        -		token.ALIAS, token.PAYEE, token.TAG, token.APPLY, token.END,

      
        52
        
        -		token.YEAR, token.DECIMALMARK, token.D, token.P, token.N, token.C:

      
        53
        
        -		return true

      
        54
        
        -	}

      
        55
        
        -	return false

      
        56
        
        -}

      
        57
        
        -

      
        58
        48
         func (p *Parser) parseEntry() ast.Entry {

      
        59
        49
         	if p.got(token.BANG) || p.got(token.AT) {

      
        60
        50
         		if isDirectiveKeyword(p.peek.Type) {

      
        61
        51
         			p.advance() // consume prefix

      
        62
        52
         		}

      
        63
        53
         	}

      
        
        54
        +

      
        64
        55
         	switch p.cur.Type {

      
        65
        56
         	case token.ILLEGAL:

      
        66
        57
         		p.errorf("illegal character %q", p.cur.Literal)

      ···
        138
        129
         	// optional status

      
        139
        130
         	tx.Status = p.parseStatus()

      
        140
        131
         

      
        141
        
        -	// optional code

      
        142
        
        -	if p.got(token.LPAREN) {

      
        143
        
        -		cs := p.cur.Span

      
        144
        
        -		p.advance()

      
        145
        
        -		var code strings.Builder

      
        146
        
        -		for p.cur.Type != token.RPAREN {

      
        147
        
        -			_, _ = code.WriteString(p.cur.Literal)

      
        148
        
        -			p.advance()

      
        149
        
        -		}

      
        150
        
        -		rp := p.cur.Span

      
        151
        
        -		p.advance()

      
        152
        
        -		tx.Code = &ast.Code{

      
        153
        
        -			Value: code.String(),

      
        154
        
        -			Span:  token.Span{Start: cs.Start, End: rp.End},

      
        155
        
        -		}

      
        156
        
        -		p.skipWhitespace()

      
        157
        
        -	} else if p.got(token.TEXT) {

      
        158
        
        -		// the lexer emits "(CODE)" as a single TEXT token; split it here

      
        
        132
        +	// optional code - the lexer emits "(CODE)" as a single TEXT token; split it here

      
        
        133
        +	if p.got(token.TEXT) {

      
        159
        134
         		if lit := p.cur.Literal; len(lit) >= 2 && lit[0] == '(' && lit[len(lit)-1] == ')' {

      
        160
        135
         			tx.Code = &ast.Code{Value: lit[1 : len(lit)-1], Span: p.cur.Span}

      
        161
        136
         			p.advance()

      ···
        168
        143
         		tx.Payee = p.parsePayee()

      
        169
        144
         

      
        170
        145
         		// check for | separator

      
        171
        
        -		if p.got(token.WHITESPACE) {

      
        172
        
        -			p.skipWhitespace()

      
        173
        
        -		}

      
        
        146
        +		p.skipWhitespace()

      
        174
        147
         

      
        175
        148
         		if p.got(token.PIPE) {

      
        176
        149
         			p.advance()

      ···
        186
        159
         	tx.Comment = p.parseOptInlineComment()

      
        187
        160
         	p.expectNewline()

      
        188
        161
         

      
        189
        
        -	// header comments — indented ; lines before first posting

      
        190
        
        -	for p.got(token.INDENT) && p.willGet(token.SEMICOLON) {

      
        191
        
        -		p.advance() // consume indent

      
        192
        
        -		c := p.parseComment()

      
        193
        
        -		tx.HeaderComments = append(tx.HeaderComments, c)

      
        194
        
        -	}

      
        195
        
        -

      
        196
        
        -	// postings

      
        197
        
        -	for p.got(token.INDENT) {

      
        198
        
        -		if p := p.parsePosting(); p != nil {

      
        199
        
        -			tx.Postings = append(tx.Postings, p)

      
        200
        
        -		}

      
        201
        
        -	}

      
        
        162
        +	tx.HeaderComments, tx.Postings = p.parseHeaderCommentsAndPostings()

      
        202
        163
         

      
        203
        164
         	tx.Span = p.span(s)

      
        204
        165
         	return tx

      ···
        222
        183
         

      
        223
        184
         	// keep spaces/tags between text tokens; stop before trailing whitespace

      
        224
        185
         	var name strings.Builder

      
        225
        
        -	for p.got(token.TEXT) || p.got(token.INT) || p.got(token.DECIMAL) || p.got(token.COMMODITYMARK) || (p.got(token.WHITESPACE) && (p.willGet(token.TEXT) || p.willGet(token.INT) || p.willGet(token.DECIMAL) || p.willGet(token.COMMODITYMARK))) {

      
        
        186
        +	for payeeWord(p.cur.Type) || (payeeWord(p.peek.Type) && p.got(token.WHITESPACE)) {

      
        226
        187
         		_, _ = name.WriteString(p.cur.Literal)

      
        227
        188
         		p.advance()

      
        228
        189
         	}

      
        229
        190
         	return &ast.Payee{Name: unquote(name.String()), Span: p.span(s)}

      
        
        191
        +}

      
        
        192
        +

      
        
        193
        +func payeeWord(t token.Type) bool {

      
        
        194
        +	switch t {

      
        
        195
        +	case token.TEXT, token.INT, token.DECIMAL, token.COMMODITYMARK:

      
        
        196
        +		return true

      
        
        197
        +	}

      
        
        198
        +	return false

      
        230
        199
         }

      
        231
        200
         

      
        232
        201
         func (p *Parser) parsePeriodicTransaction() *ast.PeriodicTransaction {

      ···
        245
        214
         	comment := p.parseOptInlineComment()

      
        246
        215
         	p.expectNewline()

      
        247
        216
         

      
        248
        
        -	// header comment

      
        249
        
        -	for p.got(token.INDENT) && p.willGet(token.SEMICOLON) {

      
        250
        
        -		p.advance()

      
        251
        
        -		pt.HeaderComments = append(pt.HeaderComments, p.parseComment())

      
        252
        
        -	}

      
        253
        
        -

      
        254
        
        -	// postings

      
        255
        
        -	for p.got(token.INDENT) {

      
        256
        
        -		if posting := p.parsePosting(); posting != nil {

      
        257
        
        -			pt.Postings = append(pt.Postings, posting)

      
        258
        
        -		}

      
        259
        
        -	}

      
        
        217
        +	pt.HeaderComments, pt.Postings = p.parseHeaderCommentsAndPostings()

      
        260
        218
         

      
        261
        219
         	pt.Span = p.span(s)

      
        262
        220
         	pt.Comment = comment

      ···
        277
        235
         	at.Comment = p.parseOptInlineComment()

      
        278
        236
         	p.expectNewline()

      
        279
        237
         

      
        280
        
        -	// header comments

      
        
        238
        +	at.HeaderComments, at.Postings = p.parseHeaderCommentsAndPostings()

      
        
        239
        +

      
        
        240
        +	at.Span = p.span(s)

      
        
        241
        +	return at

      
        
        242
        +}

      
        
        243
        +

      
        
        244
        +func (p *Parser) parseHeaderCommentsAndPostings() (comments []*ast.Comment, postings []*ast.Posting) {

      
        281
        245
         	for p.got(token.INDENT) && p.willGet(token.SEMICOLON) {

      
        282
        
        -		p.advance()

      
        283
        
        -		at.HeaderComments = append(at.HeaderComments, p.parseComment())

      
        
        246
        +		p.advance() // consume indent

      
        
        247
        +		comments = append(comments, p.parseComment())

      
        284
        248
         	}

      
        285
        249
         

      
        286
        
        -	// postings

      
        287
        250
         	for p.got(token.INDENT) {

      
        288
        
        -		if p := p.parsePosting(); p != nil {

      
        289
        
        -			at.Postings = append(at.Postings, p)

      
        
        251
        +		if posting := p.parsePosting(); posting != nil {

      
        
        252
        +			postings = append(postings, posting)

      
        290
        253
         		}

      
        291
        254
         	}

      
        292
        255
         

      
        293
        
        -	at.Span = p.span(s)

      
        294
        
        -	return at

      
        
        256
        +	return comments, postings

      
        295
        257
         }

      
        296
        258
         

      
        297
        259
         func (p *Parser) parsePeriod() ast.Period {

      ···
        363
        325
         

      
        364
        326
         func (p *Parser) parseComment() *ast.Comment {

      
        365
        327
         	s := p.cur.Span

      
        366
        
        -	marker := p.cur.Literal[0]

      
        367
        
        -	p.advance()

      
        368
        
        -	p.skipWhitespace()

      
        369
        
        -

      
        370
        
        -	var text string

      
        371
        
        -	if p.got(token.TEXT) {

      
        372
        
        -		text = p.cur.Literal

      
        373
        
        -		p.advance()

      
        374
        
        -	}

      
        375
        
        -

      
        
        328
        +	c := p.parseCommentRest(s)

      
        376
        329
         	p.expectNewline()

      
        377
        
        -

      
        378
        
        -	return &ast.Comment{

      
        379
        
        -		Marker: marker,

      
        380
        
        -		Text:   text,

      
        381
        
        -		Span:   p.span(s),

      
        382
        
        -	}

      
        
        330
        +	c.Span = p.span(s) // comment spans its line through the newline

      
        
        331
        +	return c

      
        383
        332
         }

      
        384
        333
         

      
        385
        334
         func (p *Parser) parseAccountDirective() *ast.AccountDirective {

      ···
        418
        367
         	switch p.cur.Type {

      
        419
        368
         	case token.COMMODITYMARK, token.TEXT, token.STRING:

      
        420
        369
         		cs := p.cur.Span

      
        421
        
        -		commodity = p.cur.Literal

      
        422
        
        -		if p.got(token.STRING) {

      
        423
        
        -			commodity = unquote(commodity)

      
        424
        
        -		}

      
        
        370
        +		commodity = unquote(p.cur.Literal)

      
        425
        371
         		p.advance()

      
        426
        372
         		commoditySpan = token.Span{Start: cs.Start, End: p.cur.Span.Start}

      
        427
        373
         		hadSpace := p.got(token.WHITESPACE)

      ···
        490
        436
         		p.errorf("expected file path, got %s", p.cur.Type)

      
        491
        437
         	}

      
        492
        438
         

      
        493
        
        -	p.skipWhitespace()

      
        494
        439
         	id.Comment = p.parseOptInlineComment()

      
        495
        440
         	p.expectNewline()

      
        496
        441
         	id.Span = p.span(s)

      ···
        507
        452
         	p.expect(token.EQ)

      
        508
        453
         	p.skipWhitespace()

      
        509
        454
         	alias.To = p.parseAccount()

      
        510
        
        -	p.skipWhitespace()

      
        511
        455
         	alias.Comment = p.parseOptInlineComment()

      
        512
        456
         	p.expectNewline()

      
        513
        457
         	alias.Span = p.span(s)

      ···
        540
        484
         	p.skipWhitespace()

      
        541
        485
         

      
        542
        486
         	name := ""

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

      
        544
        
        -		name = p.cur.Literal

      
        545
        
        -		p.advance()

      
        546
        
        -	} else if p.got(token.STRING) {

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

      
        547
        488
         		name = unquote(p.cur.Literal)

      
        548
        489
         		p.advance()

      
        549
        490
         	}

      ···
        572
        513
         		p.errorf("expected year, got %s", p.cur.Type)

      
        573
        514
         	}

      
        574
        515
         

      
        575
        
        -	p.skipWhitespace()

      
        576
        516
         	year.Comment = p.parseOptInlineComment()

      
        577
        517
         	p.expectNewline()

      
        578
        518
         	year.Span = p.span(s)

      ···
        594
        534
         		p.advance()

      
        595
        535
         	}

      
        596
        536
         

      
        597
        
        -	p.skipWhitespace()

      
        598
        537
         	mark.Comment = p.parseOptInlineComment()

      
        599
        538
         	p.expectNewline()

      
        600
        539
         	mark.Span = p.span(s)

      ···
        607
        546
         	p.expect(token.D)

      
        608
        547
         	p.skipWhitespace()

      
        609
        548
         	com.Amount = *p.parseAmount()

      
        610
        
        -	p.skipWhitespace()

      
        611
        549
         	com.Comment = p.parseOptInlineComment()

      
        612
        550
         	p.expectNewline()

      
        613
        551
         	com.Span = p.span(s)

      ···
        637
        575
         		}

      
        638
        576
         	}

      
        639
        577
         

      
        640
        
        -	p.skipWhitespace()

      
        641
        578
         	cd.Comment = p.parseOptInlineComment()

      
        642
        579
         	p.expectNewline()

      
        643
        580
         	cd.Span = p.span(s)

      ···
        654
        591
         		id.Text = p.cur.Literal

      
        655
        592
         		p.advance()

      
        656
        593
         	}

      
        657
        
        -	p.skipWhitespace()

      
        658
        594
         	id.Comment = p.parseOptInlineComment()

      
        659
        595
         

      
        660
        596
         	p.expectNewline()

      ···
        682
        618
         

      
        683
        619
         	mp.Amount = *p.parseAmount()

      
        684
        620
         

      
        685
        
        -	p.skipWhitespace()

      
        686
        621
         	mp.Comment = p.parseOptInlineComment()

      
        687
        622
         

      
        688
        623
         	p.expectNewline()

      ···
        809
        744
         	st := ast.Status{}

      
        810
        745
         	switch p.cur.Type {

      
        811
        746
         	case token.STAR:

      
        812
        
        -		p.advance()

      
        813
        
        -		p.skipWhitespace()

      
        814
        747
         		st.Value = ast.StatusCleared

      
        815
        748
         	case token.BANG:

      
        
        749
        +		st.Value = ast.StatusPending

      
        
        750
        +	}

      
        
        751
        +	if st.Value != ast.StatusNone {

      
        816
        752
         		p.advance()

      
        817
        753
         		p.skipWhitespace()

      
        818
        
        -		st.Value = ast.StatusPending

      
        819
        
        -	default:

      
        820
        
        -		st.Value = ast.StatusNone

      
        821
        754
         	}

      
        822
        755
         	st.Span = p.span(s)

      
        823
        756
         	return st

      ···
        827
        760
         	switch p.cur.Type {

      
        828
        761
         	default:

      
        829
        762
         		return false

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

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

      
        831
        764
         		return true

      
        832
        765
         	}

      
        833
        766
         }

      ···
        844
        777
         		amt.Span = p.span(s)

      
        845
        778
         	}()

      
        846
        779
         

      
        
        780
        +	p.parseAmountSign(amt)

      
        
        781
        +	p.skipWhitespace()

      
        
        782
        +

      
        847
        783
         	// commodity before quantity: $10.00, eur 10.00

      
        848
        784
         	if p.got(token.COMMODITYMARK) || p.got(token.TEXT) || p.got(token.STRING) {

      
        849
        785
         		cs := p.cur.Span

      ···
        855
        791
         			amt.HasSpace = true

      
        856
        792
         			p.skipWhitespace()

      
        857
        793
         		}

      
        858
        
        -		switch p.cur.Type {

      
        859
        
        -		case token.MINUS:

      
        860
        
        -			amt.IsNegative = true

      
        861
        
        -			p.advance()

      
        862
        
        -		case token.PLUS:

      
        863
        
        -			p.advance()

      
        864
        
        -		}

      
        865
        
        -		p.skipWhitespace()

      
        866
        
        -		p.parseQuantityInto(amt)

      
        867
        
        -	} else {

      
        868
        
        -		// optional sign

      
        869
        
        -		switch p.cur.Type {

      
        870
        
        -		case token.MINUS:

      
        871
        
        -			amt.IsNegative = true

      
        872
        
        -			p.advance()

      
        873
        
        -		case token.PLUS:

      
        874
        
        -			p.advance()

      
        875
        
        -		}

      
        876
        
        -		p.skipWhitespace()

      
        
        794
        +	}

      
        877
        795
         

      
        878
        
        -		// commodity before quantity: -$120, -eur 120:

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

      
        880
        
        -			cs := p.cur.Span

      
        881
        
        -			amt.Commodity = unquote(p.cur.Literal)

      
        882
        
        -			amt.CommodityPos = ast.CommodityBefore

      
        883
        
        -			p.advance()

      
        884
        
        -			amt.CommoditySpan = token.Span{Start: cs.Start, End: p.cur.Span.Start}

      
        885
        
        -			if p.got(token.WHITESPACE) {

      
        886
        
        -				amt.HasSpace = true

      
        887
        
        -				p.skipWhitespace()

      
        888
        
        -			}

      
        889
        
        -		}

      
        
        796
        +	// optional sign after commodity: $ -10

      
        
        797
        +	p.parseAmountSign(amt)

      
        
        798
        +	p.skipWhitespace()

      
        890
        799
         

      
        891
        
        -		p.parseQuantityInto(amt)

      
        
        800
        +	p.parseQuantityInto(amt)

      
        892
        801
         

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

      
        894
        
        -		if amt.Commodity == "" {

      
        895
        
        -			switch p.cur.Type {

      
        896
        
        -			case token.WHITESPACE:

      
        897
        
        -				p.skipWhitespace()

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

      
        899
        
        -					cs := p.cur.Span

      
        900
        
        -					amt.HasSpace = true

      
        901
        
        -					amt.Commodity = unquote(p.cur.Literal)

      
        902
        
        -					amt.CommodityPos = ast.CommodityAfter

      
        903
        
        -					p.advance()

      
        904
        
        -					amt.CommoditySpan = token.Span{Start: cs.Start, End: p.cur.Span.Start}

      
        905
        
        -				}

      
        906
        
        -			case token.COMMODITYMARK, token.TEXT, token.STRING:

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

      
        
        803
        +	if amt.Commodity == "" {

      
        
        804
        +		switch p.cur.Type {

      
        
        805
        +		case token.WHITESPACE:

      
        
        806
        +			p.skipWhitespace()

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

      
        907
        808
         				cs := p.cur.Span

      
        
        809
        +				amt.HasSpace = true

      
        908
        810
         				amt.Commodity = unquote(p.cur.Literal)

      
        909
        811
         				amt.CommodityPos = ast.CommodityAfter

      
        910
        812
         				p.advance()

      
        911
        813
         				amt.CommoditySpan = token.Span{Start: cs.Start, End: p.cur.Span.Start}

      
        912
        814
         			}

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

      
        
        816
        +			cs := p.cur.Span

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

      
        
        818
        +			amt.CommodityPos = ast.CommodityAfter

      
        
        819
        +			p.advance()

      
        
        820
        +			amt.CommoditySpan = token.Span{Start: cs.Start, End: p.cur.Span.Start}

      
        913
        821
         		}

      
        914
        822
         	}

      
        915
        823
         

      
        916
        824
         	return amt

      
        
        825
        +}

      
        
        826
        +

      
        
        827
        +// parseAmountSign consumes an optional leading +/- into IsNegative.

      
        
        828
        +func (p *Parser) parseAmountSign(amt *ast.Amount) {

      
        
        829
        +	switch p.cur.Type {

      
        
        830
        +	case token.MINUS:

      
        
        831
        +		amt.IsNegative = true

      
        
        832
        +		p.advance()

      
        
        833
        +	case token.PLUS:

      
        
        834
        +		p.advance()

      
        
        835
        +	}

      
        917
        836
         }

      
        918
        837
         

      
        919
        838
         func (p *Parser) parseAmountWithOptExpr() *ast.Amount {

      ···
        933
        852
         			QuantityFmt: ast.QuantityFormat{Decimal: '.'},

      
        934
        853
         		}

      
        935
        854
         		if len(lit) >= 2 && lit[0] == '(' && lit[len(lit)-1] == ')' {

      
        936
        
        -			inner := lit[1 : len(lit)-1]

      
        937
        
        -			i := 0

      
        938
        
        -			for i < len(inner) && (inner[i] == ' ' || inner[i] == '\t') {

      
        939
        
        -				i++

      
        940
        
        -			}

      
        941
        
        -			j := len(inner)

      
        942
        
        -			for j > i && (inner[j-1] == ' ' || inner[j-1] == '\t') {

      
        943
        
        -				j--

      
        944
        
        -			}

      
        945
        
        -			amt.Expr = inner[i:j]

      
        
        855
        +			amt.Expr = strings.Trim(lit[1:len(lit)-1], " \t")

      
        946
        856
         		}

      
        947
        857
         		amt.Span = p.cur.Span

      
        948
        858
         		p.advance()

      ···
        1000
        910
         	// optional amount - after two spaces

      
        1001
        911
         	if p.got(token.WHITESPACE) {

      
        1002
        912
         		p.skipWhitespace()

      
        1003
        
        -		if p.isAmountStart() || p.got(token.STAR) {

      
        
        913
        +		if p.isAmountStart() {

      
        1004
        914
         			posting.Amount = p.parseAmountWithOptExpr()

      
        1005
        915
         		}

      
        1006
        916
         	}

      
        1007
        917
         

      
        1008
        918
         	// optional cost '@' or '@@'

      
        1009
        
        -	if p.got(token.WHITESPACE) {

      
        1010
        
        -		p.skipWhitespace()

      
        1011
        
        -	}

      
        
        919
        +	p.skipWhitespace()

      
        1012
        920
         	if p.got(token.AT) || p.got(token.ATAT) {

      
        1013
        921
         		posting.Cost = p.parseCost()

      
        1014
        922
         	}

      
        1015
        923
         

      
        1016
        924
         	// optional balance assertion or assignment

      
        1017
        
        -	if p.got(token.WHITESPACE) {

      
        1018
        
        -		p.skipWhitespace()

      
        1019
        
        -	}

      
        
        925
        +	p.skipWhitespace()

      
        1020
        926
         	if p.got(token.COLON) && p.willGet(token.EQ) {

      
        1021
        927
         		p.advance() // consume ':' of ':='

      
        1022
        928
         		posting.Balance = p.parseBalanceAssertion()

      ···
        1139
        1045
         		return ast.Date{Span: p.span(s)}

      
        1140
        1046
         	}

      
        1141
        1047
         

      
        1142
        
        -	sep := byte(0)

      
        1143
        
        -	lit := tok.Literal

      
        1144
        
        -	for i := 0; i < len(lit); i++ {

      
        1145
        
        -		if lit[i] == '/' || lit[i] == '-' || lit[i] == '.' {

      
        1146
        
        -			sep = lit[i]

      
        1147
        
        -			break

      
        1148
        
        -		}

      
        1149
        
        -	}

      
        1150
        
        -	if sep == 0 {

      
        1151
        
        -		p.errorf("invalid date format: %q", lit)

      
        
        1048
        +	year, month, day, sep, err := parseDateLiteral(tok.Literal)

      
        
        1049
        +	if err != nil {

      
        
        1050
        +		p.errorf("%v", err)

      
        1152
        1051
         		return ast.Date{Span: p.span(s)}

      
        1153
        1052
         	}

      
        1154
        
        -

      
        1155
        
        -	parts := strings.Split(lit, string(sep))

      
        1156
        
        -

      
        1157
        
        -	// M/D or MM/DD (year inferred)

      
        1158
        
        -	if len(parts) == 2 {

      
        1159
        
        -		month, err := strconv.Atoi(parts[0])

      
        1160
        
        -		day, err2 := strconv.Atoi(parts[1])

      
        1161
        
        -		if err != nil || err2 != nil {

      
        1162
        
        -			p.errorf("invalid date literal: %q", lit)

      
        1163
        
        -			return ast.Date{Span: p.span(s)}

      
        1164
        
        -		}

      
        1165
        
        -		if month < 1 || month > 12 {

      
        1166
        
        -			p.errorf("invalid month %d in %q", month, lit)

      
        1167
        
        -			return ast.Date{Span: p.span(s)}

      
        1168
        
        -		}

      
        1169
        
        -		if day < 1 || day > 31 {

      
        1170
        
        -			p.errorf("invalid day %d in %q", day, lit)

      
        1171
        
        -			return ast.Date{Span: p.span(s)}

      
        1172
        
        -		}

      
        1173
        
        -		return ast.Date{Year: p.defaultYear, Month: month, Day: day, Sep: sep, Span: p.span(s)}

      
        1174
        
        -	}

      
        1175
        
        -

      
        1176
        
        -	if len(parts) != 3 {

      
        1177
        
        -		p.errorf("invalid date format: %q", lit)

      
        1178
        
        -		return ast.Date{Span: p.span(s)}

      
        1179
        
        -	}

      
        1180
        
        -

      
        1181
        
        -	year, err := strconv.Atoi(parts[0])

      
        1182
        
        -	month, err2 := strconv.Atoi(parts[1])

      
        1183
        
        -	day, err3 := strconv.Atoi(parts[2])

      
        1184
        
        -	if err != nil || err2 != nil || err3 != nil {

      
        1185
        
        -		p.errorf("invalid date literal: %q", lit)

      
        1186
        
        -		return ast.Date{Span: p.span(s)}

      
        1187
        
        -	}

      
        1188
        
        -	if month < 1 || month > 12 {

      
        1189
        
        -		p.errorf("invalid month %d in %q", month, lit)

      
        1190
        
        -		return ast.Date{Span: p.span(s)}

      
        1191
        
        -	}

      
        1192
        
        -	if day < 1 || day > 31 {

      
        1193
        
        -		p.errorf("invalid day %d in %q", day, lit)

      
        1194
        
        -		return ast.Date{Span: p.span(s)}

      
        
        1053
        +	if year == 0 {

      
        
        1054
        +		year = p.defaultYear

      
        1195
        1055
         	}

      
        1196
        1056
         

      
        1197
        
        -	return ast.Date{

      
        1198
        
        -		Year:  year,

      
        1199
        
        -		Month: month,

      
        1200
        
        -		Day:   day,

      
        1201
        
        -		Sep:   sep,

      
        1202
        
        -		Span:  p.span(s),

      
        1203
        
        -	}

      
        
        1057
        +	return ast.Date{Year: year, Month: month, Day: day, Sep: sep, Span: p.span(s)}

      
        1204
        1058
         }

      
        1205
        1059
         

      
        1206
        1060
         func (p *Parser) parseOptInlineComment() *ast.Comment {

      
        1207
        1061
         	p.skipWhitespace()

      
        1208
        
        -	if p.cur.Type != token.SEMICOLON {

      
        
        1062
        +	if !p.got(token.SEMICOLON) {

      
        1209
        1063
         		return nil

      
        1210
        1064
         	}

      
        
        1065
        +	return p.parseCommentRest(p.cur.Span)

      
        
        1066
        +}

      
        1211
        1067
         

      
        1212
        
        -	s := p.cur.Span

      
        
        1068
        +// parseCommentRest consumes a comment marker at p.cur, then optional text;

      
        
        1069
        +// s anchors the span at the marker's start.

      
        
        1070
        +func (p *Parser) parseCommentRest(s token.Span) *ast.Comment {

      
        1213
        1071
         	marker := p.cur.Literal[0]

      
        1214
        
        -	p.advance() // consume marker

      
        
        1072
        +	p.advance()

      
        1215
        1073
         	p.skipWhitespace()

      
        1216
        1074
         

      
        1217
        1075
         	text := ""

      ···
        1330
        1188
         	})

      
        1331
        1189
         }

      
        1332
        1190
         

      
        
        1191
        +func isDirectiveKeyword(t token.Type) bool {

      
        
        1192
        +	switch t {

      
        
        1193
        +	case token.COMMENTKW, token.ACCOUNT, token.COMMODITY, token.INCLUDE,

      
        
        1194
        +		token.ALIAS, token.PAYEE, token.TAG, token.APPLY, token.END,

      
        
        1195
        +		token.YEAR, token.DECIMALMARK, token.D, token.P, token.N, token.C:

      
        
        1196
        +		return true

      
        
        1197
        +	}

      
        
        1198
        +	return false

      
        
        1199
        +}

      
        
        1200
        +

      
        1333
        1201
         func (p *Parser) sync() {

      
        1334
        1202
         	for {

      
        1335
        1203
         		switch p.cur.Type {

      ···
        1337
        1205
         			return

      
        1338
        1206
         		case token.NEWLINE:

      
        1339
        1207
         			p.advance()

      
        1340
        
        -			switch p.cur.Type {

      
        1341
        
        -			case token.DATE, token.ACCOUNT, token.COMMODITY,

      
        1342
        
        -				token.INCLUDE, token.ALIAS, token.PAYEE,

      
        1343
        
        -				token.TAG, token.YEAR, token.D, token.P,

      
        1344
        
        -				token.APPLY, token.END, token.COMMENTKW,

      
        1345
        
        -				token.DECIMALMARK, token.TILDE, token.N, token.EQ:

      
        
        1208
        +			t := p.cur.Type

      
        
        1209
        +			if isDirectiveKeyword(t) || t == token.DATE || t == token.TILDE || t == token.EQ {

      
        1346
        1210
         				return

      
        1347
        1211
         			}

      
        1348
        1212
         		default:

      ···
        1417
        1281
         	return ast.QuantityFormat{Decimal: dec, Thousands: thou, Precision: prec}

      
        1418
        1282
         }

      
        1419
        1283
         

      
        
        1284
        +// parseSimpleDate  parses full YYYY/MM/DD date literal embedded in free text.

      
        1420
        1285
         func parseSimpleDate(s string) ast.Date {

      
        1421
        
        -	if len(s) < 8 {

      
        
        1286
        +	year, month, day, sep, err := parseDateLiteral(s)

      
        
        1287
        +	if err != nil {

      
        1422
        1288
         		return ast.Date{}

      
        1423
        1289
         	}

      
        1424
        
        -	sep := byte('-')

      
        1425
        
        -	if strings.Contains(s, "/") {

      
        1426
        
        -		sep = byte('/')

      
        1427
        
        -	} else if strings.Contains(s, ".") {

      
        1428
        
        -		sep = byte('.')

      
        
        1290
        +	return ast.Date{Year: year, Month: month, Day: day, Sep: sep}

      
        
        1291
        +}

      
        
        1292
        +

      
        
        1293
        +// parseDateLiteral parses and validates a date literal.

      
        
        1294
        +func parseDateLiteral(lit string) (year, month, day int, sep byte, err error) {

      
        
        1295
        +	sep = dateSeparator(lit)

      
        
        1296
        +	if sep == 0 {

      
        
        1297
        +		return 0, 0, 0, 0, fmt.Errorf("invalid date format: %q", lit)

      
        1429
        1298
         	}

      
        1430
        
        -	parts := strings.Split(s, string(sep))

      
        1431
        
        -	if len(parts) != 3 {

      
        1432
        
        -		return ast.Date{}

      
        
        1299
        +

      
        
        1300
        +	parts := strings.Split(lit, string(sep))

      
        
        1301
        +	if len(parts) != 2 && len(parts) != 3 {

      
        
        1302
        +		return 0, 0, 0, 0, fmt.Errorf("invalid date format: %q", lit)

      
        1433
        1303
         	}

      
        1434
        
        -	year, _ := strconv.Atoi(parts[0])

      
        1435
        
        -	month, _ := strconv.Atoi(parts[1])

      
        1436
        
        -	day, _ := strconv.Atoi(parts[2])

      
        1437
        
        -	return ast.Date{Year: year, Month: month, Day: day, Sep: sep}

      
        
        1304
        +

      
        
        1305
        +	nums := make([]int, len(parts))

      
        
        1306
        +	for i, part := range parts {

      
        
        1307
        +		if nums[i], err = strconv.Atoi(part); err != nil {

      
        
        1308
        +			return 0, 0, 0, 0, fmt.Errorf("invalid date literal: %q", lit)

      
        
        1309
        +		}

      
        
        1310
        +	}

      
        
        1311
        +

      
        
        1312
        +	month = nums[len(parts)-2]

      
        
        1313
        +	if month < 1 || month > 12 {

      
        
        1314
        +		return 0, 0, 0, 0, fmt.Errorf("invalid month %d in %q", month, lit)

      
        
        1315
        +	}

      
        
        1316
        +

      
        
        1317
        +	day = nums[len(parts)-1]

      
        
        1318
        +	if day < 1 || day > 31 {

      
        
        1319
        +		return 0, 0, 0, 0, fmt.Errorf("invalid day %d in %q", day, lit)

      
        
        1320
        +	}

      
        
        1321
        +

      
        
        1322
        +	if len(parts) == 2 {

      
        
        1323
        +		return 0, month, day, sep, nil

      
        
        1324
        +	}

      
        
        1325
        +	return nums[0], month, day, sep, nil

      
        
        1326
        +}

      
        
        1327
        +

      
        
        1328
        +func dateSeparator(lit string) byte {

      
        
        1329
        +	for i := 0; i < len(lit); i++ {

      
        
        1330
        +		if lit[i] == '/' || lit[i] == '-' || lit[i] == '.' {

      
        
        1331
        +			return lit[i]

      
        
        1332
        +		}

      
        
        1333
        +	}

      
        
        1334
        +	return 0

      
        1438
        1335
         }