From 85d956c08c8d3ffbd527ef247753cfd7d38bd8bd Mon Sep 17 00:00:00 2001 From: DualFroz Date: Fri, 4 Sep 2026 02:56:39 +0200 Subject: [PATCH] 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. --- quartile.go | 33 ++++++++++++++++++++++++++------- quartile_test.go | 27 +++++++++++++++++++++++++++ 2 files changed, 53 insertions(+), 7 deletions(-) diff --git a/quartile.go b/quartile.go index 40bbf6e..e36d52d 100644 --- a/quartile.go +++ b/quartile.go @@ -32,10 +32,20 @@ func Quartile(input Float64Data) (Quartiles, error) { c2 = c1 + 1 } - // Find the Medians with the cutoff points - Q1, _ := Median(copy[:c1]) - Q2, _ := Median(copy) - Q3, _ := Median(copy[c2:]) + // Empty halves make Median return ErrEmptyInput; propagate it instead of + // returning NaN quartiles. + Q1, err := Median(copy[:c1]) + 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 @@ -46,7 +56,10 @@ func InterQuartileRange(input Float64Data) (float64, error) { if input.Len() == 0 { return math.NaN(), EmptyInputErr } - qs, _ := Quartile(input) + qs, err := Quartile(input) + if err != nil { + return math.NaN(), err + } iqr := qs.Q3 - qs.Q1 return iqr, nil } @@ -56,7 +69,10 @@ func Midhinge(input Float64Data) (float64, error) { if input.Len() == 0 { return math.NaN(), EmptyInputErr } - qs, _ := Quartile(input) + qs, err := Quartile(input) + if err != nil { + return math.NaN(), err + } mh := (qs.Q1 + qs.Q3) / 2 return mh, nil } @@ -68,7 +84,10 @@ func Trimean(input Float64Data) (float64, error) { } 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 } diff --git a/quartile_test.go b/quartile_test.go index 93d524e..9f4bf8a 100644 --- a/quartile_test.go +++ b/quartile_test.go @@ -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) { s1 := []float64{102, 104, 105, 107, 108, 109, 110, 112, 115, 116, 118} iqr, _ := stats.InterQuartileRange(s1) @@ -53,6 +65,11 @@ func TestInterQuartileRange(t *testing.T) { if err == nil { 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) { @@ -67,6 +84,11 @@ func TestMidhinge(t *testing.T) { if err == nil { 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) { @@ -81,4 +103,9 @@ func TestTrimean(t *testing.T) { if err == nil { 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") + } }