NumberMethods.java

package org.klojang.convert;

import org.klojang.check.Check;
import org.klojang.util.ClassMethods;
import org.klojang.util.MathMethods;

import java.math.BigDecimal;
import java.math.BigInteger;
import java.util.*;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.function.UnaryOperator;

import static java.math.BigDecimal.ONE;
import static org.klojang.util.ClassMethods.*;
import static org.klojang.util.ObjectMethods.isEmpty;
import static org.klojang.convert.ToFloatConversion.BIG_MAX_FLOAT;
import static org.klojang.convert.ToFloatConversion.BIG_MIN_FLOAT;
import static org.klojang.convert.TypeConversionException.inputTypeNotSupported;

/**
 * Methods for parsing, inspecting and converting {@code Number} instances.
 *
 * <p>NB For mathematical operations, see {@link MathMethods}.
 *
 * @author Ayco Holleman
 */
public final class NumberMethods {

  /**
   * {@code Double.MIN_VALUE} converted to a {@code BigDecimal}.
   */
  public static final BigDecimal MIN_DOUBLE_BD =
      new BigDecimal(Double.toString(Double.MIN_VALUE));

  /**
   * {@code Double.MAX_VALUE} converted to a {@code BigDecimal}.
   */
  public static final BigDecimal MAX_DOUBLE_BD =
      new BigDecimal(Double.toString(Double.MAX_VALUE));

  /**
   * {@code Long.MIN_VALUE} converted to a {@code BigDecimal}.
   */
  public static final BigDecimal MIN_LONG_BD =
      new BigDecimal(Long.MIN_VALUE);

  /**
   * {@code Long.MAX_VALUE} converted to a {@code BigDecimal}.
   */
  public static final BigDecimal MAX_LONG_BD =
      new BigDecimal(Long.MAX_VALUE);

  /**
   * {@code Long.MIN_VALUE} converted to a {@code BigInteger}.
   */
  public static final BigInteger MIN_LONG_BI =
      BigInteger.valueOf(Long.MIN_VALUE);

  /**
   * {@code Long.MAX_VALUE} converted to a {@code BigInteger}.
   */
  public static final BigInteger MAX_LONG_BI =
      BigInteger.valueOf(Long.MAX_VALUE);

  static Predicate<Number> yes() {return n -> true;}

  private static final Map<Class<? extends Number>, Function<Number, BigDecimal>>
      toBigDecimal =
      Map.of(
          BigDecimal.class, BigDecimal.class::cast,
          BigInteger.class, x -> new BigDecimal((BigInteger) x),
          Double.class, x -> new BigDecimal(Double.toString((double) x)),
          Float.class, x -> new BigDecimal(Float.toString((float) x)),
          Long.class, x -> new BigDecimal((Long) x),
          AtomicLong.class, x -> new BigDecimal(((AtomicLong) x).get()),
          Integer.class, x -> new BigDecimal((Integer) x),
          AtomicInteger.class, x -> new BigDecimal(((AtomicInteger) x).get()),
          Short.class, x -> new BigDecimal((Short) x),
          Byte.class, x -> new BigDecimal((Byte) x)
      );

  private static final Map<Class<?>, Function<String, Number>> parsers = Map.of(
      BigDecimal.class, NumberMethods::parseBigDecimal,
      BigInteger.class, NumberMethods::parseBigInteger,
      Double.class, NumberMethods::parseDouble,
      Float.class, NumberMethods::parseFloat,
      Long.class, NumberMethods::parseLong,
      AtomicLong.class, s -> new AtomicLong(parseLong(s)),
      Integer.class, NumberMethods::parseInt,
      AtomicInteger.class, s -> new AtomicInteger(parseInt(s)),
      Short.class, NumberMethods::parseShort,
      Byte.class, NumberMethods::parseByte);

  private static final Map<Class<?>, Predicate<String>> stringFitsInto = Map.of(
      BigDecimal.class, NumberMethods::isBigDecimal,
      BigInteger.class, NumberMethods::isBigInteger,
      Double.class, NumberMethods::isDouble,
      Float.class, NumberMethods::isFloat,
      Long.class, NumberMethods::isLong,
      AtomicLong.class, NumberMethods::isLong,
      Integer.class, NumberMethods::isInt,
      AtomicInteger.class, NumberMethods::isInt,
      Short.class, NumberMethods::isShort,
      Byte.class, NumberMethods::isByte);

