feat: Initial version

This commit is contained in:
rjianu
2023-07-12 15:05:07 +03:00
commit b1f95ed90c
9 changed files with 326 additions and 0 deletions

0
.gitignore vendored Normal file
View File

49
csv.go Normal file
View 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
View 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
View 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")
)

3
go.mod Normal file
View File

@@ -0,0 +1,3 @@
module github.com/Serares/coolStats
go 1.20

61
main.go Normal file
View 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
View 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
View 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
1 IP Address Timestamp Response Time Bytes
2 192.168.0.199 1520698621 236 3475
3 192.168.0.88 1520698776 220 3200
4 192.168.0.199 1520699033 226 3200
5 192.168.0.100 1520699142 218 3475
6 192.168.0.199 1520699379 238 3822

21
testdata/example2.csv vendored Normal file
View 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
1 IP Address Timestamp Response Time Bytes
2 192.168.0.199 1520698621 236 3475
3 192.168.0.88 1520698776 220 3200
4 192.168.0.199 1520699033 226 3200
5 192.168.0.100 1520699142 218 3475
6 192.168.0.199 1520699379 238 3822
7 192.168.0.199 1520699379 238 3822
8 192.168.0.199 1520699379 238 3822
9 192.168.0.199 1520699379 238 3822
10 192.168.0.199 1520699379 238 3822
11 192.168.0.199 1520699379 238 3822
12 192.168.0.199 1520699379 238 3822
13 192.168.0.199 1520699379 238 3822
14 192.168.0.199 1520699379 238 3822
15 192.168.0.199 1520699379 238 3822
16 192.168.0.199 1520699379 238 3822
17 192.168.0.199 1520699379 238 3822
18 192.168.0.199 1520699379 238 3822
19 192.168.0.199 1520699379 238 3822
20 192.168.0.199 1520699379 238 3822
21 192.168.0.199 1520699379 238 3822