tme

Toggl like Time Manager
git clone git://gtms.dev:tme
Log | Files | Refs

commit 43b7ada48fb26971cb5e0cb256d82157fb47a736
parent e715a580b779311dc42bf90a0d7802891f80c546
Author: Tomas Nemec <nemi@skaut.cz>
Date:   Fri, 10 Feb 2023 20:34:47 +0100

feat: report

Diffstat:
Mcommand.go | 90++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-------------------
Mmain.go | 4+++-
Atme/completed_entry.go | 71+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mtme/entry.go | 6+++---
Mtme/entry_test.go | 18+++++++++---------
Dtme/final_entry.go | 71-----------------------------------------------------------------------
Mtme/group.go | 31+++++++++++++++++++++++++++++++
Mtme/running_entry.go | 4++--
Mtme/time.go | 6+++++-
Mtme_test.go | 12++++++------
10 files changed, 199 insertions(+), 114 deletions(-)

diff --git a/command.go b/command.go @@ -37,7 +37,7 @@ func (c *Command) nextArg() (string, error) { func (c Command) add() { if len(c.args) != 3 { - fmt.Fprintln(os.Stderr, "add <path> <start> <stop>") + fmt.Fprintln(os.Stderr, "add <group> <start> <stop>") os.Exit(1) } @@ -61,7 +61,7 @@ func (c Command) add() { // TODO(tms) 22.10.22: maybe ask if user want create new folder (fe: typo) group.Create() - entry, err := tme.NewFinalEntry(start, stop) + entry, err := tme.NewCompletedEntry(start, stop) if err != nil { fmt.Fprintln(os.Stderr, err) os.Exit(1) @@ -77,7 +77,7 @@ func (c Command) add() { func (c Command) start() { if len(c.args) != 2 { - fmt.Fprintln(os.Stderr, "start <path> <start>") + fmt.Fprintln(os.Stderr, "start <group> <start>") os.Exit(1) } @@ -106,7 +106,7 @@ func (c Command) start() { func (c Command) stop() { if len(c.args) != 2 { - fmt.Fprintln(os.Stderr, "stop <path> <stop>") + fmt.Fprintln(os.Stderr, "stop <group> <stop>") os.Exit(1) } @@ -153,7 +153,7 @@ func (c Command) stop() { func (c Command) ls() { if len(c.args) > 1 { - fmt.Fprintln(os.Stderr, "ls [<path>]") + fmt.Fprintln(os.Stderr, "ls [<group>]") os.Exit(1) } @@ -168,7 +168,7 @@ func (c Command) ls() { func (c Command) lsr() { if len(c.args) > 1 { - fmt.Fprintln(os.Stderr, "ls [<path>]") + fmt.Fprintln(os.Stderr, "lsr [<group>]") os.Exit(1) } @@ -177,7 +177,7 @@ func (c Command) lsr() { groupPath, _ = c.nextArg() } - groups, err := groupTree(c.rootPath, groupPath) + groups, err := groupsRecursive(c.rootPath, groupPath) if err != nil { fmt.Fprintln(os.Stderr, err) os.Exit(1) @@ -188,6 +188,51 @@ func (c Command) lsr() { } } +func (c Command) report() { + if len(c.args) < 1 || len(c.args) > 2 { + fmt.Fprintln(os.Stderr, "report <since> [<group>]") + os.Exit(1) + } + + sinceArg, _ := c.nextArg() + sinceTime, err := c.entryTime.ParseArg(sinceArg) + if err != nil { + fmt.Fprintf(os.Stderr, "[since] time (%s) could not be parsed\n", sinceArg) + os.Exit(1) + } + + groupPath := "" + if len(c.args) == 1 { + groupPath, _ = c.nextArg() + } + + groups, err := groupsRecursive(c.rootPath, groupPath) + if err != nil { + fmt.Fprintln(os.Stderr, err) + os.Exit(1) + } + + var duration time.Duration + for _, group := range groups { + entries, err := group.ListCompleted(c.entryTime) + if err != nil { + fmt.Fprintln(os.Stderr, err) + os.Exit(1) + } + + for _, entry := range entries { + if entry.Start.After(sinceTime) { + duration += entry.Duration() + formatEntry(group, entry) + } + } + } + + if duration > 0 { + fmt.Printf("%s\n", duration.Round(time.Second)) + } +} + func formatEntries(group tme.Group, entryTime *tme.Time) { entries, err := group.List(entryTime) if err != nil { @@ -196,24 +241,27 @@ func formatEntries(group tme.Group, entryTime *tme.Time) { } for _, entry := range entries { + formatEntry(group, entry) + } +} - groupPath := group.Path - duration := entry.Duration().Round(time.Second) - - switch e := entry.(type) { - case tme.FinalEntry: - var start string - start = e.Start.Format(tme.DateTimeLayout) - stop := e.Stop.Format(tme.DateTimeLayout) - fmt.Printf("%v\t%v\t%v\t%v\n", groupPath, start, stop, duration) - case tme.RunningEntry: - start := e.Start.Format(tme.DateTimeLayout) - fmt.Printf("%v\t%v\t%s\t%v\n", groupPath, start, "running", duration) - } +func formatEntry(group tme.Group, entry tme.Entry) { + groupPath := group.Path + duration := entry.Duration().Round(time.Second) + + switch e := entry.(type) { + case tme.CompletedEntry: + var start string + start = e.Start.Format(tme.DateTimeLayout) + stop := e.Stop.Format(tme.DateTimeLayout) + fmt.Printf("%s\t%s\t%v\t%s\n", groupPath, start, stop, duration) + case tme.RunningEntry: + start := e.Start.Format(tme.DateTimeLayout) + fmt.Printf("%s\t%s\t%s\t%s\n", groupPath, start, "running", duration) } } -func groupTree(rootPath string, groupPath string) ([]tme.Group, error) { +func groupsRecursive(rootPath string, groupPath string) ([]tme.Group, error) { var groups []tme.Group err := filepath.WalkDir(path.Join(rootPath, groupPath), func(path string, d fs.DirEntry, err error) error { if d.IsDir() { diff --git a/main.go b/main.go @@ -31,7 +31,7 @@ func main() { cmd := args[0] args = args[1:] // shift - entryTime := tme.NewTime() + entryTime := tme.NewTimeToday() command := Command{rootPath, args, entryTime} if cmd == "add" { command.add() @@ -43,5 +43,7 @@ func main() { command.ls() } else if cmd == "lsr" { command.lsr() + } else if cmd == "report" { + command.report() } } diff --git a/tme/completed_entry.go b/tme/completed_entry.go @@ -0,0 +1,71 @@ +package tme + +import ( + "errors" + "fmt" + "os" + "path" + "strings" + "time" +) + +type CompletedEntry struct { + Start time.Time + Stop time.Time +} + +func NewCompletedEntry(start time.Time, stop time.Time) (CompletedEntry, error) { + if start.After(stop) { + return CompletedEntry{}, errors.New("duration must be positive") + } + + return CompletedEntry{ + Start: start, + Stop: stop, + }, nil +} + +func NewCompletedEntryFromPath(entryPath string, entryTime *Time) (CompletedEntry, error) { + if _, err := os.Stat(entryPath); os.IsNotExist(err) { + return CompletedEntry{}, err + } + + data, err := os.ReadFile(entryPath) + if err != nil { + return CompletedEntry{}, err + } + + // TODO(tms) 11.11.22: format check + + lines := strings.Split(string(data), "\n") + start, _ := entryTime.ParseEntry(lines[0]) + stop, _ := entryTime.ParseEntry(lines[1]) + + return CompletedEntry{ + Start: start, + Stop: stop, + }, nil +} + +func (e CompletedEntry) Data() string { + return fmt.Sprintf("%s\n%s\n", e.Start.Format(DataTimeLayout), e.Stop.Format(DataTimeLayout)) +} + +func (e CompletedEntry) FileName() string { + return strings.Join([]string{e.Start.Format(FileNameLayout), e.Stop.Format(FileNameLayout)}, "_") +} + +func (e CompletedEntry) FullPath(group Group) string { + return path.Join(group.FullPath(), e.FileName()) +} + +func (e CompletedEntry) Exists(group Group) bool { + if _, err := os.Stat(e.FullPath(group)); !os.IsNotExist(err) { + return true + } + return false +} + +func (e CompletedEntry) Duration() time.Duration { + return e.Stop.Sub(e.Start) +} diff --git a/tme/entry.go b/tme/entry.go @@ -16,12 +16,12 @@ type Entry interface { func NewEntryFromPath(entryPath string, entryTime *Time) (Entry, error) { if _, err := os.Stat(entryPath); os.IsNotExist(err) { - return FinalEntry{}, err + return CompletedEntry{}, err } data, err := os.ReadFile(entryPath) if err != nil { - return FinalEntry{}, err + return CompletedEntry{}, err } // TODO(tms) 11.11.22: format check @@ -37,7 +37,7 @@ func NewEntryFromPath(entryPath string, entryTime *Time) (Entry, error) { } else { start, _ := entryTime.ParseEntry(lines[0]) stop, _ := entryTime.ParseEntry(lines[1]) - return FinalEntry{ + return CompletedEntry{ Start: start, Stop: stop, }, nil diff --git a/tme/entry_test.go b/tme/entry_test.go @@ -6,12 +6,12 @@ import ( ) func TestEntry(t *testing.T) { - entryTime := NewTime() + entryTime := NewTimeToday() t.Run("positive duration is ok", func(b *testing.T) { start, _ := entryTime.ParseArg("5:00") stop, _ := entryTime.ParseArg("6:00") - _, err := NewFinalEntry(start, stop) + _, err := NewCompletedEntry(start, stop) if err != nil { b.Error(err) } @@ -20,7 +20,7 @@ func TestEntry(t *testing.T) { t.Run("same duration is kind of ok?", func(b *testing.T) { start, _ := entryTime.ParseArg("6:00") stop, _ := entryTime.ParseArg("6:00") - _, err := NewFinalEntry(start, stop) + _, err := NewCompletedEntry(start, stop) if err != nil { b.Error(err) } @@ -29,7 +29,7 @@ func TestEntry(t *testing.T) { t.Run("negative duration should fail", func(b *testing.T) { start, _ := entryTime.ParseArg("6:00") stop, _ := entryTime.ParseArg("5:00") - _, err := NewFinalEntry(start, stop) + _, err := NewCompletedEntry(start, stop) if err == nil { b.Error(err) } @@ -38,7 +38,7 @@ func TestEntry(t *testing.T) { func TestStartStopEntry(t *testing.T) { basePath := setUp() - entryTime := NewTime() + entryTime := NewTimeToday() t.Cleanup(func() { tearDown(basePath) }) start, _ := entryTime.ParseArg("6:00") @@ -53,7 +53,7 @@ func TestStartStopEntry(t *testing.T) { } stop, _ := entryTime.ParseArg("7:00") - finalEntry, err := startEntry.Stop(stop) + completedEntry, err := startEntry.Stop(stop) if err != nil { t.Error(err) } @@ -63,9 +63,9 @@ func TestStartStopEntry(t *testing.T) { t.Fatalf(`want %q to be deleted`, startEntry.FullPath(group)) } - group.Add(finalEntry) - if _, err := os.Stat(finalEntry.FullPath(group)); os.IsNotExist(err) { - t.Fatalf(`want %q to exist`, finalEntry.FullPath(group)) + group.Add(completedEntry) + if _, err := os.Stat(completedEntry.FullPath(group)); os.IsNotExist(err) { + t.Fatalf(`want %q to exist`, completedEntry.FullPath(group)) } } diff --git a/tme/final_entry.go b/tme/final_entry.go @@ -1,71 +0,0 @@ -package tme - -import ( - "errors" - "fmt" - "os" - "path" - "strings" - "time" -) - -type FinalEntry struct { - Start time.Time - Stop time.Time -} - -func NewFinalEntry(start time.Time, stop time.Time) (FinalEntry, error) { - if start.After(stop) { - return FinalEntry{}, errors.New("duration must be positive") - } - - return FinalEntry{ - Start: start, - Stop: stop, - }, nil -} - -func NewFinalEntryFromPath(entryPath string, entryTime *Time) (FinalEntry, error) { - if _, err := os.Stat(entryPath); os.IsNotExist(err) { - return FinalEntry{}, err - } - - data, err := os.ReadFile(entryPath) - if err != nil { - return FinalEntry{}, err - } - - // TODO(tms) 11.11.22: format check - - lines := strings.Split(string(data), "\n") - start, _ := entryTime.ParseEntry(lines[0]) - stop, _ := entryTime.ParseEntry(lines[1]) - - return FinalEntry{ - Start: start, - Stop: stop, - }, nil -} - -func (e FinalEntry) Data() string { - return fmt.Sprintf("%s\n%s\n", e.Start.Format(DataTimeLayout), e.Stop.Format(DataTimeLayout)) -} - -func (e FinalEntry) FileName() string { - return strings.Join([]string{e.Start.Format(FileNameLayout), e.Stop.Format(FileNameLayout)}, "_") -} - -func (e FinalEntry) FullPath(group Group) string { - return path.Join(group.FullPath(), e.FileName()) -} - -func (e FinalEntry) Exists(group Group) bool { - if _, err := os.Stat(e.FullPath(group)); !os.IsNotExist(err) { - return true - } - return false -} - -func (e FinalEntry) Duration() time.Duration { - return e.Stop.Sub(e.Start) -} diff --git a/tme/group.go b/tme/group.go @@ -91,3 +91,34 @@ func (g Group) List(entryTime *Time) ([]Entry, error) { return entries, nil } + +func (g Group) ListCompleted(entryTime *Time) ([]CompletedEntry, error) { + entries, err := g.List(entryTime) + if err != nil { + return []CompletedEntry{}, err + } + + return listFilter[CompletedEntry](entries) +} + +func (g Group) ListRunning(entryTime *Time) ([]RunningEntry, error) { + entries, err := g.List(entryTime) + if err != nil { + return []RunningEntry{}, err + } + + return listFilter[RunningEntry](entries) +} + +func listFilter[K CompletedEntry | RunningEntry](entries []Entry) ([]K, error) { + var completedEntries []K + + for _, entry := range entries { + switch e := entry.(type) { + case K: + completedEntries = append(completedEntries, e) + } + } + + return completedEntries, nil +} diff --git a/tme/running_entry.go b/tme/running_entry.go @@ -56,8 +56,8 @@ func (e RunningEntry) Exists(group Group) bool { return false } -func (e RunningEntry) Stop(stop time.Time) (FinalEntry, error) { - return NewFinalEntry(e.Start, stop) +func (e RunningEntry) Stop(stop time.Time) (CompletedEntry, error) { + return NewCompletedEntry(e.Start, stop) } func (e RunningEntry) Duration() time.Duration { diff --git a/tme/time.go b/tme/time.go @@ -13,7 +13,11 @@ type Time struct { context time.Time } -func NewTime() *Time { +func NewTime(t time.Time) *Time { + return &Time{t} +} + +func NewTimeToday() *Time { return &Time{time.Now()} } diff --git a/tme_test.go b/tme_test.go @@ -13,12 +13,12 @@ import ( func TestAdd(t *testing.T) { basePath := setUp() t.Cleanup(func() { tearDown(basePath) }) - entryTime := tme.NewTime() + entryTime := tme.NewTimeToday() group := createAndCheckGroup(t, basePath, "project") start, _ := entryTime.ParseArg("10:00") stop, _ := entryTime.ParseArg("11:00") - tme, err := tme.NewFinalEntry(start, stop) + tme, err := tme.NewCompletedEntry(start, stop) if err != nil { t.Error(err) } @@ -37,7 +37,7 @@ func TestStart(t *testing.T) { t.Cleanup(func() { tearDown(basePath) }) entryDir := "project" fullEntryDir := strings.Join([]string{basePath, entryDir}, "/") - entryTime := tme.NewTime() + entryTime := tme.NewTimeToday() group := createAndCheckGroup(t, basePath, "project") @@ -59,7 +59,7 @@ func TestStart(t *testing.T) { func TestStop(t *testing.T) { basePath := setUp() t.Cleanup(func() { tearDown(basePath) }) - entryTime := tme.NewTime() + entryTime := tme.NewTimeToday() group := tme.NewGroup(basePath, "project") group.Create() @@ -69,7 +69,7 @@ func TestStop(t *testing.T) { group.Add(startEntry) stop, _ := entryTime.ParseArg("11:00") - entry, err := tme.NewFinalEntry(startEntry.Start, stop) + entry, err := tme.NewCompletedEntry(startEntry.Start, stop) if err != nil { t.Error(err) } @@ -91,7 +91,7 @@ func TestStop(t *testing.T) { // start, _ := entryTime.ParseArg("10:00") // stop, _ := entryTime.ParseArg("11:00") -// entry, _ := tme.NewFinalEntry(start, stop) +// entry, _ := tme.NewCompletedEntry(start, stop) // group.Add(entry) // lines, err := group.FormatList(entryTime)