001/*
002 * Copyright 2002-2016 the original author or authors.
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 *      https://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016
017package org.springframework.util;
018
019import java.math.BigDecimal;
020import java.math.BigInteger;
021import java.text.DecimalFormat;
022import java.text.NumberFormat;
023import java.text.ParseException;
024import java.util.Collections;
025import java.util.HashSet;
026import java.util.Set;
027
028/**
029 * Miscellaneous utility methods for number conversion and parsing.
030 * <p>Mainly for internal use within the framework; consider Apache's
031 * Commons Lang for a more comprehensive suite of number utilities.
032 *
033 * @author Juergen Hoeller
034 * @author Rob Harrop
035 * @since 1.1.2
036 */
037public abstract class NumberUtils {
038
039        private static final BigInteger LONG_MIN = BigInteger.valueOf(Long.MIN_VALUE);
040
041        private static final BigInteger LONG_MAX = BigInteger.valueOf(Long.MAX_VALUE);
042
043        /**
044         * Standard number types (all immutable):
045         * Byte, Short, Integer, Long, BigInteger, Float, Double, BigDecimal.
046         */
047        public static final Set<Class<?>> STANDARD_NUMBER_TYPES;
048
049        static {
050                Set<Class<?>> numberTypes = new HashSet<Class<?>>(8);
051                numberTypes.add(Byte.class);
052                numberTypes.add(Short.class);
053                numberTypes.add(Integer.class);
054                numberTypes.add(Long.class);
055                numberTypes.add(BigInteger.class);
056                numberTypes.add(Float.class);
057                numberTypes.add(Double.class);
058                numberTypes.add(BigDecimal.class);
059                STANDARD_NUMBER_TYPES = Collections.unmodifiableSet(numberTypes);
060        }
061
062
063        /**
064         * Convert the given number into an instance of the given target class.
065         * @param number the number to convert
066         * @param targetClass the target class to convert to
067         * @return the converted number
068         * @throws IllegalArgumentException if the target class is not supported
069         * (i.e. not a standard Number subclass as included in the JDK)
070         * @see java.lang.Byte
071         * @see java.lang.Short
072         * @see java.lang.Integer
073         * @see java.lang.Long
074         * @see java.math.BigInteger
075         * @see java.lang.Float
076         * @see java.lang.Double
077         * @see java.math.BigDecimal
078         */
079        @SuppressWarnings("unchecked")
080        public static <T extends Number> T convertNumberToTargetClass(Number number, Class<T> targetClass)
081                        throws IllegalArgumentException {
082
083                Assert.notNull(number, "Number must not be null");
084                Assert.notNull(targetClass, "Target class must not be null");
085
086                if (targetClass.isInstance(number)) {
087                        return (T) number;
088                }
089                else if (Byte.class == targetClass) {
090                        long value = checkedLongValue(number, targetClass);
091                        if (value < Byte.MIN_VALUE || value > Byte.MAX_VALUE) {
092                                raiseOverflowException(number, targetClass);
093                        }
094                        return (T) Byte.valueOf(number.byteValue());
095                }
096                else if (Short.class == targetClass) {
097                        long value = checkedLongValue(number, targetClass);
098                        if (value < Short.MIN_VALUE || value > Short.MAX_VALUE) {
099                                raiseOverflowException(number, targetClass);
100                        }
101                        return (T) Short.valueOf(number.shortValue());
102                }
103                else if (Integer.class == targetClass) {
104                        long value = checkedLongValue(number, targetClass);
105                        if (value < Integer.MIN_VALUE || value > Integer.MAX_VALUE) {
106                                raiseOverflowException(number, targetClass);
107                        }
108                        return (T) Integer.valueOf(number.intValue());
109                }
110                else if (Long.class == targetClass) {
111                        long value = checkedLongValue(number, targetClass);
112                        return (T) Long.valueOf(value);
113                }
114                else if (BigInteger.class == targetClass) {
115                        if (number instanceof BigDecimal) {
116                                // do not lose precision - use BigDecimal's own conversion
117                                return (T) ((BigDecimal) number).toBigInteger();
118                        }
119                        else {
120                                // original value is not a Big* number - use standard long conversion
121                                return (T) BigInteger.valueOf(number.longValue());
122                        }
123                }
124                else if (Float.class == targetClass) {
125                        return (T) Float.valueOf(number.floatValue());
126                }
127                else if (Double.class == targetClass) {
128                        return (T) Double.valueOf(number.doubleValue());
129                }
130                else if (BigDecimal.class == targetClass) {
131                        // always use BigDecimal(String) here to avoid unpredictability of BigDecimal(double)
132                        // (see BigDecimal javadoc for details)
133                        return (T) new BigDecimal(number.toString());
134                }
135                else {
136                        throw new IllegalArgumentException("Could not convert number [" + number + "] of type [" +
137                                        number.getClass().getName() + "] to unsupported target class [" + targetClass.getName() + "]");
138                }
139        }
140
141        /**
142         * Check for a {@code BigInteger}/{@code BigDecimal} long overflow
143         * before returning the given number as a long value.
144         * @param number the number to convert
145         * @param targetClass the target class to convert to
146         * @return the long value, if convertible without overflow
147         * @throws IllegalArgumentException if there is an overflow
148         * @see #raiseOverflowException
149         */
150        private static long checkedLongValue(Number number, Class<? extends Number> targetClass) {
151                BigInteger bigInt = null;
152                if (number instanceof BigInteger) {
153                        bigInt = (BigInteger) number;
154                }
155                else if (number instanceof BigDecimal) {
156                        bigInt = ((BigDecimal) number).toBigInteger();
157                }
158                // Effectively analogous to JDK 8's BigInteger.longValueExact()
159                if (bigInt != null && (bigInt.compareTo(LONG_MIN) < 0 || bigInt.compareTo(LONG_MAX) > 0)) {
160                        raiseOverflowException(number, targetClass);
161                }
162                return number.longValue();
163        }
164
165        /**
166         * Raise an <em>overflow</em> exception for the given number and target class.
167         * @param number the number we tried to convert
168         * @param targetClass the target class we tried to convert to
169         * @throws IllegalArgumentException if there is an overflow
170         */
171        private static void raiseOverflowException(Number number, Class<?> targetClass) {
172                throw new IllegalArgumentException("Could not convert number [" + number + "] of type [" +
173                                number.getClass().getName() + "] to target class [" + targetClass.getName() + "]: overflow");
174        }
175
176        /**
177         * Parse the given {@code text} into a {@link Number} instance of the given
178         * target class, using the corresponding {@code decode} / {@code valueOf} method.
179         * <p>Trims the input {@code String} before attempting to parse the number.
180         * <p>Supports numbers in hex format (with leading "0x", "0X", or "#") as well.
181         * @param text the text to convert
182         * @param targetClass the target class to parse into
183         * @return the parsed number
184         * @throws IllegalArgumentException if the target class is not supported
185         * (i.e. not a standard Number subclass as included in the JDK)
186         * @see Byte#decode
187         * @see Short#decode
188         * @see Integer#decode
189         * @see Long#decode
190         * @see #decodeBigInteger(String)
191         * @see Float#valueOf
192         * @see Double#valueOf
193         * @see java.math.BigDecimal#BigDecimal(String)
194         */
195        @SuppressWarnings("unchecked")
196        public static <T extends Number> T parseNumber(String text, Class<T> targetClass) {
197                Assert.notNull(text, "Text must not be null");
198                Assert.notNull(targetClass, "Target class must not be null");
199                String trimmed = StringUtils.trimAllWhitespace(text);
200
201                if (Byte.class == targetClass) {
202                        return (T) (isHexNumber(trimmed) ? Byte.decode(trimmed) : Byte.valueOf(trimmed));
203                }
204                else if (Short.class == targetClass) {
205                        return (T) (isHexNumber(trimmed) ? Short.decode(trimmed) : Short.valueOf(trimmed));
206                }
207                else if (Integer.class == targetClass) {
208                        return (T) (isHexNumber(trimmed) ? Integer.decode(trimmed) : Integer.valueOf(trimmed));
209                }
210                else if (Long.class == targetClass) {
211                        return (T) (isHexNumber(trimmed) ? Long.decode(trimmed) : Long.valueOf(trimmed));
212                }
213                else if (BigInteger.class == targetClass) {
214                        return (T) (isHexNumber(trimmed) ? decodeBigInteger(trimmed) : new BigInteger(trimmed));
215                }
216                else if (Float.class == targetClass) {
217                        return (T) Float.valueOf(trimmed);
218                }
219                else if (Double.class == targetClass) {
220                        return (T) Double.valueOf(trimmed);
221                }
222                else if (BigDecimal.class == targetClass || Number.class == targetClass) {
223                        return (T) new BigDecimal(trimmed);
224                }
225                else {
226                        throw new IllegalArgumentException(
227                                        "Cannot convert String [" + text + "] to target class [" + targetClass.getName() + "]");
228                }
229        }
230
231        /**
232         * Parse the given {@code text} into a {@link Number} instance of the
233         * given target class, using the supplied {@link NumberFormat}.
234         * <p>Trims the input {@code String} before attempting to parse the number.
235         * @param text the text to convert
236         * @param targetClass the target class to parse into
237         * @param numberFormat the {@code NumberFormat} to use for parsing (if
238         * {@code null}, this method falls back to {@link #parseNumber(String, Class)})
239         * @return the parsed number
240         * @throws IllegalArgumentException if the target class is not supported
241         * (i.e. not a standard Number subclass as included in the JDK)
242         * @see java.text.NumberFormat#parse
243         * @see #convertNumberToTargetClass
244         * @see #parseNumber(String, Class)
245         */
246        public static <T extends Number> T parseNumber(String text, Class<T> targetClass, NumberFormat numberFormat) {
247                if (numberFormat != null) {
248                        Assert.notNull(text, "Text must not be null");
249                        Assert.notNull(targetClass, "Target class must not be null");
250                        DecimalFormat decimalFormat = null;
251                        boolean resetBigDecimal = false;
252                        if (numberFormat instanceof DecimalFormat) {
253                                decimalFormat = (DecimalFormat) numberFormat;
254                                if (BigDecimal.class == targetClass && !decimalFormat.isParseBigDecimal()) {
255                                        decimalFormat.setParseBigDecimal(true);
256                                        resetBigDecimal = true;
257                                }
258                        }
259                        try {
260                                Number number = numberFormat.parse(StringUtils.trimAllWhitespace(text));
261                                return convertNumberToTargetClass(number, targetClass);
262                        }
263                        catch (ParseException ex) {
264                                throw new IllegalArgumentException("Could not parse number: " + ex.getMessage());
265                        }
266                        finally {
267                                if (resetBigDecimal) {
268                                        decimalFormat.setParseBigDecimal(false);
269                                }
270                        }
271                }
272                else {
273                        return parseNumber(text, targetClass);
274                }
275        }
276
277        /**
278         * Determine whether the given {@code value} String indicates a hex number,
279         * i.e. needs to be passed into {@code Integer.decode} instead of
280         * {@code Integer.valueOf}, etc.
281         */
282        private static boolean isHexNumber(String value) {
283                int index = (value.startsWith("-") ? 1 : 0);
284                return (value.startsWith("0x", index) || value.startsWith("0X", index) || value.startsWith("#", index));
285        }
286
287        /**
288         * Decode a {@link java.math.BigInteger} from the supplied {@link String} value.
289         * <p>Supports decimal, hex, and octal notation.
290         * @see BigInteger#BigInteger(String, int)
291         */
292        private static BigInteger decodeBigInteger(String value) {
293                int radix = 10;
294                int index = 0;
295                boolean negative = false;
296
297                // Handle minus sign, if present.
298                if (value.startsWith("-")) {
299                        negative = true;
300                        index++;
301                }
302
303                // Handle radix specifier, if present.
304                if (value.startsWith("0x", index) || value.startsWith("0X", index)) {
305                        index += 2;
306                        radix = 16;
307                }
308                else if (value.startsWith("#", index)) {
309                        index++;
310                        radix = 16;
311                }
312                else if (value.startsWith("0", index) && value.length() > 1 + index) {
313                        index++;
314                        radix = 8;
315                }
316
317                BigInteger result = new BigInteger(value.substring(index), radix);
318                return (negative ? result.negate() : result);
319        }
320
321}