diff --git a/cmd/ttt/fmt.go b/cmd/ttt/fmt.go index e064542..c44b490 100644 --- a/cmd/ttt/fmt.go +++ b/cmd/ttt/fmt.go @@ -15,7 +15,7 @@ func fmtDate(d time.Time) string { } func fmtTime(d time.Time) string { - return d.Format("Mon 2006-01-02 15:04:05") + return d.Format("Mon 2006-01-02 15:04") } func fmtDurationTrim(d time.Duration, style durationFmt) string { @@ -39,3 +39,16 @@ func fmtDuration(d time.Duration, style durationFmt) string { } return d.String() } + +func parseTime(arg string) (time.Time, error) { + var t time.Time + var err error + if len(arg) > 5 { + t, err = time.Parse("2006-01-02 15:04", arg) + } else { + today := time.Now() + t, err = time.Parse("15:04", arg) + t = t.AddDate(today.Year(), int(today.Month())-1, today.Day()-1) + } + return t, err +} diff --git a/cmd/ttt/fmt_test.go b/cmd/ttt/fmt_test.go index c98f321..ddb0a09 100644 --- a/cmd/ttt/fmt_test.go +++ b/cmd/ttt/fmt_test.go @@ -5,6 +5,7 @@ package main import ( + "fmt" "testing" "time" ) @@ -46,3 +47,24 @@ func TestFmtDurationNegative(t *testing.T) { t.Errorf("expected: '-1.03' got:'%s'", str) } } + +func TestParseTodayTime(t *testing.T) { + now := time.Now() + d, _ := parseTime("10:00") + fmt.Println(d) + if d.Year() != now.Year() || d.Month() != now.Month() || d.Day() != now.Day() { + t.Errorf("expected todays date: '%s' got: '%s'", now, d) + } + dfmt := d.Format("15:04:05.999999999Z07:00") + if dfmt != "10:00:00Z" { + t.Errorf("expected: '10:00:00Z', got: '%s'", dfmt) + } +} + +func TestParseDateTime(t *testing.T) { + d, _ := parseTime("2022-03-19 10:00") + dfmt := d.Format(time.RFC3339Nano) + if dfmt != "2022-03-19T10:00:00Z" { + t.Errorf("expected: '2022-03-19T10:00:00Z', got: '%s'", dfmt) + } +} diff --git a/cmd/ttt/main.go b/cmd/ttt/main.go index 2e9c113..81276b3 100644 --- a/cmd/ttt/main.go +++ b/cmd/ttt/main.go @@ -5,6 +5,7 @@ package main import ( + "errors" "flag" "fmt" "os/user" @@ -82,9 +83,24 @@ func parseCmd() (command, appCtx) { app.durationFmt = hours case "decimal": app.durationFmt = decimal + default: + return errors.New("Unrecognized duration format. Please use clock|hours|decimal") } return nil }) + flag.Func("time", "specify record start or end time with 1m resolution. format=now|HH:mm|YYYY-MM-DD HH:mm (default \"now\" with specified resolution)", + func(arg string) error { + if arg == "now" { + app.opTime = time.Now().Round(time.Minute) + } + if len(arg) > 5 { + app.opTime, err = time.Parse("2006-01-02 15:04", arg) + } else { + app.opTime, err = time.Parse("15:04", arg) + } + return err + }) + flag.DurationVar(&app.resolution, "resolution", 15*time.Minute, "specify the resolution the input timestamps should be rounded to.") flag.Usage = func() { w := flag.CommandLine.Output() fmt.Fprint(w, usage) @@ -93,8 +109,10 @@ func parseCmd() (command, appCtx) { flag.Parse() cmd := defaultCmd - if flag.NArg() >= 1 { + if flag.NArg() == 1 { cmd = command(flag.Arg(0)) + } else if flag.NArg() > 1 { + quitParamErr("Unrecognized arguments after command: " + fmt.Sprint(flag.Args()[1:])) } return cmd, app } diff --git a/cmd/ttt/terminal.go b/cmd/ttt/terminal.go index d90844c..1f5fa36 100644 --- a/cmd/ttt/terminal.go +++ b/cmd/ttt/terminal.go @@ -21,6 +21,9 @@ type appCtx struct { *ttt.TimeTrackingDb durationFmt + resolution time.Duration + + opTime time.Time } type durationFmt int @@ -33,6 +36,7 @@ const ( func (app *appCtx) Init() { var err error app.TimeTrackingDb, err = ttt.LoadDb(app.filename) + app.opTime = app.cleanDts(time.Now()) quitErr(err) } func (app *appCtx) InitEmpty() { @@ -41,11 +45,11 @@ func (app *appCtx) InitEmpty() { quitErr(err) } func (app *appCtx) Start() { - err := app.StartRecord(time.Now()) + err := app.StartRecord(app.opTime) quitErr(err) } func (app *appCtx) End() { - err := app.EndRecord(time.Now()) + err := app.EndRecord(app.opTime) quitErr(err) } @@ -53,15 +57,15 @@ func (app *appCtx) Status() { rec, err := app.GetCurrentRecord() quitErr(err) if rec == (ttt.Record{}) { - fmt.Println("No records have been tracked yet. Use `ttt start` to begin your first time tracking record.") + fmt.Println("No records have been tracked yet. Use `ttt start` to begin a new record.") } else if rec.Active() { duration := time.Now().Sub(*rec.Start) - fmt.Printf("Current record is active for %s (since %s)\n", fmtDurationTrim(duration, app.durationFmt), fmtTime(*rec.Start)) + fmt.Printf("Current record is active since %s, (%s)\n", fmtTime(*rec.Start), fmtDurationTrim(duration, app.durationFmt)) fmt.Println("Use `ttt end` to end the record.") } else { duration := rec.End.Sub(*rec.Start) fmt.Println("No record is active at the moment. Use `ttt start` to begin a new record.") - fmt.Printf("Last record was active for %s (from %s to %s )\n", fmtDurationTrim(duration, app.durationFmt), fmtTime(*rec.Start), fmtTime(*rec.End)) + fmt.Printf("Last record was active from %s to %s, (%s)\n", fmtTime(*rec.Start), fmtTime(*rec.End), fmtDurationTrim(duration, app.durationFmt)) } } @@ -136,3 +140,7 @@ func (app *appCtx) totalRow(worked, saldo time.Duration) table.Row { fmtDuration(saldo, app.durationFmt), } } + +func (app *appCtx) cleanDts(dts time.Time) time.Time { + return dts.Round(app.resolution) +} diff --git a/cmd/ttt/util.go b/cmd/ttt/util.go index 8463b98..c4a31ba 100644 --- a/cmd/ttt/util.go +++ b/cmd/ttt/util.go @@ -6,6 +6,7 @@ package main import ( "bufio" + "flag" "fmt" "os" "strings" @@ -19,6 +20,12 @@ func quitErr(err error) { os.Exit(1) } +func quitParamErr(err string) { + fmt.Println(err) + flag.Usage() + os.Exit(2) +} + func isFile(filename string) bool { info, err := os.Stat(filename) if err != nil { diff --git a/record.go b/record.go index afc3e16..df15abc 100644 --- a/record.go +++ b/record.go @@ -36,7 +36,6 @@ func (t *TimeTrackingDb) StartRecord(dts time.Time) error { if rec.Active() { return fmt.Errorf("%w", ActiveRecordExistsError) } else { - dts = t.cleanDts(dts) rec.Start = &dts _, err := t.db.Exec("INSERT INTO records (start) VALUES(?);", rec.Start) return err @@ -49,7 +48,6 @@ func (t *TimeTrackingDb) EndRecord(dts time.Time) error { return err } if rec.Active() { - dts = t.cleanDts(dts) rec.End = &dts _, err := t.db.Exec("UPDATE records SET end=? WHERE rowid=?;", rec.End, rec.id) return err @@ -67,7 +65,3 @@ func (t *TimeTrackingDb) GetCurrentRecord() (Record, error) { } return rec, err } - -func (t *TimeTrackingDb) cleanDts(dts time.Time) time.Time { - return dts.Round(t.config.inputResolution) -} diff --git a/record_test.go b/record_test.go index 1f77e3a..cf2e64f 100644 --- a/record_test.go +++ b/record_test.go @@ -12,7 +12,7 @@ import ( func TestStartNewRecord(t *testing.T) { tdb := withDb("someRecords.csv", t) defer tdb.Close() - err := tdb.StartRecord(parseTime("2022-02-02T09:17:02+01:00")) + err := tdb.StartRecord(parseTime("2022-02-02T09:15:00+01:00")) if err != nil { t.Fatal(err) } @@ -33,7 +33,7 @@ func TestStartRecordAllreadyActive(t *testing.T) { func TestEndRecord(t *testing.T) { tdb := withDb("someRecordsWithStartNew.csv", t) defer tdb.Close() - err := tdb.EndRecord(parseTime("2022-02-02T17:59:00+01:00")) + err := tdb.EndRecord(parseTime("2022-02-02T18:00:00+01:00")) if err != nil { t.Fatal(err) } diff --git a/schema.go b/schema.go index b211165..065ebae 100644 --- a/schema.go +++ b/schema.go @@ -84,7 +84,6 @@ func schemaPatches() []schemaPatch { return []schemaPatch{ { "CREATE TABLE config (property text, value text);", - "INSERT INTO config VALUES('input_resolution', '15m');", "INSERT INTO config VALUES('break_threshold', '6h'), ('break_deduction', '30m');", "INSERT INTO config VALUES('monday_hours', '7h 42m');", "INSERT INTO config VALUES('tuesday_hours', '7h 42m');", diff --git a/ttt.go b/ttt.go index 8b5c498..b2b0b24 100644 --- a/ttt.go +++ b/ttt.go @@ -22,11 +22,10 @@ type TimeTrackingDb struct { } type timeTrackingConfig struct { - inputResolution time.Duration - breakThreshold time.Duration - breakDeduction time.Duration - workingHours [7]time.Duration - holidays string + breakThreshold time.Duration + breakDeduction time.Duration + workingHours [7]time.Duration + holidays string } func LoadDb(filename string) (*TimeTrackingDb, error) { @@ -79,8 +78,6 @@ func (t *TimeTrackingDb) loadConfig() error { } switch property { - case "input_resolution": - t.config.inputResolution, errs = parseDuration(value, errs) case "break_deduction": t.config.breakDeduction, errs = parseDuration(value, errs) case "break_threshold":