This is an automated email from the ASF dual-hosted git repository.

edwardxu pushed a commit to branch unstable
in repository https://gitbox.apache.org/repos/asf/kvrocks.git


The following commit(s) were added to refs/heads/unstable by this push:
     new e61c81478 feat(tdigest): implement TDIGEST.TRIMMED_MEAN command (#3312)
e61c81478 is described below

commit e61c81478c27a2309081d219b9dcb8f81b19d9aa
Author: chakkk309 <[email protected]>
AuthorDate: Wed Mar 25 09:42:49 2026 +0800

    feat(tdigest): implement TDIGEST.TRIMMED_MEAN command (#3312)
    
    Co-authored-by: Copilot <[email protected]>
    Co-authored-by: Edward Xu <[email protected]>
---
 src/commands/cmd_tdigest.cc                    |  67 ++++++++++++++
 src/types/redis_tdigest.cc                     |  36 ++++++++
 src/types/redis_tdigest.h                      |   6 ++
 src/types/tdigest.h                            |  43 +++++++++
 tests/cppunit/types/tdigest_test.cc            |  78 +++++++++++++++++
 tests/gocase/unit/type/tdigest/tdigest_test.go | 117 +++++++++++++++++++++++++
 6 files changed, 347 insertions(+)

diff --git a/src/commands/cmd_tdigest.cc b/src/commands/cmd_tdigest.cc
index f839a3f7f..ad144bf37 100644
--- a/src/commands/cmd_tdigest.cc
+++ b/src/commands/cmd_tdigest.cc
@@ -45,6 +45,11 @@ constexpr auto kInfoUnmergedWeight = "Unmerged weight";
 constexpr auto kInfoObservations = "Observations";
 constexpr auto kInfoTotalCompressions = "Total compressions";
 constexpr auto kNan = "nan";
+
+constexpr const char *errParseLowCutQuantile = "error parsing 
low_cut_percentile";
+constexpr const char *errParseHighCutQuantile = "error parsing 
high_cut_percentile";
+constexpr const char *errCutQuantileRange = "low_cut_percentile and 
high_cut_percentile should be in [0,1]";
+constexpr const char *errLowCutQuantileLess = "low_cut_percentile should be 
lower than high_cut_percentile";
 }  // namespace
 
 class CommandTDigestCreate : public Commander {
@@ -492,6 +497,67 @@ class CommandTDigestMerge : public Commander {
   TDigestMergeOptions options_;
 };
 
+class CommandTDigestTrimmedMean : public Commander {
+ public:
+  Status Parse(const std::vector<std::string> &args) override {
+    if (args.size() != 4) {
+      return {Status::RedisParseErr, errWrongNumOfArguments};
+    }
+
+    key_name_ = args[1];
+
+    auto low_cut_quantile = ParseFloat(args[2]);
+    if (!low_cut_quantile || std::isnan(*low_cut_quantile)) {
+      return {Status::RedisParseErr, errParseLowCutQuantile};
+    }
+    low_cut_quantile_ = *low_cut_quantile;
+
+    auto high_cut_quantile = ParseFloat(args[3]);
+    if (!high_cut_quantile || std::isnan(*high_cut_quantile)) {
+      return {Status::RedisParseErr, errParseHighCutQuantile};
+    }
+    high_cut_quantile_ = *high_cut_quantile;
+
+    if (!std::isfinite(low_cut_quantile_) || low_cut_quantile_ < 0.0 || 
low_cut_quantile_ > 1.0) {
+      return {Status::RedisParseErr, errCutQuantileRange};
+    }
+    if (!std::isfinite(high_cut_quantile_) || high_cut_quantile_ < 0.0 || 
high_cut_quantile_ > 1.0) {
+      return {Status::RedisParseErr, errCutQuantileRange};
+    }
+    if (low_cut_quantile_ >= high_cut_quantile_) {
+      return {Status::RedisParseErr, errLowCutQuantileLess};
+    }
+
+    return Status::OK();
+  }
+
+  Status Execute(engine::Context &ctx, Server *srv, Connection *conn, 
std::string *output) override {
+    TDigest tdigest(srv->storage, conn->GetNamespace());
+    TDigestTrimmedMeanResult result;
+
+    auto s = tdigest.TrimmedMean(ctx, key_name_, low_cut_quantile_, 
high_cut_quantile_, &result);
+    if (!s.ok()) {
+      if (s.IsNotFound()) {
+        return {Status::RedisExecErr, errKeyNotFound};
+      }
+      return {Status::RedisExecErr, s.ToString()};
+    }
+
+    if (!result.mean.has_value()) {
+      *output = redis::BulkString(kNan);
+    } else {
+      *output = redis::BulkString(util::Float2String(*result.mean));
+    }
+
+    return Status::OK();
+  }
+
+ private:
+  std::string key_name_;
+  double low_cut_quantile_;
+  double high_cut_quantile_;
+};
+
 std::vector<CommandKeyRange> GetMergeKeyRange(const std::vector<std::string> 
&args) {
   auto numkeys = ParseInt<int>(args[2], 10).ValueOr(0);
   return {{1, 1, 1}, {3, 2 + numkeys, 1}};
@@ -507,6 +573,7 @@ REDIS_REGISTER_COMMANDS(TDigest, 
MakeCmdAttr<CommandTDigestCreate>("tdigest.crea
                         
MakeCmdAttr<CommandTDigestByRevRank>("tdigest.byrevrank", -3, "read-only", 1, 
1, 1),
                         MakeCmdAttr<CommandTDigestByRank>("tdigest.byrank", 
-3, "read-only", 1, 1, 1),
                         
MakeCmdAttr<CommandTDigestQuantile>("tdigest.quantile", -3, "read-only", 1, 1, 
1),
+                        
MakeCmdAttr<CommandTDigestTrimmedMean>("tdigest.trimmed_mean", 4, "read-only", 
1, 1, 1),
                         MakeCmdAttr<CommandTDigestReset>("tdigest.reset", 2, 
"write", 1, 1, 1),
                         MakeCmdAttr<CommandTDigestMerge>("tdigest.merge", -4, 
"write", GetMergeKeyRange));
 }  // namespace redis
diff --git a/src/types/redis_tdigest.cc b/src/types/redis_tdigest.cc
index c44ff6b28..dcdc0f44c 100644
--- a/src/types/redis_tdigest.cc
+++ b/src/types/redis_tdigest.cc
@@ -759,6 +759,42 @@ rocksdb::Status 
TDigest::applyNewCentroids(ObserverOrUniquePtr<rocksdb::WriteBat
   return rocksdb::Status::OK();
 }
 
+rocksdb::Status TDigest::TrimmedMean(engine::Context& ctx, const Slice& 
digest_name, double low_cut_quantile,
+                                     double high_cut_quantile, 
TDigestTrimmedMeanResult* result) {
+  auto ns_key = AppendNamespacePrefix(digest_name);
+  TDigestMetadata metadata;
+
+  {
+    LockGuard guard(storage_->GetLockManager(), ns_key);
+    if (auto status = getMetaDataByNsKey(ctx, ns_key, &metadata); 
!status.ok()) {
+      return status;
+    }
+
+    if (metadata.total_observations == 0) {
+      result->mean.reset();
+      return rocksdb::Status::OK();
+    }
+
+    if (auto status = mergeNodes(ctx, ns_key, &metadata); !status.ok()) {
+      return status;
+    }
+  }
+
+  // Dump centroids and create DummyCentroids wrapper for TDigest algorithm
+  std::vector<Centroid> centroids;
+  if (auto status = dumpCentroids(ctx, ns_key, metadata, &centroids); 
!status.ok()) {
+    return status;
+  }
+  auto dump_centroids = DummyCentroids<false>(metadata, centroids);
+  auto trimmed_mean_result = TDigestTrimmedMean(dump_centroids, 
low_cut_quantile, high_cut_quantile);
+  if (!trimmed_mean_result) {
+    return rocksdb::Status::InvalidArgument(trimmed_mean_result.Msg());
+  }
+
+  result->mean = *trimmed_mean_result;
+  return rocksdb::Status::OK();
+}
+
 std::string TDigest::internalSegmentGuardPrefixKey(const TDigestMetadata& 
metadata, const std::string& ns_key,
                                                    SegmentType seg) const {
   std::string prefix_key;
diff --git a/src/types/redis_tdigest.h b/src/types/redis_tdigest.h
index 236ec9eb4..a949c5832 100644
--- a/src/types/redis_tdigest.h
+++ b/src/types/redis_tdigest.h
@@ -53,6 +53,10 @@ struct TDigestQuantitleResult {
   std::optional<std::vector<double>> quantiles;
 };
 
+struct TDigestTrimmedMeanResult {
+  std::optional<double> mean;
+};
+
 class TDigest : public SubKeyScanner {
  public:
   using Slice = rocksdb::Slice;
@@ -85,6 +89,8 @@ class TDigest : public SubKeyScanner {
                             std::vector<double>* result);
   rocksdb::Status ByRank(engine::Context& ctx, const Slice& digest_name, const 
std::vector<int>& inputs,
                          std::vector<double>* result);
+  rocksdb::Status TrimmedMean(engine::Context& ctx, const Slice& digest_name, 
double low_cut_quantile,
+                              double high_cut_quantile, 
TDigestTrimmedMeanResult* result);
   rocksdb::Status GetMetaData(engine::Context& context, const Slice& 
digest_name, TDigestMetadata* metadata);
 
  private:
diff --git a/src/types/tdigest.h b/src/types/tdigest.h
index b9c4a6fcb..1f152c1e0 100644
--- a/src/types/tdigest.h
+++ b/src/types/tdigest.h
@@ -309,3 +309,46 @@ inline Status TDigestRank(TD&& td, const 
std::vector<double>& inputs, std::vecto
   }
   return Status::OK();
 }
+
+template <typename TD>
+inline StatusOr<double> TDigestTrimmedMean(TD&& td, double low_cut_quantile, 
double high_cut_quantile) {
+  if (td.Size() == 0) {
+    return std::numeric_limits<double>::quiet_NaN();
+  }
+
+  const double total_weight = td.TotalWeight();
+  const double leftmost_weight = std::floor(total_weight * low_cut_quantile);
+  const double rightmost_weight = std::ceil(total_weight * high_cut_quantile);
+
+  double count_done = 0.0;
+  double trimmed_sum = 0.0;
+  double trimmed_count = 0.0;
+
+  auto iter = td.Begin();
+  while (iter->Valid()) {
+    auto centroid = GET_OR_RET(iter->GetCentroid());
+    const double n_weight = centroid.weight;
+    double count_add = n_weight;
+
+    // Keep only the portion of this centroid that overlaps with the trimmed 
rank range.
+    count_add -= std::min(std::max(0.0, leftmost_weight - count_done), 
count_add);
+    count_add = std::min(std::max(0.0, rightmost_weight - count_done), 
count_add);
+
+    count_done += n_weight;
+
+    trimmed_sum += centroid.mean * count_add;
+    trimmed_count += count_add;
+
+    if (count_done >= rightmost_weight) {
+      break;
+    }
+
+    iter->Next();
+  }
+
+  if (trimmed_count == 0.0) {
+    return std::numeric_limits<double>::quiet_NaN();
+  }
+
+  return trimmed_sum / trimmed_count;
+}
diff --git a/tests/cppunit/types/tdigest_test.cc 
b/tests/cppunit/types/tdigest_test.cc
index ed12df136..9fc1fb1f7 100644
--- a/tests/cppunit/types/tdigest_test.cc
+++ b/tests/cppunit/types/tdigest_test.cc
@@ -524,3 +524,81 @@ TEST_F(RedisTDigestTest, ByRank_And_ByRevRank) {
   EXPECT_EQ(result[0], 1.0) << "Rank 0 should be minimum";
   EXPECT_TRUE(std::isinf(result[3])) << "Rank >= total_weight should be 
infinity";
 }
+
+TEST_F(RedisTDigestTest, TrimmedMean) {
+  std::string test_digest_name = "test_digest_trimmed_mean" + 
std::to_string(util::GetTimeStampMS());
+  bool exists = false;
+  auto status = tdigest_->Create(*ctx_, test_digest_name, {100}, &exists);
+  ASSERT_FALSE(exists);
+  ASSERT_TRUE(status.ok());
+
+  std::vector<double> values = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
+  status = tdigest_->Add(*ctx_, test_digest_name, values);
+  ASSERT_TRUE(status.ok()) << status.ToString();
+
+  redis::TDigestTrimmedMeanResult result;
+  status = tdigest_->TrimmedMean(*ctx_, test_digest_name, 0.1, 0.9, &result);
+  ASSERT_TRUE(status.ok()) << status.ToString();
+  ASSERT_TRUE(result.mean.has_value());
+  EXPECT_NEAR(*result.mean, 5.5, 0.01);
+
+  status = tdigest_->TrimmedMean(*ctx_, test_digest_name, 0.0, 1.0, &result);
+  ASSERT_TRUE(status.ok()) << status.ToString();
+  ASSERT_TRUE(result.mean.has_value());
+  EXPECT_NEAR(*result.mean, 5.5, 0.01);
+
+  status = tdigest_->TrimmedMean(*ctx_, test_digest_name, 0.25, 0.75, &result);
+  ASSERT_TRUE(status.ok()) << status.ToString();
+  ASSERT_TRUE(result.mean.has_value());
+  EXPECT_NEAR(*result.mean, 5.5, 0.01);
+}
+
+TEST_F(RedisTDigestTest, TrimmedMeanEmptyDigest) {
+  std::string test_digest_name = "test_digest_trimmed_mean_empty" + 
std::to_string(util::GetTimeStampMS());
+  bool exists = false;
+  auto status = tdigest_->Create(*ctx_, test_digest_name, {100}, &exists);
+  ASSERT_FALSE(exists);
+  ASSERT_TRUE(status.ok());
+
+  redis::TDigestTrimmedMeanResult result;
+  status = tdigest_->TrimmedMean(*ctx_, test_digest_name, 0.1, 0.9, &result);
+  ASSERT_TRUE(status.ok()) << status.ToString();
+  ASSERT_FALSE(result.mean.has_value());
+}
+
+TEST_F(RedisTDigestTest, TrimmedMeanUnorderedInput) {
+  std::string test_digest_name = "test_digest_trimmed_mean_unordered" + 
std::to_string(util::GetTimeStampMS());
+  bool exists = false;
+  auto status = tdigest_->Create(*ctx_, test_digest_name, {100}, &exists);
+  ASSERT_FALSE(exists);
+  ASSERT_TRUE(status.ok());
+
+  std::vector<double> values = {5, 2, 8, 1, 9, 3, 7, 4, 6, 10};
+  status = tdigest_->Add(*ctx_, test_digest_name, values);
+  ASSERT_TRUE(status.ok()) << status.ToString();
+
+  redis::TDigestTrimmedMeanResult result;
+  status = tdigest_->TrimmedMean(*ctx_, test_digest_name, 0.1, 0.9, &result);
+  ASSERT_TRUE(status.ok()) << status.ToString();
+  ASSERT_TRUE(result.mean.has_value());
+  EXPECT_NEAR(*result.mean, 5.5, 0.01);
+}
+
+TEST_F(RedisTDigestTest, TrimmedMeanComplexInput) {
+  std::string test_digest_name = "test_digest_trimmed_mean_complex" + 
std::to_string(util::GetTimeStampMS());
+  bool exists = false;
+  auto status = tdigest_->Create(*ctx_, test_digest_name, {100}, &exists);
+  ASSERT_FALSE(exists);
+  ASSERT_TRUE(status.ok());
+
+  std::vector<double> values = {-10, 5, -3, 5, 0, 5, 3, -5, 10, -10};
+  status = tdigest_->Add(*ctx_, test_digest_name, values);
+  ASSERT_TRUE(status.ok()) << status.ToString();
+
+  redis::TDigestTrimmedMeanResult result;
+  status = tdigest_->TrimmedMean(*ctx_, test_digest_name, 0.2, 0.8, &result);
+  ASSERT_TRUE(status.ok()) << status.ToString();
+  ASSERT_TRUE(result.mean.has_value());
+  ASSERT_FALSE(std::isnan(*result.mean));
+  EXPECT_NEAR(*result.mean, 5.0 / 6.0, 0.01);
+}
diff --git a/tests/gocase/unit/type/tdigest/tdigest_test.go 
b/tests/gocase/unit/type/tdigest/tdigest_test.go
index bcc4a832c..6c58d8402 100644
--- a/tests/gocase/unit/type/tdigest/tdigest_test.go
+++ b/tests/gocase/unit/type/tdigest/tdigest_test.go
@@ -40,6 +40,11 @@ const (
        errMsgKeyNotExist                     = "key does not exist"
        errNumkeysMustBePositive              = "numkeys need to be a positive 
integer"
        errCompressionParameterMustBePositive = "compression parameter needs to 
be a positive integer"
+       errMsgParseLowCutQuantile             = "error parsing 
low_cut_percentile"
+       errMsgParseHighCutQuantile            = "error parsing 
high_cut_percentile"
+       errMsgLowCutQuantileRange             = "low_cut_percentile and 
high_cut_percentile should be in [0,1]"
+       errMsgHighCutQuantileRange            = "low_cut_percentile and 
high_cut_percentile should be in [0,1]"
+       errMsgLowCutQuantileLess              = "low_cut_percentile should be 
lower than high_cut_percentile"
 )
 
 type tdigestInfo struct {
@@ -717,6 +722,118 @@ func tdigestTests(t *testing.T, configs 
util.KvrocksServerConfigs) {
                        require.EqualValues(t, expected[i], rank, "REVRANK 
mismatch at index %d", i)
                }
        })
+
+       t.Run("TDIGEST.TRIMMED_MEAN with non-existent key", func(t *testing.T) {
+               require.ErrorContains(t, rdb.Do(ctx, "TDIGEST.TRIMMED_MEAN", 
"nonexistent", "0.1", "0.9").Err(), errMsgKeyNotExist)
+       })
+
+       t.Run("TDIGEST.TRIMMED_MEAN with empty tdigest", func(t *testing.T) {
+               emptyKey := "tdigest_empty"
+               require.NoError(t, rdb.Do(ctx, "TDIGEST.CREATE", emptyKey, 
"compression", "100").Err())
+
+               result := rdb.Do(ctx, "TDIGEST.TRIMMED_MEAN", emptyKey, "0.1", 
"0.9")
+               require.NoError(t, result.Err())
+               require.Equal(t, "nan", result.Val())
+       })
+
+       t.Run("TDIGEST.TRIMMED_MEAN with basic data set", func(t *testing.T) {
+               key := "tdigest_basic"
+               require.NoError(t, rdb.Do(ctx, "TDIGEST.CREATE", key, 
"compression", "100").Err())
+               require.NoError(t, rdb.Do(ctx, "TDIGEST.ADD", key, "1", "2", 
"3", "4", "5", "6", "7", "8", "9", "10").Err())
+
+               result := rdb.Do(ctx, "TDIGEST.TRIMMED_MEAN", key, "0.1", "0.9")
+               require.NoError(t, result.Err())
+               mean, err := strconv.ParseFloat(result.Val().(string), 64)
+               require.NoError(t, err)
+               require.InDelta(t, 5.5, mean, 0.01)
+       })
+
+       t.Run("TDIGEST.TRIMMED_MEAN with no trimming", func(t *testing.T) {
+               key := "tdigest_no_trim"
+               require.NoError(t, rdb.Do(ctx, "TDIGEST.CREATE", key, 
"compression", "100").Err())
+               require.NoError(t, rdb.Do(ctx, "TDIGEST.ADD", key, "1", "2", 
"3", "4", "5", "6", "7", "8", "9", "10").Err())
+
+               result := rdb.Do(ctx, "TDIGEST.TRIMMED_MEAN", key, "0", "1")
+               require.NoError(t, result.Err())
+               mean, err := strconv.ParseFloat(result.Val().(string), 64)
+               require.NoError(t, err)
+               require.InDelta(t, 5.5, mean, 0.01)
+       })
+
+       t.Run("TDIGEST.TRIMMED_MEAN with skewed data", func(t *testing.T) {
+               key := "tdigest_skewed"
+               require.NoError(t, rdb.Do(ctx, "TDIGEST.CREATE", key, 
"compression", "100").Err())
+               require.NoError(t, rdb.Do(ctx, "TDIGEST.ADD", key, "1", "1", 
"1", "1", "1", "10", "100").Err())
+
+               result := rdb.Do(ctx, "TDIGEST.TRIMMED_MEAN", key, "0.2", "0.8")
+               require.NoError(t, result.Err())
+               mean, err := strconv.ParseFloat(result.Val().(string), 64)
+               require.NoError(t, err)
+               require.InDelta(t, 2.8, mean, 0.01)
+       })
+
+       t.Run("TDIGEST.TRIMMED_MEAN wrong number of arguments", func(t 
*testing.T) {
+               require.ErrorContains(t, rdb.Do(ctx, 
"TDIGEST.TRIMMED_MEAN").Err(), errMsgWrongNumberArg)
+               require.ErrorContains(t, rdb.Do(ctx, "TDIGEST.TRIMMED_MEAN", 
"key").Err(), errMsgWrongNumberArg)
+               require.ErrorContains(t, rdb.Do(ctx, "TDIGEST.TRIMMED_MEAN", 
"key", "0.1").Err(), errMsgWrongNumberArg)
+               require.ErrorContains(t, rdb.Do(ctx, "TDIGEST.TRIMMED_MEAN", 
"key", "0.1", "0.9", "extra").Err(), errMsgWrongNumberArg)
+       })
+
+       t.Run("TDIGEST.TRIMMED_MEAN invalid quantile ranges", func(t 
*testing.T) {
+               key := "tdigest_invalid"
+               require.NoError(t, rdb.Do(ctx, "TDIGEST.CREATE", key, 
"compression", "100").Err())
+               require.NoError(t, rdb.Do(ctx, "TDIGEST.ADD", key, "1", "2", 
"3", "4", "5").Err())
+
+               require.ErrorContains(t, rdb.Do(ctx, "TDIGEST.TRIMMED_MEAN", 
key, "-0.1", "0.9").Err(), errMsgLowCutQuantileRange)
+               require.ErrorContains(t, rdb.Do(ctx, "TDIGEST.TRIMMED_MEAN", 
key, "0.1", "1.1").Err(), errMsgHighCutQuantileRange)
+               require.ErrorContains(t, rdb.Do(ctx, "TDIGEST.TRIMMED_MEAN", 
key, "0.9", "0.1").Err(), errMsgLowCutQuantileLess)
+               require.ErrorContains(t, rdb.Do(ctx, "TDIGEST.TRIMMED_MEAN", 
key, "0.5", "0.5").Err(), errMsgLowCutQuantileLess)
+       })
+
+       t.Run("TDIGEST.TRIMMED_MEAN invalid quantile parsing", func(t 
*testing.T) {
+               key := "tdigest_invalid_parse"
+               require.NoError(t, rdb.Do(ctx, "TDIGEST.CREATE", key, 
"compression", "100").Err())
+               require.NoError(t, rdb.Do(ctx, "TDIGEST.ADD", key, "1", "2", 
"3", "4", "5").Err())
+
+               require.ErrorContains(t, rdb.Do(ctx, "TDIGEST.TRIMMED_MEAN", 
key, "abc", "0.9").Err(), errMsgParseLowCutQuantile)
+               require.ErrorContains(t, rdb.Do(ctx, "TDIGEST.TRIMMED_MEAN", 
key, "nan", "0.9").Err(), errMsgParseLowCutQuantile)
+               require.ErrorContains(t, rdb.Do(ctx, "TDIGEST.TRIMMED_MEAN", 
key, "0.1", "abc").Err(), errMsgParseHighCutQuantile)
+               require.ErrorContains(t, rdb.Do(ctx, "TDIGEST.TRIMMED_MEAN", 
key, "0.1", "nan").Err(), errMsgParseHighCutQuantile)
+       })
+
+       t.Run("TDIGEST.TRIMMED_MEAN with single value", func(t *testing.T) {
+               key := "tdigest_single"
+               require.NoError(t, rdb.Do(ctx, "TDIGEST.CREATE", key, 
"compression", "100").Err())
+               require.NoError(t, rdb.Do(ctx, "TDIGEST.ADD", key, "42", 
"42").Err())
+
+               result := rdb.Do(ctx, "TDIGEST.TRIMMED_MEAN", key, "0.1", "0.9")
+               require.NoError(t, result.Err())
+               mean, err := strconv.ParseFloat(result.Val().(string), 64)
+               require.NoError(t, err)
+               require.InDelta(t, 42.0, mean, 0.01)
+       })
+
+       t.Run("TDIGEST.TRIMMED_MEAN with extreme trimming", func(t *testing.T) {
+               key := "tdigest_extreme"
+               require.NoError(t, rdb.Do(ctx, "TDIGEST.CREATE", key, 
"compression", "100").Err())
+               require.NoError(t, rdb.Do(ctx, "TDIGEST.ADD", key, "1", "2", 
"3", "4", "5", "6", "7", "8", "9", "10").Err())
+
+               result := rdb.Do(ctx, "TDIGEST.TRIMMED_MEAN", key, "0.4", "0.6")
+               require.NoError(t, result.Err())
+               mean, err := strconv.ParseFloat(result.Val().(string), 64)
+               require.NoError(t, err)
+               require.InDelta(t, 5.5, mean, 0.01)
+       })
+
+       t.Run("TDIGEST.TRIMMED_MEAN with nearly equal quantiles", func(t 
*testing.T) {
+               key := "tdigest_nearly_equal"
+               require.NoError(t, rdb.Do(ctx, "TDIGEST.CREATE", key, 
"compression", "1000").Err())
+               require.NoError(t, rdb.Do(ctx, "TDIGEST.ADD", key, "1", "2", 
"3", "4", "5", "6", "7", "8", "9", "10").Err())
+
+               result := rdb.Do(ctx, "TDIGEST.TRIMMED_MEAN", key, "0.5", 
"0.5000000001")
+               require.NoError(t, result.Err())
+               require.Equal(t, "6", result.Val())
+       })
 }
 
 func TestTDigestByRankAndByRevRank(t *testing.T) {

Reply via email to