/*
* Copyright 2014 Matthias Braun, Martin Gangl
*
* This library is free software: you can redistribute it and/or modify it under
* the terms of the GNU Lesser General Public License as published by the Free
* Software Foundation, either version 3 of the License, or (at your option) any
* later version.
*
* This library is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
* FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
* details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this library. If not, see <http://www.gnu.org/licenses/>.
*/
package eu.bges;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.math.RoundingMode;
import java.text.NumberFormat;
import java.util.Locale;
import java.util.Objects;
/**
* Constants and utility functions for dealing with numbers.
* <p>
* Enable strict floating point calculations (in accordance with <a
* href="https://en.wikipedia.org/wiki/IEEE_754"> IEEE 754</a>) to ensure
* portability between JVMs.
*
* @author Matthias Braun
* @author Martin Gangl
*
*/
public final strictfp class MathUtil {
/**
* Matches a single hexadecimal digit.
*/
public static final String HEX_REGEX = "[0-9a-fA-F]";
/**
* Use this number to signal that the value is unknown
*/
public static final BigDecimal UNKNOWN_VAL = BigDecimal
.valueOf(-Double.MAX_VALUE);
/**
* This scale is used in {@link #div(int, int)} to round the result.
*/
public static final int DEFAULT_SCALE = 10;
private static final String NO_NULL_PARAMS = "Only non-null input is allowed";
/** This is a utility class not meant to be instantiated by others. */
private MathUtil() {
}
/**
* Converts the string representation of a binary number to its hexadecimal
* equivalent.
* <p>
* If {@code omitLeadingZeros} is false, the leading zeros of {@code binary}
* are converted to hex as well.
* <p>
* Examples:
*
* <pre>
* binToHex("1111", true) → "F"
* binToHex("0001", true) → "1"
* binToHex("0000 0001", false) → "01"
* binToHex("0 0001", false) → "01"
* binToHex("1101 0100 0101 1000 1001", true) → "D4589"
* binToHex("0 0000 0011 0110", false) → "0036"
* </pre>
*
* @param binary
* string containing a binary coded value (little endian)
* @param omitLeadingZeros
* whether the resulting hex should not have the leading zeros of
* {@code binary}
* @return a hexadecimal representation of the given binary string
* @throws NullPointerException
* if {@code binary} is null
* @throws NumberFormatException
* if {@code binary} does not represent a binary number
*/
public static String binToHex(final String binary,
final boolean omitLeadingZeros) {
Objects.requireNonNull(binary, NO_NULL_PARAMS);
final String binNoSpace = binary.replaceAll("\\s+", "");
// Convert binary to integer
final BigInteger integer = new BigInteger(binNoSpace, 2);
// Convert to uppercase hex (radix sixteen); no leading zeros
final StringBuilder hex = new StringBuilder(integer.toString(16)
.toUpperCase(Locale.ENGLISH));
// Prepend leading zeroes to the hex string
if (!omitLeadingZeros) {
/*
* Get a binary string with a complete last nibble but no further
* leading zeros
*/
final String binWithoutLeadZeros = hexToBin(hex.toString(), true);
final int nrOfLeadingBinZeros = binNoSpace.length()
- binWithoutLeadZeros.length();
// Nr of complete nibbles that are all zero
int nrOfLeadingHexZeroes = nrOfLeadingBinZeros / 4;
// Last nibble is incomplete --> add another zero to the result
if (nrOfLeadingBinZeros % 4 != 0) {
nrOfLeadingHexZeroes++;
}
for (int i = 0; i < nrOfLeadingHexZeroes; i++) {
hex.insert(0, '0');
}
}
return hex.toString();
}
/**
* Converts a boolean {@code value} to a short.
*
* @param value
* boolean value to be converted
* @return 1 if true; 0 if false
*/
public static short booleanToShort(final boolean value) {
if (value) {
return 1;
} else {
return 0;
}
}
/**
* Converts a decimal number to hexadecimal and prepends zeroes if the hex
* number has less digits than {@code minNrOfDigits}.
* <p>
* This is useful when converting RGB values (ranging from 0 to 255) to
* their hexadecimal representation that need to have six digits in total.
*
* @param dec
* the decimal number to convert
* @param minNrOfDigits
* the minimum number of digits the resulting hex number should
* have
* @return the converted and padded number in hexadecimal (all upper case)
*/
public static String decToPaddedHex(final long dec, final int minNrOfDigits) {
// Convert the decimal to hex and put it in a string builder
final StringBuilder builder = new StringBuilder(Long.toHexString(dec));
final int zeroesToPad = minNrOfDigits - builder.length();
// Prepend some zeroes
for (int i = 0; i < zeroesToPad; i++) {
builder.insert(0, "0");
}
return builder.toString().toUpperCase(Locale.ENGLISH);
}
/**
* Divides two integers.
* <p>
* If the result has more than {@value #DEFAULT_SCALE} digits after the
* decimal point, {@link RoundingMode#HALF_UP} is used.
*
* @param dividend
* integer that's divided by the divisor
* @param divisor
* integer that divides the dividend
* @return the quotient of {@code dividend} and {@code divisor} as a
* {@link BigDecimal} with no trailing zeros
* @throws ArithmeticException
* if {@code divisor} is zero
*/
public static BigDecimal div(final int dividend, final int divisor) {
return div(dividend, divisor, DEFAULT_SCALE);
}
/**
* Divides two integers.
* <p>
* If the result exceeds the specified {@code scale},
* {@link RoundingMode#HALF_UP} is used.
*
* @param dividend
* integer that's divided by the divisor
* @param divisor
* integer that divides the dividend
* @param scale
* round the result to this place after the comma
* @return the quotient of {@code dividend} and {@code divisor} as a
* {@link BigDecimal} with no trailing zeros
* @throws ArithmeticException
* if {@code divisor} is zero
*/
public static BigDecimal div(final int dividend, final int divisor,
final int scale) {
final BigDecimal bigDividend = BigDecimal.valueOf(dividend);
final BigDecimal bigDivisor = BigDecimal.valueOf(divisor);
final BigDecimal quotient = bigDividend.divide(bigDivisor, scale,
RoundingMode.HALF_UP);
return quotient.stripTrailingZeros();
}
/**
* Converts the string representation of a hexadecimal number to its binary
* equivalent.
* <p>
* If {@code completeNibble} is true, zeros are prepended to the resulting
* string until the last nibble is complete. If false, the returned string
* has no zeros before the leftmost 1 bit.
* <p>
* Examples:
*
* <pre>
* hexToBin("F", false) → "1111"
* hexToBin("a", false) → "1010"
* hexToBin("1", false) → "1"
* hexToBin("1", true) → "0001"
* hexToBin("7A BC 90 1F", true) → "01111010101111001001000000011111"
* </pre>
*
* @param hex
* string containing a hexadecimal coded value (little endian)
* @param completeNibble
* whether the last half byte is prepended with zeros
* @return a binary representation of the given hexadecimal string
* @throws NullPointerException
* if {@code hex} is null
* @throws NumberFormatException
* if {@code hex} does not represent a hexadecimal number
*/
public static String hexToBin(final String hex, final boolean completeNibble) {
Objects.requireNonNull(hex, NO_NULL_PARAMS);
final String hexNoSpace = hex.replaceAll("\\s+", "");
// Convert hex to integer
final BigInteger integer = new BigInteger(hexNoSpace, 16);
// Convert to binary (radix two); has no leading zeros
final StringBuilder bin = new StringBuilder(integer.toString(2));
if (completeNibble) {
// Insert zeros until the nibble is complete
while (bin.length() % 4 != 0) {
bin.insert(0, '0');
}
}
return bin.toString();
}
/**
* Gets the biggest number among some integers.
*
* @param firstNr
* there has to be at least one number to get the maximum from
* @param rest
* the other numbers
* @return the greatest among {@code firstNr} and the {@code rest}
*/
public static int max(final int firstNr, final int... rest) {
int currMaximum = firstNr;
for (final int i : rest) {
if (i > currMaximum) {
currMaximum = i;
}
}
return currMaximum;
}
/**
* Gets the smallest number among some integers.
*
* @param firstNr
* there has to be at least one number to get the minimum from
* @param rest
* the other numbers
* @return the smallest among {@code firstNr} and the {@code rest}
*/
public static int min(final int firstNr, final int... rest) {
int currMinimum = firstNr;
for (final int i : rest) {
if (i < currMinimum) {
currMinimum = i;
}
}
return currMinimum;
}
/**
* Parses a {@link BigDecimal} from a {@code string}.
* <p>
* Uses the rightmost comma or dot as the decimal point.
* <p>
* If the string is empty or consists of whitespace, {@link #UNKNOWN_VAL} is
* returned.
* <p>
* Acceptable input:
*
* <pre>
* 1.005,4
* 1 000 123
* 1,005,49
* 1,005,0500,4
* 1,,,,005.4
* </pre>
*
* @param string
* to be parsed (comma symbol . and , are allowed; preceding
* occurrences of . or , will be ignored)
* @return a double value representation of the current string
* @throws NumberFormatException
* if an non-parsable string has been given (e.g., it contains
* letters or is null)
*/
public static BigDecimal parseDec(final String string) {
if (string == null) {
throw new NumberFormatException("Can't parse null");
}
final String noSpace = string.replaceAll("\\s+", "");
final String noSpaceAndOnlyDots = noSpace.replace(',', '.');
if (noSpaceAndOnlyDots.isEmpty()) {
return UNKNOWN_VAL;
}
boolean decimalPointFound = false;
// Number with commas and dots removed except for the first one
final StringBuilder cleanNr = new StringBuilder(
noSpaceAndOnlyDots.length());
// Create the clean number by parsing the input backwards
for (int i = noSpaceAndOnlyDots.length() - 1; i >= 0; i--) {
final char currChar = noSpaceAndOnlyDots.charAt(i);
if (currChar == '.') {
// The rightmost decimal point is accepted, others are ignored
if (!decimalPointFound) {
decimalPointFound = true;
cleanNr.append(currChar);
}
} else {
// Current char is not a decimal point
cleanNr.append(currChar);
}
}
return new BigDecimal(cleanNr.reverse().toString());
}
/**
* Parses a long from a string.
* <p>
* This works if the string represents a double, a long or an integer.
* <p>
* If the string represents a double, it's rounded.
*
* @param str
* the string to parse
* @return the parsed long
* @throws NumberFormatException
* if {@code str} does not represent a number
* @throws NullPointerException
* if {@code str} is null
*/
public static long parseToLong(final String str) {
Objects.requireNonNull(str, NO_NULL_PARAMS);
final double d = Double.parseDouble(str);
// Rounded to a long
return Math.round(d);
}
/**
* Rounds a double to a certain {@code decimalPlace.}
* <p>
* Examples:
*
* <pre>
* round(1.46, 1) → 1.5
* round(1.005, 2) → 1.01
* round(123.0054, 3) → 123.005
* </pre>
*
* Note that {@code double}'s precision is about 16 decimal digits after the
* comma: Rounding to a decimal place after that will probably yield
* unexpected results.
*
* @param roundThis
* the double that is rounded
* @param decimalPlace
* the decimal place to which the number is rounded
* @return the rounded number as a {@link BigDecimal}
*/
public static BigDecimal round(final double roundThis,
final int decimalPlace) {
final BigDecimal rounded = BigDecimal.valueOf(roundThis).setScale(
decimalPlace, RoundingMode.HALF_UP);
return rounded.stripTrailingZeros();
}
/**
* Converts a short value to boolean.
* <p>
* 1 → true; 0 → false
*
* @param value
* short value to be converted
* @return true if 1; false if 0
* @throws IllegalArgumentException
* if {@code value} is neither 1 nor 0
*/
public static boolean shortToBoolean(final short value) {
if (value != 0 && value != 1) {
throw new IllegalArgumentException("Input has to be either 1 or 0");
}
return value == 1;
}
/**
* Converts a value to its percent representation.
* <p>
* The resulting percentage is rounded to three decimal places.
* <p>
* The output is locale-dependent: The decimal separator and the position of
* the comma will vary according to the current locale of the JVM.
* <p>
* Example for English (UK) locale:
*
* <pre>
* 0.99 → "99%"
* 0.053 → "5.3%"
* 0.053335 → "5.334%"
* </pre>
*
* @param val
* to be formatted as a percent value
* @return a percent representation of the given value
*/
public static String toPercent(final BigDecimal val) {
final NumberFormat nf = NumberFormat.getPercentInstance();
// The default rounding mode is HALF_EVEN
nf.setRoundingMode(RoundingMode.HALF_UP);
nf.setMaximumFractionDigits(3);
return nf.format(val);
}
}