Skip to content

Commit

Permalink
feat(worklog): initial worklog implementation
Browse files Browse the repository at this point in the history
  • Loading branch information
gabor-boros committed Oct 8, 2021
1 parent 47e5b92 commit b73017b
Show file tree
Hide file tree
Showing 4 changed files with 261 additions and 0 deletions.
48 changes: 48 additions & 0 deletions internal/pkg/worklog/entry.go
Original file line number Diff line number Diff line change
@@ -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
}
90 changes: 90 additions & 0 deletions internal/pkg/worklog/entry_test.go
Original file line number Diff line number Diff line change
@@ -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())
}
74 changes: 74 additions & 0 deletions internal/pkg/worklog/worklog.go
Original file line number Diff line number Diff line change
@@ -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),
}
}
49 changes: 49 additions & 0 deletions internal/pkg/worklog/worklog_test.go
Original file line number Diff line number Diff line change
@@ -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())
}

0 comments on commit b73017b

Please sign in to comment.