advent-of-code/2021/05/line/line.go

239 lines
5.4 KiB
Go

package line
import (
"bufio"
"bytes"
"fmt"
"io"
"strconv"
)
type Line struct {
X1, Y1, X2, Y2 int
}
// Token represents a lexical token.
type Token int
const (
// Special tokens
ILLEGAL Token = iota
EOF
WHITESPACE
// Literals
INT // integers
// Misc characters
COMMA // ,
ARROW // ->
)
func isWhitespace(ch rune) bool {
return ch == ' ' || ch == '\t' || ch == '\n'
}
func isDigit(ch rune) bool {
return (ch >= '0' && ch <= '9')
}
var eof = rune(0)
// Scanner represents a lexical scanner.
type Scanner struct {
r *bufio.Reader
}
// NewScanner returns a new instance of Scanner.
func NewScanner(r io.Reader) *Scanner {
return &Scanner{r: bufio.NewReader(r)}
}
// read reads the next rune from the bufferred reader.
// Returns the rune(0) if an error occurs (or io.EOF is returned).
func (s *Scanner) read() rune {
ch, _, err := s.r.ReadRune()
if err != nil {
return eof
}
return ch
}
// unread places the previously read rune back on the reader.
func (s *Scanner) unread() { _ = s.r.UnreadRune() }
// Scan returns the next token and literal value.
func (s *Scanner) Scan() (tok Token, lit string) {
// Read the next rune.
ch := s.read()
// If we see whitespace then consume all contiguous whitespace.
// If we see a digit then consume as an int.
if isWhitespace(ch) {
return s.scanWhitespace(ch)
} else if isDigit(ch) {
return s.scanInt(ch)
}
// Otherwise read the individual character.
switch ch {
case eof:
return EOF, ""
case ',':
return COMMA, string(ch)
case '-':
return s.scanArrow(ch)
}
return ILLEGAL, string(ch)
}
// scanWhitespace consumes the current rune and all contiguous whitespace.
func (s *Scanner) scanWhitespace(read rune) (tok Token, lit string) {
// Create a buffer and read the current character into it.
var buf bytes.Buffer
buf.WriteRune(read)
// Read every subsequent whitespace character into the buffer.
// Non-whitespace characters and EOF will cause the loop to exit.
for {
if ch := s.read(); ch == eof {
break
} else if !isWhitespace(ch) {
s.unread()
break
} else {
buf.WriteRune(ch)
}
}
return WHITESPACE, buf.String()
}
// scanInt consumes the current rune and all contiguous digit runes.
func (s *Scanner) scanInt(read rune) (tok Token, lit string) {
// Create a buffer and read the current character into it.
var buf bytes.Buffer
buf.WriteRune(read)
// Read every subsequent ident character into the buffer.
// Non-ident characters and EOF will cause the loop to exit.
for {
if ch := s.read(); ch == eof {
break
} else if !isDigit(ch) {
s.unread()
break
} else {
_, _ = buf.WriteRune(ch)
}
}
// Otherwise return as a regular identifier.
return INT, buf.String()
}
func (s *Scanner) scanArrow(read rune) (tok Token, lit string) {
// Create a buffer and read the current character into it.
var buf bytes.Buffer
buf.WriteRune(read)
// Read every subsequent ident character into the buffer.
// Non-ident characters and EOF will cause the loop to exit.
for {
if ch := s.read(); ch == eof {
break
} else if ch == '>' {
_, _ = buf.WriteRune(ch)
return ARROW, buf.String()
} else {
_, _ = buf.WriteRune(ch)
break
}
}
// Otherwise return as a regular identifier.
return ILLEGAL, buf.String()
}
// Parser represents a parser.
type Parser struct {
s *Scanner
buf struct {
tok Token // last read token
lit string // last read literal
n int // buffer size (max=1)
}
}
// NewParser returns a new instance of Parser.
func NewParser(r io.Reader) *Parser {
return &Parser{s: NewScanner(r)}
}
// scan returns the next token from the underlying scanner.
// If a token has been unscanned then read that instead.
func (p *Parser) scan() (tok Token, lit string) {
// If we have a token on the buffer, then return it.
if p.buf.n != 0 {
p.buf.n = 0
return p.buf.tok, p.buf.lit
}
// Otherwise read the next token from the scanner.
tok, lit = p.s.Scan()
// Save it to the buffer in case we unscan later.
p.buf.tok, p.buf.lit = tok, lit
return
}
// unscan pushes the previously read token back onto the buffer.
func (p *Parser) unscan() { p.buf.n = 1 }
// scanIgnoreWhitespace scans the next non-whitespace token.
func (p *Parser) scanIgnoreWhitespace() (tok Token, lit string) {
tok, lit = p.scan()
if tok == WHITESPACE {
tok, lit = p.scan()
}
return
}
func (p *Parser) Parse() (*Line, error) {
l := &Line{}
if tok, lit := p.scanIgnoreWhitespace(); tok != INT {
return nil, fmt.Errorf("found %q, expected INT", lit)
} else {
l.X1, _ = strconv.Atoi(lit)
}
if tok, lit := p.scanIgnoreWhitespace(); tok != COMMA {
return nil, fmt.Errorf("found %q, expected COMMA", lit)
}
if tok, lit := p.scanIgnoreWhitespace(); tok != INT {
return nil, fmt.Errorf("found %q, expected INT", lit)
} else {
l.Y1, _ = strconv.Atoi(lit)
}
if tok, lit := p.scanIgnoreWhitespace(); tok != ARROW {
return nil, fmt.Errorf("found %q, expected ARROW", lit)
}
if tok, lit := p.scanIgnoreWhitespace(); tok != INT {
return nil, fmt.Errorf("found %q, expected INT", lit)
} else {
l.X2, _ = strconv.Atoi(lit)
}
if tok, lit := p.scanIgnoreWhitespace(); tok != COMMA {
return nil, fmt.Errorf("found %q, expected COMMA", lit)
}
if tok, lit := p.scanIgnoreWhitespace(); tok != INT {
return nil, fmt.Errorf("found %q, expected INT", lit)
} else {
l.Y2, _ = strconv.Atoi(lit)
}
if tok, lit := p.scan(); tok != WHITESPACE {
return nil, fmt.Errorf("found %q, expected WHITESPACE", lit)
}
return l, nil
}