You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@druid.apache.org by cw...@apache.org on 2021/08/13 17:28:09 UTC

[druid] branch master updated: Add SQL functions to format numbers into human readable format (#10635)

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

cwylie pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/druid.git


The following commit(s) were added to refs/heads/master by this push:
     new e40be0a  Add SQL functions to format numbers into human readable format (#10635)
e40be0a is described below

commit e40be0ae2813722a08d185e1f69255eff87094e5
Author: frank chen <fr...@outlook.com>
AuthorDate: Sat Aug 14 01:27:49 2021 +0800

    Add SQL functions to format numbers into human readable format (#10635)
    
    * add binary_byte_format/decimal_byte_format/decimal_format
    
    * clean code
    
    * fix doc
    
    * fix review comments
    
    * add spelling check rules
    
    * remove extra param
    
    * improve type handling and null handling
    
    * remove extra zeros
    
    * fix tests and add space between unit suffix and number as most size-format functions do
    
    * fix tests
    
    * add examples
    
    * change function names according to review comments
    
    * fix merge
    
    Signed-off-by: frank chen <fr...@outlook.com>
    
    * no need to configure NullHandling explicitly for tests
    
    Signed-off-by: frank chen <fr...@outlook.com>
    
    * fix tests in SQL-Compatible mode
    
    Signed-off-by: frank chen <fr...@outlook.com>
    
    * Resolve review comments
    
    * Update SQL test case to check null handling
    
    * Fix intellij inspections
    
    * Add more examples
    
    * Fix example
---
 .../druid/java/util/common/HumanReadableBytes.java | 108 ++++++++++++
 .../java/org/apache/druid/math/expr/Function.java  | 100 +++++++++++
 .../java/util/common/HumanReadableBytesTest.java   |  92 ++++++++++
 .../org/apache/druid/math/expr/FunctionTest.java   | 178 +++++++++++++++++++
 docs/misc/math-expr.md                             |   9 +
 docs/querying/sql.md                               |   4 +
 .../HumanReadableFormatOperatorConversion.java     | 123 +++++++++++++
 .../sql/calcite/planner/DruidOperatorTable.java    |   9 +
 .../apache/druid/sql/calcite/CalciteQueryTest.java | 104 +++++++++++
 .../sql/calcite/expression/ExpressionsTest.java    | 190 +++++++++++++++++++++
 website/.spelling                                  |   5 +
 11 files changed, 922 insertions(+)

diff --git a/core/src/main/java/org/apache/druid/java/util/common/HumanReadableBytes.java b/core/src/main/java/org/apache/druid/java/util/common/HumanReadableBytes.java
index 9d93854..b63e0f2 100644
--- a/core/src/main/java/org/apache/druid/java/util/common/HumanReadableBytes.java
+++ b/core/src/main/java/org/apache/druid/java/util/common/HumanReadableBytes.java
@@ -208,4 +208,112 @@ public class HumanReadableBytes
       throw new IAE("Invalid format or out of range of long: %s", rawNumber);
     }
   }
+
+  public enum UnitSystem
+  {
+    /**
+     * also known as IEC format
+     * eg: B, KiB, MiB, GiB ...
+     */
+    BINARY_BYTE,
+
+    /**
+     * also known as SI format
+     * eg: B, KB, MB ...
+     */
+    DECIMAL_BYTE,
+
+    /**
+     * simplified SI format without 'B' indicator
+     * eg: K, M, G ...
+     */
+    DECIMAL
+  }
+
+  /**
+   * Returns a human-readable string version of input value
+   *
+   * @param bytes      input value. Negative value is also allowed
+   * @param precision  [0,3]
+   * @param unitSystem which unit system is adopted to format the input value, see {@link UnitSystem}
+   */
+  public static String format(long bytes, long precision, UnitSystem unitSystem)
+  {
+    if (precision < 0 || precision > 3) {
+      throw new IAE("precision [%d] must be in the range of [0,3]", precision);
+    }
+
+    String pattern = "%." + precision + "f %s%s";
+    switch (unitSystem) {
+      case BINARY_BYTE:
+        return BinaryFormatter.format(bytes, pattern, "B");
+      case DECIMAL_BYTE:
+        return DecimalFormatter.format(bytes, pattern, "B");
+      case DECIMAL:
+        return DecimalFormatter.format(bytes, pattern, "").trim();
+      default:
+        throw new IAE("Unkonwn unit system[%s]", unitSystem);
+    }
+  }
+
+  static class BinaryFormatter
+  {
+    private static final String[] UNITS = {"", "Ki", "Mi", "Gi", "Ti", "Pi", "Ei"};
+
+    static String format(long bytes, String pattern, String suffix)
+    {
+      if (bytes > -1024 && bytes < 1024) {
+        return bytes + " " + suffix;
+      }
+
+      if (bytes == Long.MIN_VALUE) {
+        /**
+         * special path for Long.MIN_VALUE
+         *
+         * Long.MIN_VALUE = 2^63 = (2^60=1EiB) * 2^3
+         */
+        return StringUtils.format(pattern, -8.0, UNITS[UNITS.length - 1], suffix);
+      }
+
+      /**
+       * A number and its binary bits are listed as fellows
+       * [0,    1KiB) = [0,    2^10)
+       * [1KiB, 1MiB) = [2^10, 2^20),
+       * [1MiB, 1GiB) = [2^20, 2^30),
+       * [1GiB, 1PiB) = [2^30, 2^40),
+       * ...
+       *
+       * So, expression (63 - Long.numberOfLeadingZeros(absValue))) helps us to get the right number of bits of the given input
+       *
+       * Internal implementaion of Long.numberOfLeadingZeros uses bit operations to do calculation so the cost is very cheap
+       */
+      int unitIndex = (63 - Long.numberOfLeadingZeros(Math.abs(bytes))) / 10;
+      return StringUtils.format(pattern, (double) bytes / (1L << (unitIndex * 10)), UNITS[unitIndex], suffix);
+    }
+  }
+
+  static class DecimalFormatter
+  {
+    private static final String[] UNITS = {"K", "M", "G", "T", "P", "E"};
+
+    static String format(long bytes, String pattern, String suffix)
+    {
+      /**
+       * handle number between (-1000, 1000) first to simply further processing
+       */
+      if (bytes > -1000 && bytes < 1000) {
+        return bytes + " " + suffix;
+      }
+
+      /**
+       * because max precision is 3, extra fraction can be ignored by use of integer division which might be a little more efficient
+       */
+      int unitIndex = 0;
+      while (bytes <= -1000_000 || bytes >= 1000_000) {
+        bytes /= 1000;
+        unitIndex++;
+      }
+      return StringUtils.format(pattern, bytes / 1000.0, UNITS[unitIndex], suffix);
+    }
+  }
 }
diff --git a/core/src/main/java/org/apache/druid/math/expr/Function.java b/core/src/main/java/org/apache/druid/math/expr/Function.java
index ed7082e..9e4a241 100644
--- a/core/src/main/java/org/apache/druid/math/expr/Function.java
+++ b/core/src/main/java/org/apache/druid/math/expr/Function.java
@@ -22,6 +22,7 @@ package org.apache.druid.math.expr;
 import com.google.common.collect.ImmutableSet;
 import org.apache.druid.common.config.NullHandling;
 import org.apache.druid.java.util.common.DateTimes;
