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