tme

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

commit 4e58099f16941ee6a6f5033774125da34088cd8e
parent 62d43b6978d914917b7be65c0fb0cabff12c8199
Author: Tomas Nemec <nemi@skaut.cz>
Date:   Sat, 11 Feb 2023 20:56:55 +0100

refactor

Diffstat:
Mcommand.go | 34++++++++++++++++------------------
Acompleted_entry.go | 71+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aentry.go | 45+++++++++++++++++++++++++++++++++++++++++++++
Aentry_test.go | 70++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mgo.mod | 2+-
Agroup.go | 124+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mmain.go | 4+---
Arunning_entry.go | 65+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atime.go | 159+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atime_test.go | 44++++++++++++++++++++++++++++++++++++++++++++
Dtme/completed_entry.go | 71-----------------------------------------------------------------------
Dtme/entry.go | 45---------------------------------------------
Dtme/entry_test.go | 84-------------------------------------------------------------------------------
Dtme/group.go | 124-------------------------------------------------------------------------------
Dtme/running_entry.go | 65-----------------------------------------------------------------
Dtme/time.go | 159-------------------------------------------------------------------------------
Dtme/time_test.go | 44--------------------------------------------
Mtme_test.go | 32+++++++++++++++-----------------
18 files changed, 611 insertions(+), 631 deletions(-)

