Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

XML optLong/getLong equivalent updates for string to number conversion. #794

Merged
merged 4 commits into from
Oct 25, 2023
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions src/main/java/org/json/JSONArray.java
Original file line number Diff line number Diff line change
Expand Up @@ -331,7 +331,7 @@ public Number getNumber(int index) throws JSONException {
if (object instanceof Number) {
return (Number)object;
}
return JSONObject.stringToNumber(object.toString());
return NumberConversionUtil.stringToNumber(object.toString());
} catch (Exception e) {
throw wrongValueFormatException(index, "number", object, e);
}
Expand Down Expand Up @@ -1078,7 +1078,7 @@ public Number optNumber(int index, Number defaultValue) {

if (val instanceof String) {
try {
return JSONObject.stringToNumber((String) val);
return NumberConversionUtil.stringToNumber((String) val);
} catch (Exception e) {
return defaultValue;
}
Expand Down
99 changes: 3 additions & 96 deletions src/main/java/org/json/JSONObject.java
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@
import java.util.Set;
import java.util.regex.Pattern;

import static org.json.NumberConversionUtil.stringToNumber;

/**
* A JSONObject is an unordered collection of name/value pairs. Its external
* form is a string wrapped in curly braces with colons between the names and
Expand Down Expand Up @@ -2380,83 +2382,7 @@ protected static boolean isDecimalNotation(final String val) {
|| 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 input 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 input) throws NumberFormatException {
String val = input;
if (val.startsWith(".")){
val = "0"+val;
}
if (val.startsWith("-.")){
val = "-0."+val.substring(2);
}
char initial = val.charAt(0);
if ((initial >= '0' && initial <= '9') || initial == '-' ) {
// decimal representation
if (isDecimalNotation(val)) {
// Use a BigDecimal all the time so we keep the original
// representation. BigDecimal doesn't support -0.0, ensure we
// keep that by forcing a decimal.
try {
BigDecimal bd = new BigDecimal(val);
if(initial == '-' && BigDecimal.ZERO.compareTo(bd)==0) {
return Double.valueOf(-0.0);
}
return bd;
} catch (NumberFormatException retryAsDouble) {
// this is to support "Hex Floats" like this: 0x1.0P-1074
try {
Double d = Double.valueOf(val);
if(d.isNaN() || d.isInfinite()) {
throw new NumberFormatException("val ["+input+"] is not a valid number.");
}
return d;
} catch (NumberFormatException ignore) {
throw new NumberFormatException("val ["+input+"] is not a valid number.");
}
}
}
val = removeLeadingZerosOfNumber(input);
initial = val.charAt(0);
if(initial == '0' && val.length() > 1) {
char at1 = val.charAt(1);
if(at1 >= '0' && at1 <= '9') {
throw new NumberFormatException("val ["+input+"] is not a valid number.");
}
} else if (initial == '-' && val.length() > 2) {
char at1 = val.charAt(1);
char at2 = val.charAt(2);
if(at1 == '0' && at2 >= '0' && at2 <= '9') {
throw new NumberFormatException("val ["+input+"] is not a valid number.");
}
}
// integer representation.
// This will narrow any values to the smallest reasonable Object representation
// (Integer, Long, or BigInteger)

// BigInteger down conversion: We use a similar bitLength 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.
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 ["+input+"] is not a valid number.");
}


/**
* Try to convert a string into a number, boolean, or null. If the string
Expand Down Expand Up @@ -2922,23 +2848,4 @@ private static JSONException recursivelyDefinedObjectException(String key) {
);
}

/**
* For a prospective number, remove the leading zeros
* @param value prospective number
* @return number without leading zeros
*/
private static String removeLeadingZerosOfNumber(String value){
if (value.equals("-")){return value;}
boolean negativeFirstChar = (value.charAt(0) == '-');
int counter = negativeFirstChar ? 1:0;
while (counter < value.length()){
if (value.charAt(counter) != '0'){
if (negativeFirstChar) {return "-".concat(value.substring(counter));}
return value.substring(counter);
}
++counter;
}
if (negativeFirstChar) {return "-0";}
return "0";
}
}
142 changes: 142 additions & 0 deletions src/main/java/org/json/NumberConversionUtil.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
package org.json;

import java.math.BigDecimal;
import java.math.BigInteger;

public class NumberConversionUtil {

/**
* 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 input 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.
*/
public static Number stringToNumber(final String input) throws NumberFormatException {
String val = input;
if (val.startsWith(".")){
val = "0"+val;
}
if (val.startsWith("-.")){
val = "-0."+val.substring(2);
}
char initial = val.charAt(0);
if ((initial >= '0' && initial <= '9') || initial == '-' ) {
// decimal representation
if (isDecimalNotation(val)) {
// Use a BigDecimal all the time so we keep the original
// representation. BigDecimal doesn't support -0.0, ensure we
// keep that by forcing a decimal.
try {
BigDecimal bd = new BigDecimal(val);
if(initial == '-' && BigDecimal.ZERO.compareTo(bd)==0) {
return Double.valueOf(-0.0);
}
return bd;
} catch (NumberFormatException retryAsDouble) {
// this is to support "Hex Floats" like this: 0x1.0P-1074
try {
Double d = Double.valueOf(val);
if(d.isNaN() || d.isInfinite()) {
throw new NumberFormatException("val ["+input+"] is not a valid number.");
}
return d;
} catch (NumberFormatException ignore) {
throw new NumberFormatException("val ["+input+"] is not a valid number.");
}
}
}
val = removeLeadingZerosOfNumber(input);
initial = val.charAt(0);
if(initial == '0' && val.length() > 1) {
char at1 = val.charAt(1);
if(at1 >= '0' && at1 <= '9') {
throw new NumberFormatException("val ["+input+"] is not a valid number.");
}
} else if (initial == '-' && val.length() > 2) {
char at1 = val.charAt(1);
char at2 = val.charAt(2);
if(at1 == '0' && at2 >= '0' && at2 <= '9') {
throw new NumberFormatException("val ["+input+"] is not a valid number.");
}
}
// integer representation.
// This will narrow any values to the smallest reasonable Object representation
// (Integer, Long, or BigInteger)

// BigInteger down conversion: We use a similar bitLength 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.
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 ["+input+"] is not a valid number.");
}

/**
* Checks if the value could be considered a number in decimal number system.
* @param value
* @return
*/
public static boolean potentialNumber(String value){
if (value == null || value.isEmpty()){
return false;
}
return potentialPositiveNumberStartingAtIndex(value, (value.charAt(0)=='-'?1:0));
}

/**
* 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.
*/
private static boolean isDecimalNotation(final String val) {
return val.indexOf('.') > -1 || val.indexOf('e') > -1
|| val.indexOf('E') > -1 || "-0".equals(val);
}

private static boolean potentialPositiveNumberStartingAtIndex(String value,int index){
if (index >= value.length()){
return false;
}
return digitAtIndex(value, (value.charAt(index)=='.'?index+1:index));
}

private static boolean digitAtIndex(String value, int index){
if (index >= value.length()){
return false;
}
return value.charAt(index) >= '0' && value.charAt(index) <= '9';
}

/**
* For a prospective number, remove the leading zeros
* @param value prospective number
* @return number without leading zeros
*/
private static String removeLeadingZerosOfNumber(String value){
if (value.equals("-")){return value;}
boolean negativeFirstChar = (value.charAt(0) == '-');
int counter = negativeFirstChar ? 1:0;
while (counter < value.length()){
if (value.charAt(counter) != '0'){
if (negativeFirstChar) {return "-".concat(value.substring(counter));}
return value.substring(counter);
}
++counter;
}
if (negativeFirstChar) {return "-0";}
return "0";
}
}
80 changes: 4 additions & 76 deletions src/main/java/org/json/XML.java
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,11 @@

import java.io.Reader;
import java.io.StringReader;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.util.Iterator;

import static org.json.NumberConversionUtil.potentialNumber;
import static org.json.NumberConversionUtil.stringToNumber;


/**
* This provides static methods to convert an XML text into a JSONObject, and to
Expand Down Expand Up @@ -486,8 +487,7 @@ public static Object stringToValue(String string) {
* produced, then the value will just be a string.
*/

char initial = string.charAt(0);
if ((initial >= '0' && initial <= '9') || initial == '-') {
if (potentialNumber(string)) {
try {
return stringToNumber(string);
} catch (Exception ignore) {
Expand All @@ -496,78 +496,6 @@ public static Object stringToValue(String string) {
return string;
}

/**
* direct copy of {@link JSONObject#stringToNumber(String)} to maintain Android support.
*/
private static Number stringToNumber(final String val) throws NumberFormatException {
char initial = val.charAt(0);
if ((initial >= '0' && initial <= '9') || initial == '-') {
// decimal representation
if (isDecimalNotation(val)) {
// Use a BigDecimal all the time so we keep the original
// representation. BigDecimal doesn't support -0.0, ensure we
// keep that by forcing a decimal.
try {
BigDecimal bd = new BigDecimal(val);
if(initial == '-' && BigDecimal.ZERO.compareTo(bd)==0) {
return Double.valueOf(-0.0);
}
return bd;
} catch (NumberFormatException retryAsDouble) {
// this is to support "Hex Floats" like this: 0x1.0P-1074
try {
Double d = Double.valueOf(val);
if(d.isNaN() || d.isInfinite()) {
throw new NumberFormatException("val ["+val+"] is not a valid number.");
}
return d;
} catch (NumberFormatException ignore) {
throw new NumberFormatException("val ["+val+"] is not a valid number.");
}
}
}
// block items like 00 01 etc. Java number parsers treat these as Octal.
if(initial == '0' && val.length() > 1) {
char at1 = val.charAt(1);
if(at1 >= '0' && at1 <= '9') {
throw new NumberFormatException("val ["+val+"] is not a valid number.");
}
} else if (initial == '-' && val.length() > 2) {
char at1 = val.charAt(1);
char at2 = val.charAt(2);
if(at1 == '0' && at2 >= '0' && at2 <= '9') {
throw new NumberFormatException("val ["+val+"] is not a valid number.");
}
}
// integer representation.
// This will narrow any values to the smallest reasonable Object representation
// (Integer, Long, or BigInteger)

// BigInteger down conversion: We use a similar bitLength 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.
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.");
}

/**
* direct copy of {@link JSONObject#isDecimalNotation(String)} to maintain Android support.
*/
private static boolean isDecimalNotation(final String val) {
return val.indexOf('.') > -1 || val.indexOf('e') > -1
|| val.indexOf('E') > -1 || "-0".equals(val);
}


/**
* Convert a well-formed (but not necessarily valid) XML string into a
* JSONObject. Some information may be lost in this transformation because
Expand Down
2 changes: 1 addition & 1 deletion src/test/java/org/json/junit/JSONMLTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -709,7 +709,7 @@ public void commentsInXML() {
@Test
public void testToJSONArray_jsonOutput() {
final String originalXml = "<root><id>01</id><id>1</id><id>00</id><id>0</id><item id=\"01\"/><title>True</title></root>";
final String expectedJsonString = "[\"root\",[\"id\",\"01\"],[\"id\",1],[\"id\",\"00\"],[\"id\",0],[\"item\",{\"id\":\"01\"}],[\"title\",true]]";
final String expectedJsonString = "[\"root\",[\"id\",1],[\"id\",1],[\"id\",0],[\"id\",0],[\"item\",{\"id\":1}],[\"title\",true]]";
final JSONArray actualJsonOutput = JSONML.toJSONArray(originalXml, false);
assertEquals(expectedJsonString, actualJsonOutput.toString());
}
Expand Down
Loading