package org.solovyev.common;
import java.math.BigDecimal;
import java.math.BigInteger;
import javax.annotation.Nonnull;
import midpcalc.Real;
import static java.lang.Math.pow;
import static midpcalc.Real.NumberFormat.FSE_ENG;
import static midpcalc.Real.NumberFormat.FSE_FIX;
import static midpcalc.Real.NumberFormat.FSE_NONE;
import static midpcalc.Real.NumberFormat.FSE_SCI;
public class NumberFormatter {
public static final char NO_GROUPING = 0;
public static final int NO_ROUNDING = -1;
public static final int DEFAULT_MAGNITUDE = 5;
public static final int MIN_PRECISION = 1;
public static final int MAX_PRECISION = 15;
private final Real.NumberFormat numberFormat = new Real.NumberFormat();
private final Real real = new Real();
private int format = FSE_NONE;
private int simpleFormatMagnitude = DEFAULT_MAGNITUDE;
private int precision = MAX_PRECISION;
private char groupingSeparator;
public void useScientificFormat(int simpleFormatMagnitude) {
this.format = FSE_SCI;
this.simpleFormatMagnitude = simpleFormatMagnitude;
}
public void useEngineeringFormat(int simpleFormatMagnitude) {
this.format = FSE_ENG;
this.simpleFormatMagnitude = simpleFormatMagnitude;
}
public void useSimpleFormat() {
this.format = FSE_NONE;
this.simpleFormatMagnitude = DEFAULT_MAGNITUDE;
}
public void setPrecision(int precision) {
if (precision == NO_ROUNDING) {
this.precision = NO_ROUNDING;
return;
}
this.precision = Math.max(MIN_PRECISION, Math.min(precision, MAX_PRECISION));
}
public void setGroupingSeparator(char groupingSeparator) {
this.groupingSeparator = groupingSeparator;
}
@Nonnull
public CharSequence format(double value) {
return format(value, 10);
}
@Nonnull
public CharSequence format(@Nonnull BigInteger value) {
return format(value, 10);
}
@Nonnull
public CharSequence format(double value, int radix) {
checkRadix(radix);
double absValue = Math.abs(value);
final boolean simpleFormat = useSimpleFormat(radix, absValue);
int precision = getPrecision();
if (simpleFormat) {
precision += 1;
final int newScale = Math.max(1, (int) (precision * Math.max(1, radix / 10f)) - 1);
value = BigDecimal.valueOf(value).setScale(newScale, BigDecimal.ROUND_HALF_UP).doubleValue();
absValue = Math.abs(value);
}
if (simpleFormat) {
numberFormat.fse = FSE_FIX;
} else if (format == FSE_NONE) {
// originally, a simple format was requested but we have to use something more appropriate, f.e. scientific
// format
numberFormat.fse = FSE_SCI;
} else {
numberFormat.fse = format;
}
numberFormat.thousand = groupingSeparator;
numberFormat.precision = precision;
numberFormat.base = radix;
numberFormat.maxwidth = simpleFormat ? 100 : 30;
if (radix == 2 && value < 0) {
return "-" + prepare(absValue);
}
return prepare(value);
}
private int getPrecision() {
return precision == NO_ROUNDING ? MAX_PRECISION : precision;
}
@Nonnull
public CharSequence format(@Nonnull BigInteger value, int radix) {
checkRadix(radix);
final BigInteger absValue = value.abs();
final boolean simpleFormat = useSimpleFormat(radix, absValue);
if (simpleFormat) {
numberFormat.fse = FSE_FIX;
} else if (format == FSE_NONE) {
// originally, a simple format was requested but we have to use something more appropriate, f.e. scientific
// format
numberFormat.fse = FSE_SCI;
} else {
numberFormat.fse = format;
}
numberFormat.thousand = groupingSeparator;
numberFormat.precision = Math.max(0, Math.min(precision, MAX_PRECISION));
numberFormat.base = radix;
numberFormat.maxwidth = simpleFormat ? 100 : 30;
if (radix == 2 && value.compareTo(BigInteger.ZERO) < 0) {
return "-" + prepare(absValue);
}
return prepare(value);
}
private void checkRadix(int radix) {
if (radix != 2 && radix != 8 && radix != 10 && radix != 16) {
throw new IllegalArgumentException("Unsupported radix: " + radix);
}
}
private boolean useSimpleFormat(int radix, double absValue) {
if (radix != 10) {
return true;
}
if (format == FSE_NONE) {
// simple format should be used only if rounding is on or if number is big enough
final boolean round = precision != NO_ROUNDING;
return round || absValue >= pow(10, -MAX_PRECISION);
}
if (pow(10, -simpleFormatMagnitude) <= absValue && absValue < pow(10, simpleFormatMagnitude)) {
return true;
}
return false;
}
private boolean useSimpleFormat(int radix, @Nonnull BigInteger absValue) {
if (radix != 10) {
return true;
}
if (format == FSE_NONE) {
return true;
}
if (absValue.compareTo(BigInteger.valueOf((long) pow(10, simpleFormatMagnitude))) < 0) {
return true;
}
return false;
}
@Nonnull
private CharSequence prepare(double value) {
return stripZeros(realFormat(value)).replace('e', 'E');
}
@Nonnull
private CharSequence prepare(@Nonnull BigInteger value) {
return stripZeros(realFormat(value)).replace('e', 'E');
}
@Nonnull
private String realFormat(double value) {
real.assign(Double.toString(value));
return real.toString(numberFormat);
}
@Nonnull
private String realFormat(@Nonnull BigInteger value) {
real.assign(value.toString());
return real.toString(numberFormat);
}
@Nonnull
private String stripZeros(@Nonnull String s) {
int dot = -1;
int firstNonZero = -1;
for (int i = 0; i < s.length(); i++) {
final char c = s.charAt(i);
if (c != '0' && c != groupingSeparator && firstNonZero == -1) {
firstNonZero = i;
}
if (c == '.') {
dot = i;
break;
}
}
if (firstNonZero == -1) {
// all zeros
return "";
}
if (dot < 0) {
// no dot - no trailing zeros
return s.substring(firstNonZero);
}
if (firstNonZero == dot) {
// one zero before dot must be kept
firstNonZero--;
}
final int e = s.lastIndexOf('e');
final int i = findLastNonZero(s, e);
final int end = i == dot ? i : i + 1;
return s.substring(firstNonZero, end) + getExponent(s, e);
}
@Nonnull
private String getExponent(@Nonnull String s, int e) {
String exponent = "";
if (e > 0) {
exponent = s.substring(e);
if (exponent.length() == 2 && exponent.charAt(1) == '0') {
exponent = "";
}
}
return exponent;
}
private int findLastNonZero(@Nonnull String s, int e) {
int i = e > 0 ? e - 1 : s.length() - 1;
for (; i >= 0; i--) {
if (s.charAt(i) != '0') {
break;
}
}
return i;
}
}