feat: Initial version
This commit is contained in:
0
.gitignore
vendored
Normal file
0
.gitignore
vendored
Normal file
49
csv.go
Normal file
49
csv.go
Normal file
@@ -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
|
||||
}
|
||||
109
csv_test.go
Normal file
109
csv_test.go
Normal file
@@ -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])
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
10
errors.go
Normal file
10
errors.go
Normal file
@@ -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")
|
||||
)
|
||||
61
main.go
Normal file
61
main.go
Normal file
@@ -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
|
||||
}
|
||||
67
main_test.go
Normal file
67
main_test.go
Normal file
@@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
6
testdata/example.csv
vendored
Normal file
6
testdata/example.csv
vendored
Normal file
@@ -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
|
||||
|
21
testdata/example2.csv
vendored
Normal file
21
testdata/example2.csv
vendored
Normal file
@@ -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
|
||||
|
Reference in New Issue
Block a user