-
Notifications
You must be signed in to change notification settings - Fork 2k
Add Quantity type #129
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
Changes from 1 commit
a295a5d
924458f
af94a84
cd4108d
6cf2c64
f37eee2
cb1bfc2
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,56 @@ | ||
package io.kubernetes.client.custom; | ||
|
||
public class BaseExponent { | ||
|
||
private final int base; | ||
|
||
private final int exponent; | ||
|
||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Line spacing is weird here. preference for: private final int base;
private final int exponent;
...
public BaseExponent(...) {
... |
||
private final Quantity.Format format; | ||
public BaseExponent(int base, int exponent, 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; | ||
|
||
if (base != that.base) return false; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit: Preference for: return base == that.base && exponent == that.exponent && format == that.format; There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ironically, in its own auto-generated code, IntelliJ suggests this quick fix. |
||
if (exponent != that.exponent) return false; | ||
return format == that.format; | ||
} | ||
|
||
@Override | ||
public int hashCode() { | ||
int result = base; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is funky, and I'm not entirely sure that it is correct. (or at least I don't understand why it is correct... Why not: this.toString().hashCode(); There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I must confess that this was entirely auto-generated by IntelliJ, however, I don't see any error in it. Given our use case, I think we can happily use |
||
result = 31 * result + exponent; | ||
result = 31 * result + (format != null ? format.hashCode() : 0); | ||
return result; | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
package io.kubernetes.client.custom; | ||
|
||
public class Pair<L, R> { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We already depend on apache-commons, rev the version to 3.0 and use the Pair implementation from there? |
||
|
||
private final L left; | ||
private final R right; | ||
|
||
public Pair(final L left, final R right) { | ||
this.left = left; | ||
this.right = right; | ||
} | ||
|
||
public L getLeft() { | ||
return left; | ||
} | ||
|
||
public R getRight() { | ||
return right; | ||
} | ||
} |
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 getBigDecimal() { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit: |
||
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,90 @@ | ||
package io.kubernetes.client.custom; | ||
|
||
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.getBigDecimal(), 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.getBigDecimal().scale() > 0; | ||
} | ||
|
||
private String toBase1024String(final Quantity quantity) { | ||
final BigDecimal amount = quantity.getBigDecimal(); | ||
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.getBigDecimal(); | ||
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 new Pair<>(mantissa * 10, exponent - 1); | ||
} else if (exponentRemainder == -1 || exponentRemainder == 2) { | ||
return new Pair<>(mantissa * 100, exponent - 2); | ||
} else { | ||
return new Pair<>(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 new Pair<>(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; | ||
} | ||
|
||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
JavaDoc?
There was a problem hiding this comment.
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
?