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}