+import org.apache.druid.java.util.common.HumanReadableBytes;
 import org.apache.druid.java.util.common.IAE;
 import org.apache.druid.java.util.common.RE;
 import org.apache.druid.java.util.common.StringUtils;
@@ -3539,4 +3540,103 @@ public interface Function
       throw new RE("Unable to slice to unknown type %s", expr.type());
     }
   }
+
+  abstract class SizeFormatFunc implements Function
+  {
+    protected abstract HumanReadableBytes.UnitSystem getUnitSystem();
+
+    @Override
+    public ExprEval apply(List<Expr> args, Expr.ObjectBinding bindings)
+    {
+      final ExprEval valueParam = args.get(0).eval(bindings);
+      if (NullHandling.sqlCompatible() && valueParam.isNumericNull()) {
+        return ExprEval.of(null);
+      }
+
+      /**
+       * only LONG and DOUBLE are allowed
+       * For a DOUBLE, it will be cast to LONG before format
+       */
+      if (valueParam.value() != null && valueParam.type() != ExprType.LONG && valueParam.type() != ExprType.DOUBLE) {
+        throw new IAE("Function[%s] needs a number as its first argument", name());
+      }
+
+      /**
+       * By default, precision is 2
+       */
+      long precision = 2;
+      if (args.size() > 1) {
+        ExprEval precisionParam = args.get(1).eval(bindings);
+        if (precisionParam.type() != ExprType.LONG) {
+          throw new IAE("Function[%s] needs an integer as its second argument", name());
+        }
+        precision = precisionParam.asLong();
+        if (precision < 0 || precision > 3) {
+          throw new IAE("Given precision[%d] of Function[%s] must be in the range of [0,3]", precision, name());
+        }
+      }
+
+      return ExprEval.of(HumanReadableBytes.format(valueParam.asLong(), precision, this.getUnitSystem()));
+    }
+
+    @Override
+    public void validateArguments(List<Expr> args)
+    {
+      if (args.size() < 1 || args.size() > 2) {
+        throw new IAE("Function[%s] needs 1 or 2 arguments", name());
+      }
+    }
+
+    @Nullable
+    @Override
+    public ExprType getOutputType(Expr.InputBindingInspector inputTypes, List<Expr> args)
+    {
+      return ExprType.STRING;
+    }
+  }
+
+  class HumanReadableDecimalByteFormatFunc extends SizeFormatFunc
+  {
+    @Override
+    public String name()
+    {
+      return "human_readable_decimal_byte_format";
+    }
+
+    @Override
+    protected HumanReadableBytes.UnitSystem getUnitSystem()
+    {
+      return HumanReadableBytes.UnitSystem.DECIMAL_BYTE;
+    }
+  }
+
+  class HumanReadableBinaryByteFormatFunc extends SizeFormatFunc
+  {
+    @Override
+    public String name()
+    {
+      return "human_readable_binary_byte_format";
+    }
+
+    @Override
+    protected HumanReadableBytes.UnitSystem getUnitSystem()
+    {
+      return HumanReadableBytes.UnitSystem.BINARY_BYTE;
+    }
+  }
+
+  class HumanReadableDecimalFormatFunc extends SizeFormatFunc
+  {
+    @Override
+    public String name()
+    {
+      return "human_readable_decimal_format";
+    }
+
+    @Override
+    protected HumanReadableBytes.UnitSystem getUnitSystem()
+    {
+      return HumanReadableBytes.UnitSystem.DECIMAL;
+    }
+  }
 }
diff --git a/core/src/test/java/org/apache/druid/java/util/common/HumanReadableBytesTest.java b/core/src/test/java/org/apache/druid/java/util/common/HumanReadableBytesTest.java
index 3d1d892..a49533d 100644
--- a/core/src/test/java/org/apache/druid/java/util/common/HumanReadableBytesTest.java
+++ b/core/src/test/java/org/apache/druid/java/util/common/HumanReadableBytesTest.java
@@ -395,6 +395,98 @@ public class HumanReadableBytesTest
     Assert.assertEquals("value must be in the range of [0, 5]", message);
   }
 
