aboutsummaryrefslogtreecommitdiff
path: root/2021/05/line
diff options
context:
space:
mode:
Diffstat (limited to '2021/05/line')
-rw-r--r--2021/05/line/line.go239
1 files changed, 239 insertions, 0 deletions
diff --git a/2021/05/line/line.go b/2021/05/line/line.go
new file mode 100644
index 0000000..aedf455
--- /dev/null
+++ b/2021/05/line/line.go
@@ -0,0 +1,239 @@
+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
+}