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 9d9385442173..b63e0f207c7e 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 @@ private static long parseInner(String rawNumber) 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 ed7082eba0e9..9e4a241118e5 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 @@ 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 ExprEval apply(List args, Expr.ObjectBinding bindings) 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 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 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 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 3d1d8922fd80..a49533d086a7 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 void testBytesRange() 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 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 8ee989846d4f..43ff07037de6 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 @@ 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 @@ public class FunctionTest extends InitializedNullHandlingTest { + @Rule + public ExpectedException expectedException = ExpectedException.none(); + private Expr.ObjectBinding bindings; @Before @@ -543,6 +549,178 @@ public void testLeast() assertExpr("least(1, null, 'A')", "1"); } + @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() { diff --git a/docs/misc/math-expr.md b/docs/misc/math-expr.md index b173db95d742..7f3b036f1561 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:
  • human_readable_binary_byte_format(1048576) returns `1.00 MiB`
  • human_readable_binary_byte_format(1048576, 3) returns `1.000 MiB`
  • | +| 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:
  • human_readable_decimal_byte_format(1000000) returns `1.00 MB`
  • human_readable_decimal_byte_format(1000000, 3) returns `1.000 MB`
  • | +| 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:
  • human_readable_decimal_format(1000000) returns `1.00 M`
  • human_readable_decimal_format(1000000, 3) returns `1.000 M`
  • | diff --git a/docs/querying/sql.md b/docs/querying/sql.md index 7783e5d699fe..50706b3afb8c 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 000000000000..f9f06e10f3a1 --- /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 221bc95255fe..0f52c3461418 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.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 FORMAT_OPERATOR_CONVERSIONS = + ImmutableList.builder() + .add(HumanReadableFormatOperatorConversion.BINARY_BYTE_FORMAT) + .add(HumanReadableFormatOperatorConversion.DECIMAL_BYTE_FORMAT) + .add(HumanReadableFormatOperatorConversion.DECIMAL_FORMAT) + .build(); + private static final List BITWISE_OPERATOR_CONVERSIONS = ImmutableList.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 6471fab4e2c3..bb1dcb85bc42 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 void testStringAggMaxBytes() throws Exception ) ); } + + + /** + * 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 9525b7559027..6124473d3272 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.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 void testOperatorConversionsDruidBinaryLongFn() 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 ce8feb2aa719..76c314a2b9b8 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