NumberUtils.java

/*
 * @copyright defined in LICENSE.txt
 */

package hera.util;

import static hera.util.ValidationUtils.assertNotNull;
import static hera.util.ValidationUtils.assertTrue;

import java.math.BigDecimal;
import java.math.BigInteger;
import java.text.DecimalFormat;
import java.text.NumberFormat;
import java.text.ParseException;
import java.util.Arrays;

public class NumberUtils {

  /**
   * Convert {@code number} to {@code targetClass} type.
   *
   * @param <T> type to convert
   * @param number instance to convert
   * @param targetClass type class to convert
   * @return converted instance
   * @throws IllegalArgumentException No rule for conversion
   */
  @SuppressWarnings("unchecked")
  public static <T> T convert(final Number number, final Class<T> targetClass)
      throws IllegalArgumentException {

    assertNotNull(number, "Number must not be null");
    assertNotNull(targetClass, "Target class must not be null");

    if (targetClass.isInstance(number)) {
      return (T) number;
    } else if (targetClass.equals(Byte.class) || targetClass.equals(byte.class)) {
      final long value = number.longValue();
      if (value < Byte.MIN_VALUE || Byte.MAX_VALUE < value) {
        throwOverflowException(number, targetClass);
      }
      return (T) Byte.valueOf((byte) value);
    } else if (targetClass.equals(Short.class) || targetClass.equals(short.class)) {
      final long value = number.longValue();
      if (value < Short.MIN_VALUE || Short.MAX_VALUE < value) {
        throwOverflowException(number, targetClass);
      }
      return (T) Short.valueOf((short) value);
    } else if (targetClass.equals(Integer.class) || targetClass.equals(int.class)) {
      final long value = number.longValue();
      if (value < Integer.MIN_VALUE || Integer.MAX_VALUE < value) {
        throwOverflowException(number, targetClass);
      }
      return (T) Integer.valueOf((int) value);
    } else if (targetClass.equals(Long.class) || targetClass.equals(long.class)) {
      return (T) Long.valueOf(number.longValue());
    } else if (targetClass.equals(BigInteger.class)) {
      if (number instanceof BigDecimal) {
        // do not lose precision - use BigDecimal's own conversion
        return (T) ((BigDecimal) number).toBigInteger();
      } else {
        // original value is not a Big* number - use standard long conversion
        return (T) BigInteger.valueOf(number.longValue());
      }
    } else if (targetClass.equals(Float.class) || targetClass.equals(float.class)) {
      return (T) Float.valueOf(number.floatValue());
    } else if (targetClass.equals(Double.class) || targetClass.equals(double.class)) {
      return (T) Double.valueOf(number.doubleValue());
    } else if (targetClass.equals(BigDecimal.class)) {
      // always use BigDecimal(String) here to avoid unpredictability of BigDecimal(double)
      // (see BigDecimal javadoc for details)
      return (T) new BigDecimal(number.toString());
    }
    throw new IllegalArgumentException(
        "Could not convert number [" + number + "] of type [" + number.getClass().getName()
            + "] to unknown target class [" + targetClass.getName() + "]");
  }

  /**
   * Throw {@link IllegalArgumentException} when {@code number}'s value is out of bound
   * {@code targetClass}'s range.
   *
   * @param number value instance
   * @param targetClass container type
   */
  private static void throwOverflowException(final Number number, final Class<?> targetClass) {
    throw new IllegalArgumentException(
        "Could not convert number [" + number + "] of type [" + number.getClass().getName()
            + "] to target class [" + targetClass.getName() + "]: overflow");
  }

  /**
   * Parse {@code text} and convert number.
   *
   * @param <T> type to convert
   * @param text string to parse
   * @param clazz type class to convert
   *
   * @return converted instance
   */
  @SuppressWarnings("unchecked")
  public static <T> T parse(final String text, final Class<?> clazz) {
    assertNotNull(text, "Text must not be null");
    assertNotNull(clazz, "Target class must not be null");

    final String trimmed = StringUtils.trim(text);

    if (clazz.equals(Byte.class) || clazz.equals(byte.class)) {
      return (T) Byte.decode(trimmed);
    } else if (clazz.equals(Short.class) || clazz.equals(short.class)) {
      return (T) Short.decode(trimmed);
    } else if (clazz.equals(Integer.class) || clazz.equals(int.class)) {
      return (T) Integer.decode(trimmed);
    } else if (clazz.equals(Long.class) || clazz.equals(long.class)) {
      return (T) Long.decode(trimmed);
    } else if (clazz.equals(BigInteger.class)) {
      return (T) decodeBigInteger(trimmed);
    } else if (clazz.equals(Float.class) || clazz.equals(float.class)) {
      return (T) Float.valueOf(trimmed);
    } else if (clazz.equals(Double.class) || clazz.equals(double.class)) {
      return (T) Double.valueOf(trimmed);
    } else if (clazz.equals(BigDecimal.class) || clazz.equals(Number.class)) {
      return (T) new BigDecimal(trimmed);
    } else {
      throw new IllegalArgumentException(
          "Cannot convert String [" + text + "] to target class [" + clazz.getName() + "]");
    }
  }