  private static final Map<Class<?>, Predicate<Number>> numberFitsInto = Map.of(
      BigDecimal.class, ToBigDecimalConversion::isLossless,
      BigInteger.class, ToBigIntegerConversion::isLossless,
      Double.class, ToDoubleConversion::isLossless,
      Float.class, ToFloatConversion::isLossless,
      Long.class, ToLongConversion::isLossless,
      AtomicLong.class, ToLongConversion::isLossless,
      Integer.class, ToIntConversion::isLossless,
      AtomicInteger.class, ToIntConversion::isLossless,
      Short.class, ToShortConversion::isLossless,
      Byte.class, ToByteConversion::isLossless);

  private static final Map<Class<?>, UnaryOperator<Number>> converters = Map.of(
      BigDecimal.class, ToBigDecimalConversion::exec,
      BigInteger.class, ToBigIntegerConversion::exec,
      Double.class, ToDoubleConversion::exec,
      Float.class, ToFloatConversion::exec,
      Long.class, ToLongConversion::exec,
      AtomicLong.class, ToLongConversion::exec,
      Integer.class, ToIntConversion::exec,
      AtomicInteger.class, ToIntConversion::exec,
      Short.class, ToShortConversion::exec,
      Byte.class, ToByteConversion::exec);

  private static final Set<Class<? extends Number>> wrappers = Set.of(Byte.class,
      Short.class,
      Integer.class,
      Long.class,
      Float.class,
      Double.class);

  private static final Set<Class<? extends Number>> integrals = Set.of(Byte.class,
      Short.class,
      Integer.class,
      AtomicInteger.class,
      Long.class,
      AtomicLong.class,
      BigInteger.class);

  private static final Set<Class<? extends Number>> supported = Set.of(Byte.class,
      Short.class,
      Integer.class,
      AtomicInteger.class,
      Long.class,
      AtomicLong.class,
      Float.class,
      Double.class,
      BigInteger.class,
      BigDecimal.class);

  private NumberMethods() {
    throw new UnsupportedOperationException();
  }

  /**
   * Returns {@code true} if the specified class is one of the standard primitive
   * number wrappers: {@code Byte}, {@code Short}, {@code Integer}, {@code Long},
   * {@code Float}, {@code Double}.
   *
   * @param numberType the class to test
   * @return whether the class is a primitive number wrapper
   * @see ClassMethods#isPrimitiveNumber(Class)
   */
  public static boolean isWrapper(Class<?> numberType) {
    Check.notNull(numberType);
    return wrappers.contains(numberType);
  }

  /**
   * Returns {@code true} if  the specified number belongs to one of the primitive
   * number wrappers.
   *
   * @param number the number to test
   * @return whether the specified number belongs to one of the primitive number
   *     wrappers
   * @see #isWrapper(Class)
   */
  public static boolean isWrapper(Number number) {
    Check.notNull(number);
    return wrappers.contains(number.getClass());
  }

  /**
   * Returns {@code true} if the specified class is one of {@code Byte},
   * {@code Short}, {@code Integer}, {@code Long}, {@code BigInteger}.
   *
   * @param type the class to test
   * @return whether the class is an integral number type
   */
  public static boolean isIntegral(Class<?> type) {
    return Check.notNull(type).ok(integrals::contains);
  }

  /**
   * Returns {@code true} if the specified number is an integral number.
   *
   * @param number the number to test
   * @return whether the specified number is an integral number
   * @see #isIntegral(Class)
   */
  public static boolean isIntegral(Number number) {
    Check.notNull(number);
    return integrals.contains(number.getClass());
  }

  /**
   * Parses the specified string into a {@code int}. If the string does not represent
   * a number, or if it cannot be parsed into an {@code int} without loss of
   * information, a {@link TypeConversionException} is thrown.
   *
   * @param s the string to be parsed
   * @return the {@code int} value represented by the string
   * @throws TypeConversionException if the string does not represent or number,
   *     or if conversion would lead to loss of information
   */
  public static int parseInt(String s) throws TypeConversionException {
    return (int) parseIntegral(s, int.class, Integer.MIN_VALUE, Integer.MAX_VALUE);
  }

