diff options
| author | Paul Buetow <paul@buetow.org> | 2026-03-04 00:01:44 +0200 |
|---|---|---|
| committer | Paul Buetow <paul@buetow.org> | 2026-03-04 00:01:44 +0200 |
| commit | 10d344d74bbeb9dcd5523efec6f2c79519bc05a4 (patch) | |
| tree | e232d7aab0b297de47edd6f674ebd3487ecb38fa /internal/cli | |
| parent | f33b95c7a26a9ac131719baaff391a0cdedb5072 (diff) | |
worktime: extract report import parsing from cli
Diffstat (limited to 'internal/cli')
| -rw-r--r-- | internal/cli/work.go | 151 | ||||
| -rw-r--r-- | internal/cli/work_test.go | 23 |
2 files changed, 16 insertions, 158 deletions
diff --git a/internal/cli/work.go b/internal/cli/work.go index ce96115..775a0d7 100644 --- a/internal/cli/work.go +++ b/internal/cli/work.go @@ -1,9 +1,9 @@ package cli import ( - "bufio" "errors" "fmt" + "io" "os" "os/exec" "path/filepath" @@ -350,11 +350,23 @@ func newWorkImportCmd() *cobra.Command { return err } - imported, err := importReportFile(ctx, args[0]) + file, err := os.Open(args[0]) if err != nil { return err } + imported, err := importReportFile(ctx, file) + closeErr := file.Close() + if err != nil { + if closeErr != nil { + return errors.Join(err, fmt.Errorf("close import file %q: %w", args[0], closeErr)) + } + return err + } + if closeErr != nil { + return fmt.Errorf("close import file %q: %w", args[0], closeErr) + } + return printOutput(cmd, fmt.Sprintf("Imported %d entries.", imported)) }, } @@ -411,139 +423,8 @@ func activeCategories(entries []worktime.Entry) []string { return active } -func importReportFile(ctx workContext, path string) (imported int, err error) { - file, err := os.Open(path) - if err != nil { - return 0, err - } - defer func() { - closeErr := file.Close() - if closeErr == nil { - return - } - wrappedCloseErr := fmt.Errorf("close import file %q: %w", path, closeErr) - if err == nil { - err = wrappedCloseErr - return - } - err = errors.Join(err, wrappedCloseErr) - }() - - scanner := bufio.NewScanner(file) - for scanner.Scan() { - line := scanner.Text() - if !strings.Contains(line, "lunch:") { - continue - } - - when, workHours, lunchHours, offHours, err := parseImportLine(line) - if err != nil { - return imported, err - } - - workSeconds := int64(workHours * float64(time.Hour/time.Second)) - lunchSeconds := int64(lunchHours * float64(time.Hour/time.Second)) - offSeconds := int64(offHours * float64(time.Hour/time.Second)) - - if lunchSeconds > 0 { - workSeconds += lunchSeconds - } - - if workSeconds > 0 { - if _, err := worktime.Add(ctx.dbDir, ctx.host, "work", time.Duration(workSeconds)*time.Second, when, ""); err != nil { - return imported, err - } - imported++ - } - if lunchSeconds > 0 { - if _, err := worktime.Add(ctx.dbDir, ctx.host, "lunch", time.Duration(lunchSeconds)*time.Second, when, ""); err != nil { - return imported, err - } - imported++ - } - if offSeconds > 0 { - if _, err := worktime.Add(ctx.dbDir, ctx.host, "off", time.Duration(offSeconds)*time.Second, when, ""); err != nil { - return imported, err - } - imported++ - } - } - - if err := scanner.Err(); err != nil { - return imported, err - } - - return imported, nil -} - -func parseImportLine(line string) (time.Time, float64, float64, float64, error) { - fields := strings.Fields(line) - if len(fields) < 7 { - return time.Time{}, 0, 0, 0, fmt.Errorf("unsupported import line: %q", line) - } - - dateToken := strings.TrimSuffix(fields[1], ":") - workToken := fields[2] - lunchToken := fields[4] - offToken := fields[6] - - when, err := parseImportDate(dateToken) - if err != nil { - return time.Time{}, 0, 0, 0, err - } - - workHours, err := parseHourToken(workToken) - if err != nil { - return time.Time{}, 0, 0, 0, err - } - lunchHours, err := parseHourToken(lunchToken) - if err != nil { - return time.Time{}, 0, 0, 0, err - } - offHours, err := parseHourToken(offToken) - if err != nil { - return time.Time{}, 0, 0, 0, err - } - - return when, workHours, lunchHours, offHours, nil -} - -func parseHourToken(token string) (float64, error) { - clean := strings.TrimSpace(token) - if idx := strings.Index(clean, ":"); idx >= 0 { - clean = clean[idx+1:] - } - clean = strings.TrimSuffix(clean, "h") - - value, err := strconv.ParseFloat(clean, 64) - if err != nil { - return 0, fmt.Errorf("parse hour token %q: %w", token, err) - } - return value, nil -} - -func parseImportDate(token string) (time.Time, error) { - trimmed := strings.TrimSpace(token) - if trimmed == "" { - return time.Time{}, errors.New("import date is empty") - } - - parsed, parseErr := timefmt.Parse(trimmed) - if parseErr == nil { - return parsed, nil - } - - layouts := []string{ - "02.01.2006", - "20060102", - } - for _, layout := range layouts { - if parsed, err := time.ParseInLocation(layout, trimmed, time.Local); err == nil { - return parsed, nil - } - } - - return time.Time{}, fmt.Errorf("unsupported import date %q: %w", token, parseErr) +func importReportFile(ctx workContext, report io.Reader) (int, error) { + return worktime.ImportReport(ctx.dbDir, ctx.host, report) } func workDBPath(dbDir, host string) string { diff --git a/internal/cli/work_test.go b/internal/cli/work_test.go index 2be0231..9621bfb 100644 --- a/internal/cli/work_test.go +++ b/internal/cli/work_test.go @@ -7,7 +7,6 @@ import ( "strconv" "strings" "testing" - "time" timrTimer "codeberg.org/snonux/timr/internal/timer" ) @@ -109,28 +108,6 @@ func TestWorkLoginLogoutWithTimerFlags(t *testing.T) { } } -func TestParseImportDateFallbackLayout(t *testing.T) { - parsed, err := parseImportDate("06.01.2026") - if err != nil { - t.Fatalf("parseImportDate() error = %v", err) - } - - expected := time.Date(2026, 1, 6, 0, 0, 0, 0, time.Local) - if parsed.Year() != expected.Year() || parsed.Month() != expected.Month() || parsed.Day() != expected.Day() { - t.Fatalf("parsed date = %v, want %v", parsed, expected) - } -} - -func TestParseImportDateIncludesUnderlyingParseError(t *testing.T) { - _, err := parseImportDate("not-a-date") - if err == nil { - t.Fatal("parseImportDate() error = nil, want error") - } - if !strings.Contains(err.Error(), "unsupported import date") { - t.Fatalf("parseImportDate() error = %v, want unsupported import date context", err) - } -} - func writeWorkConfig(t *testing.T, dbDir, host string) string { return writeWorkConfigWithAuto(t, dbDir, host, false) } |
