~ghost08/wt

26cc613dfe7433c199f57557b8f6f11630cd9933 — ghost08 2 years ago 238d07d
Add start and end
8 files changed, 278 insertions(+), 35 deletions(-)

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

M entries.go => entries.go +82 -1
@@ 1,6 1,12 @@
package main

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

var zeroTime time.Time

type Entries []Entry



@@ 11,3 17,78 @@ type Entry struct {
	Start       time.Time
	End         time.Time
}

func (e Entry) String() string {
	return fmt.Sprintf(
		"{Project:%s, Description:'%s', Start: %s, End: %s}",
		e.Project,
		e.Description,
		e.Start.Format("2006-01-02 15:04:05"),
		e.End.Format("2006-01-02 15:04:05"),
	)
}

func AppendEntry(es Entries, e Entry) (Entries, error) {
	if e.End == zeroTime && len(es) > 0 {
		e.End = es[len(es)-1].Start.Add(-time.Second)
	}
	if !es.Before(e) {
		return nil, fmt.Errorf("entry (%s) isn't before other entries, entries must be sorted", e)
	}
	return append(es, e), nil
}

/*
func (es Entries) Len() int {
	return len(es)
}

func (es Entries) Less(i, j int) bool {
	return es[i].Start.Before(es[j].Start)
}

func (es Entries) Swap(i, j int) {
	e := es[i]
	es[i] = es[j]
	es[j] = e
}
*/

func (es Entries) Before(e Entry) bool {
	if len(es) == 0 {
		return true
	}
	last := es[len(es)-1]
	return last.Start.After(e.End)
}

func (es Entries) Save(f io.Writer) error {
	var y, m, d int
	for i, e := range es {
		ey, em, ed := e.Date.Date()
		if y != ey || m != int(em) || d != ed {
			y, m, d = ey, int(em), ed
			if _, err := fmt.Fprintf(f, "%04d-%02d-%02d\n", y, m, d); err != nil {
				return err
			}
		}
		if _, err := fmt.Fprintf(
			f,
			"\t%s\t%s\t%s",
			e.Project,
			e.Description,
			e.Start.Format("15:04:05"),
		); err != nil {
			return err
		}
		if i == 0 || !es[i-1].Start.Equal(e.End.Add(time.Second)) {
			if _, err := fmt.Fprintf(f, "-%s", e.End.Format("15:04:05")); err != nil {
				return err
			}
		}
		if _, err := fmt.Fprintf(f, "\n"); err != nil {
			return err
		}
	}
	return nil
}

M entries_test.go => entries_test.go +1 -5
@@ 1,7 1,6 @@
package main

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


@@ 12,11 11,8 @@ func TestParse(t *testing.T) {
		t.Fatal(err)
	}
	defer f.Close()
	entries, err := Parse(f)
	_, err = Parse(f)
	if err != nil {
		t.Fatal(err)
	}
	for _, e := range entries {
		log.Println(e)
	}
}

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

go 1.16

require github.com/alecthomas/kong v0.2.16

A go.sum => go.sum +7 -0
@@ 0,0 1,7 @@
github.com/alecthomas/kong v0.2.16 h1:F232CiYSn54Tnl1sJGTeHmx4vJDNLVP2b9yCVMOQwHQ=
github.com/alecthomas/kong v0.2.16/go.mod h1:kQOmtJgV+Lb4aj+I2LEn40cbtawdWJ9Y8QLq+lElKxE=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=

M lexer.go => lexer.go +5 -6
@@ 4,7 4,6 @@ import (
	"bufio"
	"fmt"
	"io"
	"log"
	"strings"
	"unicode"
)


