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