From a8d4e4734fe499a832527e333473daad61416f8c Mon Sep 17 00:00:00 2001 From: "John J. Aylward" Date: Tue, 16 May 2017 19:38:01 -0400 Subject: [PATCH 01/11] adjustments to opt methods in reference to https://github.com/stleary/JSON-java/issues/334 --- JSONObject.java | 113 ++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 94 insertions(+), 19 deletions(-) diff --git a/JSONObject.java b/JSONObject.java index b71b3b618..ad08c7970 100644 --- a/JSONObject.java +++ b/JSONObject.java @@ -540,7 +540,7 @@ public BigInteger getBigInteger(String key) throws JSONException { 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); } } @@ -556,11 +556,14 @@ public BigInteger getBigInteger(String key) throws JSONException { */ 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); } } @@ -578,10 +581,10 @@ public double getDouble(String key) throws JSONException { Object object = this.get(key); try { return object instanceof Number ? ((Number) object).doubleValue() - : Double.parseDouble((String) object); + : new BigDecimal((String) object).doubleValue(); } catch (Exception e) { throw new JSONException("JSONObject[" + quote(key) - + "] is not a number."); + + "] is not a number.", e); } } @@ -602,7 +605,7 @@ public int getInt(String key) throws JSONException { : Integer.parseInt((String) object); } catch (Exception e) { throw new JSONException("JSONObject[" + quote(key) - + "] is not an int."); + + "] is not an int.", e); } } @@ -659,7 +662,7 @@ public long getLong(String key) throws JSONException { : Long.parseLong((String) object); } catch (Exception e) { throw new JSONException("JSONObject[" + quote(key) - + "] is not a long."); + + "] is not a long.", e); } } @@ -678,7 +681,7 @@ public static String[] getNames(JSONObject jo) { int i = 0; while (iterator.hasNext()) { names[i] = iterator.next(); - i += 1; + i++; } return names; } @@ -933,7 +936,15 @@ public boolean optBoolean(String key) { * @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; @@ -965,8 +976,23 @@ public double optDouble(String key) { * @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(); + } try { - return this.getBigInteger(key); + // 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. + return new BigDecimal(val.toString()).toBigInteger(); } catch (Exception e) { return defaultValue; } @@ -984,8 +1010,25 @@ public BigInteger optBigInteger(String key, BigInteger defaultValue) { * @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){ + return new BigDecimal(((Double) 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(key); + return new BigDecimal(val.toString()); } catch (Exception e) { return defaultValue; } @@ -1003,11 +1046,21 @@ public BigDecimal optBigDecimal(String key, BigDecimal defaultValue) { * @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 new BigDecimal((String) val).doubleValue(); + } catch (Exception e) { + return defaultValue; + } + } + return defaultValue; } /** @@ -1035,11 +1088,22 @@ public int optInt(String key) { * @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(val.toString()).intValue(); + } catch (Exception e) { + return defaultValue; + } + } + return defaultValue; } /** @@ -1093,11 +1157,22 @@ public long optLong(String key) { * @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(val.toString()).longValue(); + } catch (Exception e) { + return defaultValue; + } + } + return defaultValue; } /** @@ -1583,7 +1658,7 @@ public static Object stringToValue(String string) { 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()); From bd4b180f4e6aae77dc2e49742dc699683edc93c3 Mon Sep 17 00:00:00 2001 From: "John J. Aylward" Date: Wed, 17 May 2017 10:51:06 -0400 Subject: [PATCH 02/11] Support for float to BigDecimal in optBigDecimal --- JSONObject.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/JSONObject.java b/JSONObject.java index ad08c7970..055788212 100644 --- a/JSONObject.java +++ b/JSONObject.java @@ -1020,8 +1020,8 @@ public BigDecimal optBigDecimal(String key, BigDecimal defaultValue) { if (val instanceof BigInteger){ return new BigDecimal((BigInteger) val); } - if (val instanceof Double){ - return new BigDecimal(((Double) val).doubleValue()); + 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){ From c46774cf1314a0a1e640374b20006594e014e9a9 Mon Sep 17 00:00:00 2001 From: "John J. Aylward" Date: Wed, 17 May 2017 11:29:26 -0400 Subject: [PATCH 03/11] * Update opt* methods for JSONArray * Add support to JSONArray and JSONObject to optionally get raw number values * Add support to JSONArray and JSONObject to optionally get float values --- JSONArray.java | 161 +++++++++++++++++++++++++++++++++++++++++++---- JSONObject.java | 162 ++++++++++++++++++++++++++++++++++++++---------- 2 files changed, 279 insertions(+), 44 deletions(-) diff --git a/JSONArray.java b/JSONArray.java index 132d46de1..18d43ce01 100644 --- a/JSONArray.java +++ b/JSONArray.java @@ -518,11 +518,63 @@ public double optDouble(int index) { * @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 new BigDecimal((String) val).doubleValue(); + } 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 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 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 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 new BigDecimal((String) val).floatValue(); + } catch (Exception e) { + return defaultValue; + } + } + return defaultValue; } /** @@ -550,11 +602,22 @@ public int optInt(int index) { * @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 +678,25 @@ public > E optEnum(Class clazz, int index, E defaultValue) * @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); + return new BigDecimal(val.toString()).toBigInteger(); } catch (Exception e) { return defaultValue; } @@ -634,8 +714,25 @@ public BigInteger optBigInteger(int index, BigInteger defaultValue) { * @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,11 +790,53 @@ public long optLong(int index) { * @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 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 new BigDecimal(val.toString()); + } catch (Exception e) { + return defaultValue; + } + } + return defaultValue; } /** diff --git a/JSONObject.java b/JSONObject.java index 055788212..13ba48ef7 100644 --- a/JSONObject.java +++ b/JSONObject.java @@ -952,20 +952,7 @@ public boolean optBoolean(String key, boolean 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 - * will be made to evaluate it as a number. - * - * @param key - * A string which is the key. - * @return An object which is the value. - */ - public double optDouble(String key) { - return this.optDouble(key, Double.NaN); - } - - /** - * Get an optional BigInteger associated with a key, or the defaultValue if + * 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. * @@ -975,31 +962,33 @@ public double optDouble(String key) { * The default. * @return An object which is the value. */ - public BigInteger optBigInteger(String key, BigInteger defaultValue) { + 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 (BigInteger) val; + return new BigDecimal((BigInteger) val); } - if (val instanceof BigDecimal){ - return ((BigDecimal) val).toBigInteger(); + 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 { - // 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. - return new BigDecimal(val.toString()).toBigInteger(); + return new BigDecimal(val.toString()); } catch (Exception e) { return defaultValue; } } /** - * Get an optional BigDecimal associated with a key, or the defaultValue if + * 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. * @@ -1009,31 +998,49 @@ public BigInteger optBigInteger(String key, BigInteger defaultValue) { * The default. * @return An object which is the value. */ - public BigDecimal optBigDecimal(String key, BigDecimal defaultValue) { + public BigInteger optBigInteger(String key, BigInteger 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); + return (BigInteger) val; + } + if (val instanceof BigDecimal){ + return ((BigDecimal) val).toBigInteger(); } if (val instanceof Double || val instanceof Float){ - return new BigDecimal(((Number) val).doubleValue()); + return new BigDecimal(((Number) val).doubleValue()).toBigInteger(); } if (val instanceof Long || val instanceof Integer || val instanceof Short || val instanceof Byte){ - return new BigDecimal(((Number) val).longValue()); + return BigInteger.valueOf(((Number) val).longValue()); } try { - return new BigDecimal(val.toString()); + // 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. + return new BigDecimal(val.toString()).toBigInteger(); } 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 + * will be made to evaluate it as a number. + * + * @param key + * A string which is the key. + * @return An object which is the value. + */ + public double optDouble(String key) { + return this.optDouble(key, Double.NaN); + } + /** * 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 @@ -1063,6 +1070,48 @@ public double optDouble(String key, double 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 index + * The index must be between 0 and length() - 1. + * @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 index + * subscript + * @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 new BigDecimal((String) val).floatValue(); + } catch (Exception e) { + return defaultValue; + } + } + return defaultValue; + } + /** * Get an optional int value associated with a key, or zero if there is no * such key or if the value is not a number. If the value is a string, an @@ -1174,6 +1223,53 @@ public long optLong(String key, long 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. + * @param defaultValue + * The default. + * @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 ({@link BigDecimal}). 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 new BigDecimal(val.toString()); + } catch (Exception e) { + return defaultValue; + } + } + return defaultValue; + } /** * Get an optional string associated with a key. It returns an empty string From fcdb8671b28eb74964d86ee53a9f96a81cd5423e Mon Sep 17 00:00:00 2001 From: "John J. Aylward" Date: Wed, 17 May 2017 11:32:44 -0400 Subject: [PATCH 04/11] grr, forgot to save changes on last commit --- JSONArray.java | 20 ++++++++++++++++++-- JSONObject.java | 8 ++++---- 2 files changed, 22 insertions(+), 6 deletions(-) diff --git a/JSONArray.java b/JSONArray.java index 18d43ce01..bfab701a5 100644 --- a/JSONArray.java +++ b/JSONArray.java @@ -536,7 +536,7 @@ public double optDouble(int index, double defaultValue) { } /** - * Get the optional double value associated with an index. NaN is returned + * 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. * @@ -549,7 +549,7 @@ public float optFloat(int index) { } /** - * Get the optional double value associated with an index. The defaultValue + * 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. * @@ -808,6 +808,22 @@ public long optLong(int index, long 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. + * @param defaultValue + * The default. + * @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, diff --git a/JSONObject.java b/JSONObject.java index 13ba48ef7..3ca47d0e1 100644 --- a/JSONObject.java +++ b/JSONObject.java @@ -1075,8 +1075,8 @@ public double optDouble(String key, double defaultValue) { * 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. + * @param key + * A key string. * @return The value. */ public float optFloat(String key) { @@ -1088,8 +1088,8 @@ public float optFloat(String key) { * 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 key + * A key string. * @param defaultValue * The default value. * @return The value. From 0c7bd725a684764ac162c061fc66129461b503b2 Mon Sep 17 00:00:00 2001 From: "John J. Aylward" Date: Wed, 17 May 2017 11:34:13 -0400 Subject: [PATCH 05/11] fixes for javadoc --- JSONArray.java | 4 +--- JSONObject.java | 2 -- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/JSONArray.java b/JSONArray.java index bfab701a5..a692ba426 100644 --- a/JSONArray.java +++ b/JSONArray.java @@ -816,8 +816,6 @@ public long optLong(int index, long defaultValue) { * * @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) { @@ -858,7 +856,7 @@ public Number optNumber(int index, Number 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 3ca47d0e1..09b431783 100644 --- a/JSONObject.java +++ b/JSONObject.java @@ -1232,8 +1232,6 @@ public long optLong(String key, long defaultValue) { * * @param key * A key string. - * @param defaultValue - * The default. * @return An object which is the value. */ public Number optNumber(String key) { From 382f62e78158b869169a7646e8b2e3ff05a7cf3d Mon Sep 17 00:00:00 2001 From: "John J. Aylward" Date: Thu, 18 May 2017 11:41:51 -0400 Subject: [PATCH 06/11] * Prevent exceptions in cases where the value is not a string. * Don't call toString when we know it's a string, just cast --- JSONObject.java | 38 ++++++++++++++++++++++---------------- 1 file changed, 22 insertions(+), 16 deletions(-) diff --git a/JSONObject.java b/JSONObject.java index 09b431783..9765df4ea 100644 --- a/JSONObject.java +++ b/JSONObject.java @@ -980,11 +980,14 @@ public BigDecimal optBigDecimal(String key, BigDecimal defaultValue) { || val instanceof Short || val instanceof Byte){ return new BigDecimal(((Number) val).longValue()); } - try { - return new BigDecimal(val.toString()); - } catch (Exception e) { - return defaultValue; + if (val instanceof String) { + try { + return new BigDecimal((String) val); + } catch (Exception e) { + return defaultValue; + } } + return defaultValue; } /** @@ -1016,16 +1019,19 @@ public BigInteger optBigInteger(String key, BigInteger defaultValue) { || val instanceof Short || val instanceof Byte){ return BigInteger.valueOf(((Number) val).longValue()); } - 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. - return new BigDecimal(val.toString()).toBigInteger(); - } catch (Exception e) { - return defaultValue; + if (val instanceof String) { + 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. + return new BigDecimal((String) val).toBigInteger(); + } catch (Exception e) { + return defaultValue; + } } + return defaultValue; } /** @@ -1147,7 +1153,7 @@ public int optInt(String key, int defaultValue) { if (val instanceof String) { try { - return new BigDecimal(val.toString()).intValue(); + return new BigDecimal((String) val).intValue(); } catch (Exception e) { return defaultValue; } @@ -1216,7 +1222,7 @@ public long optLong(String key, long defaultValue) { if (val instanceof String) { try { - return new BigDecimal(val.toString()).longValue(); + return new BigDecimal((String) val).longValue(); } catch (Exception e) { return defaultValue; } @@ -1261,7 +1267,7 @@ public Number optNumber(String key, Number defaultValue) { if (val instanceof String) { try { - return new BigDecimal(val.toString()); + return new BigDecimal((String) val); } catch (Exception e) { return defaultValue; } From c28a2bdf394a0a950aea82607d539749a9bb9c7e Mon Sep 17 00:00:00 2001 From: "John J. Aylward" Date: Thu, 18 May 2017 13:07:32 -0400 Subject: [PATCH 07/11] * reverts changes to getDouble and related optDouble and optFloat * Updates optNumber to be smarter about which object it uses to parse strings --- JSONObject.java | 47 ++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 42 insertions(+), 5 deletions(-) diff --git a/JSONObject.java b/JSONObject.java index 9765df4ea..be547a752 100644 --- a/JSONObject.java +++ b/JSONObject.java @@ -581,7 +581,7 @@ public double getDouble(String key) throws JSONException { Object object = this.get(key); try { return object instanceof Number ? ((Number) object).doubleValue() - : new BigDecimal((String) object).doubleValue(); + : Double.parseDouble(object.toString()); } catch (Exception e) { throw new JSONException("JSONObject[" + quote(key) + "] is not a number.", e); @@ -1068,7 +1068,7 @@ public double optDouble(String key, double defaultValue) { } if (val instanceof String) { try { - return new BigDecimal((String) val).doubleValue(); + return Double.parseDouble((String) val); } catch (Exception e) { return defaultValue; } @@ -1110,7 +1110,7 @@ public float optFloat(String key, float defaultValue) { } if (val instanceof String) { try { - return new BigDecimal((String) val).floatValue(); + return Float.parseFloat((String) val); } catch (Exception e) { return defaultValue; } @@ -1247,7 +1247,7 @@ public Number optNumber(String key) { /** * 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 + * 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 @@ -1267,7 +1267,44 @@ public Number optNumber(String key, Number defaultValue) { if (val instanceof String) { try { - return new BigDecimal((String) val); + // decimal representation + if (((String)val).indexOf('.')>=0 || ((String)val).indexOf('e')>=0 || ((String)val).indexOf('E')>=0) { + // quick dirty way to see if we need a BigDecimal instead of a Double + if (((String)val).length()>14) { + return new BigDecimal((String)val); + } + return Double.valueOf((String)val); + } + // integer representation. + // This will narrow any values to the smallest reasonable Object representation + // (Integer, Long, or BigInteger) + // 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 + + // string version + if(((String)val).length()<=9){ + return Integer.valueOf((String)val); + } + if(((String)val).length()<=18){ + return Long.valueOf((String)val); + } + return new BigInteger((String)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? + + //BigInteger bi = new BigInteger((String)val); + //if(bi.bitLength()<=31){ + // return Integer.valueOf(bi.intValue()); + //} + //if(bi.bitLength()<=63){ + // return Long.valueOf(bi.longValue()); + //} + //return bi; } catch (Exception e) { return defaultValue; } From 1ab5260a7a1041c56f1aafec56768184bcc1dd70 Mon Sep 17 00:00:00 2001 From: "John J. Aylward" Date: Thu, 18 May 2017 14:24:34 -0400 Subject: [PATCH 08/11] * Adds methods getNUmber and getFloat to JSONArray and JSONObject * Extracts the stringToNumber logic that the optNumber method uses to reuse it between classes * Fixes -0 issue with optNumber/getNumber --- JSONArray.java | 50 +++++++++++++++-- JSONObject.java | 144 +++++++++++++++++++++++++++++++++++------------- 2 files changed, 151 insertions(+), 43 deletions(-) diff --git a/JSONArray.java b/JSONArray.java index a692ba426..6f7439aa5 100644 --- a/JSONArray.java +++ b/JSONArray.java @@ -248,6 +248,49 @@ public double getDouble(int index) throws JSONException { } } + /** + * 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."); + } + } + /** * Get the enum value associated with an index. * @@ -266,9 +309,8 @@ public > E getEnum(Class clazz, int index) throws JSONExcep // 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; } @@ -845,7 +887,7 @@ public Number optNumber(int index, Number defaultValue) { if (val instanceof String) { try { - return new BigDecimal(val.toString()); + return JSONObject.stringToNumber((String) val); } catch (Exception e) { return defaultValue; } diff --git a/JSONObject.java b/JSONObject.java index be547a752..bba5779c0 100644 --- a/JSONObject.java +++ b/JSONObject.java @@ -588,6 +588,50 @@ public double getDouble(String key) throws JSONException { } } + /** + * 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); + } + } + /** * Get the int value associated with a key. * @@ -1267,51 +1311,14 @@ public Number optNumber(String key, Number defaultValue) { if (val instanceof String) { try { - // decimal representation - if (((String)val).indexOf('.')>=0 || ((String)val).indexOf('e')>=0 || ((String)val).indexOf('E')>=0) { - // quick dirty way to see if we need a BigDecimal instead of a Double - if (((String)val).length()>14) { - return new BigDecimal((String)val); - } - return Double.valueOf((String)val); - } - // integer representation. - // This will narrow any values to the smallest reasonable Object representation - // (Integer, Long, or BigInteger) - // 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 - - // string version - if(((String)val).length()<=9){ - return Integer.valueOf((String)val); - } - if(((String)val).length()<=18){ - return Long.valueOf((String)val); - } - return new BigInteger((String)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? - - //BigInteger bi = new BigInteger((String)val); - //if(bi.bitLength()<=31){ - // return Integer.valueOf(bi.intValue()); - //} - //if(bi.bitLength()<=63){ - // return Long.valueOf(bi.longValue()); - //} - //return bi; + 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, @@ -1757,6 +1764,65 @@ public boolean similar(Object other) { } } + /** + * Converts a string to a number using the narrowest possible type. Possible + * returns for this function are BigDecimal, Double, BigInteger, Long, and Integer. + * + * An Exception is thrown if + * + * @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 (val.indexOf('.') > -1 || val.indexOf('e') > -1 + || val.indexOf('E') > -1 + || "-0".equals(val)) { + // quick dirty way to see if we need a BigDecimal instead of a Double + if (val.length()>14) { + return new BigDecimal(val); + } + return Double.valueOf(val); + } + // integer representation. + // This will narrow any values to the smallest reasonable Object representation + // (Integer, Long, or BigInteger) + // 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 + + // string version + 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((String)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 * can't be converted, return the string. From a7f8ff24df65578ae966d4d12d92218bb3909984 Mon Sep 17 00:00:00 2001 From: "John J. Aylward" Date: Thu, 18 May 2017 14:41:42 -0400 Subject: [PATCH 09/11] correct string check for JSONObject optBigDecimal and optBigInteger --- JSONObject.java | 34 +++++++++++++++------------------- 1 file changed, 15 insertions(+), 19 deletions(-) diff --git a/JSONObject.java b/JSONObject.java index bba5779c0..d835438c8 100644 --- a/JSONObject.java +++ b/JSONObject.java @@ -1024,14 +1024,12 @@ public BigDecimal optBigDecimal(String key, BigDecimal defaultValue) { || val instanceof Short || val instanceof Byte){ return new BigDecimal(((Number) val).longValue()); } - if (val instanceof String) { - try { - return new BigDecimal((String) val); - } catch (Exception e) { - return defaultValue; - } + // 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; } - return defaultValue; } /** @@ -1063,19 +1061,17 @@ public BigInteger optBigInteger(String key, BigInteger defaultValue) { || val instanceof Short || val instanceof Byte){ return BigInteger.valueOf(((Number) val).longValue()); } - if (val instanceof String) { - 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. - return new BigDecimal((String) val).toBigInteger(); - } catch (Exception e) { - return defaultValue; - } + // 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. + return new BigDecimal(val.toString()).toBigInteger(); + } catch (Exception e) { + return defaultValue; } - return defaultValue; } /** From 849b392c01f60306d63909e0e5cb9e1ccd0e14af Mon Sep 17 00:00:00 2001 From: "John J. Aylward" Date: Thu, 18 May 2017 19:49:50 -0400 Subject: [PATCH 10/11] updates the getNumber/optNumber to not return invalid Doubles --- JSONArray.java | 6 +++- JSONObject.java | 78 +++++++++++++++++++++++++++++++------------------ 2 files changed, 54 insertions(+), 30 deletions(-) diff --git a/JSONArray.java b/JSONArray.java index 6f7439aa5..2783bf7ac 100644 --- a/JSONArray.java +++ b/JSONArray.java @@ -738,7 +738,11 @@ public BigInteger optBigInteger(int index, BigInteger defaultValue) { return BigInteger.valueOf(((Number) val).longValue()); } try { - return new BigDecimal(val.toString()).toBigInteger(); + final String valStr = val.toString(); + if(JSONObject.isDecimalNotation(valStr)) { + return new BigDecimal(valStr).toBigInteger(); + } + return new BigInteger(valStr); } catch (Exception e) { return defaultValue; } diff --git a/JSONObject.java b/JSONObject.java index d835438c8..05e4f4072 100644 --- a/JSONObject.java +++ b/JSONObject.java @@ -1068,7 +1068,11 @@ public BigInteger optBigInteger(String key, BigInteger defaultValue) { // 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. - return new BigDecimal(val.toString()).toBigInteger(); + final String valStr = val.toString(); + if(isDecimalNotation(valStr)) { + return new BigDecimal(valStr).toBigInteger(); + } + return new BigInteger(valStr); } catch (Exception e) { return defaultValue; } @@ -1759,12 +1763,22 @@ public boolean similar(Object other) { 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. - * - * An Exception is thrown if + * 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. @@ -1775,46 +1789,52 @@ protected static Number stringToNumber(final String val) throws NumberFormatExce char initial = val.charAt(0); if ((initial >= '0' && initial <= '9') || initial == '-') { // decimal representation - if (val.indexOf('.') > -1 || val.indexOf('e') > -1 - || val.indexOf('E') > -1 - || "-0".equals(val)) { + 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); } - return Double.valueOf(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 - - // string version - if(val.length()<=9){ - return Integer.valueOf(val); - } - if(val.length()<=18){ - return Long.valueOf(val); - } - return new BigInteger(val); + //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((String)val); - //if(bi.bitLength()<=31){ - // return Integer.valueOf(bi.intValue()); - //} - //if(bi.bitLength()<=63){ - // return Long.valueOf(bi.longValue()); - //} - //return bi; + 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."); } @@ -1849,9 +1869,9 @@ public static Object stringToValue(String string) { 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; From 04d6e83fc215001decb989a9223ac7c983383e8f Mon Sep 17 00:00:00 2001 From: "John J. Aylward" Date: Fri, 19 May 2017 09:49:22 -0400 Subject: [PATCH 11/11] * Missed JSONArray optFloat and optDouble for the revert * prevents erasure of stack trace for rethrown exceptions --- JSONArray.java | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/JSONArray.java b/JSONArray.java index 2783bf7ac..a1147e7d8 100644 --- a/JSONArray.java +++ b/JSONArray.java @@ -244,7 +244,7 @@ public double getDouble(int index) throws JSONException { 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); } } @@ -287,7 +287,7 @@ public Number getNumber(int index) throws JSONException { } return JSONObject.stringToNumber(object.toString()); } catch (Exception e) { - throw new JSONException("JSONArray[" + index + "] is not a number."); + throw new JSONException("JSONArray[" + index + "] is not a number.", e); } } @@ -331,7 +331,7 @@ public BigDecimal getBigDecimal (int index) throws JSONException { return new BigDecimal(object.toString()); } catch (Exception e) { throw new JSONException("JSONArray[" + index + - "] could not convert to BigDecimal."); + "] could not convert to BigDecimal.", e); } } @@ -351,7 +351,7 @@ public BigInteger getBigInteger (int index) throws JSONException { return new BigInteger(object.toString()); } catch (Exception e) { throw new JSONException("JSONArray[" + index + - "] could not convert to BigInteger."); + "] could not convert to BigInteger.", e); } } @@ -370,7 +370,7 @@ public int getInt(int index) throws JSONException { 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); } } @@ -426,7 +426,7 @@ public long getLong(int index) throws JSONException { 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); } } @@ -569,7 +569,7 @@ public double optDouble(int index, double defaultValue) { } if (val instanceof String) { try { - return new BigDecimal((String) val).doubleValue(); + return Double.parseDouble((String) val); } catch (Exception e) { return defaultValue; } @@ -611,7 +611,7 @@ public float optFloat(int index, float defaultValue) { } if (val instanceof String) { try { - return new BigDecimal((String) val).floatValue(); + return Float.parseFloat((String) val); } catch (Exception e) { return defaultValue; }