  /**
   * Returns {@code true} if the specified string can be parsed into an {@code int}
   * without loss of information. The argument is allowed to be {@code null}, in
   * which case the return value will be {@code false}.
   *
   * @param s the string to be parsed
   * @return whether the specified string can be parsed into an {@code int} without
   *     causing integer overflow
   */
  public static boolean isInt(String s) {
    return isIntegral(s, Integer.MIN_VALUE, Integer.MAX_VALUE);
  }

  /**
   * Returns an empty {@code OptionalInt} if the specified string cannot be parsed
   * into a 32-bit integer, else an {@code OptionalInt} containing the {@code int}
   * value parsed out of the string.
   *
   * @param s the string to be parsed
   * @return an {@code OptionalInt} containing the {@code int} value parsed out of
   *     the string
   */
  public static OptionalInt toInt(String s) {
    return toIntegral(s, Integer.MIN_VALUE, Integer.MAX_VALUE);
  }

  /**
   * Parses the specified string into a {@code long}. If the string does not
   * represent a number, or if it cannot be parsed into a {@code long} without loss
   * of information, a {@link TypeConversionException} is thrown.
   *
   * @param s the string to be parsed
   * @return the {@code long} value represented by the string
   * @throws TypeConversionException if the string does not represent or number,
   *     or if conversion would lead to loss of information
   */
  public static long parseLong(String s) throws TypeConversionException {
    if (!isEmpty(s)) {
      BigDecimal bd;
      try {
        bd = new BigDecimal(s);
      } catch (NumberFormatException e) {
        throw new TypeConversionException(s, long.class, e.toString());
      }
      if (fitsIntoLong(bd)) {
        return bd.longValue();
      }
      throw TypeConversionException.targetTypeTooNarrow(s, long.class);
    }
    throw new TypeConversionException(s, long.class);
  }

  /**
   * Returns {@code true} if the specified string can be parsed into a {@code long}
   * without loss of information. The argument is allowed to be {@code null}, in
   * which case the return value will be {@code false}.
   *
   * @param s the string to be parsed
   * @return whether the specified string can be parsed into a {@code long} without
   *     causing integer overflow
   */
  public static boolean isLong(String s) {
    if (!isEmpty(s)) {
      try {
        return fitsIntoLong(new BigDecimal(s));
      } catch (NumberFormatException ignored) {
      }
    }
    return false;
  }

  /**
   * Returns an empty {@code OptionalLong} if the specified string cannot be parsed
   * into a 64-bit integer, else an {@code OptionalLong} containing the {@code long}
   * value parsed out of the string.
   *
   * @param s the string to be parsed
   * @return an {@code OptionalLong} containing the {@code long} value parsed out of
   *     the string
   */
  public static OptionalLong toLong(String s) {
    if (!isEmpty(s)) {
      try {
        BigDecimal bd = new BigDecimal(s);
        if (fitsIntoLong(bd)) {
          return OptionalLong.of(bd.longValue());
        }
      } catch (NumberFormatException ignored) {
      }
    }
    return OptionalLong.empty();
  }

  /**
   * Parses the specified string into a {@code double}. If the string does not
   * represent a number, or if it cannot be parsed into a {@code double} without loss
   * of information, a {@link TypeConversionException} is thrown.
   *
   * @param s the string to be parsed
   * @return the {@code double} value represented by the string
   * @throws TypeConversionException if the string does not represent or number,
   *     or if conversion would lead to loss of information
   */
  public static double parseDouble(String s) throws TypeConversionException {
    if (!isEmpty(s)) {
      BigDecimal bd;
      try {
        bd = new BigDecimal(s);
      } catch (NumberFormatException e) {
        throw new TypeConversionException(s, double.class, e.toString());
      }
      BigDecimal x = bd.abs();
      if (x.compareTo(MIN_DOUBLE_BD) >= 0 && x.compareTo(
          MAX_DOUBLE_BD) <= 0) {
        return bd.doubleValue();
      }
    }
    throw TypeConversionException.targetTypeTooNarrow(s, float.class);
  }

  /**
   * Returns {@code true} if the specified string can be parsed into a {@code double}
   * without loss of information. The argument is allowed to be {@code null}, in
   * which case the return value will be {@code false}.
   *
   * @param s the string to be parsed
   * @return whether he specified string can be parsed into a regular, finite
   *     {@code double}
   */
  public static boolean isDouble(String s) {
    if (!isEmpty(s)) {
      try {
        parseDouble(s);
        return true;
      } catch (TypeConversionException ignored) {
      }
    }
    return false;
  }

