~ghost08/wt

04851bfda7316fd0d7d6ac6256b66185c41bd082 — ghost08 3 years ago
init
6 files changed, 519 insertions(+), 0 deletions(-)

A data.txt
A entries.go
A entries_test.go
A go.mod
A lexer.go
A parser.go
A  => data.txt +9 -0
@@ 1,9 @@
2021-05-26
	nws	AICON3-101 Pridat pracovne skupiny	08:00:00-12:30:32
	km	KMSF5-234 Nieco nieco	12:30:33
	km	KMSF5-233 safhidsa fisaduf idsa  fnidsafn uisadhf  nieco	13:30:33-15:34:03
2021-05-27
	nws	AICON3-101 Pridat pracovne skupiny	08:00:00-12:00:00
	km	KMSF5-234 Nieco nieco	12:30:33
	km	KMSF5-233 safhidsa fisaduf idsa  fnidsafn uisadhf  nieco	13:30:33-18:24:13
	km	KMSF5-236 task task	18:24:14

A  => entries.go +13 -0
@@ 1,13 @@
package main

import "time"

type Entries []Entry

type Entry struct {
	Project     string
	Description string
	Date        time.Time
	Start       time.Time
	End         time.Time
}

A  => entries_test.go +22 -0
@@ 1,22 @@
package main

import (
	"log"
	"os"
	"testing"
)

func TestParse(t *testing.T) {
	f, err := os.Open("data.txt")
	if err != nil {
		t.Fatal(err)
	}
	defer f.Close()
	entries, err := Parse(f)
	if err != nil {
		t.Fatal(err)
	}
	for _, e := range entries {
		log.Println(e)
	}
}

A  => go.mod +3 -0
@@ 1,3 @@
module git.sr.ht/~ghost08/wf

go 1.16

A  => lexer.go +334 -0
@@ 1,334 @@
package main

import (
	"bufio"
	"fmt"
	"io"
	"log"
	"strings"
	"unicode"
)

//itemType identifies the type of lex items.
type itemType int

const (
	itemError itemType = iota // error occurred; value is text of error
	itemEOF

	//basic items
	itemDate
	itemProject
	itemDescription
	itemTime
)

type item struct {
	typ itemType
	val string
}

func (i item) String() string {
	switch i.typ {
	case itemEOF:
		return "EOF"
	case itemError:
		return i.val
	}
	return fmt.Sprintf("%q", i.val)
}

var (
	eof = rune(0)
)

//stateFn represents the state of the scanner as a function that returns the next state.
type stateFn func(*lexer) stateFn

// lexer holds the state of the scanner.
type lexer struct {
	input              *bufio.Reader   // the data being scanned.
	buf                strings.Builder //the data already scanned
	line, pos, prevpos int
	items              chan item // channel of scanned items.
}

func (l *lexer) posString() string {
	return fmt.Sprintf("line: %d, pos: %d", l.line, l.pos)
}

//run lexes the input by executing state functions until the state is nil.
func (l *lexer) run() {
	for state := lexStart; state != nil; {
		state = state(l)
	}
	close(l.items) // No more tokens will be delivered.
}

// emit passes an item back to the client.
func (l *lexer) emit(t itemType) {
	log.Println("EMIT", t, l.buf.String())
	l.items <- item{t, l.buf.String()}
	l.buf.Reset()
}

// read reads the next rune from the bufferred reader.
// Returns the rune(0) if an error occurs (or io.EOF is returned).
func (l *lexer) read() rune {
	ch, _, err := l.input.ReadRune()
	if ch == '\n' {
		l.line++
		l.prevpos = l.pos
		l.pos = 0
	} else {
		l.pos++
		if err != nil {
			return eof
		}
	}
	l.buf.WriteRune(ch)
	return ch
}

// unread places the previously read rune back on the reader.
func (l *lexer) unread() {
	l.input.UnreadRune()
	if l.pos == 0 {
		l.pos = l.prevpos
		if l.line == 0 {
			panic("Cannot unread! No runes readed")
		}
		l.line--
	} else {
		l.pos--
	}
	buf := l.buf.String()
	l.buf.Reset()
	if len(buf) > 0 {
		l.buf.WriteString(buf[:len(buf)-1])
	}
}

//peek returns but does not consume the next rune in the input.
func (l *lexer) peek() rune {
	r := l.read()
	l.unread()
	return r
}