+  @Test
+  public void testFormatInBinaryByte()
+  {
+    Assert.assertEquals("-8.00 EiB", HumanReadableBytes.format(Long.MIN_VALUE, 2, HumanReadableBytes.UnitSystem.BINARY_BYTE));
+    Assert.assertEquals("-8.000 EiB", HumanReadableBytes.format(Long.MIN_VALUE, 3, HumanReadableBytes.UnitSystem.BINARY_BYTE));
+
+    Assert.assertEquals("-2.00 GiB", HumanReadableBytes.format(Integer.MIN_VALUE, 2, HumanReadableBytes.UnitSystem.BINARY_BYTE));
+    Assert.assertEquals("-32.00 KiB", HumanReadableBytes.format(Short.MIN_VALUE, 2, HumanReadableBytes.UnitSystem.BINARY_BYTE));
+
+    Assert.assertEquals("-128 B", HumanReadableBytes.format(Byte.MIN_VALUE, 2, HumanReadableBytes.UnitSystem.BINARY_BYTE));
+    Assert.assertEquals("-1 B", HumanReadableBytes.format(-1, 2, HumanReadableBytes.UnitSystem.BINARY_BYTE));
+    Assert.assertEquals("0 B", HumanReadableBytes.format(0, 2, HumanReadableBytes.UnitSystem.BINARY_BYTE));
+    Assert.assertEquals("1 B", HumanReadableBytes.format(1, 2, HumanReadableBytes.UnitSystem.BINARY_BYTE));
+
+    Assert.assertEquals("1.00 KiB", HumanReadableBytes.format(1024L, 2, HumanReadableBytes.UnitSystem.BINARY_BYTE));
+    Assert.assertEquals("1.00 MiB", HumanReadableBytes.format(1024L * 1024, 2, HumanReadableBytes.UnitSystem.BINARY_BYTE));
+    Assert.assertEquals("1.00 GiB", HumanReadableBytes.format(1024L * 1024 * 1024, 2, HumanReadableBytes.UnitSystem.BINARY_BYTE));
+    Assert.assertEquals("1.00 TiB", HumanReadableBytes.format(1024L * 1024 * 1024 * 1024, 2, HumanReadableBytes.UnitSystem.BINARY_BYTE));
+    Assert.assertEquals("1.00 PiB", HumanReadableBytes.format(1024L * 1024 * 1024 * 1024 * 1024, 2, HumanReadableBytes.UnitSystem.BINARY_BYTE));
+    Assert.assertEquals("8.00 EiB", HumanReadableBytes.format(Long.MAX_VALUE, 2, HumanReadableBytes.UnitSystem.BINARY_BYTE));
+  }
+
+  @Test
+  public void testPrecisionInBinaryFormat()
+  {
+    Assert.assertEquals("1 KiB", HumanReadableBytes.format(1500, 0, HumanReadableBytes.UnitSystem.BINARY_BYTE));
+    Assert.assertEquals("1.5 KiB", HumanReadableBytes.format(1500, 1, HumanReadableBytes.UnitSystem.BINARY_BYTE));
+    Assert.assertEquals("1.46 KiB", HumanReadableBytes.format(1500, 2, HumanReadableBytes.UnitSystem.BINARY_BYTE));
+    Assert.assertEquals("1.465 KiB", HumanReadableBytes.format(1500, 3, HumanReadableBytes.UnitSystem.BINARY_BYTE));
+  }
+
+  @Test
+  public void testPrecisionInDecimalFormat()
+  {
+    Assert.assertEquals("1 KB", HumanReadableBytes.format(1456, 0, HumanReadableBytes.UnitSystem.DECIMAL_BYTE));
+    Assert.assertEquals("1.5 KB", HumanReadableBytes.format(1456, 1, HumanReadableBytes.UnitSystem.DECIMAL_BYTE));
+    Assert.assertEquals("1.46 KB", HumanReadableBytes.format(1456, 2, HumanReadableBytes.UnitSystem.DECIMAL_BYTE));
+    Assert.assertEquals("1.456 KB", HumanReadableBytes.format(1456, 3, HumanReadableBytes.UnitSystem.DECIMAL_BYTE));
+  }
+
+  @Test
+  public void testFormatInDecimalByte()
+  {
+    Assert.assertEquals("1 B", HumanReadableBytes.format(1, 2, HumanReadableBytes.UnitSystem.DECIMAL_BYTE));
+    Assert.assertEquals("1.00 KB", HumanReadableBytes.format(1000L, 2, HumanReadableBytes.UnitSystem.DECIMAL_BYTE));
+    Assert.assertEquals("1.00 MB", HumanReadableBytes.format(1000L * 1000, 2, HumanReadableBytes.UnitSystem.DECIMAL_BYTE));
+    Assert.assertEquals("1.00 GB", HumanReadableBytes.format(1000L * 1000 * 1000, 2, HumanReadableBytes.UnitSystem.DECIMAL_BYTE));
+    Assert.assertEquals("1.00 TB", HumanReadableBytes.format(1000L * 1000 * 1000 * 1000, 2, HumanReadableBytes.UnitSystem.DECIMAL_BYTE));
+    Assert.assertEquals("1.00 PB", HumanReadableBytes.format(1000L * 1000 * 1000 * 1000 * 1000, 2, HumanReadableBytes.UnitSystem.DECIMAL_BYTE));
+    Assert.assertEquals("9.22 EB", HumanReadableBytes.format(Long.MAX_VALUE, 2, HumanReadableBytes.UnitSystem.DECIMAL_BYTE));
+
+    Assert.assertEquals("100.00 KB", HumanReadableBytes.format(99999, 2, HumanReadableBytes.UnitSystem.DECIMAL_BYTE));
+    Assert.assertEquals("99.999 KB", HumanReadableBytes.format(99999, 3, HumanReadableBytes.UnitSystem.DECIMAL_BYTE));
+
+    Assert.assertEquals("999.9 PB", HumanReadableBytes.format(999_949_999_999_999_999L, 1, HumanReadableBytes.UnitSystem.DECIMAL_BYTE));
+    Assert.assertEquals("999.95 PB", HumanReadableBytes.format(999_949_999_999_999_999L, 2, HumanReadableBytes.UnitSystem.DECIMAL_BYTE));
+    Assert.assertEquals("999.949 PB", HumanReadableBytes.format(999_949_999_999_999_999L, 3, HumanReadableBytes.UnitSystem.DECIMAL_BYTE));
+  }
+
+  @Test
+  public void testFormatInDecimal()
+  {
+    Assert.assertEquals("1", HumanReadableBytes.format(1, 2, HumanReadableBytes.UnitSystem.DECIMAL));
+    Assert.assertEquals("999", HumanReadableBytes.format(999, 2, HumanReadableBytes.UnitSystem.DECIMAL));
+    Assert.assertEquals("-999", HumanReadableBytes.format(-999, 2, HumanReadableBytes.UnitSystem.DECIMAL));
+    Assert.assertEquals("-1.00 K", HumanReadableBytes.format(-1000, 2, HumanReadableBytes.UnitSystem.DECIMAL));
+    Assert.assertEquals("1.00 K", HumanReadableBytes.format(1000L, 2, HumanReadableBytes.UnitSystem.DECIMAL));
+    Assert.assertEquals("1.00 M", HumanReadableBytes.format(1000L * 1000, 2, HumanReadableBytes.UnitSystem.DECIMAL));
+    Assert.assertEquals("1.00 G", HumanReadableBytes.format(1000L * 1000 * 1000, 2, HumanReadableBytes.UnitSystem.DECIMAL));
+    Assert.assertEquals("1.00 T", HumanReadableBytes.format(1000L * 1000 * 1000 * 1000, 2, HumanReadableBytes.UnitSystem.DECIMAL));
+    Assert.assertEquals("1.00 P", HumanReadableBytes.format(1000L * 1000 * 1000 * 1000 * 1000, 2, HumanReadableBytes.UnitSystem.DECIMAL));
+    Assert.assertEquals("-9.22 E", HumanReadableBytes.format(Long.MIN_VALUE, 2, HumanReadableBytes.UnitSystem.DECIMAL));
+    Assert.assertEquals("9.22 E", HumanReadableBytes.format(Long.MAX_VALUE, 2, HumanReadableBytes.UnitSystem.DECIMAL));
+  }
+
+  @Test
+  public void testInvalidPrecisionArgumentLowerBound()
+  {
+    expectedException.expect(IAE.class);
+    expectedException.expectMessage("precision [-1] must be in the range of [0,3]");
+    Assert.assertEquals("1.00", HumanReadableBytes.format(1, -1, HumanReadableBytes.UnitSystem.DECIMAL));
+  }
+
+  @Test
+  public void testInvalidPrecisionArgumentUpperBound()
+  {
+    expectedException.expect(IAE.class);
+    expectedException.expectMessage("precision [4] must be in the range of [0,3]");
+    Assert.assertEquals("1", HumanReadableBytes.format(1, 3, HumanReadableBytes.UnitSystem.DECIMAL));
+    Assert.assertEquals("1", HumanReadableBytes.format(1, 4, HumanReadableBytes.UnitSystem.DECIMAL));
+  }
+
   private static <T> String validate(T obj)
   {
     Validator validator = Validation.buildDefaultValidatorFactory()
diff --git a/core/src/test/java/org/apache/druid/math/expr/FunctionTest.java b/core/src/test/java/org/apache/druid/math/expr/FunctionTest.java
index 8ee9898..43ff070 100644
--- a/core/src/test/java/org/apache/druid/math/expr/FunctionTest.java
+++ b/core/src/test/java/org/apache/druid/math/expr/FunctionTest.java
@@ -22,12 +22,15 @@ package org.apache.druid.math.expr;
 import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.ImmutableSet;
 import org.apache.druid.common.config.NullHandling;
+import org.apache.druid.java.util.common.IAE;
 import org.apache.druid.java.util.common.Pair;
 import org.apache.druid.java.util.common.StringUtils;
 import org.apache.druid.testing.InitializedNullHandlingTest;
 import org.junit.Assert;
 import org.junit.Before;
+import org.junit.Rule;
 import org.junit.Test;
+import org.junit.rules.ExpectedException;
 
 import javax.annotation.Nullable;
 import java.math.BigDecimal;
@@ -37,6 +40,9 @@ import java.util.Set;
 
 public class FunctionTest extends InitializedNullHandlingTest
 {
+  @Rule
+  public ExpectedException expectedException = ExpectedException.none();
+
   private Expr.ObjectBinding bindings;
 
   @Before
@@ -544,6 +550,178 @@ public class FunctionTest extends InitializedNullHandlingTest
   }
 
   @Test
+  public void testSizeFormat()
+  {
+    assertExpr("human_readable_binary_byte_format(-1024)", "-1.00 KiB");
+    assertExpr("human_readable_binary_byte_format(1024)", "1.00 KiB");
+    assertExpr("human_readable_binary_byte_format(1024*1024)", "1.00 MiB");
+    assertExpr("human_readable_binary_byte_format(1024*1024*1024)", "1.00 GiB");
+    assertExpr("human_readable_binary_byte_format(1024*1024*1024*1024)", "1.00 TiB");
+    assertExpr("human_readable_binary_byte_format(1024*1024*1024*1024*1024)", "1.00 PiB");
+
+    assertExpr("human_readable_decimal_byte_format(-1000)", "-1.00 KB");
+    assertExpr("human_readable_decimal_byte_format(1000)", "1.00 KB");
+    assertExpr("human_readable_decimal_byte_format(1000*1000)", "1.00 MB");
+    assertExpr("human_readable_decimal_byte_format(1000*1000*1000)", "1.00 GB");
+    assertExpr("human_readable_decimal_byte_format(1000*1000*1000*1000)", "1.00 TB");
+
+    assertExpr("human_readable_decimal_format(-1000)", "-1.00 K");
+    assertExpr("human_readable_decimal_format(1000)", "1.00 K");
+    assertExpr("human_readable_decimal_format(1000*1000)", "1.00 M");
+    assertExpr("human_readable_decimal_format(1000*1000*1000)", "1.00 G");
+    assertExpr("human_readable_decimal_format(1000*1000*1000*1000)", "1.00 T");
+  }
+
+  @Test
+  public void testSizeFormatWithDifferentPrecision()
+  {
+    assertExpr("human_readable_binary_byte_format(1024, 0)", "1 KiB");
+    assertExpr("human_readable_binary_byte_format(1024*1024, 1)", "1.0 MiB");
+    assertExpr("human_readable_binary_byte_format(1024*1024*1024, 2)", "1.00 GiB");
+    assertExpr("human_readable_binary_byte_format(1024*1024*1024*1024, 3)", "1.000 TiB");
+
+    assertExpr("human_readable_decimal_byte_format(1234, 0)", "1 KB");
+    assertExpr("human_readable_decimal_byte_format(1234*1000, 1)", "1.2 MB");
+    assertExpr("human_readable_decimal_byte_format(1234*1000*1000, 2)", "1.23 GB");
+    assertExpr("human_readable_decimal_byte_format(1234*1000*1000*1000, 3)", "1.234 TB");
+
+    assertExpr("human_readable_decimal_format(1234, 0)", "1 K");
+    assertExpr("human_readable_decimal_format(1234*1000,1)", "1.2 M");
+    assertExpr("human_readable_decimal_format(1234*1000*1000,2)", "1.23 G");
+    assertExpr("human_readable_decimal_format(1234*1000*1000*1000,3)", "1.234 T");
+  }
+
+  @Test
+  public void testSizeFormatWithEdgeCases()
+  {
+    //a nonexist value is null which is treated as 0
+    assertExpr("human_readable_binary_byte_format(nonexist)", NullHandling.sqlCompatible() ? null : "0 B");
+
+    //f = 12.34
+    assertExpr("human_readable_binary_byte_format(f)", "12 B");
+
+    //nan is Double.NaN
+    assertExpr("human_readable_binary_byte_format(nan)", "0 B");
+
+    //inf = Double.POSITIVE_INFINITY
+    assertExpr("human_readable_binary_byte_format(inf)", "8.00 EiB");
+
+    //inf = Double.NEGATIVE_INFINITY
+    assertExpr("human_readable_binary_byte_format(-inf)", "-8.00 EiB");
+
+    // o = 0
+    assertExpr("human_readable_binary_byte_format(o)", "0 B");
+
+    // od = 0D
+    assertExpr("human_readable_binary_byte_format(od)", "0 B");
+
+    // of = 0F
+    assertExpr("human_readable_binary_byte_format(of)", "0 B");
+  }
+
+  @Test
+  public void testSizeForatInvalidArgumentType()
+  {
+    try {
+      //x = "foo"
+      Parser.parse("human_readable_binary_byte_format(x)", ExprMacroTable.nil())
+            .eval(bindings);
+
+      // for sqlCompatible, function above returns null and goes here
+      // but for non-sqlCompatible, it must not go to here
+      Assert.assertTrue(NullHandling.sqlCompatible() ? true : false);
+    }
+    catch (IAE e) {
+      Assert.assertEquals("Function[human_readable_binary_byte_format] needs a number as its first argument", e.getMessage());
+    }
+
+    try {
+      //x = "foo"
+      Parser.parse("human_readable_binary_byte_format(1024, x)", ExprMacroTable.nil())
+            .eval(bindings);
+
+      //must not go to here
+      Assert.assertTrue(false);
+    }
+    catch (IAE e) {
+      Assert.assertEquals("Function[human_readable_binary_byte_format] needs an integer as its second argument", e.getMessage());
+    }
+
+    try {
+      //of = 0F
+      Parser.parse("human_readable_binary_byte_format(1024, of)", ExprMacroTable.nil())
+            .eval(bindings);
+
+      //must not go to here
+      Assert.assertTrue(false);
+    }
+    catch (IAE e) {
+      Assert.assertEquals("Function[human_readable_binary_byte_format] needs an integer as its second argument", e.getMessage());
+    }
+
+    try {
+      //of = 0F
+      Parser.parse("human_readable_binary_byte_format(1024, nonexist)", ExprMacroTable.nil())
+            .eval(bindings);
+
+      //must not go to here
+      Assert.assertTrue(false);
+    }
+    catch (IAE e) {
+      Assert.assertEquals("Function[human_readable_binary_byte_format] needs an integer as its second argument", e.getMessage());
+    }
+  }
+
+  @Test
+  public void testSizeFormatInvalidPrecision()
+  {
+    try {
+      Parser.parse("human_readable_binary_byte_format(1024, maxLong)", ExprMacroTable.nil())
+            .eval(bindings);
+      Assert.assertTrue(false);
+    }
+    catch (IAE e) {
+      Assert.assertEquals("Given precision[9223372036854775807] of Function[human_readable_binary_byte_format] must be in the range of [0,3]", e.getMessage());
+    }
+
+    try {
+      Parser.parse("human_readable_binary_byte_format(1024, minLong)", ExprMacroTable.nil())
+            .eval(bindings);
+      Assert.assertTrue(false);
+    }
+    catch (IAE e) {
+      Assert.assertEquals("Given precision[-9223372036854775808] of Function[human_readable_binary_byte_format] must be in the range of [0,3]", e.getMessage());
+    }
+
+    try {
+      Parser.parse("human_readable_binary_byte_format(1024, -1)", ExprMacroTable.nil())
+            .eval(bindings);
+      Assert.assertTrue(false);
+    }
+    catch (IAE e) {
+      Assert.assertEquals("Given precision[-1] of Function[human_readable_binary_byte_format] must be in the range of [0,3]", e.getMessage());
+    }
+
+    try {
+      Parser.parse("human_readable_binary_byte_format(1024, 4)", ExprMacroTable.nil())
+            .eval(bindings);
+      Assert.assertTrue(false);
+    }
+    catch (IAE e) {
+      Assert.assertEquals("Given precision[4] of Function[human_readable_binary_byte_format] must be in the range of [0,3]", e.getMessage());
+    }
+  }
+
+  @Test
+  public void testSizeFormatInvalidArgumentSize()
+  {
+    expectedException.expect(IAE.class);
+    expectedException.expectMessage("Function[human_readable_binary_byte_format] needs 1 or 2 arguments");
+    Parser.parse("human_readable_binary_byte_format(1024, 2, 3)", ExprMacroTable.nil())
+          .eval(bindings);
+  }
+
+  @Test
   public void testBitwise()
   {
     // happy path maths
diff --git a/docs/misc/math-expr.md b/docs/misc/math-expr.md
index b173db9..7f3b036 100644
--- a/docs/misc/math-expr.md
+++ b/docs/misc/math-expr.md
@@ -260,3 +260,12 @@ supported features:
 * math functions: `abs`, `acos`, `asin`, `atan`, `cbrt`, `ceil`, `cos`, `cosh`, `cot`, `exp`, `expm1`, `floor`, `getExponent`, `log`, `log10`, `log1p`, `nextUp`, `rint`, `signum`, `sin`, `sinh`, `sqrt`, `tan`, `tanh`, `toDegrees`, `toRadians`, `ulp`, `atan2`, `copySign`, `div`, `hypot`, `max`, `min`, `nextAfter`,  `pow`, `remainder`, `scalb` are supported for numeric types
 * time functions: `timestamp_floor` (with constant granularity argument) is supported for numeric types
 * other: `parse_long` is supported for numeric and string types
+
+
+## Other functions
+
+| function | description |
+| --- | --- |
+| human_readable_binary_byte_format(value[, precision]) | Format a number in human-readable [IEC](https://en.wikipedia.org/wiki/Binary_prefix) format. `precision` must be in the range of [0,3] (default: 2). For example:<li> human_readable_binary_byte_format(1048576) returns `1.00 MiB`</li><li>human_readable_binary_byte_format(1048576, 3) returns `1.000 MiB`</li> |
+| human_readable_decimal_byte_format(value[, precision]) | Format a number in human-readable [SI](https://en.wikipedia.org/wiki/Binary_prefix) format. `precision` must be in the range of [0,3] (default: 2). For example:<li> human_readable_decimal_byte_format(1000000) returns `1.00 MB`</li><li>human_readable_decimal_byte_format(1000000, 3) returns `1.000 MB`</li> |
+| human_readable_decimal_format(value[, precision]) | Format a number in human-readable SI format. `precision` must be in the range of [0,3] (default: 2). For example:<li>human_readable_decimal_format(1000000) returns `1.00 M`</li><li>human_readable_decimal_format(1000000, 3) returns `1.000 M`</li>  |
diff --git a/docs/querying/sql.md b/docs/querying/sql.md
index 7783e5d..50706b3 100644
--- a/docs/querying/sql.md
+++ b/docs/querying/sql.md
@@ -413,6 +413,10 @@ to FLOAT. At runtime, Druid will widen 32-bit floats to 64-bit for most expressi
 |`BITWISE_SHIFT_LEFT(expr1, expr2)`|Returns the result of `expr1 << expr2`. Double values will be implicitly cast to longs, use `BITWISE_CONVERT_DOUBLE_TO_LONG_BITS` to perform bitwise operations directly with doubles|
 |`BITWISE_SHIFT_RIGHT(expr1, expr2)`|Returns the result of `expr1 >> expr2`. Double values will be implicitly cast to longs, use `BITWISE_CONVERT_DOUBLE_TO_LONG_BITS` to perform bitwise operations directly with doubles|
 |`BITWISE_XOR(expr1, expr2)`|Returns the result of `expr1 ^ expr2`. Double values will be implicitly cast to longs, use `BITWISE_CONVERT_DOUBLE_TO_LONG_BITS` to perform bitwise operations directly with doubles|
+|`HUMAN_READABLE_BINARY_BYTE_FORMAT(value[, precision])`| Format a number in human-readable [IEC](https://en.wikipedia.org/wiki/Binary_prefix) format. For example, HUMAN_READABLE_BINARY_BYTE_FORMAT(1048576) returns `1.00 MiB`. `precision` must be in the range of [0,3] (default: 2). |
+|`HUMAN_READABLE_DECIMAL_BYTE_FORMAT(value[, precision])`| Format a number in human-readable [SI](https://en.wikipedia.org/wiki/Binary_prefix) format. HUMAN_READABLE_DECIMAL_BYTE_FORMAT(1048576) returns `1.04 MB`. `precision` must be in the range of [0,3] (default: 2). `precision` must be in the range of [0,3] (default: 2). |
+|`HUMAN_READABLE_DECIMAL_FORMAT(value[, precision])`| Format a number in human-readable SI format. For example, HUMAN_READABLE_DECIMAL_FORMAT(1048576) returns `1.04 M`. `precision` must be in the range of [0,3] (default: 2). |
+
 
 ### String functions
 
diff --git a/sql/src/main/java/org/apache/druid/sql/calcite/expression/builtin/HumanReadableFormatOperatorConversion.java b/sql/src/main/java/org/apache/druid/sql/calcite/expression/builtin/HumanReadableFormatOperatorConversion.java
new file mode 100644
index 0000000..f9f06e1
--- /dev/null
+++ b/sql/src/main/java/org/apache/druid/sql/calcite/expression/builtin/HumanReadableFormatOperatorConversion.java
@@ -0,0 +1,123 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.druid.sql.calcite.expression.builtin;
+
+import org.apache.calcite.rel.type.RelDataType;
+import org.apache.calcite.rex.RexNode;
+import org.apache.calcite.sql.SqlCallBinding;
+import org.apache.calcite.sql.SqlFunction;
+import org.apache.calcite.sql.SqlFunctionCategory;
+import org.apache.calcite.sql.SqlOperandCountRange;
+import org.apache.calcite.sql.SqlOperator;
+import org.apache.calcite.sql.type.SqlOperandCountRanges;
+import org.apache.calcite.sql.type.SqlOperandTypeChecker;
+import org.apache.calcite.sql.type.SqlTypeName;
+import org.apache.druid.java.util.common.StringUtils;
+import org.apache.druid.segment.column.RowSignature;
+import org.apache.druid.sql.calcite.expression.DruidExpression;
+import org.apache.druid.sql.calcite.expression.OperatorConversions;
+import org.apache.druid.sql.calcite.expression.SqlOperatorConversion;
+import org.apache.druid.sql.calcite.planner.PlannerContext;
+
+public class HumanReadableFormatOperatorConversion implements SqlOperatorConversion
+{
+  public static final SqlOperatorConversion BINARY_BYTE_FORMAT = new HumanReadableFormatOperatorConversion("human_readable_binary_byte_format");
+  public static final SqlOperatorConversion DECIMAL_BYTE_FORMAT = new HumanReadableFormatOperatorConversion("human_readable_decimal_byte_format");
+  public static final SqlOperatorConversion DECIMAL_FORMAT = new HumanReadableFormatOperatorConversion("human_readable_decimal_format");
+
+  private final String name;
+  private final SqlFunction sqlFunction;
+
+  private HumanReadableFormatOperatorConversion(String name)
+  {
+    this.sqlFunction = OperatorConversions
+        .operatorBuilder(StringUtils.toUpperCase(name))
+        .operandTypeChecker(new HumanReadableFormatOperandTypeChecker())
+        .functionCategory(SqlFunctionCategory.STRING)
+        .returnTypeCascadeNullable(SqlTypeName.VARCHAR)
+        .build();
+
+    this.name = name;
+  }
+
+  @Override
+  public SqlOperator calciteOperator()
+  {
+    return sqlFunction;
+  }
+
+  @Override
+  public DruidExpression toDruidExpression(
+      final PlannerContext plannerContext,
+      final RowSignature rowSignature,
+      final RexNode rexNode
+  )
+  {
+    return OperatorConversions.convertCall(plannerContext, rowSignature, rexNode, name);
+  }
+
+  private static class HumanReadableFormatOperandTypeChecker implements SqlOperandTypeChecker
+  {
+    @Override
+    public boolean checkOperandTypes(SqlCallBinding callBinding, boolean throwOnFailure)
+    {
+      boolean isSigatureError = false;
+      final RelDataType firstArgType = callBinding.getOperandType(0);
+      if (!SqlTypeName.NUMERIC_TYPES.contains(firstArgType.getSqlTypeName())) {
+        isSigatureError = true;
+      }
+      if (callBinding.getOperandCount() > 1) {
+        final RelDataType secondArgType = callBinding.getOperandType(1);
+        if (!SqlTypeName.NUMERIC_TYPES.contains(secondArgType.getSqlTypeName())) {
+          isSigatureError = true;
+        }
+      }
+      if (isSigatureError && throwOnFailure) {
+        throw callBinding.newValidationSignatureError();
+      } else {
+        return isSigatureError;
+      }
+    }
+
+    @Override
+    public SqlOperandCountRange getOperandCountRange()
+    {
+      return SqlOperandCountRanges.between(1, 2);
+    }
+
+    @Override
+    public String getAllowedSignatures(SqlOperator op, String opName)
+    {
+      return StringUtils.format("%s(Number, [Precision])", opName);
+    }
+
+    @Override
+    public Consistency getConsistency()
+    {
+      return Consistency.NONE;
+    }
+
+    @Override
+    public boolean isOptional(int i)
+    {
+      return i > 0;
+    }
+  }
+}
diff --git a/sql/src/main/java/org/apache/druid/sql/calcite/planner/DruidOperatorTable.java b/sql/src/main/java/org/apache/druid/sql/calcite/planner/DruidOperatorTable.java
index 221bc95..0f52c34 100644
--- a/sql/src/main/java/org/apache/druid/sql/calcite/planner/DruidOperatorTable.java
+++ b/sql/src/main/java/org/apache/druid/sql/calcite/planner/DruidOperatorTable.java
@@ -75,6 +75,7 @@ import org.apache.druid.sql.calcite.expression.builtin.DateTruncOperatorConversi
 import org.apache.druid.sql.calcite.expression.builtin.ExtractOperatorConversion;
 import org.apache.druid.sql.calcite.expression.builtin.FloorOperatorConversion;
 import org.apache.druid.sql.calcite.expression.builtin.GreatestOperatorConversion;
+import org.apache.druid.sql.calcite.expression.builtin.HumanReadableFormatOperatorConversion;
 import org.apache.druid.sql.calcite.expression.builtin.IPv4AddressMatchOperatorConversion;
 import org.apache.druid.sql.calcite.expression.builtin.IPv4AddressParseOperatorConversion;
 import org.apache.druid.sql.calcite.expression.builtin.IPv4AddressStringifyOperatorConversion;
@@ -252,6 +253,13 @@ public class DruidOperatorTable implements SqlOperatorTable
                    .add(new IPv4AddressStringifyOperatorConversion())
                    .build();
 
+  private static final List<SqlOperatorConversion> FORMAT_OPERATOR_CONVERSIONS =
+      ImmutableList.<SqlOperatorConversion>builder()
+                   .add(HumanReadableFormatOperatorConversion.BINARY_BYTE_FORMAT)
+                   .add(HumanReadableFormatOperatorConversion.DECIMAL_BYTE_FORMAT)
+                   .add(HumanReadableFormatOperatorConversion.DECIMAL_FORMAT)
+                   .build();
+
   private static final List<SqlOperatorConversion> BITWISE_OPERATOR_CONVERSIONS =
       ImmutableList.<SqlOperatorConversion>builder()
                    .add(OperatorConversions.druidBinaryLongFn("BITWISE_AND", "bitwiseAnd"))
@@ -344,6 +352,7 @@ public class DruidOperatorTable implements SqlOperatorTable
                    .addAll(MULTIVALUE_STRING_OPERATOR_CONVERSIONS)
                    .addAll(REDUCTION_OPERATOR_CONVERSIONS)
                    .addAll(IPV4ADDRESS_OPERATOR_CONVERSIONS)
+                   .addAll(FORMAT_OPERATOR_CONVERSIONS)
                    .addAll(BITWISE_OPERATOR_CONVERSIONS)
                    .build();
 
diff --git a/sql/src/test/java/org/apache/druid/sql/calcite/CalciteQueryTest.java b/sql/src/test/java/org/apache/druid/sql/calcite/CalciteQueryTest.java
index 6471fab..bb1dcb8 100644
--- a/sql/src/test/java/org/apache/druid/sql/calcite/CalciteQueryTest.java
+++ b/sql/src/test/java/org/apache/druid/sql/calcite/CalciteQueryTest.java
@@ -18714,4 +18714,108 @@ public class CalciteQueryTest extends BaseCalciteQueryTest
         )
     );
   }
+
+
+  /**
+   * see {@link CalciteTests#RAW_ROWS1_WITH_NUMERIC_DIMS} for the input data source of this test
+   */
+  @Test
+  public void testHumanReadableFormatFunction() throws Exception
+  {
+    // For the row where dim1 = '1', m1 = 4.0 and l1 is null
+    testQuery(
+        "SELECT m1, "
+        + "HUMAN_READABLE_BINARY_BYTE_FORMAT(45678),"
+        + "HUMAN_READABLE_BINARY_BYTE_FORMAT(m1*12345),"
+        + "HUMAN_READABLE_BINARY_BYTE_FORMAT(m1*12345, 0), "
+        + "HUMAN_READABLE_DECIMAL_BYTE_FORMAT(m1*12345), "
+        + "HUMAN_READABLE_DECIMAL_FORMAT(m1*12345), "
+        + "HUMAN_READABLE_BINARY_BYTE_FORMAT(l1),"
+        + "HUMAN_READABLE_DECIMAL_BYTE_FORMAT(l1), "
+        + "HUMAN_READABLE_DECIMAL_FORMAT(l1) "
+        + "FROM numfoo WHERE dim1 = '1' LIMIT 1",
+        ImmutableList.of(
+            newScanQueryBuilder()
+                .dataSource(CalciteTests.DATASOURCE3)
+                .intervals(querySegmentSpec(Filtration.eternity()))
+                //
+                // NOTE: the first expression HUMAN_READABLE_BINARY_BYTE_FORMAT(45678) in SQL is calculated during SQL parse phase,
+                // so the converted Druid native query is its result intead of the raw function call
+                //
+                .virtualColumns(expressionVirtualColumn("v0", "'44.61 KiB'", ValueType.STRING),
+                                expressionVirtualColumn("v1", "human_readable_binary_byte_format((\"m1\" * 12345))", ValueType.STRING),
+                                expressionVirtualColumn("v2", "human_readable_binary_byte_format((\"m1\" * 12345),0)", ValueType.STRING),
+                                expressionVirtualColumn("v3", "human_readable_decimal_byte_format((\"m1\" * 12345))", ValueType.STRING),
+                                expressionVirtualColumn("v4", "human_readable_decimal_format((\"m1\" * 12345))", ValueType.STRING),
+                                expressionVirtualColumn("v5", "human_readable_binary_byte_format(\"l1\")", ValueType.STRING),
+                                expressionVirtualColumn("v6", "human_readable_decimal_byte_format(\"l1\")", ValueType.STRING),
+                                expressionVirtualColumn("v7", "human_readable_decimal_format(\"l1\")", ValueType.STRING)
+                )
+                .columns("m1", "v0", "v1", "v2", "v3", "v4", "v5", "v6", "v7")
+                .filters(selector("dim1", "1", null))
+                .resultFormat(ScanQuery.ResultFormat.RESULT_FORMAT_COMPACTED_LIST)
+                .limit(1)
+                .context(QUERY_CONTEXT_DEFAULT)
+                .build()
+        ),
+        ImmutableList.of(
+            new Object[]{(float) 4.0,
+                         "44.61 KiB", // 45678 / 1024
+                         "48.22 KiB", // = m1(4.0) * 12345 / 1024
+                         "48 KiB", // = m1(4.0) * 12345 / 1024, precision = 0
+                         "49.38 KB", // decimal byte format, m1(4.0) * 12345 / 1000,
+                         "49.38 K", // decimal format, m1(4.0) * 12345 / 1000,
+                         NullHandling.replaceWithDefault() ? "0 B" : null,
+                         NullHandling.replaceWithDefault() ? "0 B" : null,
+                         NullHandling.replaceWithDefault() ? "0" : null
+            }
+        )
+    );
+  }
+
+  @Test
+  public void testHumanReadableFormatFunctionExceptionWithWrongNumberType() throws Exception
+  {
+    this.expectedException.expect(SqlPlanningException.class);
+    this.expectedException.expectMessage("Supported form(s): HUMAN_READABLE_BINARY_BYTE_FORMAT(Number, [Precision])");
+    testQuery(
+        "SELECT HUMAN_READABLE_BINARY_BYTE_FORMAT('45678')",
+        Collections.emptyList(),
+        Collections.emptyList()
+    );
+  }
+
+  @Test
+  public void testHumanReadableFormatFunctionWithWrongPrecisionType() throws Exception
+  {
+    this.expectedException.expect(SqlPlanningException.class);
+    this.expectedException.expectMessage("Supported form(s): HUMAN_READABLE_BINARY_BYTE_FORMAT(Number, [Precision])");
+    testQuery(
+        "SELECT HUMAN_READABLE_BINARY_BYTE_FORMAT(45678, '2')",
+        Collections.emptyList(),
+        Collections.emptyList()
+    );
+  }
+
+  @Test
+  public void testHumanReadableFormatFunctionWithInvalidNumberOfArguments() throws Exception
+  {
+    this.expectedException.expect(SqlPlanningException.class);
+
+    /*
+     * frankly speaking, the exception message thrown here is a little bit confusion
+     * it says it's 'expecting 1 arguments' but acturally HUMAN_READABLE_BINARY_BYTE_FORMAT supports 1 or 2 arguments
+     *
+     * The message is returned from {@link org.apache.calcite.sql.validate.SqlValidatorImpl#handleUnresolvedFunction},
+     * and we can see from its implementation that it gets the min number arguments to format the exception message.
+     *
+     */
+    this.expectedException.expectMessage(
+        "Invalid number of arguments to function 'HUMAN_READABLE_BINARY_BYTE_FORMAT'. Was expecting 1 arguments");
+    testQuery(
+        "SELECT HUMAN_READABLE_BINARY_BYTE_FORMAT(45678, 2, 1)",
+        Collections.emptyList(),
+        Collections.emptyList()
+    );
+  }
 }
diff --git a/sql/src/test/java/org/apache/druid/sql/calcite/expression/ExpressionsTest.java b/sql/src/test/java/org/apache/druid/sql/calcite/expression/ExpressionsTest.java
index 9525b75..6124473 100644
--- a/sql/src/test/java/org/apache/druid/sql/calcite/expression/ExpressionsTest.java
+++ b/sql/src/test/java/org/apache/druid/sql/calcite/expression/ExpressionsTest.java
@@ -42,6 +42,7 @@ import org.apache.druid.segment.column.ValueType;
 import org.apache.druid.segment.virtual.ExpressionVirtualColumn;
 import org.apache.druid.sql.calcite.expression.builtin.ContainsOperatorConversion;
 import org.apache.druid.sql.calcite.expression.builtin.DateTruncOperatorConversion;
+import org.apache.druid.sql.calcite.expression.builtin.HumanReadableFormatOperatorConversion;
 import org.apache.druid.sql.calcite.expression.builtin.LPadOperatorConversion;
 import org.apache.druid.sql.calcite.expression.builtin.LeftOperatorConversion;
 import org.apache.druid.sql.calcite.expression.builtin.ParseLongOperatorConversion;
@@ -76,6 +77,7 @@ public class ExpressionsTest extends ExpressionTestBase
       .add("t", ValueType.LONG)
       .add("a", ValueType.LONG)
       .add("b", ValueType.LONG)
+      .add("p", ValueType.LONG)
       .add("x", ValueType.FLOAT)
       .add("y", ValueType.LONG)
       .add("z", ValueType.FLOAT)
@@ -98,6 +100,7 @@ public class ExpressionsTest extends ExpressionTestBase
       .put("t", DateTimes.of("2000-02-03T04:05:06").getMillis())
       .put("a", 10)
       .put("b", 25)
+      .put("p", 3)
       .put("x", 2.25)
       .put("y", 3.0)
       .put("z", -2.25)
@@ -2080,4 +2083,191 @@ public class ExpressionsTest extends ExpressionTestBase
         null
     );
   }