@@ 67,7 66,7 @@ func (l *lexer) run() {

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


@@ 149,7 148,7 @@ func (l *lexer) readDigits() string {
func (l *lexer) readDate() {
	l.readDigits()
	if l.buf.Len() != 4 {
		l.errorf("date must be in format YYYY-MM-DD")
		l.errorf("date must be in format YYYY-MM-DD (%s)", l.buf.String())
		return
	}
	if r := l.read(); r != '-' {


@@ 158,7 157,7 @@ func (l *lexer) readDate() {
	}
	l.readDigits()
	if l.buf.Len() != 7 {
		l.errorf("date must be in format YYYY-MM-DD")
		l.errorf("date must be in format YYYY-MM-DD (%s)", l.buf.String())
		return
	}
	if r := l.read(); r != '-' {


@@ 167,7 166,7 @@ func (l *lexer) readDate() {
	}
	l.readDigits()
	if l.buf.Len() != 10 {
		l.errorf("date must be in format YYYY-MM-DD")
		l.errorf("date must be in format YYYY-MM-DD (%s)", l.buf.String())
		return
	}
	l.emit(itemDate)


@@ 228,7 227,7 @@ func (l *lexer) acceptToTab() {
	for {
		ch := l.read()
		if ch == '\t' {
			l.input.UnreadRune()
			l.unread()
			return
		}
		if ch == '\r' || ch == '\n' || ch == eof {

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

import (
	"fmt"
	"log"
	"os"
	"path/filepath"
	"time"

	"github.com/alecthomas/kong"
)

var CLI struct {
	Start struct {
		Project     string `arg required help:"project name"`
		Description string `arg required help:"task description"`
	} `cmd help:"start a new entry"`
	End    struct{} `cmd help:"end a running entry"`
	Status struct{} `cmd help:"print the running entry"`
	Report struct {
		Month  string `arg optional help:"select the month to export in format: YYYYMM (default is the previous month)"`
		Output string `optional short:"o" default:"report.xlsx" help:"output file path"`
	} `cmd help:"export data to a spreadsheet"`
	DataFile string `optional short:"d" default:"$HOME/.local/wt.data" help:"path to the wt data file"`
}

func main() {
	ctx := kong.Parse(&CLI)
	switch ctx.Command() {
	case "start <project> <description>":
		if err := start(); err != nil {
			fmt.Fprintf(os.Stderr, "%s", err)
			os.Exit(1)
		}
	case "end":
		if err := end(); err != nil {
			fmt.Fprintf(os.Stderr, "%s", err)
			os.Exit(1)
		}
	case "report", "report <month>":
		if err := report(); err != nil {
			fmt.Fprintf(os.Stderr, "%s", err)
			os.Exit(1)
		}
	case "status":
		if err := status(); err != nil {
			fmt.Fprintf(os.Stderr, "%s", err)
			os.Exit(1)
		}
	default:
		log.Fatal(ctx.Command())
	}
}

func loadEntries() (Entries, error) {
	if CLI.DataFile == "$HOME/.local/wt.data" {
		home, err := os.UserHomeDir()
		if err != nil {
			return nil, fmt.Errorf("getting user home dir: %w", err)
		}
		CLI.DataFile = filepath.Join(home, ".local", "wt.data")
	}
	f, err := os.Open(CLI.DataFile)
	if err != nil {
		if os.IsNotExist(err) {
			return nil, nil
		}
		return nil, fmt.Errorf("opening data file (%s): %w", CLI.DataFile, err)
	}
	defer f.Close()
	return Parse(f)
}

func saveEntries(es Entries) error {
	f, err := os.Create(CLI.DataFile)
	if err != nil {
		return fmt.Errorf("cannot write to data file (%s): %w", CLI.DataFile, err)
	}
	if err := es.Save(f); err != nil {
		return fmt.Errorf("writing to data file (%s): %w", CLI.DataFile, err)
	}
	if err := f.Close(); err != nil {
		return fmt.Errorf("closing data file (%s): %w", CLI.DataFile, err)
	}
	return nil
}

func start() error {
	now := time.Now()
	y, m, d := now.Date()
	es, err := loadEntries()
	if err != nil {
		return fmt.Errorf("loading entries: %w", err)
	}
	if len(es) != 0 {
		last := &es[0]
		//if the first entry is from yesterday and isn't ended
		fy, fm, fd := last.Date.Date()
		if (y > fy || m > fm || d > fd) && last.End == zeroTime {
			return fmt.Errorf("last entry is from previous day and isn't ended (%s)", last.String())
		}
		if last.End.After(now) || last.End == zeroTime {
			last.End = now.Add(-time.Second)
		}
	}
	es = append(Entries{
		Entry{
			Project:     CLI.Start.Project,
			Description: CLI.Start.Description,
			Date:        time.Date(y, m, d, 0, 0, 0, 0, now.Location()),
			Start:       now,
		},
	},
		es...,
	)
	if err := saveEntries(es); err != nil {
		return err
	}
	return nil
}

func end() error {
	now := time.Now()
	y, m, d := now.Date()
	es, err := loadEntries()
	if err != nil {
		return fmt.Errorf("loading entries: %w", err)
	}
	if len(es) == 0 {
		return fmt.Errorf("cannot end running entry, data file is empty")
	}
	last := &es[0]
	fy, fm, fd := last.Date.Date()
	if (y > fy || m > fm || d > fd) && last.End == zeroTime {
		return fmt.Errorf("last entry is from previous day and isn't ended (%s)", last.String())
	}
	if last.End != zeroTime {
		return fmt.Errorf("last entry already ended (%s)", last)
	}
	last.End = time.Now()
	if err := saveEntries(es); err != nil {
		return err
	}
	fmt.Printf("Ending entry %s", last)
	return nil
}

func status() error {
	return nil
}

func report() error {
	return nil
}

M parser.go => parser.go +21 -17
@@ 71,12 71,9 @@ func parseDay(s *scanner) (Entries, error) {
		e.Date = d
		i := s.next()
		if i.typ == itemError {
			return nil, fmt.Errorf("1: %s", i.val)
			return nil, fmt.Errorf("%s", i.val)
		}
		if i.typ == itemEOF {
			break
		}
		if i.typ == itemDate {
		if i.typ == itemEOF || i.typ == itemDate {
			s.backup()
			break
		}


@@ 86,7 83,7 @@ func parseDay(s *scanner) (Entries, error) {
		e.Project = i.val
		i = s.next()
		if i.typ == itemError {
			return nil, fmt.Errorf("2: %s", i.val)
			return nil, fmt.Errorf("%s", i.val)
		}
		if i.typ != itemDescription {
			return nil, fmt.Errorf("expected description item at %s", s.l.posString())


@@ 94,7 91,7 @@ func parseDay(s *scanner) (Entries, error) {
		e.Description = i.val
		i = s.next()
		if i.typ == itemError {
			return nil, fmt.Errorf("3: %s", i.val)
			return nil, fmt.Errorf("%s", i.val)
		}
		if i.typ != itemTime {
			return nil, fmt.Errorf("expected time item at %s", s.l.posString())


@@ 106,16 103,14 @@ func parseDay(s *scanner) (Entries, error) {
		}
		i = s.next()
		if i.typ == itemError {
			return nil, fmt.Errorf("4: %s", i.val)
			return nil, fmt.Errorf("%s", i.val)
		}
		if i.typ == itemProject {
		if i.typ == itemProject || i.typ == itemDate {
			s.backup()
			entries = append(entries, e)
			continue
		}
		if i.typ == itemDate {
			s.backup()
			entries = append(entries, e)
			entries, err = AppendEntry(entries, e)
			if err != nil {
				return nil, fmt.Errorf("error at %s: %w", s.l.posString(), err)
			}
			continue
		}
		if i.typ == itemTime {


@@ 123,11 118,20 @@ func parseDay(s *scanner) (Entries, error) {
			if err != nil {
				return nil, fmt.Errorf("error parsing end time at %s: %w", s.l.posString(), err)
			}
			entries = append(entries, e)
			if e.Start.After(e.End) {
				return nil, fmt.Errorf("start time is after end time at %s", s.l.posString())
			}
			entries, err = AppendEntry(entries, e)
			if err != nil {
				return nil, fmt.Errorf("error at %s: %w", s.l.posString(), err)
			}
		}
		if i.typ == itemEOF {
			s.backup()
			entries = append(entries, e)
			entries, err = AppendEntry(entries, e)
			if err != nil {
				return nil, fmt.Errorf("error at %s: %w", s.l.posString(), err)
			}
			break
		}
	}