//readLetters reads all runes that are letters
func (l *lexer) readLetters() string {
	var buf strings.Builder
	for {
		ch := l.read()
		if ch == eof {
			break
		} else if !unicode.IsLetter(ch) {
			l.unread()
			break
		}
		buf.WriteRune(ch)
	}
	return buf.String()
}

//readDigits reads all runes that are letters
func (l *lexer) readDigits() string {
	for {
		if ch := l.read(); ch == eof {
			break
		} else if !unicode.IsDigit(ch) {
			l.unread()
			break
		}
	}
	ret := l.buf.String()
	return ret
}

func (l *lexer) readDate() {
	l.readDigits()
	if l.buf.Len() != 4 {
		l.errorf("date must be in format YYYY-MM-DD")
		return
	}
	if r := l.read(); r != '-' {
		l.errorf("unexpected character after year in date (%c) expected (-)", r)
		return
	}
	l.readDigits()
	if l.buf.Len() != 7 {
		l.errorf("date must be in format YYYY-MM-DD")
		return
	}
	if r := l.read(); r != '-' {
		l.errorf("unexpected character after month in date (%c) expected (-)", r)
		return
	}
	l.readDigits()
	if l.buf.Len() != 10 {
		l.errorf("date must be in format YYYY-MM-DD")
		return
	}
	l.emit(itemDate)
	l.accept("\n")
	l.buf.Reset()
}

func (l *lexer) readTime() {
	hh := l.readDigits()
	if len(hh) != 2 {
		l.errorf("time must be in format HH:MM:SS")
		return
	}
	if r := l.read(); r != ':' {
		l.errorf("time must be in format HH:MM:SS")
		return
	}
	mm := l.readDigits()
	if len(mm) != 5 {
		l.errorf("time must be in format HH:MM:SS")
		return
	}
	if r := l.read(); r != ':' {
		l.errorf("time must be in format HH:MM:SS")
		return
	}
	ss := l.readDigits()
	if len(ss) != 8 {
		l.errorf("time must be in format HH:MM:SS")
		return
	}
	l.emit(itemTime)
}

//acceptToLineBreak reads entire string to line break
func (l *lexer) acceptToLineBreak() {
	for {
		if ch := l.read(); ch == eof {
			break
		} else if ch == '\r' || ch == '\n' {
			if ch == '\r' {
				r, _, _ := l.input.ReadRune()
				if r != '\n' {
					l.input.UnreadRune()
				}
			}
			if ch = l.read(); unicode.IsSpace(ch) {
				continue
			}
			l.unread()
			l.unread()
			break
		}
	}
}

func (l *lexer) acceptToTab() {
	for {
		ch := l.read()
		if ch == '\t' {
			l.input.UnreadRune()
			return
		}
		if ch == '\r' || ch == '\n' || ch == eof {
			l.errorf("entry text (project/task name) cannot contain new line and must be separated by tab")
			return
		}
	}
}

//acceptRun consumes a run of runes from the valid set.
func (l *lexer) accept(valid string) {
	for strings.ContainsRune(valid, l.read()) {
	}
	l.unread()
}

func (l *lexer) acceptWhitespace() {
	l.accept(" \t\n\r")
	l.buf.Reset()
}

func (l *lexer) ignoreWhitespace() {
	for {
		ch, _, err := l.input.ReadRune()
		if ch == '\n' {
			l.line++
			l.prevpos = l.pos
			l.pos = 0
		} else {
			if !unicode.IsSpace(ch) {
				l.input.UnreadRune()
				return
			}
			l.pos++
			if err != nil {
				return
			}
		}
	}
}

//errorf returns an error token and terminates the scan
//by passing back a nil pointer that will be the next
//state, terminating l.run.
func (l *lexer) errorf(format string, args ...interface{}) stateFn {
	l.items <- item{
		itemError,
		fmt.Sprintf("%d:%d:"+format, append([]interface{}{l.line, l.pos}, args...)...),
	}
	return nil
}

func lex(input io.Reader) *lexer {
	l := &lexer{
		input: bufio.NewReader(input),
		items: make(chan item, 5),
	}
	go l.run() // Concurrently run state machine.
	return l
}

func lexStart(l *lexer) stateFn {
	l.readDate()
	return lexEntry
}

