Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Quantity type #129

Merged
merged 7 commits into from
Nov 29, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
4 changes: 4 additions & 0 deletions kubernetes/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,10 @@
<artifactId>joda-time</artifactId>
<version>${jodatime-version}</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
</dependency>
<!-- test dependencies -->
<dependency>
<groupId>junit</groupId>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package io.kubernetes.client.custom;

public class BaseExponent {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

JavaDoc?

Copy link
Contributor Author

@lewisheadden lewisheadden Nov 28, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Happy to add JavaDoc - to clarify, this is an across the board ask rather than just for BaseExponent?


private final int base;
private final int exponent;
private final Quantity.Format format;

public BaseExponent(final int base, final int exponent, final Quantity.Format format) {
this.base = base;
this.exponent = exponent;
this.format = format;
}

public int getBase() {
return base;
}

public int getExponent() {
return exponent;
}

public Quantity.Format getFormat() {
return format;
}

@Override
public String toString() {
return "BaseExponent{" +
"base=" + base +
", exponent=" + exponent +
", format=" + format +
'}';
}

@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;

BaseExponent that = (BaseExponent) o;

return base == that.base && exponent == that.exponent && format == that.format;
}

@Override
public int hashCode() {
return this.toString().hashCode();
}
}
71 changes: 71 additions & 0 deletions kubernetes/src/main/java/io/kubernetes/client/custom/Quantity.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
package io.kubernetes.client.custom;

import com.google.gson.TypeAdapter;
import com.google.gson.annotations.JsonAdapter;
import com.google.gson.stream.JsonReader;
import com.google.gson.stream.JsonWriter;

import java.io.IOException;
import java.math.BigDecimal;