  /**
   * Returns an empty {@code OptionalDouble} if the specified string cannot be parsed
   * into a {@code double} value, else an {@code OptionalDouble} containing the
   * {@code double} value parsed out of the string.
   *
   * @param s the string to be parsed
   * @return an {@code OptionalDouble} containing the {@code double} value parsed out
   *     of the string
   */
  public static OptionalDouble toDouble(String s) {
    if (!isEmpty(s)) {
      try {
        return OptionalDouble.of(parseDouble(s));
      } catch (TypeConversionException ignored) {
      }
    }
    return OptionalDouble.empty();
  }

  /**
   * Parses the specified string into a {@code float}. If the string does not
   * represent a number, or if it cannot be parsed into a {@code float} without loss
   * of information, a {@link TypeConversionException} is thrown.
   *
   * @param s the string to be parsed
   * @return the {@code float} value represented by the string
   * @throws TypeConversionException if the string does not represent or number,
   *     or if conversion would lead to loss of information
   */
  public static float parseFloat(String s) throws TypeConversionException {
    if (!isEmpty(s)) {
      BigDecimal bd;
      try {
        bd = new BigDecimal(s);
      } catch (NumberFormatException e) {
        throw new TypeConversionException(s, double.class, e.toString());
      }
      BigDecimal x = bd.abs();
      if (x.compareTo(BIG_MIN_FLOAT) >= 0 && x.compareTo(BIG_MAX_FLOAT) <= 0) {
        return bd.floatValue();
      }
    }
    throw TypeConversionException.targetTypeTooNarrow(s, float.class);
  }

  /**
   * Returns {@code true} if the specified string can be parsed into a {@code float}
   * without loss of information. The argument is allowed to be {@code null}, in
   * which case the return value will be {@code false}.
   *
   * @param s the string to be parsed
   * @return whether he specified string can be parsed into a regular, finite
   *     {@code float}
   */
  public static boolean isFloat(String s) {
    if (!isEmpty(s)) {
      try {
        parseFloat(s);
        return true;
      } catch (TypeConversionException ignored) {
      }
    }
    return false;
  }

  /**
   * Returns an empty {@code OptionalDouble} if the specified string cannot be parsed
   * into a regular, finite {@code float} value, else an {@code OptionalDouble}
   * containing the {@code float} value parsed out of the string.
   *
   * @param s the string to be parsed
   * @return an {@code OptionalDouble} containing the {@code float} value parsed out
   *     of the string
   */
  public static OptionalDouble toFloat(String s) {
    if (!isEmpty(s)) {
      try {
        return OptionalDouble.of(parseFloat(s));
      } catch (TypeConversionException ignored) {
      }
    }
    return OptionalDouble.empty();
  }

  /**
   * Parses the specified string into a {@code short}. If the string does not
   * represent a number, or if it cannot be parsed into a {@code short} without loss
   * of information, a {@link TypeConversionException} is thrown.
   *
   * @param s the string to be parsed
   * @return the {@code short} value represented by the string
   * @throws TypeConversionException if the string does not represent or number,
   *     or if conversion would lead to loss of information
   */
  public static short parseShort(String s) throws TypeConversionException {
    return (short) parseIntegral(s, short.class, Short.MIN_VALUE, Short.MAX_VALUE);
  }

  /**
   * Returns {@code true} if the specified string can be parsed into a {@code short}
   * without loss of information. The argument is allowed to be {@code null}, in
   * which case the return value will be {@code false}.
   *
   * @param s the string to be parsed
   * @return whether he specified string can be parsed into a {@code short} without
   *     causing integer overflow
   */
  public static boolean isShort(String s) {
    return isIntegral(s, Short.MIN_VALUE, Short.MAX_VALUE);
  }

  /**
   * Returns an empty {@code OptionalInt} if the specified string cannot be parsed
   * into a 16-bit integer, else an {@code OptionalInt} containing the {@code short}
   * value parsed out of the string.
   *
   * @param s the string to be parsed
   * @return an {@code OptionalInt} containing the {@code short} value parsed out of
   *     the string
   */
  public static OptionalInt toShort(String s) {
    return toIntegral(s, Short.MIN_VALUE, Short.MAX_VALUE);
  }

