Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add create record command #118

Merged
merged 4 commits into from
Jun 23, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
72 changes: 72 additions & 0 deletions cli/create.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ func createCommand(t *core.Timetrace) *cobra.Command {
}

create.AddCommand(createProjectCommand(t))
create.AddCommand(createRecordCommand(t))

return create
}
Expand Down Expand Up @@ -44,3 +45,74 @@ func createProjectCommand(t *core.Timetrace) *cobra.Command {

return createProject
}

func createRecordCommand(t *core.Timetrace) *cobra.Command {
var options startOptions
createRecord := &cobra.Command{
Use: "record <PROJECT KEY> {<YYYY-MM-DD>|today|yesterday} <HH-MM> <HH-MM>",
Short: "Create a new record",
Args: cobra.ExactArgs(4),
Run: func(cmd *cobra.Command, args []string) {
key := args[0]
FelixTheodor marked this conversation as resolved.
Show resolved Hide resolved
project, err := t.LoadProject(key)
if err != nil {
out.Err("Failed to get project: %s", key)
return
}

date, err := t.Formatter().ParseDate(args[1])
if err != nil {
out.Err("failed to parse date: %s", err.Error())
return
}

start, err := t.Formatter().ParseTime(args[2])
if err != nil {
out.Err("failed to parse start time: %s", err.Error())
return
}
start = t.Formatter().CombineDateAndTime(date, start)

end, err := t.Formatter().ParseTime(args[3])
if err != nil {
out.Err("failed to parse end time: %s", err.Error())
return
}
end = t.Formatter().CombineDateAndTime(date, end)

if end.Before(start) {
out.Err("end time is before start time!")
return
}

record := core.Record{
Project: project,
Start: start,
End: &end,
IsBillable: options.isBillable,
}

collides, err := t.RecordCollides(record)
if err != nil {
out.Err("Error on check if record collides: %s", err.Error())
return
}
if collides {
out.Err("Record collides with other record!")
return
}

if err := t.SaveRecord(record, false); err != nil {
out.Err("Failed to create record: %s", err.Error())
return
}

out.Success("Created record %s in project %s", t.Formatter().TimeString(record.Start), key)
},
}

createRecord.Flags().BoolVarP(&options.isBillable, "billable", "b",
false, `mark tracked time as billable`)

return createRecord
}
16 changes: 16 additions & 0 deletions core/formatter.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,22 @@ func (f *Formatter) ParseDate(input string) (time.Time, error) {
return date, nil
}

// ParseTime parses a time from an input string in the configured timeLayout
func (f *Formatter) ParseTime(input string) (time.Time, error) {
date, err := time.Parse(f.timeLayout(), input)
if err != nil {
return time.Time{}, err
}
return date, nil
}

// CombineDateAndTime takes a date and a time and combines them to the time
// struct that represents the given time on the given day
func (f *Formatter) CombineDateAndTime(d, t time.Time) time.Time {
year, month, day := d.Date()
return time.Date(year, month, day, t.Hour(), t.Minute(), t.Second(), t.Nanosecond(), time.Local)
}
FelixTheodor marked this conversation as resolved.
Show resolved Hide resolved

const (
defaultTimeLayout = "15:04"
default12HoursTimeLayout = "03:04PM"
Expand Down
37 changes: 37 additions & 0 deletions core/timetrace.go
Original file line number Diff line number Diff line change
Expand Up @@ -234,3 +234,40 @@ func (t *Timetrace) latestNonEmptyDir(dirs []string) (string, error) {

return "", ErrAllDirectoriesEmpty
}

// RecordCollides checks if the time of a record collides
// with other records of the same day and returns a bool
func (t *Timetrace) RecordCollides(toCheck Record) (bool, error) {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Glad that we finally have this function. It should be tested though - you can do it yourself if you want to, otherwise I'll write the test.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hm, I am not quite sure how to write the test for it. I would be glad to hear your approach - mocking the fs, or just creating a full core in the test? The easiest idea that came to my mind would be refactoring the part of the code that actually checks for collision to another method and just test this one. In this case, no mocking or fiddling with the core would be needed, right?

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, you could just move the collision logic in a function that accepts Record instances and test that function. The loading of the records will be tested separately.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay, I just did that and added the tests for it. I think all possible scenarios are tested, but if I missed some it should be easy to add more :)

allRecords, err := t.loadAllRecords(toCheck.Start)
if err != nil {
return false, err
}

if toCheck.Start.Day() != toCheck.End.Day() {
moreRecords, err := t.loadAllRecords(*toCheck.End)
if err != nil {
return false, err
}
for _, rec := range moreRecords {
allRecords = append(allRecords, rec)
}
}

return collides(toCheck, allRecords), nil
}

func collides(toCheck Record, allRecords []*Record) bool {
for _, rec := range allRecords {
if rec.Start.Before(toCheck.Start) && rec.End.After(toCheck.Start) {
return true
} else if rec.Start.Before(*toCheck.End) && rec.End.After(*toCheck.End) {
return true
} else if toCheck.Start.Before(rec.Start) && toCheck.End.After(rec.Start) {
return true
} else if toCheck.Start.Before(*rec.End) && toCheck.End.After(*rec.End) {
return true
}
}

return false
}
53 changes: 53 additions & 0 deletions core/timetrace_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,59 @@ import (
"time"
)

func newTestRecord(s int, e int) Record {
start := time.Now().Add(time.Duration(s) * time.Minute)
end := time.Now().Add(time.Duration(e) * time.Minute)
return Record{Start: start, End: &end}
}

func TestCollides(t *testing.T) {
savedRec := newTestRecord(-60, -1)
allRecs := []*Record{&savedRec}

// rec1 starts and end after savedRec
rec1 := newTestRecord(-1, 0)

if collides(rec1, allRecs) {
t.Error("records should not collide")
}

// rec2 starts in savedRec, ends after
rec2 := newTestRecord(-30, 1)

if !collides(rec2, allRecs) {
t.Error("records should collide")
}

// rec3 start before savedRec, ends inside
rec3 := newTestRecord(-75, -30)

if !collides(rec3, allRecs) {
t.Error("records should collide")
}

// rec4 starts and ends before savedRec
rec4 := newTestRecord(-75, -70)

if collides(rec4, allRecs) {
t.Error("records should not collide")
}

// rec5 starts and ends inside savedRec
rec5 := newTestRecord(-40, -20)

if !collides(rec5, allRecs) {
t.Error("records should collide")
}

// rec6 starts before and ends after savedRec
rec6 := newTestRecord(-70, 10)

if !collides(rec6, allRecs) {
t.Error("records should collide")
}
}

func TestFormatDuration(t *testing.T) {

tt := []struct {
Expand Down