fix: propagate error from Quartile for inputs too small for quartiles

Quartile computed the median of empty slices for inputs with fewer than
three elements and discarded the resulting error, returning NaN quartiles
with a nil error. The silent NaN then leaked into InterQuartileRange,
Midhinge and Trimean. Propagate the error instead, mirroring the existing
EmptyInputErr handling.
This commit is contained in:
dualfroz committed 2026-09-06 00:42:21 +02:00
1 parent f9994fd0aa
commit 85d956c08c
2 files changed
+53 -7

No files matched your search

+26 -7
View File
@@ -32,10 +32,20 @@ func Quartile(input Float64Data) (Quartiles, error) {
c2 = c1 + 1 c2 = c1 + 1
} }
// Find the Medians with the cutoff points // Empty halves make Median return ErrEmptyInput; propagate it instead of
Q1, _ := Median(copy[:c1]) // returning NaN quartiles.
Q2, _ := Median(copy) Q1, err := Median(copy[:c1])
Q3, _ := Median(copy[c2:]) if err != nil {
return Quartiles{}, err
}
Q2, err := Median(copy)
if err != nil {
return Quartiles{}, err
}
Q3, err := Median(copy[c2:])
if err != nil {
return Quartiles{}, err
}
return Quartiles{Q1, Q2, Q3}, nil return Quartiles{Q1, Q2, Q3}, nil
@@ -46,7 +56,10 @@ func InterQuartileRange(input Float64Data) (float64, error) {
if input.Len() == 0 { if input.Len() == 0 {
return math.NaN(), EmptyInputErr return math.NaN(), EmptyInputErr
} }
qs, _ := Quartile(input) qs, err := Quartile(input)
if err != nil {
return math.NaN(), err
}
iqr := qs.Q3 - qs.Q1 iqr := qs.Q3 - qs.Q1
return iqr, nil return iqr, nil
} }
@@ -56,7 +69,10 @@ func Midhinge(input Float64Data) (float64, error) {
if input.Len() == 0 { if input.Len() == 0 {
return math.NaN(), EmptyInputErr return math.NaN(), EmptyInputErr
} }
qs, _ := Quartile(input) qs, err := Quartile(input)
if err != nil {
return math.NaN(), err
}
mh := (qs.Q1 + qs.Q3) / 2 mh := (qs.Q1 + qs.Q3) / 2
return mh, nil return mh, nil
} }
@@ -68,7 +84,10 @@ func Trimean(input Float64Data) (float64, error) {
} }
c := sortedCopy(input) c := sortedCopy(input)
q, _ := Quartile(c) q, err := Quartile(c)
if err != nil {
return math.NaN(), err
}
return (q.Q1 + (q.Q2 * 2) + q.Q3) / 4, nil return (q.Q1 + (q.Q2 * 2) + q.Q3) / 4, nil
} }
+27
View File
@@ -41,6 +41,18 @@ func TestQuartile(t *testing.T) {
} }
} }
func TestQuartileSingleElement(t *testing.T) {
// A single element has no halves, so Quartile must error rather than
// return NaN with a nil error.
quartiles, err := stats.Quartile([]float64{1})
if err == nil {
t.Errorf("Single element slice should have returned an error")
}
if quartiles != (stats.Quartiles{}) {
t.Errorf("Errored result should be the zero Quartiles value, got %+v", quartiles)
}
}
func TestInterQuartileRange(t *testing.T) { func TestInterQuartileRange(t *testing.T) {
s1 := []float64{102, 104, 105, 107, 108, 109, 110, 112, 115, 116, 118} s1 := []float64{102, 104, 105, 107, 108, 109, 110, 112, 115, 116, 118}
iqr, _ := stats.InterQuartileRange(s1) iqr, _ := stats.InterQuartileRange(s1)
@@ -53,6 +65,11 @@ func TestInterQuartileRange(t *testing.T) {
if err == nil { if err == nil {
t.Errorf("Empty slice should have returned an error") t.Errorf("Empty slice should have returned an error")
} }
_, err = stats.InterQuartileRange([]float64{1})
if err == nil {
t.Errorf("Single element slice should have returned an error")
}
} }
func TestMidhinge(t *testing.T) { func TestMidhinge(t *testing.T) {
@@ -67,6 +84,11 @@ func TestMidhinge(t *testing.T) {
if err == nil { if err == nil {
t.Errorf("Empty slice should have returned an error") t.Errorf("Empty slice should have returned an error")
} }
_, err = stats.Midhinge([]float64{1})
if err == nil {
t.Errorf("Single element slice should have returned an error")
}
} }
func TestTrimean(t *testing.T) { func TestTrimean(t *testing.T) {
@@ -81,4 +103,9 @@ func TestTrimean(t *testing.T) {
if err == nil { if err == nil {
t.Errorf("Empty slice should have returned an error") t.Errorf("Empty slice should have returned an error")
} }
_, err = stats.Trimean([]float64{1})
if err == nil {
t.Errorf("Single element slice should have returned an error")
}
} }