func lexEntry(l *lexer) stateFn {
	if r := l.read(); r != '\t' {
		return l.errorf("entry must start with tab, got (%c)", r)
	}
	l.buf.Reset()
	l.acceptToTab()
	l.emit(itemProject)
	l.read()
	l.buf.Reset()
	l.acceptToTab()
	l.emit(itemDescription)
	l.read()
	l.buf.Reset()
	l.readTime()
	if r := l.read(); r == '-' {
		l.buf.Reset()
		l.readTime()
	} else {
		l.unread()
	}
	if r := l.read(); r != '\r' && r != '\n' {
		if r != eof {
			l.emit(itemEOF)
			return nil
		}
		return l.errorf("after start and end times must be a new line, found (%c)", r)
	}
	l.buf.Reset()
	if r := l.peek(); r == '\t' {
		return lexEntry
	}
	if r := l.peek(); r == eof {
		l.emit(itemEOF)
		return nil
	}
	return lexStart
}

A  => parser.go +138 -0
@@ 1,138 @@
package main

import (
	"fmt"
	"io"
	"time"
)

func Parse(r io.Reader) (Entries, error) {
	s := &scanner{l: lex(r)}
	entries, err := parseEntries(s)
	if err != nil {
		return nil, fmt.Errorf("error parsing Entries: %w", err)
	}
	return entries, nil
}

type scanner struct {
	curItem  item
	prevItem *item
	l        *lexer
}

func (s *scanner) next() item {
	if s.prevItem == nil {
		s.curItem = <-s.l.items
		return s.curItem
	}
	i := *s.prevItem
	s.prevItem = nil
	return i
}

func (s *scanner) backup() {
	s.prevItem = &s.curItem
}

func parseEntries(s *scanner) (Entries, error) {
	var entries Entries
	for {
		es, err := parseDay(s)
		if err != nil {
			return nil, err
		}
		if es == nil {
			break
		}
		entries = append(entries, es...)
	}
	return entries, nil
}

func parseDay(s *scanner) (Entries, error) {
	dateItem := s.next()
	if dateItem.typ == itemError {
		return nil, fmt.Errorf("error at %s: %s", s.l.posString(), dateItem.val)
	}
	if dateItem.typ == itemEOF {
		return nil, nil
	}
	if dateItem.typ != itemDate {
		return nil, fmt.Errorf("Expected date item")
	}
	d, err := time.Parse("2006-01-02", dateItem.val)
	if err != nil {
		return nil, fmt.Errorf("Error parsing date at %s: %w", s.l.posString(), err)
	}
	var entries Entries
	for {
		var e Entry
		e.Date = d
		i := s.next()
		if i.typ == itemError {
			return nil, fmt.Errorf("1: %s", i.val)
		}
		if i.typ == itemEOF {
			break
		}
		if i.typ == itemDate {
			s.backup()
			break
		}
		if i.typ != itemProject {
			return nil, fmt.Errorf("expected project item at %s", s.l.posString())
		}
		e.Project = i.val
		i = s.next()
		if i.typ == itemError {
			return nil, fmt.Errorf("2: %s", i.val)
		}
		if i.typ != itemDescription {
			return nil, fmt.Errorf("expected description item at %s", s.l.posString())
		}
		e.Description = i.val
		i = s.next()
		if i.typ == itemError {
			return nil, fmt.Errorf("3: %s", i.val)
		}
		if i.typ != itemTime {
			return nil, fmt.Errorf("expected time item at %s", s.l.posString())
		}
		var err error
		e.Start, err = time.Parse("2006-01-02 15:04:05", dateItem.val+" "+i.val)
		if err != nil {
			return nil, fmt.Errorf("error parsing start time at %s: %w", s.l.posString(), err)
		}
		i = s.next()
		if i.typ == itemError {
			return nil, fmt.Errorf("4: %s", i.val)
		}
		if i.typ == itemProject {
			s.backup()
			entries = append(entries, e)
			continue
		}
		if i.typ == itemDate {
			s.backup()
			entries = append(entries, e)
			continue
		}
		if i.typ == itemTime {
			e.End, err = time.Parse("2006-01-02 15:04:05", dateItem.val+" "+i.val)
			if err != nil {
				return nil, fmt.Errorf("error parsing end time at %s: %w", s.l.posString(), err)
			}
			entries = append(entries, e)
		}
		if i.typ == itemEOF {
			s.backup()
			entries = append(entries, e)
			break
		}
	}
	if len(entries) == 0 {
		return nil, fmt.Errorf("day must have at least one entry at %s", s.l.posString())
	}
	return entries, nil
}