all repos

clerk @ a7ca45c45f1c26525fb20f33ea16ae41bc4db348

missing tooling for ledger/hledger

clerk/journal/lexer/lexer.go (view raw)

Oleksandr Smirnov Oleksandr Smirnov
olexsmir@gmail.com
journal: split "EUR123" into commodity+quantity, fix quoted commodities in postings, 1 month ago
1
package lexer
2
3
import (
4
	"strings"
5
	"unicode"
6
	"unicode/utf8"
7
8
	"olexsmir.xyz/clerk/journal/token"
9
)
10
11
type Mode uint
12
13
const (
14
	// start of a line, nothing consumed
15
	ModeDefault Mode = iota
16
17
	// after ; # * % ; at start of line, or anywhere inline
18
	// everything until \n is comment text
19
	ModeComment
20
21
	// after lexing a date at column 0
22
	// expects: optional status, optional code, description, comment
23
	ModeTransaction
24
25
	// after lexing an indent at start of line
26
	// expects: account name, then two spaces, then amount
27
	ModePosting
28
29
	// after ~, period expression
30
	// expects: period, optional description (after 2+ spaces), optional comment
31
	ModePeriodic
32
33
	// after =, automates transaction
34
	// expects: expression
35
	ModeAutomated
36
37
	// after a directive keyword like account, commodity, include
38
	// expects: rest of directive content
39
	ModeDirective
40
)
41
42
type Lexer struct {
43
	file  string
44
	input []byte
45
	mode  Mode
46
47
	ch     rune // current rune (0 = EOF/sentinel)
48
	chSize int  // byte size of current rune
49
	pos    int  // current byte offset (points at ch)
50
	rpos   int  // next byte offset to read (one ahead of pos)
51
	col    int  // current column (1-based)
52
	line   int  // current line (1-based)
53
54
	transactionPastStatus bool
55
	postingExpectAccount  bool
56
	readingNoteAfterPipe  bool
57
}
58
59
func New(file string, input []byte) *Lexer {
60
	l := &Lexer{
61
		file:  file,
62
		input: input,
63
		line:  1,
64
	}
65
	l.advance()
66
	if l.ch == '\uFEFF' { // start of the input
67
		l.advance()
68
	}
69
	return l
70
}
71
72
// Next returns next token in the input
73
func (l *Lexer) Next() token.Token {
74
	switch l.mode {
75
	case ModeDefault:
76
		return l.lexDefault()
77
	case ModeComment:
78
		return l.lexComment()
79
	case ModeTransaction:
80
		return l.lexTransaction()
81
	case ModePosting:
82
		return l.lexPosting()
83
	case ModePeriodic:
84
		return l.lexPeriodic()
85
	case ModeAutomated:
86
		return l.lexAutomated()
87
	case ModeDirective:
88
		return l.lexDirective()
89
	}
90
	panic("unreachable")
91
}
92
93
func (l *Lexer) lexDefault() token.Token {
94
	switch {
95
	case l.ch == 0:
96
		return l.token(token.EOF, "")
97
	case l.ch == '\n':
98
		return l.lexNewline()
99
	case l.ch == '\r':
100
		l.col = 0
101
		l.advance()
102
		return l.lexNewline()
103
	case l.ch == ' ' || l.ch == '\t':
104
		tok := l.lexIndent()
105
		l.mode = ModePosting
106
		l.postingExpectAccount = true
107
		return tok
108
	case l.ch == ';' || l.ch == '#' || l.ch == '%':
109
		l.mode = ModeComment
110
		return l.lexSingle(token.SEMICOLON) // todo: ??
111
	case l.ch == '*': // * at col 0 == comment
112
		l.mode = ModeComment
113
		return l.lexSingle(token.STAR)
114
	case l.ch == '~':
115
		l.mode = ModePeriodic
116
		return l.lexSingle(token.TILDE)
117
	case l.ch == '=':
118
		l.mode = ModeAutomated
119
		return l.lexSingle(token.EQ)
120
	case l.ch == '+':
121
		return l.lexSingle(token.PLUS)
122
	case l.ch == '-':
123
		return l.lexSingle(token.MINUS)
124
	case l.ch == '.':
125
		return l.lexSingle(token.TEXT)
126
	case l.ch == '!':
127
		return l.lexSingle(token.BANG)
128
	case l.ch == '@':
129
		return l.lexSingle(token.AT)
130
	case l.isAlpha():
131
		return l.lexKeyword()
132
	case l.isDigit():
133
		if !l.isDate() {
134
			s := l.save()
135
			for l.isDigit() || l.ch == '-' || l.ch == '/' || l.ch == '.' {
136
				l.advance()
137
			}
138
			return token.Token{Type: token.ILLEGAL, Literal: string(l.input[s.offset:l.pos]), Span: l.span(s)}
139
		}
140
		tok := l.lexDate()
141
		l.mode = ModeTransaction
142
		l.transactionPastStatus = false
143
		return tok
144
	default:
145
		s := l.save()
146
		l.advance()
147
		return token.Token{Type: token.ILLEGAL, Literal: string(l.input[s.offset:l.pos]), Span: l.span(s)}
148
	}
149
}
150
151
func (l *Lexer) lexComment() token.Token {
152
	if l.ch == '\n' || l.ch == 0 {
153
		l.mode = ModeDefault
154
		return l.lexNewline()
155
	}
156
157
	for l.ch == ' ' || l.ch == '\t' {
158
		l.lexWhitespace()
159
	}
160
161
	if l.ch == '\n' || l.ch == 0 {
162
		l.mode = ModeDefault
163
		return l.lexNewline()
164
	}
165
166
	s := l.save()
167
	for l.ch != '\n' && l.ch != 0 {
168
		l.advance()
169
	}
170
	return token.Token{Type: token.TEXT, Literal: string(l.input[s.offset:l.pos]), Span: l.span(s)}
171
}
172
173
func (l *Lexer) lexTransaction() token.Token {
174
	if l.readingNoteAfterPipe {
175
		l.readingNoteAfterPipe = false
176
		return l.lexNote()
177
	}
178
179
	switch l.ch {
180
	case 0:
181
		return l.token(token.EOF, "")
182
	case '\n':
183
		l.mode = ModeDefault
184
		return l.lexNewline()
185
	case '\r':
186
		l.col = 0
187
		l.advance()
188
		return l.lexNewline()
189
	case ' ', '\t':
190
		return l.lexWhitespace()
191
	case ';':
192
		l.mode = ModeComment
193
		return l.lexSingle(token.SEMICOLON)
194
	case '*':
195
		if !l.transactionPastStatus {
196
			l.transactionPastStatus = true
197
			return l.lexSingle(token.STAR)
198
		}
199
		return l.lexText()
200
	case '!':
201
		if !l.transactionPastStatus {
202
			l.transactionPastStatus = true
203
			return l.lexSingle(token.BANG)
204
		}
205
		return l.lexText()
206
	case '|':
207
		l.transactionPastStatus = true
208
		l.readingNoteAfterPipe = true
209
		return l.lexSingle(token.PIPE)
210
	case '+':
211
		return l.lexSingle(token.PLUS)
212
	case '-':
213
		return l.lexSingle(token.MINUS)
214
	case '=':
215
		return l.lexEquals()
216
	case '"', '\'':
217
		return l.lexString()
218
	default: // description / payee
219
		if l.isDate() { // secondsry date after =
220
			return l.lexDate()
221
		}
222
		return l.lexText()
223
	}
224
}
225
226
func (l *Lexer) lexNote() token.Token {
227
	for l.ch == ' ' || l.ch == '\t' {
228
		l.advance()
229
	}
230
	if l.ch == 0 || l.ch == '\n' || l.ch == ';' {
231
		return l.Next()
232
	}
233
	s := l.save()
234
	for l.ch != 0 && l.ch != '\n' && l.ch != ';' {
235
		l.advance()
236
	}
237
	lit := string(l.input[s.offset:l.pos])
238
	for len(lit) > 0 && (lit[len(lit)-1] == ' ' || lit[len(lit)-1] == '\t') {
239
		lit = lit[:len(lit)-1]
240
	}
241
	return token.Token{Type: token.TEXT, Literal: lit, Span: l.span(s)}
242
}
243
244
func (l *Lexer) lexPeriodic() token.Token {
245
	switch l.ch {
246
	case 0:
247
		return l.token(token.EOF, "")
248
	case '\n':
249
		l.mode = ModeDefault
250
		return l.lexNewline()
251
	case '\r':
252
		l.col = 0
253
		l.advance()
254
		return l.lexNewline()
255
	case ';':
256
		l.mode = ModeComment
257
		return l.lexSingle(token.SEMICOLON)
258
	case ' ', '\t':
259
		return l.lexWhitespace()
260
	default:
261
		return l.lexText()
262
	}
263
}
264
265
func (l *Lexer) lexAutomated() token.Token {
266
	switch l.ch {
267
	case 0:
268
		return l.token(token.EOF, "")
269
	case '\n':
270
		l.mode = ModeDefault
271
		return l.lexNewline()
272
	case '\r':
273
		l.col = 0
274
		l.advance()
275
		return l.lexNewline()
276
	case ' ', '\t':
277
		return l.lexWhitespace()
278
	case ';':
279
		l.mode = ModeComment
280
		return l.lexSingle(token.SEMICOLON)
281
	default:
282
		return l.lexText()
283
	}
284
}
285
286
func (l *Lexer) lexPosting() token.Token {
287
	switch {
288
	case l.ch == 0:
289
		l.postingExpectAccount = false
290
		return l.token(token.EOF, "")
291
	case l.ch == '\n':
292
		l.postingExpectAccount = false
293
		l.mode = ModeDefault
294
		return l.lexNewline()
295
	case l.ch == ';':
296
		l.postingExpectAccount = false
297
		l.mode = ModeComment
298
		return l.lexSingle(token.SEMICOLON)
299
	case l.ch == ' ' || l.ch == '\t':
300
		return l.lexWhitespace()
301
	case l.postingExpectAccount && l.ch == '*':
302
		return l.lexSingle(token.STAR)
303
	case l.postingExpectAccount && l.ch == '!':
304
		return l.lexSingle(token.BANG)
305
	case l.ch == '=':
306
		return l.lexEquals()
307
	case l.ch == '@':
308
		return l.lexAt()
309
	case l.ch == '{':
310
		return l.lexLBrace()
311
	case l.ch == '}':
312
		return l.lexRBrace()
313
	case l.ch == '(':
314
		if !l.postingExpectAccount {
315
			return l.lexParenExpr()
316
		}
317
		return l.lexSingle(token.LPAREN)
318
	case l.ch == ')':
319
		return l.lexSingle(token.RPAREN)
320
	case l.ch == '[':
321
		return l.lexSingle(token.LBRACKET)
322
	case l.ch == ']':
323
		return l.lexSingle(token.RBRACKET)
324
	case l.ch == ':':
325
		return l.lexSingle(token.COLON)
326
	case l.postingExpectAccount && l.ch != '*' && l.ch != '!' && l.ch != '(' && l.ch != '[':
327
		return l.lexAccountNamePosting()
328
	case l.ch == '*': // after account name
329
		return l.lexSingle(token.STAR)
330
	case l.isDigit(), l.ch == '.':
331
		return l.lexNumber()
332
	case l.ch == '-':
333
		return l.lexSingle(token.MINUS)
334
	case l.ch == '+':
335
		return l.lexSingle(token.PLUS)
336
	case l.isCommodityStart():
337
		return l.lexCommodityMark()
338
	case l.ch == '"' || l.ch == '\'':
339
		return l.lexString()
340
	case l.ch >= 'a' && l.ch <= 'z':
341
		return l.lexCommodityMark()
342
	default:
343
		return l.lexAccountNamePosting()
344
	}
345
}
346
347
func (l *Lexer) lexDirective() token.Token {
348
	switch l.ch {
349
	case '\n', 0:
350
		l.mode = ModeDefault
351
		return l.lexNewline()
352
	case ';':
353
		l.mode = ModeComment
354
		return l.lexSingle(token.SEMICOLON)
355
	case ' ', '\t':
356
		return l.lexWhitespace()
357
	case '=':
358
		return l.lexSingle(token.EQ)
359
	case '+':
360
		return l.lexSingle(token.PLUS)
361
	case '-':
362
		return l.lexSingle(token.MINUS)
363
	case '"', '\'':
364
		return l.lexString()
365
	case ':':
366
		return l.lexSingle(token.COLON)
367
	default:
368
		if l.isCommodityStart() {
369
			return l.lexCommodityMark()
370
		}
371
		if l.isTime() {
372
			return l.lexTime()
373
		}
374
		if l.isDate() {
375
			return l.lexDate()
376
		}
377
		if l.isDigit() {
378
			return l.lexNumber()
379
		}
380
		return l.lexAccountNameDirective()
381
	}
382
}
383
384
func (l *Lexer) lexSingle(kind token.Type) token.Token {
385
	s := l.save()
386
	l.advance()
387
	return token.Token{
388
		Type:    kind,
389
		Literal: string(l.input[s.offset:l.pos]),
390
		Span:    l.span(s),
391
	}
392
}
393
394
func (l *Lexer) lexNewline() token.Token {
395
	s := l.save()
396
	l.advance()
397
	l.mode = ModeDefault
398
	return token.Token{Type: token.NEWLINE, Literal: "\n", Span: l.span(s)}
399
}
400
401
func (l *Lexer) lexWhitespace() token.Token {
402
	s := l.save()
403
	for l.ch == ' ' || l.ch == '\t' {
404
		l.advance()
405
	}
406
	lit := string(l.input[s.offset:l.pos])
407
	return token.Token{Type: token.WHITESPACE, Literal: lit, Span: l.span(s)}
408
}
409
410
func (l *Lexer) lexIndent() token.Token {
411
	s := l.save()
412
	for l.ch == ' ' || l.ch == '\t' {
413
		l.advance()
414
	}
415
	lit := string(l.input[s.offset:l.pos])
416
	return token.Token{Type: token.INDENT, Literal: lit, Span: l.span(s)}
417
}
418
419
func (l *Lexer) lexEquals() token.Token {
420
	s := l.save()
421
	l.advance()
422
	if l.ch == '=' {
423
		l.advance()
424
		switch l.ch {
425
		case '=':
426
			l.advance()
427
			return token.Token{Type: token.EQEQEQ, Literal: "===", Span: l.span(s)}
428
		case '*':
429
			l.advance()
430
			return token.Token{Type: token.EQEQEQ, Literal: "==*", Span: l.span(s)}
431
		default:
432
			return token.Token{Type: token.EQEQ, Literal: "==", Span: l.span(s)}
433
		}
434
	}
435
	return token.Token{Type: token.EQ, Literal: "=", Span: l.span(s)}
436
}
437
438
func (l *Lexer) lexAt() token.Token {
439
	s := l.save()
440
	l.advance()
441
	if l.ch == '@' {
442
		l.advance()
443
		return token.Token{Type: token.ATAT, Literal: "@@", Span: l.span(s)}
444
	}
445
	return token.Token{Type: token.AT, Literal: "@", Span: l.span(s)}
446
}
447
448
func (l *Lexer) lexText() token.Token {
449
	s := l.save()
450
	l.advance()
451
	for l.ch != '\n' && l.ch != ';' && l.ch != 0 && l.ch != ' ' && l.ch != '\t' {
452
		l.advance()
453
	}
454
	lit := string(l.input[s.offset:l.pos])
455
	return token.Token{Type: token.TEXT, Literal: lit, Span: l.span(s)}
456
}
457
458
// lexAccountNameDirective reads accout name in directive context.
459
// stops at any whitespace, supports multi-word names("Taxi Fare").
460
func (l *Lexer) lexAccountNameDirective() token.Token {
461
	s := l.save()
462
	for l.ch != '\n' && l.ch != ';' && l.ch != 0 && l.ch != ')' && l.ch != ']' && l.ch != ':' && l.ch != ' ' && l.ch != '\t' {
463
		l.advance()
464
	}
465
	lit := string(l.input[s.offset:l.pos])
466
	return token.Token{Type: token.TEXT, Literal: lit, Span: l.span(s)}
467
}
468
469
// lexAccountNamePosting reads an account name in posting context.
470
// stops at two consecutive spaces.
471
func (l *Lexer) lexAccountNamePosting() token.Token {
472
	s := l.save()
473
	for l.ch != '\n' && l.ch != ';' && l.ch != 0 && l.ch != ')' && l.ch != ']' && l.ch != ':' {
474
		if l.isTwoSpaces() {
475
			break
476
		}
477
		l.advance()
478
	}
479
	if l.ch != ':' {
480
		l.postingExpectAccount = false
481
	}
482
	lit := string(l.input[s.offset:l.pos])
483
	return token.Token{Type: token.TEXT, Literal: lit, Span: l.span(s)}
484
}
485
486
func (l *Lexer) lexParenExpr() token.Token {
487
	s := l.save()
488
	depth := 0
489
	for l.ch != '\n' && l.ch != 0 {
490
		if l.ch == '(' {
491
			depth++
492
		} else if l.ch == ')' {
493
			depth--
494
			if depth == 0 {
495
				l.advance()
496
				break
497
			}
498
		}
499
		l.advance()
500
	}
501
	lit := string(l.input[s.offset:l.pos])
502
	return token.Token{Type: token.PARENEXPR, Literal: lit, Span: l.span(s)}
503
}
504
505
func (l *Lexer) lexNumber() token.Token {
506
	s := l.save()
507
	for {
508
		if l.isDigit() || l.ch == '.' || l.ch == ',' || l.ch == '_' || l.ch == '\'' {
509
			l.advance()
510
		} else if l.ch == ' ' && (l.peek() >= '0' && l.peek() <= '9') {
511
			l.advance()
512
		} else {
513
			break
514
		}
515
	}
516
	lit := string(l.input[s.offset:l.pos])
517
	kind := token.INT
518
	if strings.ContainsAny(lit, "., ") {
519
		kind = token.DECIMAL
520
	}
521
	return token.Token{Type: kind, Literal: lit, Span: l.span(s)}
522
}
523
524
func (l *Lexer) lexKeyword() token.Token {
525
	s := l.save()
526
	for l.ch != 0 && l.ch != '\n' && l.ch != '\r' && l.ch != ' ' && l.ch != '\t' && l.ch != ';' {
527
		l.advance()
528
	}
529
	lit := string(l.input[s.offset:l.pos])
530
	kind := l.keyword(lit)
531
	if kind == token.ILLEGAL { // todo: report an error ??
532
		kind = token.TEXT
533
	} else {
534
		l.mode = ModeDirective
535
	}
536
	return token.Token{Type: kind, Literal: lit, Span: l.span(s)}
537
}
538
539
func (l *Lexer) lexDate() token.Token {
540
	s := l.save()
541
	for l.isDigit() || (l.isDateSep() && l.peekIsDigit()) {
542
		l.advance()
543
	}
544
	return token.Token{Type: token.DATE, Literal: string(l.input[s.offset:l.pos]), Span: l.span(s)}
545
}
546
547
func isSymbolChar(r rune) bool {
548
	return r == '$' || unicode.In(r, unicode.Sc)
549
}
550
551
func (l *Lexer) lexString() token.Token {
552
	s := l.save()
553
	quote := l.ch
554
	l.advance() // consume the quote character
555
	for l.ch != quote && l.ch != '\n' && l.ch != 0 {
556
		l.advance()
557
	}
558
	if l.ch == quote {
559
		l.advance()
560
	}
561
	return token.Token{Type: token.STRING, Literal: string(l.input[s.offset:l.pos]), Span: l.span(s)}
562
}
563
564
func (l *Lexer) lexCommodityMark() token.Token {
565
	s := l.save()
566
567
	if l.ch == '"' {
568
		l.advance()
569
		for l.ch != '"' && l.ch != '\n' && l.ch != 0 {
570
			l.advance()
571
		}
572
		if l.ch == '"' {
573
			l.advance()
574
		}
575
		return token.Token{Type: token.COMMODITYMARK, Literal: string(l.input[s.offset:l.pos]), Span: l.span(s)}
576
	}
577
578
	if unicode.IsLetter(l.ch) {
579
		for unicode.IsLetter(l.ch) {
580
			l.advance()
581
		}
582
		return token.Token{Type: token.COMMODITYMARK, Literal: string(l.input[s.offset:l.pos]), Span: l.span(s)}
583
	}
584
585
	if isSymbolChar(l.ch) {
586
		for isSymbolChar(l.ch) {
587
			l.advance()
588
		}
589
		return token.Token{Type: token.COMMODITYMARK, Literal: string(l.input[s.offset:l.pos]), Span: l.span(s)}
590
	}
591
592
	l.advance()
593
	return token.Token{Type: token.COMMODITYMARK, Literal: string(l.input[s.offset:l.pos]), Span: l.span(s)}
594
}
595
596
func (l *Lexer) lexLBrace() token.Token {
597
	s := l.save()
598
	l.advance()
599
	if l.ch == '{' {
600
		l.advance()
601
		return token.Token{Type: token.LBRACELBRACE, Literal: "{{", Span: l.span(s)}
602
	}
603
	return token.Token{Type: token.LBRACE, Literal: "{", Span: l.span(s)}
604
}
605
606
func (l *Lexer) lexRBrace() token.Token {
607
	s := l.save()
608
	l.advance()
609
	if l.ch == '}' {
610
		l.advance()
611
		return token.Token{Type: token.RBRACERBRACE, Literal: "}}", Span: l.span(s)}
612
	}
613
	return token.Token{Type: token.RBRACE, Literal: "}", Span: l.span(s)}
614
}
615
616
func (l *Lexer) advance() {
617
	if l.rpos >= len(l.input) {
618
		l.ch = 0
619
		l.chSize = 0
620
	} else {
621
		r, size := utf8.DecodeRune(l.input[l.rpos:])
622
		l.ch = r
623
		l.chSize = size
624
	}
625
	l.pos = l.rpos
626
	l.rpos += l.chSize
627
	if l.ch == '\n' || l.ch == '\r' {
628
		l.line++
629
		l.col = 0
630
	} else {
631
		l.col++
632
	}
633
}
634
635
func (l *Lexer) peek() rune {
636
	r, _ := utf8.DecodeRune(l.input[l.rpos:])
637
	return r
638
}
639
640
func (l *Lexer) peekN(n int) byte {
641
	if l.pos+n >= len(l.input) {
642
		return 0
643
	}
644
	return l.input[l.pos+n]
645
}
646
647
func (l *Lexer) isDigit() bool { return l.ch >= '0' && l.ch <= '9' }
648
func (l *Lexer) isAlpha() bool {
649
	return (l.ch >= 'a' && l.ch <= 'z') ||
650
		(l.ch >= 'A' && l.ch <= 'Z')
651
}
652
653
func (l *Lexer) isTwoSpaces() bool { return l.ch == ' ' && l.peek() == ' ' }
654
655
func (l *Lexer) isDateSep() bool { return l.ch == '-' || l.ch == '/' || l.ch == '.' }
656
657
func (l *Lexer) peekIsDigit() bool {
658
	r := l.peek()
659
	return r >= '0' && r <= '9'
660
}
661
662
func (l *Lexer) isCommodityStart() bool {
663
	if l.ch == '$' || (l.ch >= 'A' && l.ch <= 'Z') {
664
		return true
665
	}
666
	if l.ch < utf8.RuneSelf {
667
		return false
668
	}
669
	return unicode.In(l.ch, unicode.Sc) || unicode.IsLetter(l.ch)
670
}
671
672
func (l *Lexer) isDate() bool {
673
	if !l.isDigit() {
674
		return false
675
	}
676
	// YYYY/M/D or YYYY/MM/DD
677
	if l.peekN(1) >= '0' && l.peekN(1) <= '9' &&
678
		l.peekN(2) >= '0' && l.peekN(2) <= '9' &&
679
		l.peekN(3) >= '0' && l.peekN(3) <= '9' {
680
		sep := l.peekN(4)
681
		if sep == '/' || sep == '-' || sep == '.' {
682
			if l.peekN(5) >= '0' && l.peekN(5) <= '9' {
683
				if l.peekN(6) == sep {
684
					return l.peekN(7) >= '0' && l.peekN(7) <= '9'
685
				}
686
				if l.peekN(7) == sep {
687
					return l.peekN(8) >= '0' && l.peekN(8) <= '9'
688
				}
689
			}
690
		}
691
		return false
692
	}
693
	// M/D or MM/DD(year inferred, only / and - separators; . is ambiguous with decimal numbers like 1.01)
694
	if (l.peekN(1) == '/' || l.peekN(1) == '-') &&
695
		l.peekN(2) >= '0' && l.peekN(2) <= '9' &&
696
		l.ch >= '1' && l.ch <= '9' {
697
		return validDay(l.peekN(2), l.peekN(3))
698
	}
699
	if (l.peekN(2) == '/' || l.peekN(2) == '-') &&
700
		l.peekN(3) >= '0' && l.peekN(3) <= '9' {
701
		m := int(l.ch-'0')*10 + int(l.peekN(1)-'0')
702
		return m >= 1 && m <= 12 && validDay(l.peekN(3), l.peekN(4))
703
	}
704
	return false
705
}
706
707
func validDay(first, second byte) bool {
708
	d := int(first - '0')
709
	if second >= '0' && second <= '9' {
710
		d = d*10 + int(second-'0')
711
	}
712
	return d >= 1 && d <= 31
713
}
714
715
func (l *Lexer) isTime() bool {
716
	if !l.isDigit() {
717
		return false
718
	}
719
	return l.peekN(2) == ':'
720
}
721
722
func (l *Lexer) lexTime() token.Token {
723
	s := l.save()
724
	for l.isDigit() || l.ch == ':' {
725
		l.advance()
726
	}
727
	return token.Token{Type: token.TIME, Literal: string(l.input[s.offset:l.pos]), Span: l.span(s)}
728
}
729
730
type savedPos struct{ offset, line, col int }
731
732
func (l *Lexer) save() savedPos {
733
	return savedPos{l.pos, l.line, l.col}
734
}
735
736
func (l *Lexer) span(s savedPos) token.Span {
737
	return token.Span{
738
		Start: token.Pos{File: l.file, Offset: s.offset, Line: s.line, Col: s.col},
739
		End:   token.Pos{File: l.file, Offset: l.pos, Line: l.line, Col: l.col},
740
	}
741
}
742
743
func (l *Lexer) token(kind token.Type, literal string) token.Token {
744
	s := savedPos{l.pos, l.line, l.col}
745
	return token.Token{Type: kind, Literal: literal, Span: l.span(s)}
746
}
747
748
func (l *Lexer) keyword(s string) token.Type {
749
	switch s {
750
	case "comment":
751
		return token.COMMENTKW
752
	case "account":
753
		return token.ACCOUNT
754
	case "commodity":
755
		return token.COMMODITY
756
	case "include":
757
		return token.INCLUDE
758
	case "alias":
759
		return token.ALIAS
760
	case "payee":
761
		return token.PAYEE
762
	case "tag":
763
		return token.TAG
764
	case "apply":
765
		return token.APPLY
766
	case "end":
767
		return token.END
768
	case "Y", "year":
769
		return token.YEAR
770
	case "decimal-mark":
771
		return token.DECIMALMARK
772
	case "D":
773
		return token.D
774
	case "P":
775
		return token.P
776
	case "N":
777
		return token.N
778
	case "C":
779
		return token.C
780
	default:
781
		return token.ILLEGAL
782
	}
783
}