package cli
import (
"errors"
"fmt"
"log"
"strings"
"keydiff/interfaces/input"
"keydiff/interfaces/output"
"keydiff/usecases"
"github.com/urfave/cli/v2"
)
const RequiredArguments = 2
func RunCLI(args []string) error {
app := &cli.App{
Name: "keydiff",
Usage: "Find differences between two CSV files using a specified key column",
Flags: []cli.Flag{
&cli.StringFlag{
Name: "key",
Usage: "Comma-separated list of column indices to use as the key (default: 0)",
Value: "0",
},
},
Action: func(c *cli.Context) error {
if c.NArg() != RequiredArguments {
return errors.New("exactly two arguments are required: <original.csv> <modified.csv>")
}
originalFilePath := c.Args().Get(0)
modifiedFilePath := c.Args().Get(1)
keyIndicesStr := c.String("key")
keyIndices := parseKeyIndices(keyIndicesStr)
reader := input.CSVReader{}
original, err := reader.Read(originalFilePath)
if err != nil {
return fmt.Errorf("error reading original file: %w", err)
}
modified, err := reader.Read(modifiedFilePath)
if err != nil {
return fmt.Errorf("error reading modified file: %w", err)
}
calculator := usecases.DiffCalculator{}
results := calculator.CalculateDiff(original, modified, keyIndices)
formatter := output.TextFormatter{}
output := formatter.Format(results)
fmt.Println(output)
return nil
},
}
err := app.Run(args)
if err != nil {
log.Println("Failed to execute CLI:", err)
return fmt.Errorf("failed to execute CLI: %w", err)
}
return nil
}
func parseKeyIndices(keyIndicesStr string) []int {
parts := strings.Split(keyIndicesStr, ",")
var indices []int
for _, part := range parts {
var index int
_, err := fmt.Sscanf(part, "%d", &index)
if err != nil {
log.Fatalf("Invalid key index: %v", err)
}
indices = append(indices, index)
}
return indices
}
package input
import (
"encoding/csv"
"fmt"
"os"
)
type CSVReader struct{}
func (r *CSVReader) Read(filePath string) ([][]string, error) {
file, err := os.Open(filePath)
if err != nil {
return nil, fmt.Errorf("failed to open file %s: %w", filePath, err)
}
defer file.Close()
reader := csv.NewReader(file)
records, err := reader.ReadAll()
if err != nil {
return nil, fmt.Errorf("failed to read CSV file %s: %w", filePath, err)
}
return records, nil
}
package output
import (
"fmt"
"strings"
"keydiff/entities"
)
type TextFormatter struct{}
func (f *TextFormatter) Format(results []entities.DiffResult) string {
var builder strings.Builder
for _, result := range results {
builder.WriteString(fmt.Sprintf("Key: %s\n", result.Key))
if result.Original != nil {
builder.WriteString(fmt.Sprintf("-: %s\n", strings.Join(result.Original, ", ")))
}
if result.Modified != nil {
builder.WriteString(fmt.Sprintf("+: %s\n", strings.Join(result.Modified, ", ")))
}
builder.WriteString("\n")
}
return builder.String()
}
package main
import (
"log"
"os"
"keydiff/framework/cli"
)
func main() {
err := cli.RunCLI(os.Args)
if err != nil {
log.Fatalf("Error: %v\n", err)
}
}
package usecases
import (
"strings"
"keydiff/entities"
)
type DiffCalculator struct{}
func (dc *DiffCalculator) CalculateDiff(original, modified [][]string, keyIndices []int) []entities.DiffResult {
results := []entities.DiffResult{}
originalMap := make(map[string][]string)
modifiedMap := make(map[string][]string)
generateKey := func(row []string, indices []int) string {
var keyParts []string
for _, index := range indices {
if index < len(row) {
keyParts = append(keyParts, row[index])
}
}
return strings.Join(keyParts, ":")
}
for _, row := range original {
key := generateKey(row, keyIndices)
if key != "" {
originalMap[key] = row
}
}
for _, row := range modified {
key := generateKey(row, keyIndices)
if key != "" {
modifiedMap[key] = row
}
}
for key, originalRow := range originalMap {
if modifiedRow, exists := modifiedMap[key]; exists {
if !equalRows(originalRow, modifiedRow) {
results = append(results, entities.DiffResult{
Key: key,
Original: originalRow,
Modified: modifiedRow,
})
}
} else {
results = append(results, entities.DiffResult{
Key: key,
Original: originalRow,
Modified: nil,
})
}
}
for key, modifiedRow := range modifiedMap {
if _, exists := originalMap[key]; !exists {
results = append(results, entities.DiffResult{
Key: key,
Original: nil,
Modified: modifiedRow,
})
}
}
return results
}
func equalRows(a, b []string) bool {
if len(a) != len(b) {
return false
}
for i := range a {
if a[i] != b[i] {
return false
}
}
return true
}