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