  /**
   * Parses the specified string into a {@code byte}. If the string does not
   * represent a number, or if it cannot be parsed into a {@code byte} without loss
   * of information, a {@link TypeConversionException} is thrown.
   *
   * @param s the string to be parsed
   * @return the {@code byte} value represented by the string
   * @throws TypeConversionException if the string does not represent or number,
   *     or if conversion would lead to loss of information
   */
  public static byte parseByte(String s) throws TypeConversionException {
    return (byte) parseIntegral(s, byte.class, Byte.MIN_VALUE, Byte.MAX_VALUE);
  }

  /**
   * Returns {@code true} if the specified string can be parsed into a {@code byte}
   * without loss of information. The argument is allowed to be {@code null}, in
   * which case the return value will be {@code false}.
   *
   * @param s the string to be parsed
   * @return whether he specified string can be parsed into a {@code byte} without
   *     causing an integer overflow
   */
  public static boolean isByte(String s) {
    return isIntegral(s, Byte.MIN_VALUE, Byte.MAX_VALUE);
  }

  /**
   * Returns an empty {@code OptionalInt} if the specified string cannot be parsed
   * into an 8-bit integer, else an {@code OptionalInt} containing the {@code byte}
   * value parsed out of the string.
   *
   * @param s the string to be parsed
   * @return an {@code OptionalInt} containing the {@code byte} value parsed out of
   *     the string
   */
  public static OptionalInt toByte(String s) {
    return toIntegral(s, Byte.MIN_VALUE, Byte.MAX_VALUE);
  }

  /**
   * Parses the specified string into a {@code BigInteger}. If the string does not
   * represent a number, or if it cannot be parsed into a {@code BigInteger} without
   * loss of information, a {@link TypeConversionException} is thrown.
   *
   * @param s the string to be parsed
   * @return the {@code BigInteger} value represented by the string
   * @throws TypeConversionException if the string does not represent or number,
   *     or if conversion would lead to loss of information
   */
  public static BigInteger parseBigInteger(String s) throws TypeConversionException {
    if (!isEmpty(s)) {
      try {
        BigDecimal bd = new BigDecimal(s);
        if (isRound(bd)) {
          return bd.toBigInteger();
        }
        throw TypeConversionException.targetTypeTooNarrow(s, BigInteger.class);
      } catch (NumberFormatException e) {
        throw new TypeConversionException(s, BigInteger.class, e.toString());
      }
    }
    throw new TypeConversionException(s, BigInteger.class);
  }

  /**
   * Returns {@code true} if the specified string can be parsed into a
   * {@code BigInteger} without loss of information. The argument is allowed to be
   * {@code null}, in which case the return value will be {@code false}.
   *
   * @param s the string to be parsed
   * @return whether he specified string can be parsed into a {@code BigInteger}
   */
  public static boolean isBigInteger(String s) {
    if (!isEmpty(s)) {
      try {
        BigDecimal bd = new BigDecimal(s);
        if (isRound(bd)) {
          return true;
        }
      } catch (NumberFormatException ignored) {
      }
    }
    return false;
  }

  /**
   * Returns an empty {@code Optional} if the specified string cannot be parsed into
   * BigInteger, else an {@code Optional} containing the {@code BigInteger} value
   * parsed out of the string.
   *
   * @param s the string to be parsed
   * @return an {@code Optional} containing the {@code BigInteger} value parsed out
   *     of the string
   */
  public static Optional<BigInteger> toBigInteger(String s) {
    if (!isEmpty(s)) {
      try {
        BigDecimal bd = new BigDecimal(s);
        if (isRound(bd)) {
          return Optional.of(bd.toBigInteger());
        }
      } catch (NumberFormatException ignored) {
      }
    }
    return Optional.empty();
  }

  /**
   * Parses the specified string into a {@code BigInteger}. If the string does not
   * represent a number, a {@link TypeConversionException} is thrown.
   *
   * @param s the string to be parsed
   * @return the {@code BigDecimal} value represented by the string
   * @throws TypeConversionException if the string does not represent or number
   */
  public static BigDecimal parseBigDecimal(String s) throws TypeConversionException {
    try {
      return new BigDecimal(s);
    } catch (NumberFormatException e) {
      throw new TypeConversionException(s, BigDecimal.class, e.toString());
    }
  }

