diff --git a/JSONArray.java b/JSONArray.java index 36e7cad..7965f3c 100644 --- a/JSONArray.java +++ b/JSONArray.java @@ -244,7 +244,50 @@ public class JSONArray implements Iterable { return object instanceof Number ? ((Number) object).doubleValue() : Double.parseDouble((String) object); } catch (Exception e) { - throw new JSONException("JSONArray[" + index + "] is not a number."); + throw new JSONException("JSONArray[" + index + "] is not a number.", e); + } + } + + /** + * Get the float value associated with a key. + * + * @param index + * The index must be between 0 and length() - 1. + * @return The numeric value. + * @throws JSONException + * if the key is not found or if the value is not a Number + * object and cannot be converted to a number. + */ + public float getFloat(int index) throws JSONException { + Object object = this.get(index); + try { + return object instanceof Number ? ((Number) object).floatValue() + : Float.parseFloat(object.toString()); + } catch (Exception e) { + throw new JSONException("JSONArray[" + index + + "] is not a number.", e); + } + } + + /** + * Get the Number value associated with a key. + * + * @param index + * The index must be between 0 and length() - 1. + * @return The numeric value. + * @throws JSONException + * if the key is not found or if the value is not a Number + * object and cannot be converted to a number. + */ + public Number getNumber(int index) throws JSONException { + Object object = this.get(index); + try { + if (object instanceof Number) { + return (Number)object; + } + return JSONObject.stringToNumber(object.toString()); + } catch (Exception e) { + throw new JSONException("JSONArray[" + index + "] is not a number.", e); } } @@ -266,9 +309,8 @@ public class JSONArray implements Iterable { // JSONException should really take a throwable argument. // If it did, I would re-implement this with the Enum.valueOf // method and place any thrown exception in the JSONException - throw new JSONException("JSONObject[" + JSONObject.quote(Integer.toString(index)) - + "] is not an enum of type " + JSONObject.quote(clazz.getSimpleName()) - + "."); + throw new JSONException("JSONArray[" + index + "] is not an enum of type " + + JSONObject.quote(clazz.getSimpleName()) + "."); } return val; } @@ -289,7 +331,7 @@ public class JSONArray implements Iterable { return new BigDecimal(object.toString()); } catch (Exception e) { throw new JSONException("JSONArray[" + index + - "] could not convert to BigDecimal."); + "] could not convert to BigDecimal.", e); } } @@ -309,7 +351,7 @@ public class JSONArray implements Iterable { return new BigInteger(object.toString()); } catch (Exception e) { throw new JSONException("JSONArray[" + index + - "] could not convert to BigInteger."); + "] could not convert to BigInteger.", e); } } @@ -328,7 +370,7 @@ public class JSONArray implements Iterable { return object instanceof Number ? ((Number) object).intValue() : Integer.parseInt((String) object); } catch (Exception e) { - throw new JSONException("JSONArray[" + index + "] is not a number."); + throw new JSONException("JSONArray[" + index + "] is not a number.", e); } } @@ -384,7 +426,7 @@ public class JSONArray implements Iterable { return object instanceof Number ? ((Number) object).longValue() : Long.parseLong((String) object); } catch (Exception e) { - throw new JSONException("JSONArray[" + index + "] is not a number."); + throw new JSONException("JSONArray[" + index + "] is not a number.", e); } } @@ -518,11 +560,63 @@ public class JSONArray implements Iterable { * @return The value. */ public double optDouble(int index, double defaultValue) { - try { - return this.getDouble(index); - } catch (Exception e) { + Object val = this.opt(index); + if (JSONObject.NULL.equals(val)) { return defaultValue; } + if (val instanceof Number){ + return ((Number) val).doubleValue(); + } + if (val instanceof String) { + try { + return Double.parseDouble((String) val); + } catch (Exception e) { + return defaultValue; + } + } + return defaultValue; + } + + /** + * Get the optional float value associated with an index. NaN is returned + * if there is no value for the index, or if the value is not a number and + * cannot be converted to a number. + * + * @param index + * The index must be between 0 and length() - 1. + * @return The value. + */ + public float optFloat(int index) { + return this.optFloat(index, Float.NaN); + } + + /** + * Get the optional float value associated with an index. The defaultValue + * is returned if there is no value for the index, or if the value is not a + * number and cannot be converted to a number. + * + * @param index + * subscript + * @param defaultValue + * The default value. + * @return The value. + */ + public float optFloat(int index, float defaultValue) { + Object val = this.opt(index); + if (JSONObject.NULL.equals(val)) { + return defaultValue; + } + if (val instanceof Number){ + return ((Number) val).floatValue(); + } + if (val instanceof String) { + try { + return Float.parseFloat((String) val); + } catch (Exception e) { + return defaultValue; + } + } + return defaultValue; } /** @@ -550,11 +644,22 @@ public class JSONArray implements Iterable { * @return The value. */ public int optInt(int index, int defaultValue) { - try { - return this.getInt(index); - } catch (Exception e) { + Object val = this.opt(index); + if (JSONObject.NULL.equals(val)) { return defaultValue; } + if (val instanceof Number){ + return ((Number) val).intValue(); + } + + if (val instanceof String) { + try { + return new BigDecimal(val.toString()).intValue(); + } catch (Exception e) { + return defaultValue; + } + } + return defaultValue; } /** @@ -615,8 +720,29 @@ public class JSONArray implements Iterable { * @return The value. */ public BigInteger optBigInteger(int index, BigInteger defaultValue) { + Object val = this.opt(index); + if (JSONObject.NULL.equals(val)) { + return defaultValue; + } + if (val instanceof BigInteger){ + return (BigInteger) val; + } + if (val instanceof BigDecimal){ + return ((BigDecimal) val).toBigInteger(); + } + if (val instanceof Double || val instanceof Float){ + return new BigDecimal(((Number) val).doubleValue()).toBigInteger(); + } + if (val instanceof Long || val instanceof Integer + || val instanceof Short || val instanceof Byte){ + return BigInteger.valueOf(((Number) val).longValue()); + } try { - return this.getBigInteger(index); + final String valStr = val.toString(); + if(JSONObject.isDecimalNotation(valStr)) { + return new BigDecimal(valStr).toBigInteger(); + } + return new BigInteger(valStr); } catch (Exception e) { return defaultValue; } @@ -634,8 +760,25 @@ public class JSONArray implements Iterable { * @return The value. */ public BigDecimal optBigDecimal(int index, BigDecimal defaultValue) { + Object val = this.opt(index); + if (JSONObject.NULL.equals(val)) { + return defaultValue; + } + if (val instanceof BigDecimal){ + return (BigDecimal) val; + } + if (val instanceof BigInteger){ + return new BigDecimal((BigInteger) val); + } + if (val instanceof Double || val instanceof Float){ + return new BigDecimal(((Number) val).doubleValue()); + } + if (val instanceof Long || val instanceof Integer + || val instanceof Short || val instanceof Byte){ + return new BigDecimal(((Number) val).longValue()); + } try { - return this.getBigDecimal(index); + return new BigDecimal(val.toString()); } catch (Exception e) { return defaultValue; } @@ -693,17 +836,73 @@ public class JSONArray implements Iterable { * @return The value. */ public long optLong(int index, long defaultValue) { - try { - return this.getLong(index); - } catch (Exception e) { + Object val = this.opt(index); + if (JSONObject.NULL.equals(val)) { return defaultValue; } + if (val instanceof Number){ + return ((Number) val).longValue(); + } + + if (val instanceof String) { + try { + return new BigDecimal(val.toString()).longValue(); + } catch (Exception e) { + return defaultValue; + } + } + return defaultValue; + } + + /** + * Get an optional {@link Number} value associated with a key, or null + * if there is no such key or if the value is not a number. If the value is a string, + * an attempt will be made to evaluate it as a number ({@link BigDecimal}). This method + * would be used in cases where type coercion of the number value is unwanted. + * + * @param index + * The index must be between 0 and length() - 1. + * @return An object which is the value. + */ + public Number optNumber(int index) { + return this.optNumber(index, null); + } + + /** + * Get an optional {@link Number} value associated with a key, or the default if there + * is no such key or if the value is not a number. If the value is a string, + * an attempt will be made to evaluate it as a number ({@link BigDecimal}). This method + * would be used in cases where type coercion of the number value is unwanted. + * + * @param index + * The index must be between 0 and length() - 1. + * @param defaultValue + * The default. + * @return An object which is the value. + */ + public Number optNumber(int index, Number defaultValue) { + Object val = this.opt(index); + if (JSONObject.NULL.equals(val)) { + return defaultValue; + } + if (val instanceof Number){ + return (Number) val; + } + + if (val instanceof String) { + try { + return JSONObject.stringToNumber((String) val); + } catch (Exception e) { + return defaultValue; + } + } + return defaultValue; } /** * Get the optional string value associated with an index. It returns an * empty string if there is no value at that index. If the value is not a - * string and is not null, then it is coverted to a string. + * string and is not null, then it is converted to a string. * * @param index * The index must be between 0 and length() - 1. diff --git a/JSONObject.java b/JSONObject.java index 512e4ae..e7db5d1 100644 --- a/JSONObject.java +++ b/JSONObject.java @@ -549,7 +549,7 @@ public class JSONObject { return new BigInteger(object.toString()); } catch (Exception e) { throw new JSONException("JSONObject[" + quote(key) - + "] could not be converted to BigInteger."); + + "] could not be converted to BigInteger.", e); } } @@ -565,11 +565,14 @@ public class JSONObject { */ public BigDecimal getBigDecimal(String key) throws JSONException { Object object = this.get(key); + if (object instanceof BigDecimal) { + return (BigDecimal)object; + } try { return new BigDecimal(object.toString()); } catch (Exception e) { throw new JSONException("JSONObject[" + quote(key) - + "] could not be converted to BigDecimal."); + + "] could not be converted to BigDecimal.", e); } } @@ -587,10 +590,54 @@ public class JSONObject { Object object = this.get(key); try { return object instanceof Number ? ((Number) object).doubleValue() - : Double.parseDouble((String) object); + : Double.parseDouble(object.toString()); } catch (Exception e) { throw new JSONException("JSONObject[" + quote(key) - + "] is not a number."); + + "] is not a number.", e); + } + } + + /** + * Get the float value associated with a key. + * + * @param key + * A key string. + * @return The numeric value. + * @throws JSONException + * if the key is not found or if the value is not a Number + * object and cannot be converted to a number. + */ + public float getFloat(String key) throws JSONException { + Object object = this.get(key); + try { + return object instanceof Number ? ((Number) object).floatValue() + : Float.parseFloat(object.toString()); + } catch (Exception e) { + throw new JSONException("JSONObject[" + quote(key) + + "] is not a number.", e); + } + } + + /** + * Get the Number value associated with a key. + * + * @param key + * A key string. + * @return The numeric value. + * @throws JSONException + * if the key is not found or if the value is not a Number + * object and cannot be converted to a number. + */ + public Number getNumber(String key) throws JSONException { + Object object = this.get(key); + try { + if (object instanceof Number) { + return (Number)object; + } + return stringToNumber(object.toString()); + } catch (Exception e) { + throw new JSONException("JSONObject[" + quote(key) + + "] is not a number.", e); } } @@ -611,7 +658,7 @@ public class JSONObject { : Integer.parseInt((String) object); } catch (Exception e) { throw new JSONException("JSONObject[" + quote(key) - + "] is not an int."); + + "] is not an int.", e); } } @@ -668,7 +715,7 @@ public class JSONObject { : Long.parseLong((String) object); } catch (Exception e) { throw new JSONException("JSONObject[" + quote(key) - + "] is not a long."); + + "] is not a long.", e); } } @@ -687,7 +734,7 @@ public class JSONObject { int i = 0; while (iterator.hasNext()) { names[i] = iterator.next(); - i += 1; + i++; } return names; } @@ -942,13 +989,104 @@ public class JSONObject { * @return The truth. */ public boolean optBoolean(String key, boolean defaultValue) { + Object val = this.opt(key); + if (NULL.equals(val)) { + return defaultValue; + } + if (val instanceof Boolean){ + return ((Boolean) val).booleanValue(); + } try { + // we'll use the get anyway because it does string conversion. return this.getBoolean(key); } catch (Exception e) { return defaultValue; } } + /** + * Get an optional BigDecimal associated with a key, or the defaultValue if + * there is no such key or if its value is not a number. If the value is a + * string, an attempt will be made to evaluate it as a number. + * + * @param key + * A key string. + * @param defaultValue + * The default. + * @return An object which is the value. + */ + public BigDecimal optBigDecimal(String key, BigDecimal defaultValue) { + Object val = this.opt(key); + if (NULL.equals(val)) { + return defaultValue; + } + if (val instanceof BigDecimal){ + return (BigDecimal) val; + } + if (val instanceof BigInteger){ + return new BigDecimal((BigInteger) val); + } + if (val instanceof Double || val instanceof Float){ + return new BigDecimal(((Number) val).doubleValue()); + } + if (val instanceof Long || val instanceof Integer + || val instanceof Short || val instanceof Byte){ + return new BigDecimal(((Number) val).longValue()); + } + // don't check if it's a string in case of unchecked Number subclasses + try { + return new BigDecimal(val.toString()); + } catch (Exception e) { + return defaultValue; + } + } + + /** + * Get an optional BigInteger associated with a key, or the defaultValue if + * there is no such key or if its value is not a number. If the value is a + * string, an attempt will be made to evaluate it as a number. + * + * @param key + * A key string. + * @param defaultValue + * The default. + * @return An object which is the value. + */ + public BigInteger optBigInteger(String key, BigInteger defaultValue) { + Object val = this.opt(key); + if (NULL.equals(val)) { + return defaultValue; + } + if (val instanceof BigInteger){ + return (BigInteger) val; + } + if (val instanceof BigDecimal){ + return ((BigDecimal) val).toBigInteger(); + } + if (val instanceof Double || val instanceof Float){ + return new BigDecimal(((Number) val).doubleValue()).toBigInteger(); + } + if (val instanceof Long || val instanceof Integer + || val instanceof Short || val instanceof Byte){ + return BigInteger.valueOf(((Number) val).longValue()); + } + // don't check if it's a string in case of unchecked Number subclasses + try { + // the other opt functions handle implicit conversions, i.e. + // jo.put("double",1.1d); + // jo.optInt("double"); -- will return 1, not an error + // this conversion to BigDecimal then to BigInteger is to maintain + // that type cast support that may truncate the decimal. + final String valStr = val.toString(); + if(isDecimalNotation(valStr)) { + return new BigDecimal(valStr).toBigInteger(); + } + return new BigInteger(valStr); + } catch (Exception e) { + return defaultValue; + } + } + /** * Get an optional double associated with a key, or NaN if there is no such * key or if its value is not a number. If the value is a string, an attempt @@ -962,44 +1100,6 @@ public class JSONObject { return this.optDouble(key, Double.NaN); } - /** - * Get an optional BigInteger associated with a key, or the defaultValue if - * there is no such key or if its value is not a number. If the value is a - * string, an attempt will be made to evaluate it as a number. - * - * @param key - * A key string. - * @param defaultValue - * The default. - * @return An object which is the value. - */ - public BigInteger optBigInteger(String key, BigInteger defaultValue) { - try { - return this.getBigInteger(key); - } catch (Exception e) { - return defaultValue; - } - } - - /** - * Get an optional BigDecimal associated with a key, or the defaultValue if - * there is no such key or if its value is not a number. If the value is a - * string, an attempt will be made to evaluate it as a number. - * - * @param key - * A key string. - * @param defaultValue - * The default. - * @return An object which is the value. - */ - public BigDecimal optBigDecimal(String key, BigDecimal defaultValue) { - try { - return this.getBigDecimal(key); - } catch (Exception e) { - return defaultValue; - } - } - /** * Get an optional double associated with a key, or the defaultValue if * there is no such key or if its value is not a number. If the value is a @@ -1012,11 +1112,63 @@ public class JSONObject { * @return An object which is the value. */ public double optDouble(String key, double defaultValue) { - try { - return this.getDouble(key); - } catch (Exception e) { + Object val = this.opt(key); + if (NULL.equals(val)) { return defaultValue; } + if (val instanceof Number){ + return ((Number) val).doubleValue(); + } + if (val instanceof String) { + try { + return Double.parseDouble((String) val); + } catch (Exception e) { + return defaultValue; + } + } + return defaultValue; + } + + /** + * Get the optional double value associated with an index. NaN is returned + * if there is no value for the index, or if the value is not a number and + * cannot be converted to a number. + * + * @param key + * A key string. + * @return The value. + */ + public float optFloat(String key) { + return this.optFloat(key, Float.NaN); + } + + /** + * Get the optional double value associated with an index. The defaultValue + * is returned if there is no value for the index, or if the value is not a + * number and cannot be converted to a number. + * + * @param key + * A key string. + * @param defaultValue + * The default value. + * @return The value. + */ + public float optFloat(String key, float defaultValue) { + Object val = this.opt(key); + if (JSONObject.NULL.equals(val)) { + return defaultValue; + } + if (val instanceof Number){ + return ((Number) val).floatValue(); + } + if (val instanceof String) { + try { + return Float.parseFloat((String) val); + } catch (Exception e) { + return defaultValue; + } + } + return defaultValue; } /** @@ -1044,11 +1196,22 @@ public class JSONObject { * @return An object which is the value. */ public int optInt(String key, int defaultValue) { - try { - return this.getInt(key); - } catch (Exception e) { + Object val = this.opt(key); + if (NULL.equals(val)) { return defaultValue; } + if (val instanceof Number){ + return ((Number) val).intValue(); + } + + if (val instanceof String) { + try { + return new BigDecimal((String) val).intValue(); + } catch (Exception e) { + return defaultValue; + } + } + return defaultValue; } /** @@ -1102,13 +1265,69 @@ public class JSONObject { * @return An object which is the value. */ public long optLong(String key, long defaultValue) { - try { - return this.getLong(key); - } catch (Exception e) { + Object val = this.opt(key); + if (NULL.equals(val)) { return defaultValue; } + if (val instanceof Number){ + return ((Number) val).longValue(); + } + + if (val instanceof String) { + try { + return new BigDecimal((String) val).longValue(); + } catch (Exception e) { + return defaultValue; + } + } + return defaultValue; + } + + /** + * Get an optional {@link Number} value associated with a key, or null + * if there is no such key or if the value is not a number. If the value is a string, + * an attempt will be made to evaluate it as a number ({@link BigDecimal}). This method + * would be used in cases where type coercion of the number value is unwanted. + * + * @param key + * A key string. + * @return An object which is the value. + */ + public Number optNumber(String key) { + return this.optNumber(key, null); } + /** + * Get an optional {@link Number} value associated with a key, or the default if there + * is no such key or if the value is not a number. If the value is a string, + * an attempt will be made to evaluate it as a number. This method + * would be used in cases where type coercion of the number value is unwanted. + * + * @param key + * A key string. + * @param defaultValue + * The default. + * @return An object which is the value. + */ + public Number optNumber(String key, Number defaultValue) { + Object val = this.opt(key); + if (NULL.equals(val)) { + return defaultValue; + } + if (val instanceof Number){ + return (Number) val; + } + + if (val instanceof String) { + try { + return stringToNumber((String) val); + } catch (Exception e) { + return defaultValue; + } + } + return defaultValue; + } + /** * Get an optional string associated with a key. It returns an empty string * if there is no such key. If the value is not a string and is not null, @@ -1569,6 +1788,81 @@ public class JSONObject { return false; } } + + /** + * Tests if the value should be tried as a decimal. It makes no test if there are actual digits. + * + * @param val value to test + * @return true if the string is "-0" or if it contains '.', 'e', or 'E', false otherwise. + */ + protected static boolean isDecimalNotation(final String val) { + return val.indexOf('.') > -1 || val.indexOf('e') > -1 + || val.indexOf('E') > -1 || "-0".equals(val); + } + + /** + * Converts a string to a number using the narrowest possible type. Possible + * returns for this function are BigDecimal, Double, BigInteger, Long, and Integer. + * When a Double is returned, it should always be a valid Double and not NaN or +-infinity. + * + * @param val value to convert + * @return Number representation of the value. + * @throws NumberFormatException thrown if the value is not a valid number. A public + * caller should catch this and wrap it in a {@link JSONException} if applicable. + */ + protected static Number stringToNumber(final String val) throws NumberFormatException { + char initial = val.charAt(0); + if ((initial >= '0' && initial <= '9') || initial == '-') { + // decimal representation + if (isDecimalNotation(val)) { + // quick dirty way to see if we need a BigDecimal instead of a Double + // this only handles some cases of overflow or underflow + if (val.length()>14) { + return new BigDecimal(val); + } + final Double d = Double.valueOf(val); + if (d.isInfinite() || d.isNaN()) { + // if we can't parse it as a double, go up to BigDecimal + // this is probably due to underflow like 4.32e-678 + // or overflow like 4.65e5324. The size of the string is small + // but can't be held in a Double. + return new BigDecimal(val); + } + return d; + } + // integer representation. + // This will narrow any values to the smallest reasonable Object representation + // (Integer, Long, or BigInteger) + + // string version + // The compare string length method reduces GC, + // but leads to smaller integers being placed in larger wrappers even though not + // needed. i.e. 1,000,000,000 -> Long even though it's an Integer + // 1,000,000,000,000,000,000 -> BigInteger even though it's a Long + //if(val.length()<=9){ + // return Integer.valueOf(val); + //} + //if(val.length()<=18){ + // return Long.valueOf(val); + //} + //return new BigInteger(val); + + // BigInteger version: We use a similar bitLenth compare as + // BigInteger#intValueExact uses. Increases GC, but objects hold + // only what they need. i.e. Less runtime overhead if the value is + // long lived. Which is the better tradeoff? This is closer to what's + // in stringToValue. + BigInteger bi = new BigInteger(val); + if(bi.bitLength()<=31){ + return Integer.valueOf(bi.intValue()); + } + if(bi.bitLength()<=63){ + return Long.valueOf(bi.longValue()); + } + return bi; + } + throw new NumberFormatException("val ["+val+"] is not a valid number."); + } /** * Try to convert a string into a number, boolean, or null. If the string @@ -1600,15 +1894,15 @@ public class JSONObject { char initial = string.charAt(0); if ((initial >= '0' && initial <= '9') || initial == '-') { try { - if (string.indexOf('.') > -1 || string.indexOf('e') > -1 - || string.indexOf('E') > -1 - || "-0".equals(string)) { + // if we want full Big Number support this block can be replaced with: + // return stringToNumber(string); + if (isDecimalNotation(string)) { Double d = Double.valueOf(string); if (!d.isInfinite() && !d.isNaN()) { return d; } } else { - Long myLong = new Long(string); + Long myLong = Long.valueOf(string); if (string.equals(myLong.toString())) { if (myLong.longValue() == myLong.intValue()) { return Integer.valueOf(myLong.intValue());