@JsonAdapter(Quantity.QuantityAdapter.class)
public class Quantity {

private final BigDecimal number;
private Format format;

public enum Format {
DECIMAL_EXPONENT(10), DECIMAL_SI(10), BINARY_SI(2);

private int base;

Format(final int base) {
this.base = base;
}

public int getBase() {
return base;
}
}

public Quantity(final BigDecimal number, final Format format) {
this.number = number;
this.format = format;
}

public BigDecimal getNumber() {
return number;
}

public Format getFormat() {
return format;
}

public static Quantity fromString(final String value) {
return new QuantityFormatter().parse(value);
}

public String toSuffixedString() {
return new QuantityFormatter().format(this);
}

@Override
public String toString() {
return "Quantity{" +
"number=" + number +
", format=" + format +
'}';
}

public class QuantityAdapter extends TypeAdapter<Quantity> {
@Override
public void write(JsonWriter jsonWriter, Quantity quantity) throws IOException {
jsonWriter.value(quantity.toSuffixedString());
}

@Override
public Quantity read(JsonReader jsonReader) throws IOException {
return Quantity.fromString(jsonReader.nextString());
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package io.kubernetes.client.custom;

public class QuantityFormatException extends RuntimeException {
public QuantityFormatException(String s) {
super(s);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
package io.kubernetes.client.custom;

import org.apache.commons.lang3.tuple.Pair;

import java.math.BigDecimal;
import java.math.MathContext;

public class QuantityFormatter {

private static final String PARTS_RE = "[eEinumkKMGTP]+";

public Quantity parse(final String value) {
if (value == null || value.isEmpty()) {
throw new QuantityFormatException("");
}
final String[] parts = value.split(PARTS_RE);
final BigDecimal numericValue = parseNumericValue(parts[0]);
final String suffix = value.substring(parts[0].length());
final BaseExponent baseExponent = new SuffixFormatter().parse(suffix);
final BigDecimal unitMultiplier = BigDecimal.valueOf(baseExponent.getBase()).pow(baseExponent.getExponent(), MathContext.DECIMAL64);
final BigDecimal unitlessValue = numericValue.multiply(unitMultiplier);
return new Quantity(unitlessValue, baseExponent.getFormat());
}

private static BigDecimal parseNumericValue(String part) {
try {
return new BigDecimal(part);
} catch (final NumberFormatException e) {
throw new QuantityFormatException("Unable to parse numeric part of quantity: " + part);
}
}

public String format(final Quantity quantity) {
switch (quantity.getFormat()) {
case DECIMAL_SI:
case DECIMAL_EXPONENT:
return toBase10String(quantity);
case BINARY_SI:
if (isFractional(quantity)) {
return toBase10String(new Quantity(quantity.getNumber(), Quantity.Format.DECIMAL_SI));
}
return toBase1024String(quantity);
default:
throw new IllegalArgumentException("Can't format a " + quantity.getFormat() + " quantity");
}
}

private boolean isFractional(Quantity quantity) {
return quantity.getNumber().scale() > 0;
}

private String toBase1024String(final Quantity quantity) {
final BigDecimal amount = quantity.getNumber();
final long value = amount.unscaledValue().longValue();
final int exponent = -amount.scale();
final Pair<Long, Integer> resultAndTimes = removeFactorsForBase(value, 1024);
return resultAndTimes.getLeft() + new SuffixFormatter().format(quantity.getFormat(), exponent + resultAndTimes.getRight() * 10);
}

private String toBase10String(final Quantity quantity) {
final BigDecimal amount = quantity.getNumber();
final long value = amount.unscaledValue().longValue();
final int exponent = -amount.scale();
final Pair<Long, Integer> resultAndTimes = removeFactorsForBase(value, 10);
final int postFactoringExponent = exponent + resultAndTimes.getRight();
final Pair<Long, Integer> valueAndExponent = ensureExponentIsMultipleOf3(resultAndTimes.getLeft(), postFactoringExponent);
return valueAndExponent.getLeft() + new SuffixFormatter().format(quantity.getFormat(), valueAndExponent.getRight());
}

private Pair<Long, Integer> ensureExponentIsMultipleOf3(final long mantissa, final int exponent) {
final long exponentRemainder = exponent % 3;
if (exponentRemainder == 1 || exponentRemainder == -2) {
return Pair.of(mantissa * 10, exponent - 1);
} else if (exponentRemainder == -1 || exponentRemainder == 2) {
return Pair.of(mantissa * 100, exponent - 2);
} else {
return Pair.of(mantissa, exponent);
}
}

private Pair<Long, Integer> removeFactorsForBase(final long value, final int base) {
int times = 0;
long result = value;
while (result >= base && result % base == 0) {
times++;
result = result / base;
}
return Pair.of(result, times);
}

}

Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
package io.kubernetes.client.custom;

import java.util.HashMap;
import java.util.Map;

public class SuffixFormatter {

private static final Map<String, BaseExponent> suffixToBinary = new HashMap<String, BaseExponent>() {
{
put("", new BaseExponent(2, 0, Quantity.Format.BINARY_SI));
put("Ki", new BaseExponent(2, 10, Quantity.Format.BINARY_SI));
put("Mi", new BaseExponent(2, 20, Quantity.Format.BINARY_SI));
put("Gi", new BaseExponent(2, 30, Quantity.Format.BINARY_SI));
put("Ti", new BaseExponent(2, 40, Quantity.Format.BINARY_SI));
put("Pi", new BaseExponent(2, 50, Quantity.Format.BINARY_SI));
put("Ei", new BaseExponent(2, 60, Quantity.Format.BINARY_SI));
}
};

private static final Map<String, BaseExponent> suffixToDecimal = new HashMap<String, BaseExponent>() {
{
put("n", new BaseExponent(10, -9, Quantity.Format.DECIMAL_SI));
put("u", new BaseExponent(10, -6, Quantity.Format.DECIMAL_SI));
put("m", new BaseExponent(10, -3, Quantity.Format.DECIMAL_SI));
put("", new BaseExponent(10, 0, Quantity.Format.DECIMAL_SI));
put("k", new BaseExponent(10, 3, Quantity.Format.DECIMAL_SI));
put("M", new BaseExponent(10, 6, Quantity.Format.DECIMAL_SI));
put("G", new BaseExponent(10, 9, Quantity.Format.DECIMAL_SI));
put("T", new BaseExponent(10, 12, Quantity.Format.DECIMAL_SI));
put("P", new BaseExponent(10, 15, Quantity.Format.DECIMAL_SI));
put("E", new BaseExponent(10, 18, Quantity.Format.DECIMAL_SI));
}
};

private static final Map<BaseExponent, String> decimalToSuffix = new HashMap<BaseExponent, String>() {
{
for (Entry<String, BaseExponent> entry : suffixToDecimal.entrySet()) {
put(entry.getValue(), entry.getKey());
}
}
};

private static final Map<BaseExponent, String> binaryToSuffix = new HashMap<BaseExponent, String>() {
{
for (Entry<String, BaseExponent> entry : suffixToBinary.entrySet()) {
put(entry.getValue(), entry.getKey());
}
}
};

public BaseExponent parse(final String suffix) {
final BaseExponent decimalSuffix = suffixToDecimal.get(suffix);
if (decimalSuffix != null) {
return decimalSuffix;
}

final BaseExponent binarySuffix = suffixToBinary.get(suffix);
if (binarySuffix != null) {
return binarySuffix;
}

if (suffix.length() > 0 && (suffix.charAt(0) == 'E' || suffix.charAt(0) == 'e')) {
return extractDecimalExponent(suffix);
}

throw new QuantityFormatException("Could not parse suffix");
}

private BaseExponent extractDecimalExponent(String suffix) {
try {
final int exponent = Integer.parseInt(suffix.substring(1));
return new BaseExponent(10, exponent, Quantity.Format.DECIMAL_EXPONENT);
} catch (final NumberFormatException e) {
throw new QuantityFormatException("Can't parse decimal exponent from " + suffix.substring(1));
}
}

public String format(final Quantity.Format format, final int exponent) {
switch (format) {
case DECIMAL_SI:
return getDecimalSiSuffix(exponent);
case BINARY_SI:
return getBinarySiSuffix(exponent);
case DECIMAL_EXPONENT:
return exponent == 0 ? "" : "e" + exponent;
default:
throw new IllegalStateException("Can't format " + format + " with exponent " + exponent);
}
}

private String getBinarySiSuffix(int exponent) {
final String suffix = binaryToSuffix.get(new BaseExponent(2, exponent, Quantity.Format.BINARY_SI));
if (suffix == null) {
throw new IllegalArgumentException("No suffix for exponent" + exponent);
}
return suffix;
}

private String getDecimalSiSuffix(int exponent) {
final String suffix = decimalToSuffix.get(new BaseExponent(10, exponent, Quantity.Format.DECIMAL_SI));
if (suffix == null) {
throw new IllegalArgumentException("No suffix for exponent" + exponent);
}
return suffix;
}

}
Loading