  /**
   * Returns {@code true} if the specified string can be parsed into a
   * {@code BigDecimal}. The argument is allowed to be {@code null}, in which case
   * the return value will be {@code false}.
   *
   * @param s the string to be parsed
   * @return whether he specified string can be parsed into a {@code BigDecimal}
   */
  public static boolean isBigDecimal(String s) {
    if (!isEmpty(s)) {
      try {
        parseBigDecimal(s);
        return true;
      } catch (TypeConversionException ignored) {
      }
    }
    return false;
  }

  /**
   * Returns an empty {@code Optional} if the specified string cannot be parsed into
   * BigDecimal, else an {@code Optional} containing the {@code BigDecimal} value
   * parsed out of the string.
   *
   * @param s the string to be parsed
   * @return an {@code Optional} containing the {@code BigDecimal} value parsed out
   *     of the string
   */
  public static Optional<BigDecimal> toBigDecimal(String s) {
    if (!isEmpty(s)) {
      try {
        return Optional.of(parseBigDecimal(s));
      } catch (TypeConversionException ignored) {
      }
    }
    return Optional.empty();
  }

  /**
   * Parses the specified string into a number of the specified type. Throws an
   * {@link TypeConversionException} if the string is not a number, or if the number
   * is too big to fit into the target type.
   *
   * @param <T> the type of {@code Number} to convert the string to
   * @param s the string to be parsed
   * @param targetType the class of the {@code Number} type
   * @return a {@code Number} of the specified type
   * @throws TypeConversionException if the string does not represent or number,
   *     or if conversion would lead to loss of information
   */
  @SuppressWarnings("unchecked")
  public static <T extends Number> T parse(String s, Class<T> targetType)
      throws TypeConversionException {
    Check.notNull(targetType, "target type");
    Function<String, Number> parser = parsers.get(targetType);
    if (parser != null) {
      return (T) parser.apply(s);
    }
    throw notSupported(s, targetType);
  }

  /**
   * Tests whether the specified string can be parsed into a {@code Number} of the
   * specified type.
   *
   * @param <T> the type of {@code Number} to convert the string to
   * @param s the string to be parsed
   * @param targetType the class of the {@code Number} type
   * @return whether the specified string can be parsed into a {@code Number} of the
   *     specified type
   */
  public static <T extends Number> boolean fitsInto(String s, Class<T> targetType) {
    Check.notNull(targetType, "target type");
    if (!isEmpty(s)) {
      Predicate<String> tester = stringFitsInto.get(targetType);
      if (tester != null) {
        return tester.test(s);
      }
      throw notSupported(s, targetType);
    }
    return false;
  }

  ////////////////////////////////////////////////////////////////////////////////
  //                           END OF PARSE METHODS                             //
  ////////////////////////////////////////////////////////////////////////////////

  /**
   * Converts a {@code Number} of an unspecified type to a {@code BigDecimal}.
   *
   * @param n the number
   * @return the {@code BigDecimal} representing the number
   */
  public static BigDecimal toBigDecimal(Number n) {
    Check.notNull(n);
    Function<Number, BigDecimal> fnc = toBigDecimal.get(n.getClass());
    if (fnc != null) {
      return fnc.apply(n);
    }
    throw inputTypeNotSupported(n, BigDecimal.class);
  }

  /**
   * Safely converts a number of an unspecified type to a number of a definite type.
   * Throws a {@link TypeConversionException} if the number cannot be converted to
   * the target type without loss of information. The number is allowed to be
   * {@code null}, in which case {@code null} will be returned.
   *
   * @param <T> the input type
   * @param <R> the output type
   * @param number the number to be converted
   * @param targetType the class of the target type
   * @return an instance of the target type
   */
  @SuppressWarnings({"unchecked"})
  public static <T extends Number, R extends Number> R convert(T number,
      Class<R> targetType) throws TypeConversionException {
    Check.notNull(targetType, "target type");
    if (number == null || number.getClass() == targetType) {
      return (R) number;
    }
    UnaryOperator<Number> fnc = converters.get(targetType);
    if (fnc != null) {
      return (R) fnc.apply(number);
    }
    throw notSupported(number, targetType);
  }