+
+  @Test
+  public void testHumanReadableBinaryByteFormat()
+  {
+    /*
+     * Basic Test
+     */
+    testHelper.testExpression(
+        HumanReadableFormatOperatorConversion.BINARY_BYTE_FORMAT.calciteOperator(),
+        ImmutableList.of(
+            testHelper.makeLiteral(1000)
+        ),
+        DruidExpression.fromExpression("human_readable_binary_byte_format(1000)"),
+        "1000 B"
+    );
+    testHelper.testExpression(
+        HumanReadableFormatOperatorConversion.BINARY_BYTE_FORMAT.calciteOperator(),
+        ImmutableList.of(
+            testHelper.makeLiteral(1024)
+        ),
+        DruidExpression.fromExpression("human_readable_binary_byte_format(1024)"),
+        "1.00 KiB"
+    );
+    testHelper.testExpression(
+        HumanReadableFormatOperatorConversion.BINARY_BYTE_FORMAT.calciteOperator(),
+        ImmutableList.of(
+            testHelper.makeLiteral(Long.MAX_VALUE)
+        ),
+        DruidExpression.fromExpression("human_readable_binary_byte_format(9223372036854775807)"),
+        "8.00 EiB"
+    );
+
+    /*
+     * NOTE: Test for Long.MIN_VALUE is skipped since ExprListnerImpl#exitLongExpr fails to parse Long.MIN_VALUE
+     * This cases has also been verified in the tests of underlying implementation
+     */
+
+    /*
+     * test input with variable reference
+     */
+    testHelper.testExpression(
+        HumanReadableFormatOperatorConversion.BINARY_BYTE_FORMAT.calciteOperator(),
+        ImmutableList.of(
+            testHelper.makeInputRef("b"),
+            testHelper.makeInputRef("p")
+        ),
+        DruidExpression.fromExpression("human_readable_binary_byte_format(\"b\",\"p\")"),
+        "25 B"
+    );
+
+    /*
+     * test different precision
+     */
+    testHelper.testExpression(
+        HumanReadableFormatOperatorConversion.BINARY_BYTE_FORMAT.calciteOperator(),
+        ImmutableList.of(
+            testHelper.makeLiteral(45000),
+            //precision 0
+            testHelper.makeLiteral(0)
+        ),
+        DruidExpression.fromExpression("human_readable_binary_byte_format(45000,0)"),
+        "44 KiB"
+    );
+    testHelper.testExpression(
+        HumanReadableFormatOperatorConversion.BINARY_BYTE_FORMAT.calciteOperator(),
+        ImmutableList.of(
+            testHelper.makeLiteral(45000),
+            //precision 1
+            testHelper.makeLiteral(1)
+        ),
+        DruidExpression.fromExpression("human_readable_binary_byte_format(45000,1)"),
+        "43.9 KiB"
+    );
+    testHelper.testExpression(
+        HumanReadableFormatOperatorConversion.BINARY_BYTE_FORMAT.calciteOperator(),
+        ImmutableList.of(
+            testHelper.makeLiteral(45000),
+            //precision 2
+            testHelper.makeLiteral(2)
+        ),
+        DruidExpression.fromExpression("human_readable_binary_byte_format(45000,2)"),
+        "43.95 KiB"
+    );
+    testHelper.testExpression(
+        HumanReadableFormatOperatorConversion.BINARY_BYTE_FORMAT.calciteOperator(),
+        ImmutableList.of(
+            testHelper.makeLiteral(45000),
+            //precision 3
+            testHelper.makeLiteral(3)
+        ),
+        DruidExpression.fromExpression("human_readable_binary_byte_format(45000,3)"),
+        "43.945 KiB"
+    );
+  }
+
+  @Test
+  public void testHumanReadableDecimalByteFormat()
+  {
+    /*
+     * Basic Test
+     */
+    testHelper.testExpression(
+        HumanReadableFormatOperatorConversion.DECIMAL_BYTE_FORMAT.calciteOperator(),
+        ImmutableList.of(
+            testHelper.makeLiteral(999)
+        ),
+        DruidExpression.fromExpression("human_readable_decimal_byte_format(999)"),
+        "999 B"
+    );
+    testHelper.testExpression(
+        HumanReadableFormatOperatorConversion.DECIMAL_BYTE_FORMAT.calciteOperator(),
+        ImmutableList.of(
+            testHelper.makeLiteral(1024)
+        ),
+        DruidExpression.fromExpression("human_readable_decimal_byte_format(1024)"),
+        "1.02 KB"
+    );
+    testHelper.testExpression(
+        HumanReadableFormatOperatorConversion.DECIMAL_BYTE_FORMAT.calciteOperator(),
+        ImmutableList.of(
+            testHelper.makeLiteral(Long.MAX_VALUE)
+        ),
+        DruidExpression.fromExpression("human_readable_decimal_byte_format(9223372036854775807)"),
+        "9.22 EB"
+    );
+
+    /*
+     * NOTE: Test for Long.MIN_VALUE is skipped since ExprListnerImpl#exitLongExpr fails to parse Long.MIN_VALUE
+     */
+
+    /*
+     * test input with variable reference
+     */
+    testHelper.testExpression(
+        HumanReadableFormatOperatorConversion.DECIMAL_BYTE_FORMAT.calciteOperator(),
+        ImmutableList.of(
+            testHelper.makeInputRef("b"),
+            testHelper.makeInputRef("p")
+        ),
+        DruidExpression.fromExpression("human_readable_decimal_byte_format(\"b\",\"p\")"),
+        "25 B"
+    );
+
+    /*
+     * test different precision
+     */
+    testHelper.testExpression(
+        HumanReadableFormatOperatorConversion.DECIMAL_BYTE_FORMAT.calciteOperator(),
+        ImmutableList.of(
+            testHelper.makeLiteral(45678),
+            //precision 0
+            testHelper.makeLiteral(0)
+        ),
+        DruidExpression.fromExpression("human_readable_decimal_byte_format(45678,0)"),
+        "46 KB"
+    );
+    testHelper.testExpression(
+        HumanReadableFormatOperatorConversion.DECIMAL_BYTE_FORMAT.calciteOperator(),
+        ImmutableList.of(
+            testHelper.makeLiteral(45678),
+            //precision 1
+            testHelper.makeLiteral(1)
+        ),
+        DruidExpression.fromExpression("human_readable_decimal_byte_format(45678,1)"),
+        "45.7 KB"
+    );
+    testHelper.testExpression(
+        HumanReadableFormatOperatorConversion.DECIMAL_BYTE_FORMAT.calciteOperator(),
+        ImmutableList.of(
+            testHelper.makeLiteral(45678),
+            //precision 2
+            testHelper.makeLiteral(2)
+        ),
+        DruidExpression.fromExpression("human_readable_decimal_byte_format(45678,2)"),
+        "45.68 KB"
+    );
+    testHelper.testExpression(
+        HumanReadableFormatOperatorConversion.DECIMAL_BYTE_FORMAT.calciteOperator(),
+        ImmutableList.of(
+            testHelper.makeLiteral(45678),
+            //precision 3
+            testHelper.makeLiteral(3)
+        ),
+        DruidExpression.fromExpression("human_readable_decimal_byte_format(45678,3)"),
+        "45.678 KB"
+    );
+  }
 }
diff --git a/website/.spelling b/website/.spelling
index ce8feb2..76c314a 100644
--- a/website/.spelling
+++ b/website/.spelling
@@ -1198,6 +1198,10 @@ unix_timestamp
 value1
 value2
 valueOf
+IEC
+human_readable_binary_byte_format
+human_readable_decimal_byte_format
+human_readable_decimal_format
  - ../docs/misc/papers-and-talks.md
 RADStack
  - ../docs/operations/api-reference.md
@@ -1603,6 +1607,7 @@ useApproximateCountDistinct
 useGroupingSetForExactDistinct
 useApproximateTopN
 wikipedia
+IEC
  - ../docs/querying/timeseriesquery.md
 fieldName1
 fieldName2

---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@druid.apache.org
For additional commands, e-mail: commits-help@druid.apache.org