From b1f95ed90c9160ac7cf68c8b561a4a4d1d5dca86 Mon Sep 17 00:00:00 2001 From: rjianu Date: Wed, 12 Jul 2023 15:05:07 +0300 Subject: [PATCH] feat: Initial version --- .gitignore | 0 csv.go | 49 +++++++++++++++++++ csv_test.go | 109 ++++++++++++++++++++++++++++++++++++++++++ errors.go | 10 ++++ go.mod | 3 ++ main.go | 61 +++++++++++++++++++++++ main_test.go | 67 ++++++++++++++++++++++++++ testdata/example.csv | 6 +++ testdata/example2.csv | 21 ++++++++ 9 files changed, 326 insertions(+) create mode 100644 .gitignore create mode 100644 csv.go create mode 100644 csv_test.go create mode 100644 errors.go create mode 100644 go.mod create mode 100644 main.go create mode 100644 main_test.go create mode 100644 testdata/example.csv create mode 100644 testdata/example2.csv diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e69de29 diff --git a/csv.go b/csv.go new file mode 100644 index 0000000..eef1e0c --- /dev/null +++ b/csv.go @@ -0,0 +1,49 @@ +package main + +import ( + "encoding/csv" + "fmt" + "io" + "strconv" +) + +type statsFunc func(data []float64) float64 + +func sum(data []float64) float64 { + sum := 0.0 + for _, v := range data { + sum += v + } + return sum +} + +func avg(data []float64) float64 { + return sum(data) / float64(len(data)) +} + +func csv2float(r io.Reader, column int) ([]float64, error) { + cr := csv.NewReader(r) + if column < 1 { + return nil, fmt.Errorf("%w: please provide a valid column number", ErrInvalidColumn) + } + column-- + allData, err := cr.ReadAll() + if err != nil { + return nil, fmt.Errorf("cannot read data from file: %w", err) + } + var data []float64 + for i, row := range allData { + if i == 0 { + continue + } + if len(row) <= column { + return nil, fmt.Errorf("%w: File has only %d columns", ErrInvalidColumn, len(row)) + } + v, err := strconv.ParseFloat(row[column], 64) + if err != nil { + return nil, fmt.Errorf("%w: %s", ErrNotNumber, err) + } + data = append(data, v) + } + return data, nil +} diff --git a/csv_test.go b/csv_test.go new file mode 100644 index 0000000..c0d38a3 --- /dev/null +++ b/csv_test.go @@ -0,0 +1,109 @@ +package main + +import ( + "bytes" + "errors" + "fmt" + "io" + "testing" + "testing/iotest" +) + +func TestOperations(t *testing.T) { + data := [][]float64{ + {10, 20, 15, 30, 45, 50, 100, 30}, + {5.5, 8, 2.2, 9.75, 8.45, 3, 2.5, 10.25, 4.75, 6.1, 7.67, 12.287, 5.47}, + {-10, -20}, + {102, 37, 44, 57, 67, 129}, + } + testCases := []struct { + name string + op statsFunc + exp []float64 + }{ + {"Sum", sum, []float64{300, 85.927, -30, 436}}, + {"Avg", avg, []float64{37.5, 6.609769230769231, -15, 72.666666666666666}}, + } + + for _, tc := range testCases { + for k, exp := range tc.exp { + name := fmt.Sprintf("%sData%d", tc.name, k) + t.Run(name, func(t *testing.T) { + res := tc.op(data[k]) + // comparing floats might not be the best solution + if res != exp { + t.Errorf("Expected %g, got %g instead", exp, res) + } + }) + } + } +} + +func TestCSV2Float(t *testing.T) { + csvData := `IP Address,Requests,Response Time + 192.168.0.199,2056,236 + 192.168.0.88,899,220 + 192.168.0.199,3054,226 + 192.168.0.100,4133,218 + 192.168.0.199,950,238 + ` + testCases := []struct { + name string + col int + exp []float64 + expErr error + r io.Reader + }{ + {name: "Column2", col: 2, + exp: []float64{2056, 899, 3054, 4133, 950}, + expErr: nil, + r: bytes.NewBufferString(csvData), + }, + {name: "Column3", col: 3, + exp: []float64{236, 220, 226, 218, 238}, + expErr: nil, + r: bytes.NewBufferString(csvData), + }, + {name: "FailRead", col: 1, + exp: nil, + expErr: iotest.ErrTimeout, + r: iotest.TimeoutReader(bytes.NewReader([]byte{0})), + }, + {name: "FailedNotNumber", col: 1, + exp: nil, expErr: ErrNotNumber, + r: bytes.NewBufferString(csvData), + }, + {name: "FailedInvalidColumn", col: 4, + exp: nil, + expErr: ErrInvalidColumn, + r: bytes.NewBufferString(csvData), + }, + } + // CSV2Float Tests execution + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + res, err := csv2float(tc.r, tc.col) + if tc.expErr != nil { + if err == nil { + t.Errorf("expected error. Got nil instead") + } + + if !errors.Is(err, tc.expErr) { + t.Errorf("Expected error %q, got %q instead", tc.expErr, err) + } + return + } + + if err != nil { + t.Errorf("Unexpected error: %q", err) + } + + for i, exp := range tc.exp { + // TODO find a way to check list equality + if res[i] != exp { + t.Errorf("Expected %g, got %g instead", exp, res[i]) + } + } + }) + } +} diff --git a/errors.go b/errors.go new file mode 100644 index 0000000..cef3dec --- /dev/null +++ b/errors.go @@ -0,0 +1,10 @@ +package main + +import "errors" + +var ( + ErrNotNumber = errors.New("Data is not numeric") + ErrInvalidColumn = errors.New("Invalid column number") + ErrNoFiles = errors.New("No input files") + ErrInvalidOperation = errors.New("Invalid operation") +) diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..ff71175 --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module github.com/Serares/coolStats + +go 1.20 diff --git a/main.go b/main.go new file mode 100644 index 0000000..0fdd5cf --- /dev/null +++ b/main.go @@ -0,0 +1,61 @@ +package main + +import ( + "flag" + "fmt" + "io" + "os" +) + +func main() { + // Verify and parse arguments + op := flag.String("op", "sum", "Operation to be executed") + column := flag.Int("col", 1, "CSV column on which to execute operation") + flag.Parse() + if err := run(flag.Args(), *op, *column, os.Stdout); err != nil { + fmt.Fprintln(os.Stderr, err) + os.Exit(1) + } +} + +func run(filenames []string, op string, column int, out io.Writer) error { + var opFunc statsFunc + if len(filenames) == 0 { + return ErrNoFiles + } + if column < 1 { + return fmt.Errorf("%w: %d", ErrInvalidColumn, column) + } + + switch op { + case "sum": + opFunc = sum + case "avg": + opFunc = avg + default: + return fmt.Errorf("%w: %s", ErrInvalidOperation, op) + } + + consolidate := make([]float64, 0) + + for _, fname := range filenames { + f, err := os.Open(fname) + if err != nil { + return fmt.Errorf("cannot open file: %w", err) + } + + data, err := csv2float(f, column) + if err != nil { + return err + } + + if err := f.Close(); err != nil { + return err + } + + consolidate = append(consolidate, data...) + } + + _, err := fmt.Fprintln(out, opFunc(consolidate)) + return err +} diff --git a/main_test.go b/main_test.go new file mode 100644 index 0000000..4912772 --- /dev/null +++ b/main_test.go @@ -0,0 +1,67 @@ +package main + +import ( + "bytes" + "errors" + "os" + "testing" +) + +func TestRun(t *testing.T) { + // Test cases for Run Tests + testCases := []struct { + name string + col int + op string + exp string + files []string + expErr error + }{ + {name: "RunAvg1File", col: 3, op: "avg", exp: "227.6\n", + files: []string{"./testdata/example.csv"}, + expErr: nil, + }, + {name: "RunAvgMultiFiles", col: 3, op: "avg", exp: "233.84\n", + files: []string{"./testdata/example.csv", "./testdata/example2.csv"}, + expErr: nil, + }, + {name: "RunFailRead", col: 2, op: "avg", exp: "", + files: []string{"./testdata/example.csv", "./testdata/fakefile.csv"}, + expErr: os.ErrNotExist, + }, {name: "RunFailColumn", col: 0, op: "avg", exp: "", + files: []string{"./testdata/example.csv"}, + expErr: ErrInvalidColumn, + }, + {name: "RunFailNoFiles", col: 2, op: "avg", exp: "", + files: []string{}, + expErr: ErrNoFiles, + }, + {name: "RunFailOperation", col: 2, op: "invalid", exp: "", + files: []string{"./testdata/example.csv"}, + expErr: ErrInvalidOperation, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + var res bytes.Buffer + err := run(tc.files, tc.op, tc.col, &res) + + if tc.expErr != nil { + if err == nil { + t.Errorf("Expected error. Got nil instead") + } + if !errors.Is(err, tc.expErr) { + t.Errorf("Expected error %q, got %q instead", tc.expErr, err) + } + return + } + if err != nil { + t.Errorf("Unexpected error: %q", err) + } + if res.String() != tc.exp { + t.Errorf("Expected %q, got %q instead", tc.exp, &res) + } + }) + } +} diff --git a/testdata/example.csv b/testdata/example.csv new file mode 100644 index 0000000..03131e3 --- /dev/null +++ b/testdata/example.csv @@ -0,0 +1,6 @@ +IP Address,Timestamp,Response Time,Bytes +192.168.0.199,1520698621,236,3475 +192.168.0.88,1520698776,220,3200 +192.168.0.199,1520699033,226,3200 +192.168.0.100,1520699142,218,3475 +192.168.0.199,1520699379,238,3822 \ No newline at end of file diff --git a/testdata/example2.csv b/testdata/example2.csv new file mode 100644 index 0000000..204470d --- /dev/null +++ b/testdata/example2.csv @@ -0,0 +1,21 @@ +IP Address,Timestamp,Response Time,Bytes +192.168.0.199,1520698621,236,3475 +192.168.0.88,1520698776,220,3200 +192.168.0.199,1520699033,226,3200 +192.168.0.100,1520699142,218,3475 +192.168.0.199,1520699379,238,3822 +192.168.0.199,1520699379,238,3822 +192.168.0.199,1520699379,238,3822 +192.168.0.199,1520699379,238,3822 +192.168.0.199,1520699379,238,3822 +192.168.0.199,1520699379,238,3822 +192.168.0.199,1520699379,238,3822 +192.168.0.199,1520699379,238,3822 +192.168.0.199,1520699379,238,3822 +192.168.0.199,1520699379,238,3822 +192.168.0.199,1520699379,238,3822 +192.168.0.199,1520699379,238,3822 +192.168.0.199,1520699379,238,3822 +192.168.0.199,1520699379,238,3822 +192.168.0.199,1520699379,238,3822 +192.168.0.199,1520699379,238,3822 \ No newline at end of file