summaryrefslogtreecommitdiff
path: root/internal/cli
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2026-03-04 00:01:44 +0200
committerPaul Buetow <paul@buetow.org>2026-03-04 00:01:44 +0200
commit10d344d74bbeb9dcd5523efec6f2c79519bc05a4 (patch)
treee232d7aab0b297de47edd6f674ebd3487ecb38fa /internal/cli
parentf33b95c7a26a9ac131719baaff391a0cdedb5072 (diff)
worktime: extract report import parsing from cli
Diffstat (limited to 'internal/cli')
-rw-r--r--internal/cli/work.go151
-rw-r--r--internal/cli/work_test.go23
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)
}