blob: c3e34c5416934a11757106a9c51b34ca7d5b2bfb [file] [log] [blame] [edit]
package vec32
import (
"math"
"testing"
"github.com/stretchr/testify/assert"
)
const (
e = MissingDataSentinel
)
func near(a, b float32) bool {
return math.Abs(float64(a-b)) < 0.001
}
func vecNear(a, b []float32) bool {
if len(a) != len(b) {
return false
}
for i, x := range a {
if !near(x, b[i]) {
return false
}
}
return true
}
func TestNew(t *testing.T) {
v := New(0)
assert.Len(t, v, 0)
v = New(1)
assert.Len(t, v, 1)
assert.Equal(t, MissingDataSentinel, v[0])
v = New(2)
assert.Len(t, v, 2)
assert.Equal(t, MissingDataSentinel, v[0])
assert.Equal(t, MissingDataSentinel, v[1])
}
func TestNorm(t *testing.T) {
testCases := []struct {
In []float32
Out []float32
}{
{
In: []float32{1.0, -1.0, e},
Out: []float32{1.0, -1.0, e},
},
{
In: []float32{e, 2.0, -2.0},
Out: []float32{e, 1.0, -1.0},
},
{
In: []float32{e},
Out: []float32{e},
},
{
In: []float32{},
Out: []float32{},
},
{
In: []float32{2.0},
Out: []float32{0.0},
},
{
In: []float32{0.0, 0.1},
Out: []float32{-0.05, 0.05},
},
}
for _, tc := range testCases {
Norm(tc.In, 0.1)
if got, want := tc.In, tc.Out; !vecNear(tc.Out, tc.In) {
t.Errorf("Norm: Got %#v Want %#v", got, want)
}
}
}
func TestFill(t *testing.T) {
testCases := []struct {
In []float32
Out []float32
}{
{
In: []float32{e, e, 2, 3, e, 5},
Out: []float32{2, 2, 2, 3, 5, 5},
},
{
In: []float32{e, 3, e},
Out: []float32{3, 3, 3},
},
{
In: []float32{e, e},
Out: []float32{0, 0},
},
{
In: []float32{e},
Out: []float32{0},
},
{
In: []float32{},
Out: []float32{},
},
}
for _, tc := range testCases {
Fill(tc.In)
if got, want := tc.In, tc.Out; !vecNear(tc.Out, tc.In) {
t.Errorf("Fill: Got %#v Want %#v", got, want)
}
}
}
func TestFillAtErrors(t *testing.T) {
testCases := []struct {
Slice []float32
Idx int
}{
{
Slice: []float32{e, e, 2, 3, e, 5},
Idx: 6,
},
{
Slice: []float32{},
Idx: 0,
},
{
Slice: []float32{4},
Idx: -1,
},
}
for _, tc := range testCases {
_, err := FillAt(tc.Slice, tc.Idx)
if err == nil {
t.Fatalf("Expected \"%v\" to fail FillAt.", tc)
}
}
}
func TestDup(t *testing.T) {
a := []float32{1, 2, MissingDataSentinel, 0}
b := Dup(a)
assert.Equal(t, a, b)
b[0] = 2
assert.NotEqual(t, a, b)
a = []float32{}
b = Dup(a)
assert.Equal(t, a, b)
}
func TestMean_ReturnsZeroIfAllMissing(t *testing.T) {
assert.Equal(t, float32(1), Mean([]float32{1, 2, e, 0}))
assert.Equal(t, float32(0), Mean([]float32{}))
assert.Equal(t, float32(0), Mean([]float32{e}))
assert.Equal(t, float32(0), Mean([]float32{e, e}))
assert.Equal(t, float32(3), Mean([]float32{1, 5}))
}
func TestMeanE_ReturnsMissingIfAllMissing(t *testing.T) {
assert.Equal(t, float32(1), MeanE([]float32{1, 2, e, 0}))
assert.Equal(t, float32(e), MeanE([]float32{}))
assert.Equal(t, float32(e), MeanE([]float32{e}))
assert.Equal(t, float32(e), MeanE([]float32{e, e}))
assert.Equal(t, float32(3), MeanE([]float32{1, 5}))
}
func TestSSE(t *testing.T) {
testCases := []struct {
Slice []float32
Base float32
SSE float32
}{
{
Slice: []float32{1, 1, e, 0},
Base: 0.0,
SSE: 2.0,
},
{
Slice: []float32{1, 1, e, 0},
Base: 1.0,
SSE: 1.0,
},
{
Slice: []float32{},
Base: 1.0,
SSE: 0.0,
},
{
Slice: []float32{e},
Base: 3.0,
SSE: 0.0,
},
}
for _, tc := range testCases {
if got, want := SSE(tc.Slice, tc.Base), tc.SSE; !near(got, want) {
t.Errorf("SSE(%v, %f) Got %v Want %v", tc.Slice, tc.Base, got, want)
}
}
}
func TestFillMeanMissing(t *testing.T) {
testCases := []struct {
Slice []float32
Mean []float32
}{
{
Slice: []float32{1, 2, e, 0},
Mean: []float32{1.0, 1.0, 1.0, 1.0},
},
{
Slice: []float32{e, e, e, e},
Mean: []float32{e, e, e, e},
},
{
Slice: []float32{e},
Mean: []float32{e},
},
{
Slice: []float32{},
Mean: []float32{},
},
{
Slice: []float32{2.0},
Mean: []float32{2.0},
},
}
for _, tc := range testCases {
v := Dup(tc.Slice)
FillMeanMissing(v)
if got, want := v, tc.Mean; !vecNear(got, want) {
t.Errorf("Mean(%v) Got %v Want %v", tc.Slice, got, want)
}
}
}
func TestFillStdDev(t *testing.T) {
testCases := []struct {
Slice []float32
Mean []float32
}{
{
Slice: []float32{0, 1, 4, 9},
Mean: []float32{3.5, 3.5, 3.5, 3.5},
},
{
Slice: []float32{e, e, e, e},
Mean: []float32{e, e, e, e},
},
{
Slice: []float32{e},
Mean: []float32{e},
},
{
Slice: []float32{},
Mean: []float32{},
},
{
Slice: []float32{2.0},
Mean: []float32{0.0},
},
}
for _, tc := range testCases {
v := Dup(tc.Slice)
FillStdDev(v)
if got, want := v, tc.Mean; !vecNear(got, want) {
t.Errorf("Mean(%v) Got %v Want %v", tc.Slice, got, want)
}
}
}
func TestFillCov(t *testing.T) {
testCases := []struct {
Slice []float32
Mean []float32
}{
{
Slice: []float32{0, 1, 4, 9},
Mean: []float32{1, 1, 1, 1},
},
{
Slice: []float32{e, e, e, e},
Mean: []float32{e, e, e, e},
},
{
Slice: []float32{e},
Mean: []float32{e},
},
{
Slice: []float32{},
Mean: []float32{},
},
{
Slice: []float32{2.0},
Mean: []float32{0.0},
},
}
for _, tc := range testCases {
v := Dup(tc.Slice)
FillCov(v)
if got, want := v, tc.Mean; !vecNear(got, want) {
t.Errorf("Mean(%v) Got %v Want %v", tc.Slice, got, want)
}
}
}
func TestScaleBy(t *testing.T) {
testCases := []struct {
Slice []float32
Scale float32
Expected []float32
}{
{
Slice: []float32{e, 0, 2, 3},
Scale: math.SmallestNonzeroFloat32,
Expected: []float32{e, 0, e, e},
},
{
Slice: []float32{e, 0, -1, 2},
Scale: 0,
Expected: []float32{e, e, e, e},
},
{
Slice: []float32{e, 0, -2, 2},
Scale: 2,
Expected: []float32{e, 0, -1, 1},
},
}
for _, tc := range testCases {
v := Dup(tc.Slice)
ScaleBy(v, tc.Scale)
if got, want := v, tc.Expected; !vecNear(got, want) {
t.Errorf("Mean(%v) Got %v Want %v", tc.Slice, got, want)
}
}
}
func TestFillStep(t *testing.T) {
testCases := []struct {
Slice []float32
Step []float32
}{
{
Slice: []float32{1, 1, 2, 2, 2},
Step: []float32{0.5, 0.5, 0.5, 0.5, 0.5},
},
{
Slice: []float32{1, 1, 0, 0, 0},
Step: []float32{e, e, e, e, e},
},
{
Slice: []float32{3, 5, 2, 2, 2},
Step: []float32{2, 2, 2, 2, 2},
},
{
Slice: []float32{3, 5, e, 2, 2},
Step: []float32{2, 2, 2, 2, 2},
},
{
Slice: []float32{3, 5, e, e, 2},
Step: []float32{2, 2, 2, 2, 2},
},
{
Slice: []float32{4, e, e, e, 2},
Step: []float32{2, 2, 2, 2, 2},
},
{
Slice: []float32{3, 5, e, e, e},
Step: []float32{e, e, e, e, e},
},
{
Slice: []float32{e, e, e, e},
Step: []float32{e, e, e, e},
},
{
Slice: []float32{e},
Step: []float32{e},
},
{
Slice: []float32{},
Step: []float32{},
},
{
Slice: []float32{1.0},
Step: []float32{e},
},
}
for _, tc := range testCases {
v := Dup(tc.Slice)
FillStep(v)
if got, want := v, tc.Step; !vecNear(got, want) {
t.Errorf("Mean(%v) Got %v Want %v", tc.Slice, got, want)
}
}
}
func TestStdDev(t *testing.T) {
type args struct {
xs []float32
base float32
}
tests := []struct {
name string
args args
want float32
}{
{
name: "zero length",
args: args{
xs: []float32{},
base: 0,
},
want: 0,
},
{
name: "length one",
args: args{
xs: []float32{1},
base: 1,
},
want: 0,
},
{
name: "length two",
args: args{
xs: []float32{1, 1},
base: 1,
},
want: 0,
},
{
name: "length two off base",
args: args{
xs: []float32{1, 1},
base: 0,
},
want: math.Sqrt2,
},
{
name: "length two off base",
args: args{
xs: []float32{1, 1},
base: 0,
},
want: math.Sqrt2,
},
{
name: "length two off base after removing sentinels",
args: args{
xs: []float32{1, e, 1},
base: 0,
},
want: math.Sqrt2,
},
{
name: "length two with negatives off base after removing sentinels",
args: args{
xs: []float32{1, e, -1},
base: 0,
},
want: math.Sqrt2,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := StdDev(tt.args.xs, tt.args.base); got != tt.want {
t.Errorf("StdDev() = %v, want %v", got, tt.want)
}
})
}
}
func TestTwoSidedStdDev(t *testing.T) {
tests := []struct {
name string
arr []float32
median float32
lower, upper float32
hasError bool
}{
{
"EmptyArray_Error",
[]float32{},
0,
0, 0,
true,
},
{
"InsufficientNonMissingData_Error",
[]float32{e, 1, 1, 2},
0,
0, 0,
true,
},
{
"ConstantArray_ZeroStdDevs",
[]float32{1, 1, 1, 1},
1,
0, 0,
false,
},
{
"ConfirmSorting_UpperHasNonZeroStdDev",
[]float32{1, 2, 1, 1},
1,
0, 1,
false,
},
{
"MissingDataValuesAreIgnored",
[]float32{1, 2, 1, 1, e, e, e},
1,
0, 1,
false,
},
{
"AnOddNumberOfValuesInArray",
[]float32{2, 2, 1, 1, 1},
1,
0, 1,
false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
median, lower, upper, err := TwoSidedStdDev(tt.arr)
if tt.hasError {
assert.Error(t, err)
} else {
assert.NoError(t, err)
}
assert.Equal(t, tt.median, median, "median")
assert.Equal(t, tt.lower, lower, "lower")
assert.Equal(t, tt.upper, upper, "upper")
})
}
}
func TestRemoveMissingDataSentinel_Success(t *testing.T) {
assert.Equal(t, []float32{1, 2}, RemoveMissingDataSentinel([]float32{e, 1, e, 2, e}))
}
func TestRemoveMissingDataSentinel_Empty_Success(t *testing.T) {
assert.Equal(t, []float32{}, RemoveMissingDataSentinel([]float32{}))
}
func TestStdDevRatio(t *testing.T) {
tests := []struct {
name string
arg []float32
stddevRatio float32
median float32
lower float32
upper float32
wantError bool
errorContains string
}{
{
name: "Too little data, needs at least 5 data points",
arg: []float32{0, 0, 0, 0},
wantError: true,
errorContains: "Insufficient",
},
{
name: "Too little data after removing MissingDataSentinels",
arg: []float32{e, e, e, e, e, e, 1.0},
wantError: true,
errorContains: "Insufficient",
},
{
name: "Catches NaN results",
arg: []float32{0, 0, 0, 0, 0},
wantError: true,
errorContains: "NaN",
},
{
name: "Catches MissingDataSentinel as last point",
arg: []float32{0, 0, 0, 0, e},
wantError: true,
errorContains: "MissingDataSentinel",
},
{
name: "-Inf result is forced to -maxStdDevRatio",
arg: []float32{1, 1, 1, 1, -1.1},
stddevRatio: -maxStdDevRatio,
median: 1,
lower: 0,
upper: 0,
},
{
name: "Inf result is forced to -maxStdDevRatio",
arg: []float32{1, 1, 1, 1, 1.1},
stddevRatio: maxStdDevRatio,
median: 1,
lower: 0,
upper: 0,
},
{
name: "Last point equals the median",
arg: []float32{1, 2, 4, 5, 3},
stddevRatio: 0,
median: 3,
lower: 2.236068,
upper: 2.236068,
},
{
name: "Last point equals the twice the median",
arg: []float32{11, 12, 13, 14, 15, 16, 17, 20},
stddevRatio: 2.7774603,
median: 14,
lower: 2.6457512,
upper: 2.1602468,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
stddevRatio, median, lower, upper, err := StdDevRatio(tt.arg)
if tt.wantError {
assert.Error(t, err)
assert.Contains(t, err.Error(), tt.errorContains)
return
}
assert.Equal(t, tt.stddevRatio, stddevRatio, "stddevRatio")
assert.Equal(t, tt.median, median, "median")
assert.Equal(t, tt.lower, lower, "lower")
assert.Equal(t, tt.upper, upper, "upper")
})
}
}
func TestToFloat64(t *testing.T) {
assert.Equal(t, []float64{}, ToFloat64(nil))
assert.Equal(t, []float64{}, ToFloat64([]float32{}))
assert.Equal(t, []float64{1.0, 2.0}, ToFloat64([]float32{1.0, 2.0}))
}
func TestIQRR(t *testing.T) {
tests := []struct {
name string
args []float32
expected []float32
}{
{
name: "Handles empty arrays.",
args: []float32{},
expected: []float32{},
},
{
name: "Handles arrays with NaN for quartile values.",
args: []float32{1},
expected: []float32{1},
},
{
name: "Handles missing data sentinels.",
args: []float32{e},
expected: []float32{e},
},
{
name: "Handles arrays with Inf values.",
args: []float32{float32(math.Inf(0))},
expected: []float32{float32(math.Inf(0))},
},
{
name: "Outliers are removed",
args: []float32{5, 7, 10, 15, 19, 21, 21, 22, 22, 23, 23, 23, 23, 23, 24, 24, 24, 24, 25},
expected: []float32{e, e, e, 15, 19, 21, 21, 22, 22, 23, 23, 23, 23, 23, 24, 24, 24, 24, 25},
},
{
name: "Outliers are removed even in the presence of missing data sentinels",
args: []float32{5, 7, 10, 15, 19, 21, 21, 22, 22, e, 23, 23, 23, 23, 23, 24, 24, 24, 24, e, 25},
expected: []float32{e, e, e, 15, 19, 21, 21, 22, 22, e, 23, 23, 23, 23, 23, 24, 24, 24, 24, e, 25},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
IQRR(tt.args)
assert.Equal(t, tt.expected, tt.args)
})
}
}
func TestSum(t *testing.T) {
assert.Equal(t, float32(0), Sum(nil))
assert.Equal(t, float32(0), Sum([]float32{e}))
assert.Equal(t, float32(3), Sum([]float32{e, 1, 2}))
}
func TestGeo_ReturnsZeroIfAllMissing(t *testing.T) {
assert.Equal(t, float32(4), Geo([]float32{2, 8}), "4 = sqrt(2*8)")
assert.Equal(t, float32(0), Geo([]float32{-2, -8}), "Return 0 on all ignored.")
assert.Equal(t, float32(8), Geo([]float32{-2, 8}), "Ignore negative numbers.")
assert.Equal(t, float32(2), Geo([]float32{2, e}), "Ingore MissingDataSentinels.")
assert.Equal(t, float32(0), Geo([]float32{}), "Return 0 on empty vector.")
}
func TestGeoE_ReturnsMissingIfAllMissing(t *testing.T) {
assert.Equal(t, float32(4), GeoE([]float32{2, 8}), "4 = sqrt(2*8)")
assert.Equal(t, float32(e), GeoE([]float32{-2, -8}), "Return 0 on all ignored.")
assert.Equal(t, float32(8), GeoE([]float32{-2, 8}), "Ignore negative numbers.")
assert.Equal(t, float32(2), GeoE([]float32{2, e}), "Ingore MissingDataSentinels.")
assert.Equal(t, float32(e), GeoE([]float32{}), "Return 0 on empty vector.")
}
func TestCount(t *testing.T) {
assert.Equal(t, float32(0), Count([]float32{}), "Empty returns 0.")
assert.Equal(t, float32(0), Count([]float32{e}), "MissingDataSentinels are ignored")
assert.Equal(t, float32(2), Count([]float32{1, 3}), "Counts all non-MissingDataSentinels values")
assert.Equal(t, float32(2), Count([]float32{1, e, 3}), "Ingores MissingDataSentinels")
}
func TestMin(t *testing.T) {
assert.Equal(t, float32(math.MaxFloat32), Min([]float32{}))
assert.Equal(t, float32(math.MaxFloat32), Min([]float32{e}))
assert.Equal(t, float32(2), Min([]float32{2}))
assert.Equal(t, float32(3), Min([]float32{5, e, 3}))
}
func TestMax(t *testing.T) {
assert.Equal(t, float32(-math.MaxFloat32), Max([]float32{}))
assert.Equal(t, float32(-math.MaxFloat32), Max([]float32{e}))
assert.Equal(t, float32(2), Max([]float32{2}))
assert.Equal(t, float32(5), Max([]float32{5, e, 3}))
}