  /**
   * Parse string {@code text} as {@code numberFormat} and convert to {@code targetClass} type
   * instance.
   *
   * @param <T> type to convert
   * @param text string to parse
   * @param targetClass type class to convert
   * @param numberFormat format to parse
   * @return converted number
   */
  public static <T> T parse(final String text, final Class<T> targetClass,
      final NumberFormat numberFormat) {
    final boolean noFormat = null == numberFormat;
    if (noFormat) {
      return parse(text, targetClass);
    }

    assertNotNull(text, "Text must not be null");
    assertNotNull(targetClass, "Target class must not be null");
    DecimalFormat decimalFormat = null;
    boolean resetBigDecimal = false;
    if (numberFormat instanceof DecimalFormat) {
      decimalFormat = (DecimalFormat) numberFormat;
      if (BigDecimal.class.equals(targetClass) && !decimalFormat.isParseBigDecimal()) {
        decimalFormat.setParseBigDecimal(true);
        resetBigDecimal = true;
      }
    }

    try {
      final Number number = numberFormat.parse(StringUtils.trim(text));
      return convert(number, targetClass);
    } catch (final ParseException ex) {
      throw new IllegalArgumentException("Could not parse number: " + ex.getMessage(), ex);
    } finally {
      if (resetBigDecimal) {
        decimalFormat.setParseBigDecimal(false);
      }
    }
  }

  /**
   * Parse {@code value} and convert to {@link BigInteger}.
   * <p>
   * Parse as hexa decimal if it has '0x' prefix. Parse as octal decimal if it has '0' prefix.
   * </p>
   *
   * @param value string to convert
   *
   * @return converted {@link BigInteger} instance
   */
  private static BigInteger decodeBigInteger(final String value) {
    int radix = 10;
    int index = 0;
    boolean negative = false;

    // Handle minus sign, if present.
    if (value.startsWith("-")) {
      negative = true;
      ++index;
    }

    // Handle radix specifier, if present.
    if (value.startsWith("0x", index) || value.startsWith("0X", index)) {
      index += 2;
      radix = 16;
    } else if (value.startsWith("#", index)) {
      ++index;
      radix = 16;
    } else if (value.startsWith("0", index) && 1 + index < value.length()) {
      ++index;
      radix = 8;
    }

    final BigInteger result = new BigInteger(value.substring(index), radix);
    return (negative ? result.negate() : result);
  }

  /**
   * Convert {@link BigInteger} into a byte array without additional sign byte to represent
   * canonical two's-complement form. Eg. 255 is {@code "11111111"} not
   * {@code "00000000 1111 1111"}.
   *
   * @param positiveNumber a positive bigInteger
   * @return a converted byte array. Empty byte array if {@code positiveNumber} is null.
   */
  public static byte[] positiveToByteArray(final BigInteger positiveNumber) {
    if (null == positiveNumber) {
      return new byte[0];
    }
    assertTrue(positiveNumber.compareTo(BigInteger.ZERO) >= 0,
        "Argument must greater than or equals to 0");

    final byte[] raw = positiveNumber.toByteArray();
    final int positiveByteCapacity =
        positiveNumber.equals(BigInteger.ZERO) ? 1 : (positiveNumber.bitLength() + 7) >>> 3;
    if (raw.length > positiveByteCapacity) {
      return Arrays.copyOfRange(raw, 1, raw.length);
    }
    return raw;
  }

  /**
   * Convert rawBytes to bigInteger. A rawBytes is considered only as positive (no extra sign bit).
   *
   * @param rawBytes a rawBytes representing positive number without sign bit.
   * @return a converted {@code BigInteger}
   */
  public static BigInteger byteArrayToPositive(final byte[] rawBytes) {
    if (null == rawBytes || rawBytes.length == 0) {
      return BigInteger.ZERO;
    }

    byte[] canonicalBytes = rawBytes;
    if ((rawBytes[0] & 0x80) != 0) {
      canonicalBytes = new byte[rawBytes.length + 1];
      canonicalBytes[0] = 0x00;
      System.arraycopy(rawBytes, 0, canonicalBytes, 1, rawBytes.length);
    }
    return new BigInteger(canonicalBytes);
  }

}