diff --git a/command.go b/command.go @@ -9,14 +9,12 @@ import ( "path/filepath" "strings" "time" - - "gtms.dev/tme/tme" ) type Command struct { rootPath string args []string - entryTime *tme.Time + entryTime *Time } func (c *Command) nextArg() (string, error) { @@ -57,11 +55,11 @@ func (c Command) add() { os.Exit(1) } - group := tme.NewGroup(c.rootPath, groupArg) + group := NewGroup(c.rootPath, groupArg) // TODO(tms) 22.10.22: maybe ask if user want create new folder (fe: typo) group.Create() - entry, err := tme.NewCompletedEntry(start, stop) + entry, err := NewCompletedEntry(start, stop) if err != nil { fmt.Fprintln(os.Stderr, err) os.Exit(1) @@ -82,7 +80,7 @@ func (c Command) start() { } groupArg, _ := c.nextArg() - group := tme.NewGroup(c.rootPath, groupArg) + group := NewGroup(c.rootPath, groupArg) // TODO(tms) 22.10.22: maybe ask if user want create new folder (fe: typo) group.Create() @@ -96,7 +94,7 @@ func (c Command) start() { } } - entry := tme.NewRunningEntry(start) + entry := NewRunningEntry(start) if entry.Exists(group) { fmt.Fprintln(os.Stderr, "entry in this group already started") @@ -113,7 +111,7 @@ func (c Command) stop() { } groupArg, _ := c.nextArg() - group := tme.NewGroup(c.rootPath, groupArg) + group := NewGroup(c.rootPath, groupArg) stop := time.Now() stopArg, err := c.nextArg() @@ -125,7 +123,7 @@ func (c Command) stop() { } } - startEntry, err := tme.NewRunningEntryFromPath(group.ActivePath(), c.entryTime) + startEntry, err := NewRunningEntryFromPath(group.ActivePath(), c.entryTime) if err != nil { fmt.Fprintf(os.Stderr, "no entry running in %q\n", groupArg) os.Exit(1) @@ -166,7 +164,7 @@ func (c Command) ls() { rootPath, _ = c.nextArg() } - group := tme.NewGroup(c.rootPath, rootPath) + group := NewGroup(c.rootPath, rootPath) formatEntries(group, c.entryTime) } @@ -251,7 +249,7 @@ func (c Command) report() { } } -func formatEntries(group tme.Group, entryTime *tme.Time) { +func formatEntries(group Group, entryTime *Time) { entries, err := group.List(entryTime) if err != nil { fmt.Fprintln(os.Stderr, err) @@ -263,17 +261,17 @@ func formatEntries(group tme.Group, entryTime *tme.Time) { } } -func formatEntry(group tme.Group, entry tme.Entry) { +func formatEntry(group Group, entry Entry) { groupPath := group.Path duration := entry.Duration().Round(time.Second) timeLayout := "15:04:05 02/01/2006" var start, stop string switch e := entry.(type) { - case tme.CompletedEntry: + case CompletedEntry: start = e.Start.Format(timeLayout) stop = e.Stop.Format(timeLayout) - case tme.RunningEntry: + case RunningEntry: start = e.Start.Format(timeLayout) stop = "running" } @@ -281,8 +279,8 @@ func formatEntry(group tme.Group, entry tme.Entry) { fmt.Printf("%s\t%s\t%v\t%s\n", groupPath, start, stop, duration) } -func groupsRecursive(rootPath string, groupPath string) ([]tme.Group, error) { - var groups []tme.Group +func groupsRecursive(rootPath string, groupPath string) ([]Group, error) { + var groups []Group err := filepath.WalkDir(path.Join(rootPath, groupPath), func(path string, d fs.DirEntry, err error) error { if d.IsDir() { if path == groupPath { @@ -290,13 +288,13 @@ func groupsRecursive(rootPath string, groupPath string) ([]tme.Group, error) { } groupPath := strings.TrimPrefix(path, rootPath) groupPath = strings.TrimPrefix(groupPath, string(os.PathSeparator)) - group := tme.NewGroup(rootPath, groupPath) + group := NewGroup(rootPath, groupPath) groups = append(groups, group) } return nil }) if err != nil { - return []tme.Group{}, err + return []Group{}, err } return groups, nil diff --git a/completed_entry.go b/completed_entry.go @@ -0,0 +1,71 @@ +package main + +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/entry.go b/entry.go @@ -0,0 +1,45 @@ +package main + +import ( + "os" + "path" + "strings" + "time" +) + +type Entry interface { + Data() string + FileName() string + FullPath(group Group) string + Duration() time.Duration +} + +func NewEntryFromPath(entryPath string, entryTime *Time) (Entry, 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 + + base := path.Base(entryPath) + lines := strings.Split(string(data), "\n") + + if base == "active" { + start, _ := entryTime.ParseEntry(lines[0]) + return RunningEntry{ + Start: start, + }, nil + } else { + start, _ := entryTime.ParseEntry(lines[0]) + stop, _ := entryTime.ParseEntry(lines[1]) + return CompletedEntry{ + Start: start, + Stop: stop, + }, nil + } +} diff --git a/entry_test.go b/entry_test.go @@ -0,0 +1,70 @@ +package main + +import ( + "os" + "testing" +) + +func TestEntry(t *testing.T) { + entryTime := NewTimeToday() + + t.Run("positive duration is ok", func(b *testing.T) { + start, _ := entryTime.ParseArg("5:00") + stop, _ := entryTime.ParseArg("6:00") + _, err := NewCompletedEntry(start, stop) + if err != nil { + b.Error(err) + } + }) + + t.Run("same duration is kind of ok?", func(b *testing.T) { + start, _ := entryTime.ParseArg("6:00") + stop, _ := entryTime.ParseArg("6:00") + _, err := NewCompletedEntry(start, stop) + if err != nil { + b.Error(err) + } + }) + + t.Run("negative duration should fail", func(b *testing.T) { + start, _ := entryTime.ParseArg("6:00") + stop, _ := entryTime.ParseArg("5:00") + _, err := NewCompletedEntry(start, stop) + if err == nil { + b.Error(err) + } + }) +} + +func TestStartStopEntry(t *testing.T) { + basePath := setUp() + entryTime := NewTimeToday() + t.Cleanup(func() { tearDown(basePath) }) + + start, _ := entryTime.ParseArg("6:00") + startEntry := NewRunningEntry(start) + + group := NewGroup(basePath, "project") + group.Create() + + group.Add(startEntry) + if _, err := os.Stat(startEntry.FullPath(group)); os.IsNotExist(err) { + t.Fatalf(`want %q to exist`, startEntry.FullPath(group)) + } + + stop, _ := entryTime.ParseArg("7:00") + completedEntry, err := startEntry.Stop(stop) + if err != nil { + t.Error(err) + } + + group.Remove(startEntry) + if _, err := os.Stat(startEntry.FullPath(group)); !os.IsNotExist(err) { + t.Fatalf(`want %q to be deleted`, startEntry.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/go.mod b/go.mod @@ -1,3 +1,3 @@ module gtms.dev/tme -go 1.19 +go 1.20 diff --git a/group.go b/group.go @@ -0,0 +1,124 @@ +package main + +import ( + "errors" + "fmt" + "io/fs" + "os" + "path" + "path/filepath" +) + +type Group struct { + rootPath string + Path string +} + +func NewGroup(rootPath, path string) Group { + return Group{ + rootPath: rootPath, + Path: path, + } +} + +func (g Group) Create() error { + if err := os.MkdirAll(g.FullPath(), os.ModePerm); err != nil { + return err + } + return nil +} + +func (g Group) Name() string { + return path.Base(g.Path) +} + +func (g Group) FullPath() string { + return path.Join(g.rootPath, g.Path) +} + +func (g Group) EntryPath(entry Entry) string { + return path.Join(g.Path, entry.FileName()) +} + +func (g Group) ActivePath() string { + return path.Join(g.FullPath(), "active") +} + +func (g Group) Exists() bool { + if _, err := os.Stat(g.FullPath()); !os.IsNotExist(err) { + return true + } + return false +} + +func (g Group) Add(entry Entry) error { + err := os.WriteFile(entry.FullPath(g), []byte(entry.Data()), os.ModePerm) + if err != nil { + return err + } + return nil +} + +func (g Group) Remove(entry Entry) error { + return os.Remove(entry.FullPath(g)) +} + +func (g Group) List(entryTime *Time) ([]Entry, error) { + if !g.Exists() { + return []Entry{}, errors.New("Group '" + g.FullPath() + "' does not exist") + } + + var entries []Entry + err := filepath.WalkDir(g.FullPath(), func(path string, d fs.DirEntry, err error) error { + if d.IsDir() { + if path == g.FullPath() { + return nil + } + return fs.SkipDir + } + + entry, err := NewEntryFromPath(path, entryTime) + if err != nil { + fmt.Fprintln(os.Stderr, err) + os.Exit(1) + } + entries = append(entries, entry) + return nil + }) + if err != nil { + return []Entry{}, err + } + + 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/main.go b/main.go @@ -3,8 +3,6 @@ package main import ( "fmt" "os" - - "gtms.dev/tme/tme" ) const ( @@ -31,7 +29,7 @@ func main() { cmd := args[0] args = args[1:] // shift - entryTime := tme.NewTimeToday() + entryTime := NewTimeToday() command := Command{rootPath, args, entryTime} if cmd == "add" { command.add() diff --git a/running_entry.go b/running_entry.go @@ -0,0 +1,65 @@ +package main + +import ( + "fmt" + "os" + "path" + "strings" + "time" +) + +type RunningEntry struct { + Start time.Time +} + +func NewRunningEntry(start time.Time) RunningEntry { + return RunningEntry{ + Start: start, + } +} + +func NewRunningEntryFromPath(entryPath string, entryTime *Time) (RunningEntry, error) { + if _, err := os.Stat(entryPath); os.IsNotExist(err) { + return RunningEntry{}, err + } + + data, err := os.ReadFile(entryPath) + if err != nil { + return RunningEntry{}, err + } + + firstLine := strings.Split(string(data), "\n")[0] + start, _ := entryTime.ParseEntry(firstLine) + + return RunningEntry{ + Start: start, + }, nil + +} + +func (e RunningEntry) Data() string { + return fmt.Sprintf("%s\n", e.Start.Format(DataTimeLayout)) +} + +func (e RunningEntry) FileName() string { + return "active" +} + +func (e RunningEntry) FullPath(group Group) string { + return path.Join(group.FullPath(), e.FileName()) +} + +func (e RunningEntry) Exists(group Group) bool { + if _, err := os.Stat(e.FullPath(group)); !os.IsNotExist(err) { + return true + } + return false +} + +func (e RunningEntry) Stop(stop time.Time) (CompletedEntry, error) { + return NewCompletedEntry(e.Start, stop) +} + +func (e RunningEntry) Duration() time.Duration { + return time.Since(e.Start) +} diff --git a/time.go b/time.go @@ -0,0 +1,159 @@ +package main + +import ( + "fmt" + "strings" + "time" +) + +const ( + FileNameLayout = "0601021504" + DataTimeLayout = time.RFC3339 +) + +type Time struct { + context time.Time +} + +func NewTime(t time.Time) *Time { + return &Time{t} +} + +func NewTimeToday() *Time { + return &Time{time.Now()} +} + +func (et *Time) ParseArg(raw string) (time.Time, error) { + return et.ParseArgDir(raw, false) +} + +func (et *Time) ParseArgRight(raw string) (time.Time, error) { + return et.ParseArgDir(raw, true) +} + +// TODO(tms) 11.02.23: Simplify +func (et *Time) ParseArgDir(raw string, shiftRight bool) (time.Time, error) { + parts := strings.Split(raw, " ") + hour := et.context.Hour() + min := et.context.Minute() + sec := et.context.Second() + year, mon, day := et.context.Date() + + zeroTime := 0 + if shiftRight { + zeroTime = 59 + } + + var parsed bool + part := parts[0] + + t, err := time.Parse("15", part) + if err == nil { + parsed = true + hour = t.Hour() + min = zeroTime + sec = zeroTime + } + + t, err = time.Parse("15:4", part) + if err == nil { + parsed = true + hour = t.Hour() + min = t.Minute() + sec = zeroTime + } + + t, err = time.Parse("15:4:5", part) + if err == nil { + parsed = true + hour = t.Hour() + min = t.Minute() + sec = t.Second() + } + + t, err = time.Parse("2/1", part) + if err == nil { + parsed = true + hour = zeroTime + min = zeroTime + sec = zeroTime + day = t.Day() + mon = t.Month() + year = et.context.Year() + } + + t, err = time.Parse("1/2006", part) + if err == nil { + parsed = true + hour = zeroTime + min = zeroTime + sec = zeroTime + + zeroDay := 1 + firstOfMonth := time.Date(t.Year(), t.Month(), 1, 0, 0, 0, 0, et.context.Location()) + lastOfMonth := firstOfMonth.AddDate(0, 1, -1) + if shiftRight { + zeroDay = lastOfMonth.Day() + } + + day = zeroDay + + mon = t.Month() + year = t.Year() + } + + t, err = time.Parse("2/1/2006", part) + if err == nil { + parsed = true + hour = zeroTime + min = zeroTime + sec = zeroTime + day = t.Day() + mon = t.Month() + year = t.Year() + } + + if !parsed && len(raw) > 0 { + return time.Now(), fmt.Errorf("Cannot parse %q", raw) + } + + if len(parts) > 1 { + part = parts[1] + t, err := time.Parse("2", part) + if err == nil { + day = t.Day() + mon = et.context.Month() + year = et.context.Year() + } + + t, err = time.Parse("2/1", part) + if err == nil { + day = t.Day() + mon = t.Month() + year = et.context.Year() + } + + t, err = time.Parse("2/1/2006", part) + if err == nil { + day = t.Day() + mon = t.Month() + year = t.Year() + } + } + + return time.Date(year, mon, day, hour, min, sec, 0, et.context.Location()), nil +} + +func (et *Time) ParseEntry(raw string) (time.Time, error) { + t, err := time.Parse(DataTimeLayout, raw) + return time.Date( + t.Year(), + t.Month(), + t.Day(), + t.Hour(), + t.Minute(), + t.Second(), + 0, + et.context.Location(), + ), err +} diff --git a/time_test.go b/time_test.go @@ -0,0 +1,44 @@ +package main + +import ( + "testing" + "time" +) + +func TestParseArg(t *testing.T) { + layout := "15:4:5 2/1/2006" + context, _ := time.Parse(layout, "1:1:1 1/1/2001") + var check = func(raw string, expected string) { + t.Helper() + t.Run("", func(b *testing.T) { + b.Helper() + tmeTime := NewTime(context) + expected, _ := time.Parse(layout, expected) + parsed, err := tmeTime.ParseArg(raw) + if err != nil { + b.Fatal(err) + } + + if !parsed.Equal(expected) { + b.Errorf("Parsed %v; Expected %v", parsed, expected) + } + }) + } + + check("5", "5:0:0 1/1/2001") + check("5:5", "5:5:0 1/1/2001") + check("5:5:5", "5:5:5 1/1/2001") + check("5:5 5", "5:5:0 5/1/2001") + check("5:5 5/5", "5:5:0 5/5/2001") + check("5:5 5/5/2005", "5:5:0 5/5/2005") + + check("5/5", "0:0:0 5/5/2001") + check("5/5/2005", "0:0:0 5/5/2005") + check("5/2005", "0:0:0 1/5/2005") + + check("5:05", "5:5:0 1/1/2001") + check("05:05", "5:5:0 1/1/2001") + check("05:5 05/5/2005", "5:5:0 5/5/2005") + check("5:5 5/05/2005", "5:5:0 5/5/2005") + check("5:05 5/05/2005", "5:5:0 5/5/2005") +} diff --git a/tme/completed_entry.go b/tme/completed_entry.go @@ -1,71 +0,0 @@ -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 @@ -1,45 +0,0 @@ -package tme - -import ( - "os" - "path" - "strings" - "time" -) - -type Entry interface { - Data() string - FileName() string - FullPath(group Group) string - Duration() time.Duration -} - -func NewEntryFromPath(entryPath string, entryTime *Time) (Entry, 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 - - base := path.Base(entryPath) - lines := strings.Split(string(data), "\n") - - if base == "active" { - start, _ := entryTime.ParseEntry(lines[0]) - return RunningEntry{ - Start: start, - }, nil - } else { - start, _ := entryTime.ParseEntry(lines[0]) - stop, _ := entryTime.ParseEntry(lines[1]) - return CompletedEntry{ - Start: start, - Stop: stop, - }, nil - } -} diff --git a/tme/entry_test.go b/tme/entry_test.go @@ -1,84 +0,0 @@ -package tme - -import ( - "os" - "testing" -) - -func TestEntry(t *testing.T) { - entryTime := NewTimeToday() - - t.Run("positive duration is ok", func(b *testing.T) { - start, _ := entryTime.ParseArg("5:00") - stop, _ := entryTime.ParseArg("6:00") - _, err := NewCompletedEntry(start, stop) - if err != nil { - b.Error(err) - } - }) - - t.Run("same duration is kind of ok?", func(b *testing.T) { - start, _ := entryTime.ParseArg("6:00") - stop, _ := entryTime.ParseArg("6:00") - _, err := NewCompletedEntry(start, stop) - if err != nil { - b.Error(err) - } - }) - - t.Run("negative duration should fail", func(b *testing.T) { - start, _ := entryTime.ParseArg("6:00") - stop, _ := entryTime.ParseArg("5:00") - _, err := NewCompletedEntry(start, stop) - if err == nil { - b.Error(err) - } - }) -} - -func TestStartStopEntry(t *testing.T) { - basePath := setUp() - entryTime := NewTimeToday() - t.Cleanup(func() { tearDown(basePath) }) - - start, _ := entryTime.ParseArg("6:00") - startEntry := NewRunningEntry(start) - - group := NewGroup(basePath, "project") - group.Create() - - group.Add(startEntry) - if _, err := os.Stat(startEntry.FullPath(group)); os.IsNotExist(err) { - t.Fatalf(`want %q to exist`, startEntry.FullPath(group)) - } - - stop, _ := entryTime.ParseArg("7:00") - completedEntry, err := startEntry.Stop(stop) - if err != nil { - t.Error(err) - } - - group.Remove(startEntry) - if _, err := os.Stat(startEntry.FullPath(group)); !os.IsNotExist(err) { - t.Fatalf(`want %q to be deleted`, startEntry.FullPath(group)) - } - - group.Add(completedEntry) - if _, err := os.Stat(completedEntry.FullPath(group)); os.IsNotExist(err) { - t.Fatalf(`want %q to exist`, completedEntry.FullPath(group)) - } -} - -func setUp() string { - tempDir, err := os.MkdirTemp("", "tme_*") - if err != nil { - panic(err) - } - - os.Setenv("TME_DIR", tempDir) - return tempDir -} - -func tearDown(tempDir string) { - os.RemoveAll(tempDir) -} diff --git a/tme/group.go b/tme/group.go @@ -1,124 +0,0 @@ -package tme - -import ( - "errors" - "fmt" - "io/fs" - "os" - "path" - "path/filepath" -) - -type Group struct { - rootPath string - Path string -} - -func NewGroup(rootPath, path string) Group { - return Group{ - rootPath: rootPath, - Path: path, - } -} - -func (g Group) Create() error { - if err := os.MkdirAll(g.FullPath(), os.ModePerm); err != nil { - return err - } - return nil -} - -func (g Group) Name() string { - return path.Base(g.Path) -} - -func (g Group) FullPath() string { - return path.Join(g.rootPath, g.Path) -} - -func (g Group) EntryPath(entry Entry) string { - return path.Join(g.Path, entry.FileName()) -} - -func (g Group) ActivePath() string { - return path.Join(g.FullPath(), "active") -} - -func (g Group) Exists() bool { - if _, err := os.Stat(g.FullPath()); !os.IsNotExist(err) { - return true - } - return false -} - -func (g Group) Add(entry Entry) error { - err := os.WriteFile(entry.FullPath(g), []byte(entry.Data()), os.ModePerm) - if err != nil { - return err - } - return nil -} - -func (g Group) Remove(entry Entry) error { - return os.Remove(entry.FullPath(g)) -} - -func (g Group) List(entryTime *Time) ([]Entry, error) { - if !g.Exists() { - return []Entry{}, errors.New("Group '" + g.FullPath() + "' does not exist") - } - - var entries []Entry - err := filepath.WalkDir(g.FullPath(), func(path string, d fs.DirEntry, err error) error { - if d.IsDir() { - if path == g.FullPath() { - return nil - } - return fs.SkipDir - } - - entry, err := NewEntryFromPath(path, entryTime) - if err != nil { - fmt.Fprintln(os.Stderr, err) - os.Exit(1) - } - entries = append(entries, entry) - return nil - }) - if err != nil { - return []Entry{}, err - } - - 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 @@ -1,65 +0,0 @@ -package tme - -import ( - "fmt" - "os" - "path" - "strings" - "time" -) - -type RunningEntry struct { - Start time.Time -} - -func NewRunningEntry(start time.Time) RunningEntry { - return RunningEntry{ - Start: start, - } -} - -func NewRunningEntryFromPath(entryPath string, entryTime *Time) (RunningEntry, error) { - if _, err := os.Stat(entryPath); os.IsNotExist(err) { - return RunningEntry{}, err - } - - data, err := os.ReadFile(entryPath) - if err != nil { - return RunningEntry{}, err - } - - firstLine := strings.Split(string(data), "\n")[0] - start, _ := entryTime.ParseEntry(firstLine) - - return RunningEntry{ - Start: start, - }, nil - -} - -func (e RunningEntry) Data() string { - return fmt.Sprintf("%s\n", e.Start.Format(DataTimeLayout)) -} - -func (e RunningEntry) FileName() string { - return "active" -} - -func (e RunningEntry) FullPath(group Group) string { - return path.Join(group.FullPath(), e.FileName()) -} - -func (e RunningEntry) Exists(group Group) bool { - if _, err := os.Stat(e.FullPath(group)); !os.IsNotExist(err) { - return true - } - return false -} - -func (e RunningEntry) Stop(stop time.Time) (CompletedEntry, error) { - return NewCompletedEntry(e.Start, stop) -} - -func (e RunningEntry) Duration() time.Duration { - return time.Since(e.Start) -} diff --git a/tme/time.go b/tme/time.go @@ -1,159 +0,0 @@ -package tme - -import ( - "fmt" - "strings" - "time" -) - -const ( - FileNameLayout = "0601021504" - DataTimeLayout = time.RFC3339 -) - -type Time struct { - context time.Time -} - -func NewTime(t time.Time) *Time { - return &Time{t} -} - -func NewTimeToday() *Time { - return &Time{time.Now()} -} - -func (et *Time) ParseArg(raw string) (time.Time, error) { - return et.ParseArgDir(raw, false) -} - -func (et *Time) ParseArgRight(raw string) (time.Time, error) { - return et.ParseArgDir(raw, true) -} - -// TODO(tms) 11.02.23: Simplify -func (et *Time) ParseArgDir(raw string, shiftRight bool) (time.Time, error) { - parts := strings.Split(raw, " ") - hour := et.context.Hour() - min := et.context.Minute() - sec := et.context.Second() - year, mon, day := et.context.Date() - - zeroTime := 0 - if shiftRight { - zeroTime = 59 - } - - var parsed bool - part := parts[0] - - t, err := time.Parse("15", part) - if err == nil { - parsed = true - hour = t.Hour() - min = zeroTime - sec = zeroTime - } - - t, err = time.Parse("15:4", part) - if err == nil { - parsed = true - hour = t.Hour() - min = t.Minute() - sec = zeroTime - } - - t, err = time.Parse("15:4:5", part) - if err == nil { - parsed = true - hour = t.Hour() - min = t.Minute() - sec = t.Second() - } - - t, err = time.Parse("2/1", part) - if err == nil { - parsed = true - hour = zeroTime - min = zeroTime - sec = zeroTime - day = t.Day() - mon = t.Month() - year = et.context.Year() - } - - t, err = time.Parse("1/2006", part) - if err == nil { - parsed = true - hour = zeroTime - min = zeroTime - sec = zeroTime - - zeroDay := 1 - firstOfMonth := time.Date(t.Year(), t.Month(), 1, 0, 0, 0, 0, et.context.Location()) - lastOfMonth := firstOfMonth.AddDate(0, 1, -1) - if shiftRight { - zeroDay = lastOfMonth.Day() - } - - day = zeroDay - - mon = t.Month() - year = t.Year() - } - - t, err = time.Parse("2/1/2006", part) - if err == nil { - parsed = true - hour = zeroTime - min = zeroTime - sec = zeroTime - day = t.Day() - mon = t.Month() - year = t.Year() - } - - if !parsed && len(raw) > 0 { - return time.Now(), fmt.Errorf("Cannot parse %q", raw) - } - - if len(parts) > 1 { - part = parts[1] - t, err := time.Parse("2", part) - if err == nil { - day = t.Day() - mon = et.context.Month() - year = et.context.Year() - } - - t, err = time.Parse("2/1", part) - if err == nil { - day = t.Day() - mon = t.Month() - year = et.context.Year() - } - - t, err = time.Parse("2/1/2006", part) - if err == nil { - day = t.Day() - mon = t.Month() - year = t.Year() - } - } - - return time.Date(year, mon, day, hour, min, sec, 0, et.context.Location()), nil -} - -func (et *Time) ParseEntry(raw string) (time.Time, error) { - t, err := time.Parse(DataTimeLayout, raw) - return time.Date( - t.Year(), - t.Month(), - t.Day(), - t.Hour(), - t.Minute(), - t.Second(), - 0, - et.context.Location(), - ), err -} diff --git a/tme/time_test.go b/tme/time_test.go @@ -1,44 +0,0 @@ -package tme - -import ( - "testing" - "time" -) - -func TestParseArg(t *testing.T) { - layout := "15:4:5 2/1/2006" - context, _ := time.Parse(layout, "1:1:1 1/1/2001") - var check = func(raw string, expected string) { - t.Helper() - t.Run("", func(b *testing.T) { - b.Helper() - tmeTime := NewTime(context) - expected, _ := time.Parse(layout, expected) - parsed, err := tmeTime.ParseArg(raw) - if err != nil { - b.Fatal(err) - } - - if !parsed.Equal(expected) { - b.Errorf("Parsed %v; Expected %v", parsed, expected) - } - }) - } - - check("5", "5:0:0 1/1/2001") - check("5:5", "5:5:0 1/1/2001") - check("5:5:5", "5:5:5 1/1/2001") - check("5:5 5", "5:5:0 5/1/2001") - check("5:5 5/5", "5:5:0 5/5/2001") - check("5:5 5/5/2005", "5:5:0 5/5/2005") - - check("5/5", "0:0:0 5/5/2001") - check("5/5/2005", "0:0:0 5/5/2005") - check("5/2005", "0:0:0 1/5/2005") - - check("5:05", "5:5:0 1/1/2001") - check("05:05", "5:5:0 1/1/2001") - check("05:5 05/5/2005", "5:5:0 5/5/2005") - check("5:5 5/05/2005", "5:5:0 5/5/2005") - check("5:05 5/05/2005", "5:5:0 5/5/2005") -} diff --git a/tme_test.go b/tme_test.go @@ -6,19 +6,17 @@ import ( "strings" "testing" "time" - - "gtms.dev/tme/tme" ) func TestAdd(t *testing.T) { basePath := setUp() t.Cleanup(func() { tearDown(basePath) }) - entryTime := tme.NewTimeToday() + entryTime := NewTimeToday() group := createAndCheckGroup(t, basePath, "project") start, _ := entryTime.ParseArg("10:00") stop, _ := entryTime.ParseArg("11:00") - tme, err := tme.NewCompletedEntry(start, stop) + tme, err := NewCompletedEntry(start, stop) if err != nil { t.Error(err) } @@ -37,12 +35,12 @@ func TestStart(t *testing.T) { t.Cleanup(func() { tearDown(basePath) }) entryDir := "project" fullEntryDir := strings.Join([]string{basePath, entryDir}, "/") - entryTime := tme.NewTimeToday() + entryTime := NewTimeToday() group := createAndCheckGroup(t, basePath, "project") start, _ := entryTime.ParseArg("10:00") - entry := tme.NewRunningEntry(start) + entry := NewRunningEntry(start) group.Add(entry) if _, err := os.Stat(fullEntryDir); os.IsNotExist(err) { @@ -59,17 +57,17 @@ func TestStart(t *testing.T) { func TestStop(t *testing.T) { basePath := setUp() t.Cleanup(func() { tearDown(basePath) }) - entryTime := tme.NewTimeToday() + entryTime := NewTimeToday() - group := tme.NewGroup(basePath, "project") + group := NewGroup(basePath, "project") group.Create() start, _ := entryTime.ParseArg("10:00") - startEntry := tme.NewRunningEntry(start) + startEntry := NewRunningEntry(start) group.Add(startEntry) stop, _ := entryTime.ParseArg("11:00") - entry, err := tme.NewCompletedEntry(startEntry.Start, stop) + entry, err := NewCompletedEntry(startEntry.Start, stop) if err != nil { t.Error(err) } @@ -84,14 +82,14 @@ func TestStop(t *testing.T) { // func ExampleList() { // basePath := setUp() -// entryTime := tme.NewTime() +// entryTime := NewTime() -// group := tme.NewGroup(basePath, "tme") +// group := NewGroup(basePath, "tme") // group.Create() // start, _ := entryTime.ParseArg("10:00") // stop, _ := entryTime.ParseArg("11:00") -// entry, _ := tme.NewCompletedEntry(start, stop) +// entry, _ := NewCompletedEntry(start, stop) // group.Add(entry) // lines, err := group.FormatList(entryTime) @@ -109,7 +107,7 @@ func TestStop(t *testing.T) { // tearDown(basePath) // } -func checkEntryFile(start time.Time, stop time.Time, data []byte, entryTime *tme.Time, t *testing.T) { +func checkEntryFile(start time.Time, stop time.Time, data []byte, entryTime *Time, t *testing.T) { lines := strings.Split(string(data), "\n") expectLines := 3 if len(lines) != expectLines { @@ -129,7 +127,7 @@ func checkEntryFile(start time.Time, stop time.Time, data []byte, entryTime *tme } } -func checkStartEntryFile(start time.Time, data []byte, entryTime *tme.Time, t *testing.T) { +func checkStartEntryFile(start time.Time, data []byte, entryTime *Time, t *testing.T) { lines := strings.Split(string(data), "\n") expectLines := 2 if len(lines) != expectLines { @@ -142,8 +140,8 @@ func checkStartEntryFile(start time.Time, data []byte, entryTime *tme.Time, t *t t.Fatalf("time mismatch! %v != %v", start, lineStart) } } -func createAndCheckGroup(t *testing.T, basePath, groupName string) tme.Group { - group := tme.NewGroup(basePath, groupName) +func createAndCheckGroup(t *testing.T, basePath, groupName string) Group { + group := NewGroup(basePath, groupName) group.Create() if _, err := os.Stat(group.FullPath()); os.IsNotExist(err) { t.Fatalf(`want %q to exist`, group.Path)