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
+}