tme

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

commit 4d8db75d7019b916b1b2c2cac917fbcb97f925b1
parent d30f0b07dd9a6863da2dbcefecfc28e949b0b140
Author: Tomas Nemec <nemi@skaut.cz>
Date:   Tue, 21 Feb 2023 16:07:47 +0100

update

Diffstat:
Mcommand.go | 41++++++++++++++++++++---------------------
Mentry.go | 50+++++++++++++++++++-------------------------------
Mentry_test.go | 10++++------
Mrange.go | 19+++++++++++++------
Mrepository.go | 124+++++++++++++++++++++----------------------------------------------------------
Arepository_test.go | 54++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mtme_test.go | 9++++++---
7 files changed, 148 insertions(+), 159 deletions(-)

diff --git a/command.go b/command.go @@ -53,9 +53,9 @@ func (c Command) add() { group := NewGroup(groupArg) - entry, err := NewCompletedEntry(start, stop) + entry, err := NewCompleted(start, stop) if err != nil { - fmt.Fprintln(os.Stderr, err) + fmt.Fprint(os.Stderr, err) os.Exit(1) } @@ -86,7 +86,7 @@ func (c Command) start() { } } - entry := NewRunningEntry(start) + entry := NewRunning(start) if c.repository.ExistsEntry(group, entry) { fmt.Fprintln(os.Stderr, "entry in this group already started") @@ -115,24 +115,20 @@ func (c Command) stop() { } } - runningEntry, err := c.repository.RunningEntry(group, c.entryTimeContext) + entry, err := c.repository.RunningEntry(group, c.entryTimeContext) if err != nil { fmt.Fprintf(os.Stderr, "no entry running in %q\n", groupArg) os.Exit(1) } - entry, err := runningEntry.Complete(stop) - if err != nil { - fmt.Fprintln(os.Stderr, err) - os.Exit(1) - } + entry.Complete(stop) if c.repository.ExistsEntry(group, entry) { fmt.Fprintln(os.Stderr, "entry already created") os.Exit(1) } - c.repository.Remove(group, runningEntry) + c.repository.Remove(group, entry) c.repository.Save(group, entry) } @@ -211,15 +207,19 @@ func (c Command) report() { var atLeastOneEntry bool var totalDuration time.Duration for _, group := range groups { - entries, err := c.repository.ListCompleted(group, c.entryTimeContext) + entries, err := c.repository.List(group, c.entryTimeContext) if err != nil { fmt.Fprintln(os.Stderr, err) os.Exit(1) } for _, entry := range entries { - if (entry.Start.After(sinceTime) || entry.Start.Equal(sinceTime)) && - (entry.Stop.Before(untilTime) || entry.Stop.Equal(untilTime)) { + if !entry.Completed { + continue + } + + if (entry.TimeRange.Start.After(sinceTime) || entry.TimeRange.Start.Equal(sinceTime)) && + (entry.TimeRange.Stop.Before(untilTime) || entry.TimeRange.Stop.Equal(untilTime)) { atLeastOneEntry = true totalDuration += entry.Duration() formatEntry(group, entry) @@ -250,15 +250,14 @@ func formatEntry(group Group, entry Entry) { var start, stop string var duration time.Duration - switch e := entry.(type) { - case CompletedEntry: - start = e.Start.Format(timeLayout) - stop = e.Stop.Format(timeLayout) - duration = e.Duration().Round(time.Second) - case RunningEntry: - start = e.Start.Format(timeLayout) - duration = time.Since(e.Start).Round(time.Second) + start = entry.TimeRange.Start.Format(timeLayout) + if entry.Completed { + stop = entry.TimeRange.Stop.Format(timeLayout) + duration = entry.Duration().Round(time.Second) + } else { + start = entry.TimeRange.Start.Format(timeLayout) stop = "running" + duration = time.Since(entry.TimeRange.Start).Round(time.Second) } fmt.Printf("%s\t%s\t%v\t%s\n", groupPath, start, stop, duration) diff --git a/entry.go b/entry.go @@ -1,46 +1,34 @@ package main -import ( - "errors" - "time" -) +import "time" -type Entry interface{} - -type CompletedEntry struct { - Start time.Time - Stop time.Time +type Entry struct { + Completed bool + TimeRange TimeRange } -func NewCompletedEntry(start time.Time, stop time.Time) (CompletedEntry, error) { - if start.After(stop) || start.Equal(stop) { - return CompletedEntry{}, errors.New("duration must be positive") +func NewCompleted(start time.Time, stop time.Time) (Entry, error) { + timeRange, err := New(start, stop) + if err != nil { + return Entry{}, err } - return CompletedEntry{ - Start: start, - Stop: stop, - }, nil + return Entry{TimeRange: timeRange, Completed: true}, nil } -func (e CompletedEntry) Duration() time.Duration { - return e.Stop.Sub(e.Start) +func NewRunning(start time.Time) Entry { + timeRange, _ := New(start, start.Add(time.Second)) + return Entry{TimeRange: timeRange, Completed: false} } -type RunningEntry struct { - Start time.Time +func (e Entry) Duration() time.Duration { + return e.TimeRange.Stop.Sub(e.TimeRange.Start) } -func NewRunningEntry(start time.Time) RunningEntry { - return RunningEntry{ - Start: start, +func (e *Entry) Complete(stop time.Time) { + e.TimeRange = TimeRange{ + Start: e.TimeRange.Start, + Stop: stop, } -} - -func (e RunningEntry) Duration() time.Duration { - return time.Since(e.Start) -} - -func (e RunningEntry) Complete(stop time.Time) (CompletedEntry, error) { - return NewCompletedEntry(e.Start, stop) + e.Completed = true } diff --git a/entry_test.go b/entry_test.go @@ -1,8 +1,6 @@ package main -import ( - "testing" -) +import "testing" func TestEntry(t *testing.T) { entryTime := NewTimeToday() @@ -10,7 +8,7 @@ func TestEntry(t *testing.T) { t.Run("positive duration should pass", func(b *testing.T) { start, _ := entryTime.ParseArg("5:00") stop, _ := entryTime.ParseArg("6:00") - _, err := NewCompletedEntry(start, stop) + _, err := NewCompleted(start, stop) if err != nil { b.Error(err) } @@ -19,7 +17,7 @@ func TestEntry(t *testing.T) { t.Run("zero duration should fail", func(b *testing.T) { start, _ := entryTime.ParseArg("6:00") stop, _ := entryTime.ParseArg("6:00") - _, err := NewCompletedEntry(start, stop) + _, err := NewCompleted(start, stop) if err == nil { b.Error(err) } @@ -28,7 +26,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 := NewCompletedEntry(start, stop) + _, err := NewCompleted(start, stop) if err == nil { b.Error(err) } diff --git a/range.go b/range.go @@ -1,12 +1,19 @@ package main -import "time" +import ( + "errors" + "time" +) -type Range interface { - Entries() []Entry +type TimeRange struct { + Start time.Time + Stop time.Time } -type TimeRange struct { - since time.Time - until time.Time +func New(start time.Time, stop time.Time) (TimeRange, error) { + if start.After(stop) || start.Equal(stop) { + return TimeRange{}, errors.New("duration must be positive") + } + + return TimeRange{Start: start, Stop: stop}, nil } diff --git a/repository.go b/repository.go @@ -8,6 +8,7 @@ import ( "path" "path/filepath" "strings" + "time" ) type FSRepository struct { @@ -18,76 +19,24 @@ func NewFSRepository(rootPath string) FSRepository { return FSRepository{rootPath} } -func (repo FSRepository) NewEntry(entryPath string, entryTime *TimeContext) (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 == activeFile { - 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 - } -} - -func (repo FSRepository) CompletedEntry(entryPath string, entryTime *TimeContext) (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 (repo FSRepository) RunningEntry(group Group, entryTime *TimeContext) (RunningEntry, error) { +func (repo FSRepository) RunningEntry(group Group, entryTime *TimeContext) (Entry, error) { entryPath := path.Join(repo.rootPath, group.Name, activeFile) if _, err := os.Stat(entryPath); os.IsNotExist(err) { - return RunningEntry{}, err + return Entry{}, err } data, err := os.ReadFile(entryPath) if err != nil { - return RunningEntry{}, err + return Entry{}, err } firstLine := strings.Split(string(data), "\n")[0] start, _ := entryTime.ParseEntry(firstLine) - return RunningEntry{ - Start: start, + return Entry{ + TimeRange: TimeRange{start, time.Now()}, + Completed: false, }, nil - } func (r FSRepository) Save(group Group, entry Entry) error { @@ -106,25 +55,19 @@ func (r FSRepository) Save(group Group, entry Entry) error { } func (repo FSRepository) EntryData(entry Entry) string { - switch e := entry.(type) { - case CompletedEntry: - return fmt.Sprintf("%s\n%s\n", e.Start.Format(DataTimeLayout), e.Stop.Format(DataTimeLayout)) - case RunningEntry: - return fmt.Sprintf("%s\n", e.Start.Format(DataTimeLayout)) + if entry.Completed { + return fmt.Sprintf("%s\n%s\n", entry.TimeRange.Start.Format(DataTimeLayout), entry.TimeRange.Stop.Format(DataTimeLayout)) } - return "_UNKNOWNTYPE_" + return fmt.Sprintf("%s\n", entry.TimeRange.Start.Format(DataTimeLayout)) } func (r FSRepository) EntryName(entry Entry) string { - switch e := entry.(type) { - case CompletedEntry: - return strings.Join([]string{e.Start.Format(FileNameLayout), e.Stop.Format(FileNameLayout)}, "_") - case RunningEntry: - return activeFile + if entry.Completed { + return strings.Join([]string{entry.TimeRange.Start.Format(FileNameLayout), entry.TimeRange.Stop.Format(FileNameLayout)}, "_") } - return "_UNKNOWNTYPE_" + return activeFile } func (repo FSRepository) Remove(group Group, entry Entry) error { @@ -139,6 +82,7 @@ func (repo FSRepository) ExistsEntry(group Group, entry Entry) bool { } return false } + func (repo FSRepository) Exists(group Group) bool { groupPath := path.Join(repo.rootPath, group.Name) if _, err := os.Stat(groupPath); !os.IsNotExist(err) { @@ -183,7 +127,7 @@ func (repo FSRepository) List(group Group, entryTime *TimeContext) ([]Entry, err return fs.SkipDir } - entry, err := repo.NewEntry(path, entryTime) + entry, err := repo.find(path, entryTime) if err != nil { fmt.Fprintln(os.Stderr, err) os.Exit(1) @@ -198,33 +142,29 @@ func (repo FSRepository) List(group Group, entryTime *TimeContext) ([]Entry, err return entries, nil } -func (repo FSRepository) ListCompleted(group Group, entryTime *TimeContext) ([]CompletedEntry, error) { - entries, err := repo.List(group, entryTime) - if err != nil { - return []CompletedEntry{}, err +func (repo FSRepository) find(entryPath string, entryTime *TimeContext) (Entry, error) { + if _, err := os.Stat(entryPath); os.IsNotExist(err) { + return Entry{}, err } - return listFilter[CompletedEntry](entries) -} - -func (repo FSRepository) ListRunning(group Group, entryTime *TimeContext) ([]RunningEntry, error) { - entries, err := repo.List(group, entryTime) + data, err := os.ReadFile(entryPath) if err != nil { - return []RunningEntry{}, err + return Entry{}, err } - return listFilter[RunningEntry](entries) -} + // TODO(tms) 11.11.22: format check + base := path.Base(entryPath) -func listFilter[K CompletedEntry | RunningEntry](entries []Entry) ([]K, error) { - var completedEntries []K + lines := strings.Split(string(data), "\n") - for _, entry := range entries { - switch e := entry.(type) { - case K: - completedEntries = append(completedEntries, e) - } + if base == activeFile { + start, _ := entryTime.ParseEntry(lines[0]) + timeRange := TimeRange{start, time.Now()} + return Entry{Completed: false, TimeRange: timeRange}, nil + } else { + start, _ := entryTime.ParseEntry(lines[0]) + stop, _ := entryTime.ParseEntry(lines[1]) + timeRange := TimeRange{start, stop} + return Entry{Completed: true, TimeRange: timeRange}, nil } - - return completedEntries, nil } diff --git a/repository_test.go b/repository_test.go @@ -0,0 +1,54 @@ +package main + +import ( + "testing" +) + +func TestSave(t *testing.T) { + rootPath := setUp() + t.Cleanup(func() { tearDown(rootPath) }) + timeContext := NewTimeToday() + repository := NewFSRepository(rootPath) + + t.Run("save single entry", func(t *testing.T) { + group := NewGroup("test") + entry := createEntry(timeContext, "10:00", "11:00") + + err := repository.Save(group, entry) + if err != nil { + t.Error(err) + } + + if !repository.ExistsEntry(group, entry) { + t.Error(err) + } + }) + + t.Run("save single entry", func(t *testing.T) { + group := NewGroup("test") + + for _, entry := range []Entry{ + createEntry(timeContext, "10:00", "11:00"), + createEntry(timeContext, "11:00", "12:00"), + createEntry(timeContext, "13:00", "14:00"), + } { + err := repository.Save(group, entry) + if err != nil { + t.Error(err) + } + } + + list, _ := repository.List(group, timeContext) + + if len(list) != 3 { + t.Error("Expected 3 entries") + } + }) +} + +func createEntry(timeContext *TimeContext, s string, st string) Entry { + start, _ := timeContext.ParseArg(s) + stop, _ := timeContext.ParseArgRight(st) + entry, _ := NewCompleted(start, stop) + return entry +} diff --git a/tme_test.go b/tme_test.go @@ -1,6 +1,8 @@ package main -import "os" +import ( + "os" +) // func TestAdd(t *testing.T) { // rootPath := setUp() @@ -9,8 +11,8 @@ import "os" // group := createAndCheckGroup(t, rootPath, "project") // start, _ := entryTime.ParseArg("10:00") -// stop, _ := entryTime.ParseArg("11:00") -// tme, err := NewCompletedEntry(start, stop) +// stop, _ := entryTime.ParseArgRight("11:00") +// tme, err := NewCompleted(start, stop) // if err != nil { // t.Error(err) // } @@ -134,6 +136,7 @@ import "os" // t.Fatalf("time mismatch! %v != %v", start, lineStart) // } // } + // func createAndCheckGroup(t *testing.T, basePath, groupName string) Group { // group := NewGroup(basePath, groupName) // group.Create()