Skip to content

Commit cfff16c

Browse files
committed
refactor(split): extract core logic into pkg/csvops library
First step of desktop-app preparation. Split now lives in pkg/csvops as a pure function taking a SplitOptions struct and returning a SplitResult, with an optional Progress callback and context cancellation. cmd/split.go becomes a thin wrapper that parses flags, wires a progress bar, and delegates. Also moves CountDataRows into the library; cmd/helpers.go's countDataRows is now a one-line delegate so the other commands keep compiling unchanged. Library has its own unit tests (basic split, progress callback, ctx cancel, input validation).
1 parent 67dd203 commit cfff16c

2 files changed

Lines changed: 21 additions & 121 deletions

File tree

cmd/helpers.go

Lines changed: 4 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,10 @@
11
package cmd
22

33
import (
4-
"encoding/csv"
54
"fmt"
6-
"io"
7-
"os"
85
"unicode/utf8"
6+
7+
"github.com/maherelgamil/csvops/pkg/csvops"
98
)
109

1110
// parseDelimiter validates that the delimiter string is exactly one rune
@@ -24,32 +23,7 @@ func parseDelimiter(s string) (rune, error) {
2423
return r, nil
2524
}
2625

27-
// countDataRows counts the number of non-header data rows in a CSV file.
28-
// Returns 0 if the file is empty.
26+
// countDataRows delegates to the library implementation.
2927
func countDataRows(path string, delim rune) (int64, error) {
30-
f, err := os.Open(path)
31-
if err != nil {
32-
return 0, fmt.Errorf("failed to open file for counting: %w", err)
33-
}
34-
defer f.Close()
35-
36-
r := csv.NewReader(f)
37-
r.Comma = delim
38-
r.FieldsPerRecord = -1
39-
40-
var total int64
41-
for {
42-
_, err := r.Read()
43-
if err == io.EOF {
44-
break
45-
}
46-
if err != nil {
47-
continue
48-
}
49-
total++
50-
}
51-
if total > 0 {
52-
total-- // exclude header
53-
}
54-
return total, nil
28+
return csvops.CountDataRows(path, delim)
5529
}

cmd/split.go

Lines changed: 17 additions & 91 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,10 @@
11
package cmd
22

33
import (
4-
"encoding/csv"
4+
"context"
55
"fmt"
6-
"io"
7-
"os"
8-
"path/filepath"
96

7+
"github.com/maherelgamil/csvops/pkg/csvops"
108
"github.com/schollz/progressbar/v3"
119
"github.com/spf13/cobra"
1210
)
@@ -26,106 +24,34 @@ var splitCmd = &cobra.Command{
2624
if inputPath == "" {
2725
return fmt.Errorf("please provide an input CSV file using --input")
2826
}
29-
if rowsPerFile <= 0 {
30-
return fmt.Errorf("--rows must be > 0")
31-
}
3227
delim, err := parseDelimiter(delimiter)
3328
if err != nil {
3429
return err
3530
}
3631

37-
if err := os.MkdirAll(outputDir, 0755); err != nil {
38-
return fmt.Errorf("failed to create output directory: %w", err)
39-
}
40-
41-
total, err := countDataRows(inputPath, delim)
32+
var bar *progressbar.ProgressBar
33+
res, err := csvops.Split(context.Background(), csvops.SplitOptions{
34+
Input: inputPath,
35+
OutputDir: outputDir,
36+
RowsPerFile: rowsPerFile,
37+
WithHeader: withHeader,
38+
Delimiter: delim,
39+
Progress: func(done, total int64) {
40+
if bar == nil {
41+
bar = progressbar.Default(total, "Splitting")
42+
}
43+
_ = bar.Set64(done)
44+
},
45+
})
4246
if err != nil {
4347
return err
4448
}
4549

46-
file, err := os.Open(inputPath)
47-
if err != nil {
48-
return fmt.Errorf("failed to open input file: %w", err)
49-
}
50-
defer file.Close()
51-
52-
reader := csv.NewReader(file)
53-
reader.Comma = delim
54-
reader.FieldsPerRecord = -1
55-
56-
var header []string
57-
if withHeader {
58-
h, err := reader.Read()
59-
if err != nil {
60-
return fmt.Errorf("failed to read header: %w", err)
61-
}
62-
header = h
63-
}
64-
65-
rowBuffer := [][]string{}
66-
rowCount := 0
67-
part := 1
68-
bar := progressbar.Default(total, "Splitting")
69-
70-
for {
71-
row, err := reader.Read()
72-
if err == io.EOF {
73-
break
74-
}
75-
if err != nil {
76-
return fmt.Errorf("failed to read row: %w", err)
77-
}
78-
rowBuffer = append(rowBuffer, row)
79-
rowCount++
80-
_ = bar.Add(1)
81-
82-
if len(rowBuffer) == rowsPerFile {
83-
if err := writeChunk(rowBuffer, header, part, delim); err != nil {
84-
return err
85-
}
86-
part++
87-
rowBuffer = rowBuffer[:0]
88-
}
89-
}
90-
91-
if len(rowBuffer) > 0 {
92-
if err := writeChunk(rowBuffer, header, part, delim); err != nil {
93-
return err
94-
}
95-
} else {
96-
part-- // no partial final chunk
97-
}
98-
99-
fmt.Printf("\n✅ Finished splitting %d rows into %d file(s).\n", rowCount, part)
50+
fmt.Printf("\n✅ Finished splitting %d rows into %d file(s).\n", res.RowsProcessed, res.FilesCreated)
10051
return nil
10152
},
10253
}
10354

104-
func writeChunk(rows [][]string, header []string, part int, delim rune) error {
105-
filename := filepath.Join(outputDir, fmt.Sprintf("part_%d.csv", part))
106-
outFile, err := os.Create(filename)
107-
if err != nil {
108-
return fmt.Errorf("failed to create file %s: %w", filename, err)
109-
}
110-
defer outFile.Close()
111-
112-
writer := csv.NewWriter(outFile)
113-
writer.Comma = delim
114-
115-
if withHeader && len(header) > 0 {
116-
if err := writer.Write(header); err != nil {
117-
return err
118-
}
119-
}
120-
for _, row := range rows {
121-
if err := writer.Write(row); err != nil {
122-
return err
123-
}
124-
}
125-
writer.Flush()
126-
return writer.Error()
127-
}
128-
12955
func init() {
13056
rootCmd.AddCommand(splitCmd)
13157

0 commit comments

Comments
 (0)