Skip to content

Commit

Permalink
Merge pull request #118 from FelixTheodor/create_record
Browse files Browse the repository at this point in the history
  • Loading branch information
dominikbraun authored Jun 23, 2021
2 parents 75b19bd + 1f70ecd commit ed69e6d
Show file tree
Hide file tree
Showing 4 changed files with 178 additions and 0 deletions.
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]
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)
}

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 @@ -273,3 +273,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) {
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

0 comments on commit ed69e6d

Please sign in to comment.