diff --git a/tdigest/double.go b/tdigest/double.go index d2063b2..10d7a69 100644 --- a/tdigest/double.go +++ b/tdigest/double.go @@ -204,6 +204,10 @@ func (d *Double) Merge(other *Double) error { return ErrEmpty } + // Extreme centroid means are not the true min/max when those centroids have weight > 1. + d.min = min(d.min, other.min) + d.max = max(d.max, other.max) + tmp := make([]doublePrecisionCentroid, 0, len(d.buffer)+len(d.centroids)+len(other.buffer)+len(other.centroids)) for _, v := range d.buffer { tmp = append(tmp, doublePrecisionCentroid{mean: v, weight: 1}) diff --git a/tdigest/double_test.go b/tdigest/double_test.go index b1077ac..d3129ee 100644 --- a/tdigest/double_test.go +++ b/tdigest/double_test.go @@ -246,6 +246,35 @@ func TestDouble_Merge(t *testing.T) { assert.Equal(t, 1.0, rank) }) + t.Run("Merge preserves deserialized min max with weighted tails", func(t *testing.T) { + other, err := newDoubleFromInternalStates(false, DefaultK, -1, 21, + []doublePrecisionCentroid{{mean: 0, weight: 4}, {mean: 10, weight: 4}, {mean: 20, weight: 4}}, + 12, nil) + assert.NoError(t, err) + + empty, err := NewDouble(DefaultK) + assert.NoError(t, err) + assert.NoError(t, empty.Merge(other)) + minVal, err := empty.MinValue() + assert.NoError(t, err) + assert.Equal(t, -1.0, minVal) + maxVal, err := empty.MaxValue() + assert.NoError(t, err) + assert.Equal(t, 21.0, maxVal) + + left, err := newDoubleFromInternalStates(false, DefaultK, 5, 15, + []doublePrecisionCentroid{{mean: 6, weight: 4}, {mean: 10, weight: 4}, {mean: 14, weight: 4}}, + 12, nil) + assert.NoError(t, err) + assert.NoError(t, left.Merge(other)) + minVal, err = left.MinValue() + assert.NoError(t, err) + assert.Equal(t, -1.0, minVal) + maxVal, err = left.MaxValue() + assert.NoError(t, err) + assert.Equal(t, 21.0, maxVal) + }) + t.Run("Merge Large", func(t *testing.T) { sk1, err := NewDouble(DefaultK) assert.NoError(t, err)