20 files changed,
353 insertions(+),
116 deletions(-)
Author:
Oleksandr Smirnov
olexsmir@gmail.com
Committed at:
2026-07-31 16:58:01 +0300
Authored at:
2026-07-31 13:22:13 +0300
Change ID:
rumyuprxwtvtwtpyzxqqmpqzrltnpwqp
Parent:
ea00934
jump to
M
internal/decimal/decimal.go
··· 3 3 import ( 4 4 "fmt" 5 5 "math/big" 6 + "strconv" 6 7 "strings" 7 8 ) 8 9 ··· 21 22 func FromString(s string) (Decimal, error) { 22 23 original := s 23 24 if s == "" { 24 - return Decimal{}, fmt.Errorf("can't convert %s to decimal", original) 25 + return Decimal{}, badDecimal(original) 25 26 } 26 27 27 - sign := 1 28 + neg := false 28 29 if s[0] == '+' || s[0] == '-' { 29 - if s[0] == '-' { 30 - sign = -1 31 - } 30 + neg = s[0] == '-' 32 31 s = s[1:] 33 32 } 34 33 35 - if s == "" { 36 - return Decimal{}, fmt.Errorf("can't convert %s to decimal", original) 34 + exp := 0 35 + if e := strings.IndexAny(s, "eE"); e >= 0 { 36 + n, err := strconv.Atoi(s[e+1:]) 37 + if err != nil { 38 + return Decimal{}, badDecimal(original) 39 + } 40 + // bounded so pow10 can't be asked to materialize an astronomically 41 + // large number (e.g. 1E1000000000000) 42 + const maxExp = 10000 43 + if n > maxExp || n < -maxExp { 44 + return Decimal{}, badDecimal(original) 45 + } 46 + exp = n 47 + s = s[:e] 37 48 } 38 49 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) 50 + intPart, fracPart := s, "" 51 + if before, after, ok := strings.Cut(s, "."); ok { 52 + if strings.IndexByte(after, '.') >= 0 { 53 + return Decimal{}, badDecimal(original) 54 + } 55 + intPart, fracPart = before, after 43 56 } 44 57 45 - intPart := s 46 - fracPart := "" 47 - if firstDot >= 0 { 48 - intPart = s[:firstDot] 49 - fracPart = s[firstDot+1:] 58 + digits := intPart + fracPart 59 + if neg { 60 + digits = "-" + digits 50 61 } 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 - } 62 + coeff, ok := new(big.Int).SetString(digits, 10) 63 + if !ok { 64 + return Decimal{}, badDecimal(original) 65 65 } 66 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) 67 + scale := len(fracPart) - exp 68 + if scale < 0 { 69 + coeff.Mul(coeff, pow10(-scale)) 70 + scale = 0 71 71 } 72 - if sign < 0 && coeff.Sign() != 0 { 73 - coeff.Neg(coeff) 74 - } 72 + return Decimal{coeff: coeff, scale: scale}.normalized(), nil 73 +} 75 74 76 - out := Decimal{coeff: coeff, scale: len(fracPart)} 77 - return out.normalized(), nil 78 -} 75 +func badDecimal(s string) error { return fmt.Errorf("can't convert %s to decimal", s) } 79 76 80 77 func (d Decimal) String() string { 81 78 if d.coeff == nil || d.coeff.Sign() == 0 { ··· 258 255 return d.coeff == nil || d.coeff.Sign() == 0 259 256 } 260 257 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 258 func (d Decimal) coeffOrZero() *big.Int { 276 259 if d.coeff == nil { 277 260 return new(big.Int) ··· 304 287 abs.Neg(abs) 305 288 } 306 289 return Decimal{coeff: abs, scale: d.scale} 290 +} 291 + 292 +func align(a, b Decimal) (aCoeff *big.Int, bCoeff *big.Int, scale int) { 293 + scale = max(b.scale, a.scale) 294 + 295 + aCoeff = a.coeffOrZero() 296 + bCoeff = b.coeffOrZero() 297 + if delta := scale - a.scale; delta > 0 { 298 + aCoeff.Mul(aCoeff, pow10(delta)) 299 + } 300 + if delta := scale - b.scale; delta > 0 { 301 + bCoeff.Mul(bCoeff, pow10(delta)) 302 + } 303 + return aCoeff, bCoeff, scale 307 304 } 308 305 309 306 func pow10(n int) *big.Int {
M
internal/decimal/decimal_test.go
··· 1 1 package decimal 2 2 3 -import "testing" 3 +import ( 4 + "strings" 5 + "testing" 6 +) 4 7 5 8 func TestNewFromString(t *testing.T) { 6 9 tests := []struct{ in, want string }{ ··· 14 17 {"-0.00", "0"}, 15 18 {"-20", "-20"}, 16 19 {"-.75", "-0.75"}, 20 + {"1E3", "1000"}, 21 + {"1E-3", "0.001"}, 22 + {"1E10000", "1" + strings.Repeat("0", 10000)}, 17 23 } 18 24 for _, tt := range tests { 19 25 got, err := FromString(tt.in) ··· 32 38 "1_000.00", "1,000.00", 33 39 "1..0", 34 40 "a1", "1a", 41 + "1E10001", "1E-10001", "1E1000000000000", 35 42 } 36 43 for _, in := range tests { 37 44 if _, err := FromString(in); err == nil {
M
journal/ast/dump.go
··· 295 295 fmt.Fprintf(b, "IsStrict: %v\n", ba.IsStrict) 296 296 indent(b, depth+1) 297 297 fmt.Fprintf(b, "IsInclusive: %v\n", ba.IsInclusive) 298 + indent(b, depth+1) 299 + fmt.Fprintf(b, "IsAssignment: %v\n", ba.IsAssignment) 298 300 dumpAmount(b, &ba.Amount, depth+1) 299 301 if ba.Cost != nil { 300 302 dumpCost(b, ba.Cost, depth+1)
M
journal/ast/entries.go
··· 109 109 } 110 110 111 111 type BalanceAssertion struct { 112 - IsStrict bool // == vs = 113 - IsInclusive bool // === 114 - Amount Amount 115 - Cost *Cost // price after @/@@ 116 - Span token.Span 112 + IsStrict bool // == vs = 113 + IsInclusive bool // =* or === (inclusive of subaccounts) 114 + IsAssignment bool // := balance assignment (not a check) 115 + Amount Amount 116 + Cost *Cost // price after @/@@ 117 + Span token.Span 117 118 } 118 119 119 120 type CommodityPos int
M
journal/lexer/lexer.go
··· 54 54 transactionPastStatus bool 55 55 postingExpectAccount bool 56 56 readingNoteAfterPipe bool 57 + 58 + // subdirective is set when the current line is an account/commodity 59 + // directive; the next indented line then lexes as directive content 60 + subdirective bool 57 61 } 58 62 59 63 func New(file string, input []byte) *Lexer { ··· 91 95 } 92 96 93 97 func (l *Lexer) lexDefault() token.Token { 98 + if l.subdirective && l.ch != ' ' && l.ch != '\t' { 99 + l.subdirective = false 100 + } 94 101 switch { 95 102 case l.ch == 0: 96 103 return l.token(token.EOF, "") ··· 102 109 return l.lexNewline() 103 110 case l.ch == ' ' || l.ch == '\t': 104 111 tok := l.lexIndent() 105 - l.mode = ModePosting 112 + if l.subdirective { 113 + l.mode = ModeDirective 114 + l.subdirective = false 115 + } else { 116 + l.mode = ModePosting 117 + } 106 118 l.postingExpectAccount = true 107 119 return tok 108 120 case l.ch == ';' || l.ch == '#' || l.ch == '%': ··· 364 376 return l.lexString() 365 377 case ':': 366 378 return l.lexSingle(token.COLON) 379 + case ')': 380 + return l.lexSingle(token.RPAREN) 381 + case ']': 382 + return l.lexSingle(token.RBRACKET) 367 383 default: 368 384 if l.isCommodityStart() { 369 385 return l.lexCommodityMark() ··· 432 448 return token.Token{Type: token.EQEQ, Literal: "==", Span: l.span(s)} 433 449 } 434 450 } 451 + if l.ch == '*' { 452 + l.advance() 453 + return token.Token{Type: token.EQSTAR, Literal: "=*", Span: l.span(s)} 454 + } 435 455 return token.Token{Type: token.EQ, Literal: "=", Span: l.span(s)} 436 456 } 437 457 ··· 509 529 l.advance() 510 530 } else if l.ch == ' ' && (l.peek() >= '0' && l.peek() <= '9') { 511 531 l.advance() 532 + } else if l.ch == 'e' || l.ch == 'E' { 533 + // exponent: consume only when E[sign]digit, so `10E ` stays an integer 534 + p := l.pos + 1 535 + if p < len(l.input) && (l.input[p] == '+' || l.input[p] == '-') { 536 + p++ 537 + } 538 + if p >= len(l.input) || l.input[p] < '0' || l.input[p] > '9' { 539 + break 540 + } 541 + l.advance() // e/E 542 + if l.ch == '+' || l.ch == '-' { 543 + l.advance() 544 + } 545 + for l.isDigit() { 546 + l.advance() 547 + } 512 548 } else { 513 549 break 514 550 } 515 551 } 516 552 lit := string(l.input[s.offset:l.pos]) 517 553 kind := token.INT 518 - if strings.ContainsAny(lit, "., ") { 554 + if strings.ContainsAny(lit, "., eE") { 519 555 kind = token.DECIMAL 520 556 } 521 557 return token.Token{Type: kind, Literal: lit, Span: l.span(s)} ··· 532 568 kind = token.TEXT 533 569 } else { 534 570 l.mode = ModeDirective 571 + l.subdirective = lit == "account" || lit == "commodity" 535 572 } 536 573 return token.Token{Type: kind, Literal: lit, Span: l.span(s)} 537 574 }
A
journal/lexer/testdata/fuzz/FuzzLexer/0745ffe051c1af1b
··· 1 +go test fuzz v1 2 +[]byte("account )")
M
journal/parser/parser.go
··· 451 451 for p.got(token.INDENT) { 452 452 p.advance() 453 453 p.skipWhitespace() 454 - if p.got(token.COMMODITYMARK) && p.cur.Literal == "format" { 454 + if p.got(token.TEXT) && p.cur.Literal == "format" { 455 455 p.advance() 456 456 p.skipWhitespace() 457 457 format = p.parseAmount() 458 - } else { 459 - for !p.got(token.NEWLINE) && !p.got(token.EOF) { 460 - p.advance() 461 - } 458 + p.expectNewline() 459 + continue 460 + } 461 + for !p.got(token.NEWLINE) && !p.got(token.EOF) { 462 + p.advance() 462 463 } 463 464 p.expectNewline() 464 465 } ··· 677 678 678 679 tok, _ := p.expect(token.COMMODITYMARK) 679 680 mp.Commodity = tok.Literal 680 - p.advance() 681 + p.skipWhitespace() 681 682 682 683 mp.Amount = *p.parseAmount() 683 684 ··· 861 862 case token.PLUS: 862 863 p.advance() 863 864 } 865 + p.skipWhitespace() 864 866 p.parseQuantityInto(amt) 865 867 } else { 866 868 // optional sign ··· 871 873 case token.PLUS: 872 874 p.advance() 873 875 } 876 + p.skipWhitespace() 874 877 875 878 // commodity before quantity: -$120, -eur 120: 876 879 if p.got(token.COMMODITYMARK) || p.got(token.TEXT) || p.got(token.STRING) { ··· 1010 1013 posting.Cost = p.parseCost() 1011 1014 } 1012 1015 1013 - // optional balance assertion 1016 + // optional balance assertion or assignment 1014 1017 if p.got(token.WHITESPACE) { 1015 1018 p.skipWhitespace() 1016 1019 } 1017 - if p.got(token.EQ) || p.got(token.EQEQ) || p.got(token.EQEQEQ) { 1020 + if p.got(token.COLON) && p.willGet(token.EQ) { 1021 + p.advance() // consume ':' of ':=' 1022 + posting.Balance = p.parseBalanceAssertion() 1023 + posting.Balance.IsAssignment = true 1024 + } else if p.got(token.EQ) || p.got(token.EQEQ) || p.got(token.EQEQEQ) || p.got(token.EQSTAR) { 1018 1025 posting.Balance = p.parseBalanceAssertion() 1019 1026 } 1020 1027 ··· 1050 1057 ba := &ast.BalanceAssertion{} 1051 1058 switch p.cur.Type { 1052 1059 case token.EQ: // basic assertion 1053 - case token.EQEQ: 1060 + case token.EQSTAR: // inclusive assertion 1061 + ba.IsInclusive = true 1062 + case token.EQEQ: // strict assertion 1054 1063 ba.IsStrict = true 1055 - case token.EQEQEQ: 1064 + case token.EQEQEQ: // strict inclusive assertion 1056 1065 ba.IsStrict = true 1057 1066 ba.IsInclusive = true 1058 1067 }
M
journal/parser/parser_test.go
··· 72 72 "transaction with header comment", 73 73 "transaction with trailing indent", 74 74 "transaction with balance assertion", 75 + "transaction with inclusive balance assertion", 76 + "transaction with balance assignment", 77 + "transaction with scientific amounts", 75 78 "transaction with virtual accounts", 76 79 "virtual postings with statuses", 77 80 "period transaction expressions",
M
journal/parser/testdata/commodity_with_subdirectives.txtar
··· 2 2 commodity UAH 3 3 format 1 000.00 UAH 4 4 note Божествена Гривня ; this gets ignored 5 + 5 6 -- expect -- 6 7 Journal 7 - CommodityDirective j:1:1-4:1 8 + CommodityDirective j:1:1-5:0 8 9 Commodity: "UAH" j:1:11-2:0 10 + Amount j:2:10-3:0 11 + Quantity: 1000 12 + Commodity: "UAH" j:2:19-3:0 13 + CommodityPos: After 14 + HasSpace: true 15 + Precision: 2 16 + Decimal: "." 17 + Thousands: " " 18 + BlankLine j:5:0-5:1
A
journal/parser/testdata/fuzz/FuzzParser/38d8778099f755a5
··· 1 +go test fuzz v1 2 +[]byte("D 00E000000000000000001000000000000")
A
journal/parser/testdata/fuzz/FuzzParser/f0ddae6d71555984
··· 1 +go test fuzz v1 2 +[]byte("P 0 10000000000E000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000")
M
journal/parser/testdata/transaction_with_balance_assertion.txtar
··· 10 10 2025/04/08 groceries3 11 11 expenses:food $10.00 12 12 assets:checking = $0.00 13 + 13 14 -- expect -- 14 15 Journal 15 16 Transaction j:1:1-5:0 ··· 29 30 BalanceAssertion j:2:24-3:0 30 31 IsStrict: false 31 32 IsInclusive: false 33 + IsAssignment: false 32 34 Amount j:2:26-3:0 33 35 Quantity: 100 34 36 Commodity: "$" j:2:26-2:27 ··· 59 61 BalanceAssertion j:6:24-7:0 60 62 IsStrict: true 61 63 IsInclusive: false 64 + IsAssignment: false 62 65 Amount j:6:27-7:0 63 66 Quantity: 100 64 67 Commodity: "$" j:6:27-6:28 ··· 74 77 BalanceAssertion j:7:19-8:0 75 78 IsStrict: true 76 79 IsInclusive: false 80 + IsAssignment: false 77 81 Amount j:7:22-8:0 78 82 Quantity: 0 79 83 Commodity: "$" j:7:22-7:23 ··· 82 86 Precision: 2 83 87 Decimal: "." 84 88 BlankLine j:9:0-9:1 85 - Transaction j:9:1-12:1 89 + Transaction j:9:1-13:0 86 90 Date: 2025/04/08 87 91 Payee: "groceries3" j:9:12-10:0 88 92 Posting j:10:1-11:1 ··· 96 100 HasSpace: false 97 101 Precision: 2 98 102 Decimal: "." 99 - Posting j:11:1-12:1 103 + Posting j:11:1-13:0 100 104 Account j:11:2-11:17 101 105 SubAccount: "assets" j:11:2-11:8 102 106 SubAccount: "checking" j:11:9-11:17 ··· 104 108 BalanceAssertion j:11:19-12:0 105 109 IsStrict: false 106 110 IsInclusive: false 111 + IsAssignment: false 107 112 Amount j:11:21-12:0 108 113 Quantity: 0 109 114 Commodity: "$" j:11:21-11:22 ··· 111 116 HasSpace: false 112 117 Precision: 2 113 118 Decimal: "." 119 + BlankLine j:13:0-13:1
A
journal/parser/testdata/transaction_with_balance_assignment.txtar
··· 1 +-- input -- 2 +2024-01-15 t 3 + a $0 := $100 4 + 5 +-- expect -- 6 +Journal 7 + Transaction j:1:1-4:0 8 + Date: 2024-01-15 9 + Payee: "t" j:1:12-2:0 10 + Posting j:2:1-4:0 11 + Account j:2:5-2:6 12 + SubAccount: "a" j:2:5-2:6 13 + Amount j:2:8-2:10 14 + Quantity: 0 15 + Commodity: "$" j:2:8-2:9 16 + CommodityPos: Before 17 + HasSpace: false 18 + Precision: 0 19 + Decimal: "." 20 + BalanceAssertion j:2:12-3:0 21 + IsStrict: false 22 + IsInclusive: false 23 + IsAssignment: true 24 + Amount j:2:14-3:0 25 + Quantity: 100 26 + Commodity: "$" j:2:14-2:15 27 + CommodityPos: Before 28 + HasSpace: false 29 + Precision: 0 30 + Decimal: "." 31 + BlankLine j:4:0-4:1
M
journal/parser/testdata/transaction_with_cost_and_assertion.txtar
··· 2 2 2026-05-11 testies 3 3 expenses:atm UAH 20.00 @ 1 USD = 0 UAH 4 4 assets:bank 5 + 5 6 -- expect -- 6 7 Journal 7 - Transaction j:1:1-4:1 8 + Transaction j:1:1-5:0 8 9 Date: 2026-05-11 9 10 Payee: "testies" j:1:12-2:0 10 11 Posting j:2:1-3:1 ··· 29 30 BalanceAssertion j:2:34-3:0 30 31 IsStrict: false 31 32 IsInclusive: false 33 + IsAssignment: false 32 34 Amount j:2:36-3:0 33 35 Quantity: 0 34 36 Commodity: "UAH" j:2:38-3:0 ··· 36 38 HasSpace: true 37 39 Precision: 0 38 40 Decimal: "." 39 - Posting j:3:1-4:1 41 + Posting j:3:1-5:0 40 42 Account j:3:2-4:0 41 43 SubAccount: "assets" j:3:2-3:8 42 44 SubAccount: "bank" j:3:9-4:0 43 45 Amount: <elided> 46 + BlankLine j:5:0-5:1
M
journal/parser/testdata/transaction_with_costs.txtar
··· 14 14 2015-01-03 money exchange office 15 15 assets:cash -20 EUR @ 7.53 HRK 16 16 assets:cash 150.60 HRK 17 + 17 18 -- expect -- 18 19 Journal 19 20 Transaction j:1:1-5:0 ··· 97 98 BalanceAssertion j:10:34-11:0 98 99 IsStrict: false 99 100 IsInclusive: false 101 + IsAssignment: false 100 102 Amount j:10:36-10:45 101 103 Quantity: 20 102 104 Commodity: "UAH" j:10:42-10:45 ··· 118 120 SubAccount: "bank" j:11:9-12:0 119 121 Amount: <elided> 120 122 BlankLine j:13:0-13:1 121 - Transaction j:13:1-16:1 123 + Transaction j:13:1-17:0 122 124 Date: 2015-01-03 123 125 Payee: "money exchange office" j:13:12-14:0 124 126 Posting j:14:1-15:1 ··· 140 142 HasSpace: true 141 143 Precision: 2 142 144 Decimal: "." 143 - Posting j:15:1-16:1 145 + Posting j:15:1-17:0 144 146 Account j:15:5-15:16 145 147 SubAccount: "assets" j:15:5-15:11 146 148 SubAccount: "cash" j:15:12-15:16 ··· 151 153 HasSpace: true 152 154 Precision: 2 153 155 Decimal: "." 156 + BlankLine j:17:0-17:1
A
journal/parser/testdata/transaction_with_inclusive_balance_assertion.txtar
··· 1 +-- input -- 2 +2024-01-15 t 3 + a $1 =* $100 4 + b $2 =*$100 5 + c $3 ==* $100 6 + 7 +-- expect -- 8 +Journal 9 + Transaction j:1:1-6:0 10 + Date: 2024-01-15 11 + Payee: "t" j:1:12-2:0 12 + Posting j:2:1-3:1 13 + Account j:2:5-2:6 14 + SubAccount: "a" j:2:5-2:6 15 + Amount j:2:8-2:10 16 + Quantity: 1 17 + Commodity: "$" j:2:8-2:9 18 + CommodityPos: Before 19 + HasSpace: false 20 + Precision: 0 21 + Decimal: "." 22 + BalanceAssertion j:2:11-3:0 23 + IsStrict: false 24 + IsInclusive: true 25 + IsAssignment: false 26 + Amount j:2:14-3:0 27 + Quantity: 100 28 + Commodity: "$" j:2:14-2:15 29 + CommodityPos: Before 30 + HasSpace: false 31 + Precision: 0 32 + Decimal: "." 33 + Posting j:3:1-4:1 34 + Account j:3:5-3:6 35 + SubAccount: "b" j:3:5-3:6 36 + Amount j:3:8-3:10 37 + Quantity: 2 38 + Commodity: "$" j:3:8-3:9 39 + CommodityPos: Before 40 + HasSpace: false 41 + Precision: 0 42 + Decimal: "." 43 + BalanceAssertion j:3:11-4:0 44 + IsStrict: false 45 + IsInclusive: true 46 + IsAssignment: false 47 + Amount j:3:13-4:0 48 + Quantity: 100 49 + Commodity: "$" j:3:13-3:14 50 + CommodityPos: Before 51 + HasSpace: false 52 + Precision: 0 53 + Decimal: "." 54 + Posting j:4:1-6:0 55 + Account j:4:5-4:6 56 + SubAccount: "c" j:4:5-4:6 57 + Amount j:4:8-4:10 58 + Quantity: 3 59 + Commodity: "$" j:4:8-4:9 60 + CommodityPos: Before 61 + HasSpace: false 62 + Precision: 0 63 + Decimal: "." 64 + BalanceAssertion j:4:11-5:0 65 + IsStrict: true 66 + IsInclusive: true 67 + IsAssignment: false 68 + Amount j:4:15-5:0 69 + Quantity: 100 70 + Commodity: "$" j:4:15-4:16 71 + CommodityPos: Before 72 + HasSpace: false 73 + Precision: 0 74 + Decimal: "." 75 + BlankLine j:6:0-6:1
A
journal/parser/testdata/transaction_with_scientific_amounts.txtar
··· 1 +-- input -- 2 +2024-01-15 t 3 + a 1E-6 BTC 4 + b EUR 1E3 5 + c -2.5e-2 $ 6 + 7 +-- expect -- 8 +Journal 9 + Transaction j:1:1-6:0 10 + Date: 2024-01-15 11 + Payee: "t" j:1:12-2:0 12 + Posting j:2:1-3:1 13 + Account j:2:5-2:6 14 + SubAccount: "a" j:2:5-2:6 15 + Amount j:2:8-3:0 16 + Quantity: 0.000001 17 + Commodity: "BTC" j:2:13-3:0 18 + CommodityPos: After 19 + HasSpace: true 20 + Precision: 0 21 + Decimal: "." 22 + Posting j:3:1-4:1 23 + Account j:3:5-3:6 24 + SubAccount: "b" j:3:5-3:6 25 + Amount j:3:8-4:0 26 + Quantity: 1000 27 + Commodity: "EUR" j:3:8-3:11 28 + CommodityPos: Before 29 + HasSpace: true 30 + Precision: 0 31 + Decimal: "." 32 + Posting j:4:1-6:0 33 + Account j:4:5-4:6 34 + SubAccount: "c" j:4:5-4:6 35 + Amount j:4:8-5:0 36 + Quantity: -0.025 37 + Commodity: "$" j:4:16-5:0 38 + CommodityPos: After 39 + HasSpace: true 40 + Precision: 4 41 + Decimal: "." 42 + BlankLine j:6:0-6:1
M
journal/parser/testdata/transaction_with_spaced_account_name.txtar
··· 2 2 2022-01-01 opening balances 3 3 assets 21 = 21 4 4 equity:opening/closing balances 5 + 5 6 -- expect -- 6 7 Journal 7 - Transaction j:1:1-4:1 8 + Transaction j:1:1-5:0 8 9 Date: 2022-01-01 9 10 Payee: "opening balances" j:1:12-2:0 10 11 Posting j:2:1-3:1 ··· 20 21 BalanceAssertion j:2:34-3:0 21 22 IsStrict: false 22 23 IsInclusive: false 24 + IsAssignment: false 23 25 Amount j:2:36-3:0 24 26 Quantity: 21 25 27 Commodity: "" ··· 27 29 HasSpace: false 28 30 Precision: 0 29 31 Decimal: "." 30 - Posting j:3:1-4:1 32 + Posting j:3:1-5:0 31 33 Account j:3:5-4:0 32 34 SubAccount: "equity" j:3:5-3:11 33 35 SubAccount: "opening/closing balances" j:3:12-4:0 34 36 Amount: <elided> 37 + BlankLine j:5:0-5:1
M
journal/token/type_string.go
··· 26 26 _ = x[EQ-15] 27 27 _ = x[EQEQ-16] 28 28 _ = x[EQEQEQ-17] 29 - _ = x[AT-18] 30 - _ = x[ATAT-19] 31 - _ = x[PIPE-20] 32 - _ = x[PLUS-21] 33 - _ = x[MINUS-22] 34 - _ = x[TILDE-23] 35 - _ = x[LPAREN-24] 36 - _ = x[RPAREN-25] 37 - _ = x[LBRACE-26] 38 - _ = x[LBRACELBRACE-27] 39 - _ = x[RBRACERBRACE-28] 40 - _ = x[RBRACE-29] 41 - _ = x[LBRACKET-30] 42 - _ = x[RBRACKET-31] 43 - _ = x[COMMODITYMARK-32] 44 - _ = x[DATE-33] 45 - _ = x[TIME-34] 46 - _ = x[PARENEXPR-35] 47 - _ = x[COMMENTKW-36] 48 - _ = x[ACCOUNT-37] 49 - _ = x[COMMODITY-38] 50 - _ = x[INCLUDE-39] 51 - _ = x[ALIAS-40] 52 - _ = x[PAYEE-41] 53 - _ = x[TAG-42] 54 - _ = x[APPLY-43] 55 - _ = x[END-44] 56 - _ = x[YEAR-45] 57 - _ = x[DECIMALMARK-46] 58 - _ = x[D-47] 59 - _ = x[P-48] 60 - _ = x[N-49] 61 - _ = x[C-50] 29 + _ = x[EQSTAR-18] 30 + _ = x[AT-19] 31 + _ = x[ATAT-20] 32 + _ = x[PIPE-21] 33 + _ = x[PLUS-22] 34 + _ = x[MINUS-23] 35 + _ = x[TILDE-24] 36 + _ = x[LPAREN-25] 37 + _ = x[RPAREN-26] 38 + _ = x[LBRACE-27] 39 + _ = x[LBRACELBRACE-28] 40 + _ = x[RBRACERBRACE-29] 41 + _ = x[RBRACE-30] 42 + _ = x[LBRACKET-31] 43 + _ = x[RBRACKET-32] 44 + _ = x[COMMODITYMARK-33] 45 + _ = x[DATE-34] 46 + _ = x[TIME-35] 47 + _ = x[PARENEXPR-36] 48 + _ = x[COMMENTKW-37] 49 + _ = x[ACCOUNT-38] 50 + _ = x[COMMODITY-39] 51 + _ = x[INCLUDE-40] 52 + _ = x[ALIAS-41] 53 + _ = x[PAYEE-42] 54 + _ = x[TAG-43] 55 + _ = x[APPLY-44] 56 + _ = x[END-45] 57 + _ = x[YEAR-46] 58 + _ = x[DECIMALMARK-47] 59 + _ = x[D-48] 60 + _ = x[P-49] 61 + _ = x[N-50] 62 + _ = x[C-51] 62 63 } 63 64 64 -const _Type_name = "ILLEGALEOFWHITESPACEINDENTNEWLINEINTDECIMALSTRINGTEXTBANGSTARPERCENTHASHSEMICOLONCOLONEQEQEQEQEQEQATATATPIPEPLUSMINUSTILDELPARENRPARENLBRACELBRACELBRACERBRACERBRACERBRACELBRACKETRBRACKETCOMMODITYMARKDATETIMEPARENEXPRCOMMENTKWACCOUNTCOMMODITYINCLUDEALIASPAYEETAGAPPLYENDYEARDECIMALMARKDPNC" 65 +const _Type_name = "ILLEGALEOFWHITESPACEINDENTNEWLINEINTDECIMALSTRINGTEXTBANGSTARPERCENTHASHSEMICOLONCOLONEQEQEQEQEQEQEQSTARATATATPIPEPLUSMINUSTILDELPARENRPARENLBRACELBRACELBRACERBRACERBRACERBRACELBRACKETRBRACKETCOMMODITYMARKDATETIMEPARENEXPRCOMMENTKWACCOUNTCOMMODITYINCLUDEALIASPAYEETAGAPPLYENDYEARDECIMALMARKDPNC" 65 66 66 -var _Type_index = [...]uint16{0, 7, 10, 20, 26, 33, 36, 43, 49, 53, 57, 61, 68, 72, 81, 86, 88, 92, 98, 100, 104, 108, 112, 117, 122, 128, 134, 140, 152, 164, 170, 178, 186, 199, 203, 207, 216, 225, 232, 241, 248, 253, 258, 261, 266, 269, 273, 284, 285, 286, 287, 288} 67 +var _Type_index = [...]uint16{0, 7, 10, 20, 26, 33, 36, 43, 49, 53, 57, 61, 68, 72, 81, 86, 88, 92, 98, 104, 106, 110, 114, 118, 123, 128, 134, 140, 146, 158, 170, 176, 184, 192, 205, 209, 213, 222, 231, 238, 247, 254, 259, 264, 267, 272, 275, 279, 290, 291, 292, 293, 294} 67 68 68 69 func (i Type) String() string { 69 70 idx := int(i) - 0