diff --git a/internal/pkg/worklog/entry.go b/internal/pkg/worklog/entry.go new file mode 100644 index 0000000..ec2babf --- /dev/null +++ b/internal/pkg/worklog/entry.go @@ -0,0 +1,48 @@ +package worklog + +import ( + "fmt" + "time" +) + +// IDNameField stands for every field that has an ID and Name. +type IDNameField struct { + ID string + Name string +} + +// IsComplete indicates if the field has both ID and Name filled. +// In case both fields are filled, it returns true, otherwise, false. +func (f IDNameField) IsComplete() bool { + return f.ID != "" && f.Name != "" +} + +// Entry represents the worklog entry and contains all the necessary data. +type Entry struct { + Client IDNameField + Project IDNameField + Task IDNameField + Summary string + Notes string + Start time.Time + BillableDuration time.Duration + UnbillableDuration time.Duration +} + +// Key returns a unique, per entry key used for grouping similar entries. +func (e *Entry) Key() string { + return fmt.Sprintf("%s:%s:%s:%s", e.Project.Name, e.Task.Name, e.Summary, e.Start.Format("2006-01-02")) +} + +// IsComplete indicates if the entry has all the necessary fields filled. +// If all the necessary fields are complete it returns true, otherwise, false. +func (e *Entry) IsComplete() bool { + hasClient := e.Client.IsComplete() + hasProject := e.Project.IsComplete() + hasTask := e.Task.IsComplete() + + isMetadataFilled := hasProject && hasClient && hasTask && e.Summary != "" + isTimeFilled := !e.Start.IsZero() && (e.BillableDuration.Seconds() > 0 || e.UnbillableDuration.Seconds() > 0) + + return isMetadataFilled && isTimeFilled +} diff --git a/internal/pkg/worklog/entry_test.go b/internal/pkg/worklog/entry_test.go new file mode 100644 index 0000000..1fdcbc3 --- /dev/null +++ b/internal/pkg/worklog/entry_test.go @@ -0,0 +1,90 @@ +package worklog_test + +import ( + "testing" + "time" + + "github.com/gabor-boros/minutes/internal/pkg/worklog" + "github.com/stretchr/testify/assert" +) + +func getTestEntry() worklog.Entry { + start := time.Date(2021, 10, 2, 5, 0, 0, 0, time.Local) + end := start.Add(time.Hour * 2) + + return worklog.Entry{ + Client: worklog.IDNameField{ + ID: "client-id", + Name: "My Awesome Company", + }, + Project: worklog.IDNameField{ + ID: "project-id", + Name: "Internal projects", + }, + Task: worklog.IDNameField{ + ID: "task-id", + Name: "TASK-0123", + }, + Summary: "Write worklog transfer CLI tool", + Notes: "It is a lot easier than expected", + Start: start, + BillableDuration: end.Sub(start), + UnbillableDuration: 0, + } +} + +func TestIDNameFieldIsComplete(t *testing.T) { + var field worklog.IDNameField + + assert.False(t, field.IsComplete()) + + field = worklog.IDNameField{ + ID: "101", + } + assert.False(t, field.IsComplete()) + + field = worklog.IDNameField{ + ID: "101", + Name: "MARVEL-101", + } + assert.True(t, field.IsComplete()) +} + +func TestEntryKey(t *testing.T) { + entry := getTestEntry() + assert.Equal(t, "Internal projects:TASK-0123:Write worklog transfer CLI tool:2021-10-02", entry.Key()) +} + +func TestEntryIsComplete(t *testing.T) { + entry := getTestEntry() + assert.True(t, entry.IsComplete()) +} + +func TestEntryIsCompleteIncomplete(t *testing.T) { + var entry worklog.Entry + + entry = getTestEntry() + entry.Client = worklog.IDNameField{} + assert.False(t, entry.IsComplete()) + + entry = getTestEntry() + entry.Project = worklog.IDNameField{} + assert.False(t, entry.IsComplete()) + + entry = getTestEntry() + entry.Task = worklog.IDNameField{} + assert.False(t, entry.IsComplete()) + + entry = getTestEntry() + entry.Summary = "" + assert.False(t, entry.IsComplete()) + + entry = getTestEntry() + entry.Start = time.Time{} + assert.False(t, entry.IsComplete()) + + entry = getTestEntry() + entry.BillableDuration = 0 + entry.UnbillableDuration = 0 + assert.False(t, entry.IsComplete()) +} diff --git a/internal/pkg/worklog/worklog.go b/internal/pkg/worklog/worklog.go new file mode 100644 index 0000000..76adb61 --- /dev/null +++ b/internal/pkg/worklog/worklog.go @@ -0,0 +1,74 @@ +package worklog + +// groupEntries ensures to group similar entries, identified by their key. +// If the keys are matching for two entries, those will be merged and their duration will be summed up, notes will be +// concatenated. +func groupEntries(entries []Entry) []Entry { + entryGroup := map[string]Entry{} + + for _, entry := range entries { + key := entry.Key() + storedEntry, isStored := entryGroup[key] + + if !isStored { + entryGroup[key] = entry + continue + } + + storedEntry.BillableDuration += entry.BillableDuration + storedEntry.UnbillableDuration += entry.UnbillableDuration + + noteSeparator := "" + if storedEntry.Notes != "" && entry.Notes != storedEntry.Notes { + if entry.Notes != "" { + noteSeparator = "; " + } + + storedEntry.Notes = storedEntry.Notes + noteSeparator + entry.Notes + } + + entryGroup[key] = storedEntry + } + + groupedEntries := make([]Entry, 0, len(entryGroup)) + for _, item := range entryGroup { + groupedEntries = append(groupedEntries, item) + } + + return groupedEntries +} + +// Worklog is the collection of multiple Entries. +type Worklog struct { + entries []Entry +} + +// entryGroup returns those entries that are matching the completeness criteria. +func (w *Worklog) entryGroup(isComplete bool) []Entry { + var entries []Entry + + for _, entry := range w.entries { + if entry.IsComplete() == isComplete { + entries = append(entries, entry) + } + } + + return entries +} + +// CompleteEntries returns those entries which necessary fields were filled. +func (w *Worklog) CompleteEntries() []Entry { + return w.entryGroup(true) +} + +// IncompleteEntries is the opposite of CompleteEntries. +func (w *Worklog) IncompleteEntries() []Entry { + return w.entryGroup(false) +} + +// NewWorklog creates a worklog from the given set of entries and groups them. +func NewWorklog(entries []Entry) Worklog { + return Worklog{ + entries: groupEntries(entries), + } +} diff --git a/internal/pkg/worklog/worklog_test.go b/internal/pkg/worklog/worklog_test.go new file mode 100644 index 0000000..82cb6c2 --- /dev/null +++ b/internal/pkg/worklog/worklog_test.go @@ -0,0 +1,49 @@ +package worklog_test + +import ( + "testing" + + "github.com/gabor-boros/minutes/internal/pkg/worklog" + "github.com/stretchr/testify/assert" +) + +func TestWorklogCompleteEntries(t *testing.T) { + completeEntry := getTestEntry() + + otherCompleteEntry := getTestEntry() + otherCompleteEntry.Notes = "Really" + + incompleteEntry := getTestEntry() + incompleteEntry.Task = worklog.IDNameField{} + + wl := worklog.NewWorklog([]worklog.Entry{ + completeEntry, + otherCompleteEntry, + incompleteEntry, + }) + + entry := wl.CompleteEntries()[0] + assert.Equal(t, "It is a lot easier than expected; Really", entry.Notes) + assert.Equal(t, []worklog.Entry{entry}, wl.CompleteEntries()) +} + +func TestWorklogIncompleteEntries(t *testing.T) { + completeEntry := getTestEntry() + + incompleteEntry := getTestEntry() + incompleteEntry.Task = worklog.IDNameField{} + + otherIncompleteEntry := getTestEntry() + otherIncompleteEntry.Task = worklog.IDNameField{} + otherIncompleteEntry.Notes = "Well, not that easy" + + wl := worklog.NewWorklog([]worklog.Entry{ + completeEntry, + incompleteEntry, + otherIncompleteEntry, + }) + + entry := wl.IncompleteEntries()[0] + assert.Equal(t, "It is a lot easier than expected; Well, not that easy", entry.Notes) + assert.Equal(t, []worklog.Entry{entry}, wl.IncompleteEntries()) +}