Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
6dd891e
add binary_byte_format/decimal_byte_format/decimal_format
FrankChen021 Nov 16, 2020
1ecbbfc
clean code
FrankChen021 Dec 4, 2020
a6afe85
fix doc
FrankChen021 Dec 4, 2020
f5fab71
fix review comments
FrankChen021 Dec 7, 2020
be3c333
add spelling check rules
FrankChen021 Dec 7, 2020
e02809a
remove extra param
FrankChen021 Dec 7, 2020
b5a3756
improve type handling and null handling
FrankChen021 Dec 9, 2020
e279c18
remove extra zeros
FrankChen021 Dec 9, 2020
91e6a55
fix tests and add space between unit suffix and number as most size-f…
FrankChen021 Dec 11, 2020
5d0fe12
fix tests
FrankChen021 Dec 23, 2020
8e4eede
merge master to resolve conflicts
FrankChen021 Feb 22, 2021
94206b6
add examples
FrankChen021 Feb 22, 2021
f19e6a5
change function names according to review comments
FrankChen021 Feb 22, 2021
5dca644
merge master to resolve conflicts
FrankChen021 Mar 5, 2021
47d22f9
fix merge
FrankChen021 Mar 5, 2021
ad7d831
no need to configure NullHandling explicitly for tests
FrankChen021 Mar 5, 2021
c3dbb1e
fix tests in SQL-Compatible mode
FrankChen021 Mar 8, 2021
f21bc87
Merge master to resolve conflicts
FrankChen021 May 13, 2021
8119545
Merge branch 'master' into size-format to resolve conflicts
FrankChen021 Jun 18, 2021
fc7cc9b
Resolve review comments
FrankChen021 Jun 23, 2021
fa1f057
Update SQL test case to check null handling
FrankChen021 Jun 23, 2021
12edaab
Fix intellij inspections
FrankChen021 Jun 24, 2021
27d9a41
Merge branch 'master' to resolve conflicts
FrankChen021 Jun 26, 2021
30eb6ed
Add more examples
FrankChen021 Jun 29, 2021
e773dac
Fix example
FrankChen021 Jun 29, 2021
f3c62a3
Merge branch 'master' into size-format
FrankChen021 Aug 13, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
}
100 changes: 100 additions & 0 deletions core/src/main/java/org/apache/druid/math/expr/Function.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -3539,4 +3540,103 @@ public ExprEval apply(List<Expr> 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<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;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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 <T> String validate(T obj)
{
Validator validator = Validation.buildDefaultValidatorFactory()
Expand Down
Loading