/*
* 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.brooklyn.util.text;
import java.util.Formattable;
import java.util.FormattableFlags;
import java.util.Formatter;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.annotation.Nullable;
import com.google.common.base.Function;
/**
* A formatter to pretty-print numeric values representing sizes in byes.
* <p>
* The {@link ByteSizeStrings#builder()} presents a fluent interface to create
* various configurations of formatting. The defaults produce metric units in
* multiples of 1000 bytes at a precision of three significant figures. This is
* the way disk space is normally measured, for example {@literal 128.1GB}.
* <p>
* Alternatively the {@link ByteSizeStrings#iso()} convenience method produces
* ISO standard units in multiples of 1024 bytes, with the same precision as the
* metric output. This is how RAM is normally measured, for example {@literal 12.4MiB}
* or {@literal 1.04GiB}.
* <p>
* Finally, the {@link ByteSizeStrings#java()} convenience method will produce
* strings suitable for use with a Java command line, as part of the {@code -Xms}
* or {@code -Xmx} options. These output integer values only, so values up to
* 10GB will be reported in MB to preserve accuracy. For size values over 1000GB,
* the output will still be formatted as GB but rounded to a mutiple of 1000.
* <p>
* The class is immutable and thread safe once built and a single instance of
* the three pre-defined configurations is created and returned buy the methods
* described above.
*
* @see Strings#makeSizeString(long)
* @see Strings#makeISOSizeString(long)
* @see Strings#makeJavaSizeString(long)
*/
public class ByteSizeStrings implements Function<Long, String> {
/**
* Configures and builds a {@link ByteSizeStrings} formatter.
*/
public static class Builder {
private String suffixBytes = "B";
private String suffixKilo = "kB";
private String suffixMega = "MB";
private String suffixGiga = "GB";
private String suffixTera = "TB";
private boolean addSpace = true;
private int bytesPerMetricUnit = 1000;
private int maxLen = 4;
private int precision = 3;
private int lowerLimit = 1;
/**
* The suffix to use when printing bytes.
*/
public Builder suffixBytes(String suffixBytes) { this.suffixBytes = suffixBytes; return this; }
/**
* The suffix to use when printing Kilobytes.
*/
public Builder suffixKilo(String suffixKilo) { this.suffixKilo = suffixKilo; return this; }
/**
* The suffix to use when printing Megabytes.
*/
public Builder suffixMega(String suffixMega) { this.suffixMega = suffixMega; return this; }
/**
* The suffix to use when printing Gigabytes.
*/
public Builder suffixGiga(String suffixGiga) { this.suffixGiga = suffixGiga; return this; }
/**
* The suffix to use when printing Terabytes.
*/
public Builder suffixTera(String suffixTera) { this.suffixTera = suffixTera; return this; }
/**
* Whether to add a space between the value and the unit suffix.
* <p>
* Defaults is {@literal true} for '5 MiB' output.
*/
public Builder addSpace(boolean addSpace) { this.addSpace = addSpace; return this; }
public Builder addSpace() { this.addSpace = true; return this; }
public Builder noSpace() { this.addSpace = false; return this; }
/**
* The number of bytes per metric usnit, usually either 1000 or 1024.
* <p>
* Used to determine when to use the next suffix string.
*/
public Builder bytesPerMetricUnit(int bytesPerMetricUnit) { this.bytesPerMetricUnit = bytesPerMetricUnit; return this; }
/**
* The maximum length of the printed number.
*
* @see Strings#makeRealString(double, int, int, int, double, boolean)
*/
public Builder maxLen(int maxLen) { this.maxLen = maxLen; return this; }
/**
* The number of digits accuracy desired in the printed number.
*
* @see Strings#makeRealString(double, int, int, int, double, boolean)
*/
public Builder precision(int precision) { this.precision = precision; return this; }
/**
* Prints using a lower suffix until the size is greater than this limit multiplied
* by bytes per metric unit, when the next highest suffix will be used.ยง
* <p>
* If this has the value 5 then sizes up to 5000 will be printed as bytes, and over 5000
* as Kilobytes.
*/
public Builder lowerLimit(int lowerLimit) { this.lowerLimit = lowerLimit; return this; }
/**
* Returns an immutable {@link ByteSizeStrings} formatter using the builder configuration.
*/
public ByteSizeStrings build() {
String space = addSpace ? " " : "";
return new ByteSizeStrings(space + suffixBytes, space + suffixKilo, space + suffixMega, space + suffixGiga,
space + suffixTera, bytesPerMetricUnit, maxLen, precision, lowerLimit);
}
}
/**
* Returns a builder for a {@link ByteSizeStrings} formatter.
*/
public static Builder builder() { return new Builder(); }
/**
* Format byte sizes suitable for Java {@code -Xms} arguments.
*/
public static final ByteSizeStrings java() { return JAVA; }
private static final ByteSizeStrings JAVA = ByteSizeStrings.builder()
.suffixBytes("")
.suffixKilo("k")
.suffixMega("m")
.suffixGiga("g")
.suffixTera("000g") // Java has no Tera suffix
.noSpace()
.bytesPerMetricUnit(1024)
.maxLen(6)
.precision(0)
.lowerLimit(10)
.build();
/**
* Formats byte sizes using ISO standard suffixes and binary multiples of 1024
*/
public static ByteSizeStrings iso() { return ISO; }
private static ByteSizeStrings ISO = ByteSizeStrings.builder()
.suffixBytes("B")
.suffixKilo("KiB")
.suffixMega("MiB")
.suffixGiga("GiB")
.suffixTera("TiB")
.bytesPerMetricUnit(1024)
.build();
/**
* Default byte size formatter using metric multiples of 1000.
*/
public static ByteSizeStrings metric() { return METRIC; }
private static ByteSizeStrings METRIC = ByteSizeStrings.builder().build();
private String suffixBytes;
private String suffixKilo;
private String suffixMega;
private String suffixGiga;
private String suffixTera;
private int bytesPerMetricUnit;
private int maxLen;
private int precision;
private int lowerLimit;
/**
* For use by the {@link Builder} only.
*/
private ByteSizeStrings(String suffixBytes, String suffixKilo, String suffixMega, String suffixGiga,
String suffixTera, int bytesPerMetricUnit, int maxLen, int precision, int lowerLimit) {
this.suffixBytes = suffixBytes;
this.suffixKilo = suffixKilo;
this.suffixMega = suffixMega;
this.suffixGiga = suffixGiga;
this.suffixTera = suffixTera;
this.bytesPerMetricUnit = bytesPerMetricUnit;
this.maxLen = maxLen;
this.precision = precision;
this.lowerLimit = lowerLimit;
}
/** @deprecated Use {@link ByteSizeStrings#builder()} */
@Deprecated
public ByteSizeStrings() { }
/** @deprecated Use {@link ByteSizeStrings.Builder#suffixBytes(String)} */
@Deprecated
public void setSuffixBytes(String suffixBytes) {
this.suffixBytes = suffixBytes;
}
/** @deprecated Use {@link ByteSizeStrings.Builder#suffixKilo(String)} */
@Deprecated
public void setSuffixKilo(String suffixKilo) {
this.suffixKilo = suffixKilo;
}
/** @deprecated Use {@link ByteSizeStrings.Builder#suffixMega(String)} */
@Deprecated
public void setSuffixMega(String suffixMega) {
this.suffixMega = suffixMega;
}
/** @deprecated Use {@link ByteSizeStrings.Builder#suffixGiga(String)} */
@Deprecated
public void setSuffixGiga(String suffixGiga) {
this.suffixGiga = suffixGiga;
}
/** @deprecated Use {@link ByteSizeStrings.Builder#suffixTera(String)} */
@Deprecated
public void setSuffixTera(String suffixTera) {
this.suffixTera = suffixTera;
}
/** @deprecated Use {@link ByteSizeStrings.Builder#bytesPerMetricUnit(int)} */
@Deprecated
public void setBytesPerMetricUnit(int bytesPerMetricUnit) {
this.bytesPerMetricUnit = bytesPerMetricUnit;
}
/** @deprecated Use {@link ByteSizeStrings.Builder#maxLen(int)} */
@Deprecated
public void setMaxLen(int maxLen) {
this.maxLen = maxLen;
}
/** @deprecated Use {@link ByteSizeStrings.Builder#precision(int)} */
@Deprecated
public void setPrecision(int precision) {
this.precision = precision;
}
/** @deprecated Use {@link ByteSizeStrings.Builder#lowerLimit(int)} */
@Deprecated
public void setLowerLimit(int lowerLimit) {
this.lowerLimit = lowerLimit;
}
/**
* Format the {@literal size} bytes as a String.
*/
public String makeSizeString(long size) {
return makeSizeString(size, precision);
}
/**
* Format the {@literal size} bytes as a String with the given precision.
*/
public String makeSizeString(long size, int precision) {
long t = size;
if (t==0) return "0"+suffixBytes;
if (t<0) return "-"+makeSizeString(-t);
long b = t%bytesPerMetricUnit;
t = t/bytesPerMetricUnit;
long kb = t%bytesPerMetricUnit;
t = t/bytesPerMetricUnit;
long mb = t%bytesPerMetricUnit;
t = t/bytesPerMetricUnit;
long gb = t%bytesPerMetricUnit;
t = t/bytesPerMetricUnit;
long tb = t;
if (tb>lowerLimit)
return Strings.makeRealString(tb + (1.0*gb/bytesPerMetricUnit), -1, precision, 0) + suffixTera;
if (gb>lowerLimit)
return Strings.makeRealString((tb*bytesPerMetricUnit) + gb + (1.0*mb/bytesPerMetricUnit), maxLen, precision, 0) + suffixGiga;
if (mb>lowerLimit)
return Strings.makeRealString((gb*bytesPerMetricUnit) + mb + (1.0*kb/bytesPerMetricUnit), maxLen, precision, 0) + suffixMega;
if (kb>lowerLimit)
return Strings.makeRealString((mb*bytesPerMetricUnit) + kb + (1.0*b/bytesPerMetricUnit), maxLen, precision, 0) + suffixKilo;
return (kb*bytesPerMetricUnit) + b + suffixBytes;
}
/**
* Returns a {@link Formattable} object that can be used with {@link String#format(String, Object...)}.
* <p>
* When used as the argument for a {@literal %s} format string element, the {@literal bytes} value
* will be formatted using the current {@link ByteSizeStrings} values, or if the alternative
* flag is set (using the {@literal %#s} format string) it will use the {@link ByteSizeStrings#metric()}
* formatter. Finally, the precision of the formatted value can be adjusted using format string
* argumenbts like {@literal %.6s}.
*
* @see http://docs.oracle.com/javase/7/docs/api/java/util/Formatter.html#syntax
*/
public Formattable formatted(final long bytes) {
return new Formattable() {
@Override
public void formatTo(Formatter formatter, int flags, int width, int precision) {
boolean alternate = (flags & FormattableFlags.ALTERNATE) == FormattableFlags.ALTERNATE;
ByteSizeStrings strings = alternate ? ByteSizeStrings.metric() : ByteSizeStrings.this;
if (precision != -1) {
formatter.format("%s", strings.makeSizeString(bytes, precision));
} else {
formatter.format("%s", strings.makeSizeString(bytes));
}
}
};
}
/**
* A {@link Function} implementation that formats its input using the current {@link ByteSizeStrings} values.
*/
@Override
@Nullable
public String apply(@Nullable Long input) {
if (input == null) return null;
return makeSizeString(input);
}
public static long parse(String sizeString) {
return parse(sizeString, null);
}
public static long parse(String sizeString, String defaultUnits) {
return parse(sizeString, defaultUnits, null);
}
/** parses the given string as a byte size string, e.g. "4gb"
* @param sizeString string to parse
* @param defaultUnit optional units to append if a number (no units) are supplied
* @param bytesMode optional evaluation mode to force 1024 or 1000 as the interpretation of the unit prefix;
* if omitted, it will depend on the units supplied,
* 1000 for {@link #metric()} (e.g. "1kB"), and
* 1024 for {@link #java()} (e.g. "1k") and {@link #iso()} (e.g. "1KiB")
* @return number of bytes represented by this string
*/
public static long parse(String sizeStringOriginal, String defaultUnit, ByteSizeStrings bytesMode) {
String sizeString = sizeStringOriginal.trim();
String units;
Matcher matcher = Pattern.compile("[A-Za-z]+").matcher(sizeString);
if (!matcher.find()) {
if (defaultUnit==null) {
throw new IllegalArgumentException("Cannot parse '"+sizeStringOriginal+"' as a size string");
}
units = defaultUnit;
} else {
units = matcher.group();
int unitsIndex = sizeString.indexOf(units);
if (sizeString.length() > unitsIndex+units.length()) {
throw new IllegalArgumentException("Cannot parse '"+sizeStringOriginal+"' as a size string");
}
sizeString = sizeString.substring(0, unitsIndex).trim();
}
int exponent = -1;
ByteSizeStrings matchedMode = null;
for (ByteSizeStrings mode: new ByteSizeStrings[] { ISO, JAVA, METRIC } ) {
matchedMode = mode;
if (units.equalsIgnoreCase(mode.suffixBytes.trim())) { exponent = 0; break; }
if (units.equalsIgnoreCase(mode.suffixKilo.trim())) { exponent = 1; break; }
if (units.equalsIgnoreCase(mode.suffixMega.trim())) { exponent = 2; break; }
if (units.equalsIgnoreCase(mode.suffixGiga.trim())) { exponent = 3; break; }
if (units.equalsIgnoreCase(mode.suffixTera.trim())) { exponent = 4; break; }
}
if (exponent==-1) {
// did not match; try other standard ones
if (units.equalsIgnoreCase("t")) {
exponent = 4;
matchedMode = java();
} else {
throw new IllegalArgumentException("Cannot parse '"+sizeStringOriginal+"' as a size string (as '"+sizeString+"' "+units+")");
}
}
double base = Double.parseDouble(sizeString.trim());
if (bytesMode==null) bytesMode=matchedMode;
while (exponent>0) {
base *= bytesMode.bytesPerMetricUnit;
exponent--;
}
return (long)base;
}
}