  /**
   * Returns {@code true} if the specified number can be converted to the specified
   * target type without loss of information. The number is allowed to be
   * {@code null}, in which case {@code true} will be returned.
   *
   * @param <T> the type of {@code Number} to convert to
   * @param number the {@code Number} to convert
   * @param targetType the type of {@code Number} to convert to
   * @return whether conversion will be lossless
   */
  public static <T extends Number> boolean fitsInto(Number number,
      Class<T> targetType) {
    Check.notNull(targetType, "target type");
    if (number == null) {
      return true;
    }
    Predicate<Number> tester = numberFitsInto.get(targetType);
    if (tester != null) {
      return tester.test(number);
    }
    throw inputTypeNotSupported(number, targetType);
  }

  /**
   * Determines whether the specified float's fractional part is zero or absent.
   *
   * @param f the {@code float} to inspect
   * @return whether the specified float's fractional part is zero or absent
   */
  public static boolean isRound(float f) {
    return isRound(new BigDecimal(Float.toString(f)));
  }

  /**
   * Determines whether the specified double's fractional part is zero or absent.
   *
   * @param d the {@code double} to inspect
   * @return whether the specified double's fractional part is zero or absent
   */
  public static boolean isRound(double d) {
    return isRound(new BigDecimal(Double.toString(d)));
  }

  /**
   * Determines whether the specified BigDecimal's fractional part is zero or
   * absent.
   *
   * @param bd the {@code BigDecimal} to inspect
   * @return whether the specified BigDecimal's fractional part is zero or absent
   */
  public static boolean isRound(BigDecimal bd) {
    // NB The 1st check is cheap and catches a lot of the cases.
    // The 2nd second is superfluous in the presence of the 3rd.
    // However, it still catches quite a few cases and seems
    // sufficiently cheap compared to the 3rd check that it seems
    // worth paying the price of wasted CPU cycles if we do fall
    // through to the 3rd check. The 3rd and 4th check are
    // equivalent, but the 3rd check seems more efficient. Needs
    // to be measured though.
    return bd.scale() <= 0
        || bd.stripTrailingZeros().scale() == 0
        || bd.divideToIntegralValue(ONE).compareTo(bd) == 0
        /*|| bd.remainder(ONE).signum() == 0 */;
  }

  private static long parseIntegral(String s,
      Class<?> type,
      long minVal,
      long maxVal) {
    if (!isEmpty(s)) {
      try {
        BigDecimal bd = new BigDecimal(s);
        long l;
        if (isRound(bd) && (l = bd.longValue()) >= minVal && l <= maxVal) {
          return l;
        }
        throw TypeConversionException.targetTypeTooNarrow(s, type);
      } catch (NumberFormatException e) {
        throw new TypeConversionException(s, int.class, e.toString());
      }
    }
    throw new TypeConversionException(s, type);
  }

  private static boolean isIntegral(String s, long minVal, long maxVal) {
    if (!isEmpty(s)) {
      try {
        BigDecimal bd = new BigDecimal(s);
        long l;
        return isRound(bd) && (l = bd.longValue()) >= minVal && l <= maxVal;
      } catch (NumberFormatException ignored) {
      }
    }
    return false;
  }

  private static OptionalInt toIntegral(String s, long minVal, long maxVal) {
    if (!isEmpty(s)) {
      try {
        BigDecimal bd = new BigDecimal(s);
        long l;
        if (isRound(bd) && (l = bd.longValue()) >= minVal && l <= maxVal) {
          return OptionalInt.of((int) l);
        }
      } catch (NumberFormatException ignored) {
      }
    }
    return OptionalInt.empty();
  }

  private static boolean fitsIntoLong(BigDecimal bd) {
    return isRound(bd)
        && bd.compareTo(MIN_LONG_BD) >= 0
        && bd.compareTo(MAX_LONG_BD) <= 0;
  }

  private static TypeConversionException notSupported(Object obj,
      Class<?> type) {
    if (isPrimitiveNumber(type)) {
      String fmt = "primitive types not supported *** call ClassMethods.box to convert %s to %s";
      String c0 = type.getName();
      String c1 = box(type).getSimpleName();
      return new TypeConversionException(obj, type, fmt, c0, c1);
    } else if (isSubtype(type, Number.class)) {
      String c0 = type.getSimpleName();
      return new TypeConversionException(obj, type, "%s not supported", c0);
    }
    String msg = "target type must be subclass of Number";
    return new TypeConversionException(obj, type, msg);
  }

}