/* Copyright 2013 Jonatan Jönsson * * Licensed 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 se.softhouse.common.numbers; import static com.google.common.base.Preconditions.checkNotNull; import static se.softhouse.common.strings.Describables.illegalArgument; import static se.softhouse.common.strings.Describers.numberDescriber; import static se.softhouse.common.strings.StringsUtil.NEWLINE; import static se.softhouse.common.strings.StringsUtil.pointingAtIndex; import java.math.BigDecimal; import java.math.BigInteger; import java.text.DecimalFormat; import java.text.DecimalFormatSymbols; import java.text.NumberFormat; import java.text.ParsePosition; import java.util.List; import java.util.Locale; import javax.annotation.CheckReturnValue; import javax.annotation.Nonnull; import javax.annotation.concurrent.Immutable; import se.softhouse.common.strings.Describable; import se.softhouse.common.strings.Describables; import com.google.common.annotations.VisibleForTesting; import com.google.common.collect.ImmutableList; import com.google.common.primitives.Longs; /** * A class that exposes static fields (and functions), such as {@link Integer#MAX_VALUE} and * {@link Integer#MIN_VALUE}, for subclasses of {@link Number} in an object oriented way.<br> * <b>Note:</b> As {@link Double} and {@link Float} are very hard to use <a * href="http://www.ibm.com/developerworks/java/library/j-jtp0114/">right</a>, their counterparts * are not available here. * * @param <T> the subclass of {@link Number} */ @Immutable public abstract class NumberType<T extends Number> { /** * Only allow classes in this package to inherit, for now */ NumberType() { } /** * Exposes static fields/methods in {@link Byte} in a {@link NumberType} */ public static final NumberType<Byte> BYTE = new ByteType(); /** * Exposes static fields/methods in {@link Short} in a {@link NumberType} */ public static final NumberType<Short> SHORT = new ShortType(); /** * Exposes static fields/methods in {@link Integer} in a {@link NumberType} */ public static final NumberType<Integer> INTEGER = new IntegerType(); /** * Exposes static fields/methods in {@link Long} in a {@link NumberType} */ public static final NumberType<Long> LONG = new LongType(); /** * Exposes static fields/methods in {@link BigInteger} in a {@link NumberType} */ public static final UnlimitedNumberType<BigInteger> BIG_INTEGER = new BigIntegerType(); /** * Exposes static fields/methods in {@link BigDecimal} in a {@link NumberType} */ public static final UnlimitedNumberType<BigDecimal> BIG_DECIMAL = new BigDecimalType(); /** * An ordered (by <a * href="http://docs.oracle.com/javase/tutorial/java/nutsandbolts/datatypes.html">data size</a>) * {@link List} of {@link NumberType}s */ public static final ImmutableList<NumberType<?>> TYPES = ImmutableList.<NumberType<?>>of(BYTE, SHORT, INTEGER, LONG, BIG_INTEGER, BIG_DECIMAL); /** * {@link NumberType}s that doesn't have any {@link #minValue()} or {@link #maxValue()} */ public static final ImmutableList<NumberType<?>> UNLIMITED_TYPES = ImmutableList.<NumberType<?>>of(BIG_INTEGER, BIG_DECIMAL); /** * <pre> * Parameters * 1st %s = the received value * 2nd %s = the minimum allowed value * 3rd %s = the maximum allowed value */ @VisibleForTesting static final String OUT_OF_RANGE = "'%s' is not in the range %s to %s"; /** * @return the static {@code MIN_VALUE} field of {@code T} * @throws UnsupportedOperationException if there isn't such a field for this type */ @CheckReturnValue @Nonnull public abstract T minValue(); /** * @return the static {@code MAX_VALUE} field of {@code T} * @throws UnsupportedOperationException if there isn't such a field for this type */ @CheckReturnValue @Nonnull public abstract T maxValue(); /** * @return a human readable string describing the type {@code T} (in lower case) */ @CheckReturnValue @Nonnull public abstract String name(); /** * @return zero as a {@code T} type */ @CheckReturnValue @Nonnull public final T defaultValue() { return from(0L); } /** * Casts {@code value} to a {@code T}, losing bits as necessary. */ @CheckReturnValue @Nonnull public abstract T from(Number value); /** * Returns <code>true</code> if {@code number} can be represented by this type without losing * any numeric information */ public boolean inRange(Number number) { Long value = number.longValue(); return value >= minValue().longValue() && value <= maxValue().longValue(); } /** * <pre> * Converts {@code value} into a {@link Number} of the type {@code T} in a {@link Locale} * sensitive way by using {@link NumberFormat}. * * For instance: * {@code Integer fortyTwo = NumberType.INTEGER.parse("42", Locale.US);} * * @throws IllegalArgumentException if the value isn't convertable to a number of type {@code T} * </pre> */ @CheckReturnValue @Nonnull public final T parse(String input, Locale inLocale) { ParsePosition parsePosition = new ParsePosition(0); // TODO(jontejj): maybe remove spaces here? refer to // http://bugs.sun.com/view_bug.do?bug_id=4510618 Number result = parser(inLocale).parse(input, parsePosition); // Make sure the whole string was parsed if(parsePosition.getIndex() != input.length() || result == null) throw illegalArgument(formatError(input, inLocale, parsePosition)); throwForOutOfRange(input, result, inLocale); return from(result); } void throwForOutOfRange(String input, Number result, Locale inLocale) throws IllegalArgumentException { if(!inRange(result)) throw illegalArgument(Describables.format(OUT_OF_RANGE, input, format(minValue(), inLocale), format(maxValue(), inLocale))); } /** * <pre> * Converts {@code value} into a {@link Number} of the type {@code T} assuming {@code value} * is expressed in a {@link Locale#US} format. * * For instance: * {@code Integer fortyTwo = NumberType.INTEGER.parse("42");} * * @throws IllegalArgumentException if the value isn't convertable to a number of type {@code T} * </pre> */ @CheckReturnValue @Nonnull public final T parse(String value) { return parse(value, Locale.US); } NumberFormat parser(Locale inLocale) { NumberFormat formatter = NumberFormat.getInstance(inLocale); formatter.setParseIntegerOnly(true); return formatter; } private static final String TEMPLATE = "'%s' is not a valid %s (Localization: %s)" + NEWLINE + " %s"; Describable formatError(Object invalidValue, Locale locale, ParsePosition positionForInvalidCharacter) { String localeInformation = locale.getDisplayName(locale); return Describables.format(TEMPLATE, invalidValue, name(), localeInformation, pointingAtIndex(positionForInvalidCharacter.getIndex())); } /** * Returns a descriptive string of the range this {@link NumberType} can {@link #parse(String)} * * @param inLocale the locale to format numbers with */ @CheckReturnValue @Nonnull public String descriptionOfValidValues(Locale inLocale) { return format(minValue(), inLocale) + " to " + format(maxValue(), inLocale); } private String format(T value, Locale inLocale) { return numberDescriber().describe(value, inLocale); } /** * @return {@link #name()} */ @Override public String toString() { return name(); } private static final class ByteType extends NumberType<Byte> { @Override public Byte minValue() { return Byte.MIN_VALUE; } @Override public Byte maxValue() { return Byte.MAX_VALUE; } @Override public Byte from(Number value) { return value.byteValue(); } @Override public String name() { return "byte"; } } private static final class IntegerType extends NumberType<Integer> { @Override public Integer minValue() { return Integer.MIN_VALUE; } @Override public Integer maxValue() { return Integer.MAX_VALUE; } @Override public Integer from(Number value) { return value.intValue(); } @Override public String name() { return "integer"; } } private static final class ShortType extends NumberType<Short> { @Override public Short minValue() { return Short.MIN_VALUE; } @Override public Short maxValue() { return Short.MAX_VALUE; } @Override public Short from(Number value) { return value.shortValue(); } @Override public String name() { return "short"; } } private static final class LongType extends NumberType<Long> { @Override public Long minValue() { return Long.MIN_VALUE; } @Override public Long maxValue() { return Long.MAX_VALUE; } @Override public Long from(Number value) { return value.longValue(); } @Override public String name() { return "long"; } @Override public boolean inRange(Number number) { return number instanceof Long || Longs.tryParse(number.toString()) != null; } } /** * A {@link NumberType} that doesn't have any {@link #minValue()} or {@link #maxValue()}. * * @param <T> the type of {@link Number} that's unlimited, {@link BigDecimal} for instance. */ @SuppressWarnings("javadoc") public abstract static class UnlimitedNumberType<T extends Number> extends NumberType<T> { UnlimitedNumberType() { } /** * @deprecated an unlimited number doesn't have any minimum value */ @Deprecated @Override public T minValue() { throw new UnsupportedOperationException(name() + " doesn't have any minValue"); } /** * @deprecated an unlimited number doesn't have any maximum value */ @Deprecated @Override public T maxValue() { throw new UnsupportedOperationException(name() + " doesn't have any maxValue"); } @Override NumberFormat parser(Locale inLocale) { DecimalFormat parser = new DecimalFormat("", new DecimalFormatSymbols(inLocale)); parser.setParseBigDecimal(true); return parser; } @Override public abstract String descriptionOfValidValues(Locale inLocale); } private static final class BigIntegerType extends UnlimitedNumberType<BigInteger> { @Override public String name() { return "big-integer"; } @Override public BigInteger from(Number value) { if(value instanceof BigInteger) return (BigInteger) value; else if(value instanceof BigDecimal) return ((BigDecimal) value).toBigInteger(); return BigInteger.valueOf(value.longValue()); } @Override public boolean inRange(Number number) { checkNotNull(number); if(number instanceof BigDecimal) return ((BigDecimal) number).scale() <= 0; return true; } @Override void throwForOutOfRange(String input, Number result, Locale inLocale) throws IllegalArgumentException { if(result instanceof BigDecimal) { boolean hasDecimals = ((BigDecimal) result).scale() > 0; if(hasDecimals) { int decimalPosition = input.indexOf('.'); throw illegalArgument(formatError(input, inLocale, new ParsePosition(decimalPosition))); } } } @Override public String descriptionOfValidValues(Locale inLocale) { checkNotNull(inLocale); return "an arbitrary integer number (practically no limits)"; } } private static final class BigDecimalType extends UnlimitedNumberType<BigDecimal> { @Override public String name() { return "big-decimal"; } @Override public boolean inRange(Number number) { checkNotNull(number); return true; } @Override public BigDecimal from(Number value) { if(value instanceof BigDecimal) return (BigDecimal) value; else if(value instanceof BigInteger) return new BigDecimal((BigInteger) value); return BigDecimal.valueOf(value.doubleValue()); } @Override public String descriptionOfValidValues(Locale inLocale) { checkNotNull(inLocale); return "an arbitrary decimal number (practically no limits)"; } } }