Skip to content

Commit 783d298

Browse files
authored
Merge pull request stleary#814 from rudrajyotib/issue813
Refactor duplicate code for stringToNumber() in JSONObject, JSONArray, and XML
2 parents 6c1bc06 + 1ceb70b commit 783d298

File tree

5 files changed

+325
-227
lines changed

5 files changed

+325
-227
lines changed

src/main/java/org/json/JSONArray.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -331,7 +331,7 @@ public Number getNumber(int index) throws JSONException {
331331
if (object instanceof Number) {
332332
return (Number)object;
333333
}
334-
return JSONObject.stringToNumber(object.toString());
334+
return NumberConversionUtil.stringToNumber(object.toString());
335335
} catch (Exception e) {
336336
throw wrongValueFormatException(index, "number", object, e);
337337
}
@@ -1078,7 +1078,7 @@ public Number optNumber(int index, Number defaultValue) {
10781078

10791079
if (val instanceof String) {
10801080
try {
1081-
return JSONObject.stringToNumber((String) val);
1081+
return NumberConversionUtil.stringToNumber((String) val);
10821082
} catch (Exception e) {
10831083
return defaultValue;
10841084
}

src/main/java/org/json/JSONObject.java

Lines changed: 4 additions & 116 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,9 @@
2828
import java.util.Set;
2929
import java.util.regex.Pattern;
3030

31+
import static org.json.NumberConversionUtil.potentialNumber;
32+
import static org.json.NumberConversionUtil.stringToNumber;
33+
3134
/**
3235
* A JSONObject is an unordered collection of name/value pairs. Its external
3336
* form is a string wrapped in curly braces with colons between the names and
@@ -2380,84 +2383,6 @@ protected static boolean isDecimalNotation(final String val) {
23802383
|| val.indexOf('E') > -1 || "-0".equals(val);
23812384
}
23822385

2383-
/**
2384-
* Converts a string to a number using the narrowest possible type. Possible
2385-
* returns for this function are BigDecimal, Double, BigInteger, Long, and Integer.
2386-
* When a Double is returned, it should always be a valid Double and not NaN or +-infinity.
2387-
*
2388-
* @param input value to convert
2389-
* @return Number representation of the value.
2390-
* @throws NumberFormatException thrown if the value is not a valid number. A public
2391-
* caller should catch this and wrap it in a {@link JSONException} if applicable.
2392-
*/
2393-
protected static Number stringToNumber(final String input) throws NumberFormatException {
2394-
String val = input;
2395-
if (val.startsWith(".")){
2396-
val = "0"+val;
2397-
}
2398-
if (val.startsWith("-.")){
2399-
val = "-0."+val.substring(2);
2400-
}
2401-
char initial = val.charAt(0);
2402-
if ((initial >= '0' && initial <= '9') || initial == '-' ) {
2403-
// decimal representation
2404-
if (isDecimalNotation(val)) {
2405-
// Use a BigDecimal all the time so we keep the original
2406-
// representation. BigDecimal doesn't support -0.0, ensure we
2407-
// keep that by forcing a decimal.
2408-
try {
2409-
BigDecimal bd = new BigDecimal(val);
2410-
if(initial == '-' && BigDecimal.ZERO.compareTo(bd)==0) {
2411-
return Double.valueOf(-0.0);
2412-
}
2413-
return bd;
2414-
} catch (NumberFormatException retryAsDouble) {
2415-
// this is to support "Hex Floats" like this: 0x1.0P-1074
2416-
try {
2417-
Double d = Double.valueOf(val);
2418-
if(d.isNaN() || d.isInfinite()) {
2419-
throw new NumberFormatException("val ["+input+"] is not a valid number.");
2420-
}
2421-
return d;
2422-
} catch (NumberFormatException ignore) {
2423-
throw new NumberFormatException("val ["+input+"] is not a valid number.");
2424-
}
2425-
}
2426-
}
2427-
val = removeLeadingZerosOfNumber(input);
2428-
initial = val.charAt(0);
2429-
if(initial == '0' && val.length() > 1) {
2430-
char at1 = val.charAt(1);
2431-
if(at1 >= '0' && at1 <= '9') {
2432-
throw new NumberFormatException("val ["+input+"] is not a valid number.");
2433-
}
2434-
} else if (initial == '-' && val.length() > 2) {
2435-
char at1 = val.charAt(1);
2436-
char at2 = val.charAt(2);
2437-
if(at1 == '0' && at2 >= '0' && at2 <= '9') {
2438-
throw new NumberFormatException("val ["+input+"] is not a valid number.");
2439-
}
2440-
}
2441-
// integer representation.
2442-
// This will narrow any values to the smallest reasonable Object representation
2443-
// (Integer, Long, or BigInteger)
2444-
2445-
// BigInteger down conversion: We use a similar bitLength compare as
2446-
// BigInteger#intValueExact uses. Increases GC, but objects hold
2447-
// only what they need. i.e. Less runtime overhead if the value is
2448-
// long lived.
2449-
BigInteger bi = new BigInteger(val);
2450-
if(bi.bitLength() <= 31){
2451-
return Integer.valueOf(bi.intValue());
2452-
}
2453-
if(bi.bitLength() <= 63){
2454-
return Long.valueOf(bi.longValue());
2455-
}
2456-
return bi;
2457-
}
2458-
throw new NumberFormatException("val ["+input+"] is not a valid number.");
2459-
}
2460-
24612386
/**
24622387
* Try to convert a string into a number, boolean, or null. If the string
24632388
* can't be converted, return the string.
@@ -2501,26 +2426,7 @@ public static Object stringToValue(String string) {
25012426
}
25022427

25032428

2504-
private static boolean potentialNumber(String value){
2505-
if (value == null || value.isEmpty()){
2506-
return false;
2507-
}
2508-
return potentialPositiveNumberStartingAtIndex(value, (value.charAt(0)=='-'?1:0));
2509-
}
2510-
2511-
private static boolean potentialPositiveNumberStartingAtIndex(String value,int index){
2512-
if (index >= value.length()){
2513-
return false;
2514-
}
2515-
return digitAtIndex(value, (value.charAt(index)=='.'?index+1:index));
2516-
}
25172429

2518-
private static boolean digitAtIndex(String value, int index){
2519-
if (index >= value.length()){
2520-
return false;
2521-
}
2522-
return value.charAt(index) >= '0' && value.charAt(index) <= '9';
2523-
}
25242430

25252431
/**
25262432
* Throw an exception if the object is a NaN or infinite number.
@@ -2922,23 +2828,5 @@ private static JSONException recursivelyDefinedObjectException(String key) {
29222828
);
29232829
}
29242830

2925-
/**
2926-
* For a prospective number, remove the leading zeros
2927-
* @param value prospective number
2928-
* @return number without leading zeros
2929-
*/
2930-
private static String removeLeadingZerosOfNumber(String value){
2931-
if (value.equals("-")){return value;}
2932-
boolean negativeFirstChar = (value.charAt(0) == '-');
2933-
int counter = negativeFirstChar ? 1:0;
2934-
while (counter < value.length()){
2935-
if (value.charAt(counter) != '0'){
2936-
if (negativeFirstChar) {return "-".concat(value.substring(counter));}
2937-
return value.substring(counter);
2938-
}
2939-
++counter;
2940-
}
2941-
if (negativeFirstChar) {return "-0";}
2942-
return "0";
2943-
}
2831+
29442832
}
Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
package org.json;
2+
3+
import java.math.BigDecimal;
4+
import java.math.BigInteger;
5+
6+
class NumberConversionUtil {
7+
8+
/**
9+
* Converts a string to a number using the narrowest possible type. Possible
10+
* returns for this function are BigDecimal, Double, BigInteger, Long, and Integer.
11+
* When a Double is returned, it should always be a valid Double and not NaN or +-infinity.
12+
*
13+
* @param input value to convert
14+
* @return Number representation of the value.
15+
* @throws NumberFormatException thrown if the value is not a valid number. A public
16+
* caller should catch this and wrap it in a {@link JSONException} if applicable.
17+
*/
18+
static Number stringToNumber(final String input) throws NumberFormatException {
19+
String val = input;
20+
if (val.startsWith(".")){
21+
val = "0"+val;
22+
}
23+
if (val.startsWith("-.")){
24+
val = "-0."+val.substring(2);
25+
}
26+
char initial = val.charAt(0);
27+
if ((initial >= '0' && initial <= '9') || initial == '-' ) {
28+
// decimal representation
29+
if (isDecimalNotation(val)) {
30+
// Use a BigDecimal all the time so we keep the original
31+
// representation. BigDecimal doesn't support -0.0, ensure we
32+
// keep that by forcing a decimal.
33+
try {
34+
BigDecimal bd = new BigDecimal(val);
35+
if(initial == '-' && BigDecimal.ZERO.compareTo(bd)==0) {
36+
return Double.valueOf(-0.0);
37+
}
38+
return bd;
39+
} catch (NumberFormatException retryAsDouble) {
40+
// this is to support "Hex Floats" like this: 0x1.0P-1074
41+
try {
42+
Double d = Double.valueOf(val);
43+
if(d.isNaN() || d.isInfinite()) {
44+
throw new NumberFormatException("val ["+input+"] is not a valid number.");
45+
}
46+
return d;
47+
} catch (NumberFormatException ignore) {
48+
throw new NumberFormatException("val ["+input+"] is not a valid number.");
49+
}
50+
}
51+
}
52+
val = removeLeadingZerosOfNumber(input);
53+
initial = val.charAt(0);
54+
if(initial == '0' && val.length() > 1) {
55+
char at1 = val.charAt(1);
56+
if(at1 >= '0' && at1 <= '9') {
57+
throw new NumberFormatException("val ["+input+"] is not a valid number.");
58+
}
59+
} else if (initial == '-' && val.length() > 2) {
60+
char at1 = val.charAt(1);
61+
char at2 = val.charAt(2);
62+
if(at1 == '0' && at2 >= '0' && at2 <= '9') {
63+
throw new NumberFormatException("val ["+input+"] is not a valid number.");
64+
}
65+
}
66+
// integer representation.
67+
// This will narrow any values to the smallest reasonable Object representation
68+
// (Integer, Long, or BigInteger)
69+
70+
// BigInteger down conversion: We use a similar bitLength compare as
71+
// BigInteger#intValueExact uses. Increases GC, but objects hold
72+
// only what they need. i.e. Less runtime overhead if the value is
73+
// long lived.
74+
BigInteger bi = new BigInteger(val);
75+
if(bi.bitLength() <= 31){
76+
return Integer.valueOf(bi.intValue());
77+
}
78+
if(bi.bitLength() <= 63){
79+
return Long.valueOf(bi.longValue());
80+
}
81+
return bi;
82+
}
83+
throw new NumberFormatException("val ["+input+"] is not a valid number.");
84+
}
85+
86+
/**
87+
* Checks if the value could be considered a number in decimal number system.
88+
* @param value
89+
* @return
90+
*/
91+
static boolean potentialNumber(String value){
92+
if (value == null || value.isEmpty()){
93+
return false;
94+
}
95+
return potentialPositiveNumberStartingAtIndex(value, (value.charAt(0)=='-'?1:0));
96+
}
97+
98+
/**
99+
* Tests if the value should be tried as a decimal. It makes no test if there are actual digits.
100+
*
101+
* @param val value to test
102+
* @return true if the string is "-0" or if it contains '.', 'e', or 'E', false otherwise.
103+
*/
104+
private static boolean isDecimalNotation(final String val) {
105+
return val.indexOf('.') > -1 || val.indexOf('e') > -1
106+
|| val.indexOf('E') > -1 || "-0".equals(val);
107+
}
108+
109+
private static boolean potentialPositiveNumberStartingAtIndex(String value,int index){
110+
if (index >= value.length()){
111+
return false;
112+
}
113+
return digitAtIndex(value, (value.charAt(index)=='.'?index+1:index));
114+
}
115+
116+
private static boolean digitAtIndex(String value, int index){
117+
if (index >= value.length()){
118+
return false;
119+
}
120+
return value.charAt(index) >= '0' && value.charAt(index) <= '9';
121+
}
122+
123+
/**
124+
* For a prospective number, remove the leading zeros
125+
* @param value prospective number
126+
* @return number without leading zeros
127+
*/
128+
private static String removeLeadingZerosOfNumber(String value){
129+
if (value.equals("-")){return value;}
130+
boolean negativeFirstChar = (value.charAt(0) == '-');
131+
int counter = negativeFirstChar ? 1:0;
132+
while (counter < value.length()){
133+
if (value.charAt(counter) != '0'){
134+
if (negativeFirstChar) {return "-".concat(value.substring(counter));}
135+
return value.substring(counter);
136+
}
137+
++counter;
138+
}
139+
if (negativeFirstChar) {return "-0";}
140+
return "0";
141+
}
142+
}

0 commit comments

Comments
 (0)