JSON: option to decode special strings as floats.

Passing JsonSpecialFloats.yes to parseJSON prompts the parser to interpret
"NaN", "Infinite", and "-Infinite" as the special floating-point values they
represent.

Adds the overload parseJSON(JSONValue, SpecialFloats) that takes a JSONValue and
the JsonSpecialFloats flag so users do not have to pass maxDepth in order to
specify special float handling behavior.
This commit is contained in:
Ryan 2015-04-10 14:11:19 -04:00
parent ef0b39334d
commit c33dca96be

View file

@ -72,7 +72,7 @@ enum JSONFloatLiteral : string
/** /**
Flag that enables encoding special float values (NaN/Inf) as strings. Flag that enables encoding special float values (NaN/Inf) as strings.
*/ */
alias SpecialFloats = Flag!"SpecialFloats"; alias JsonSpecialFloats = Flag!"JsonSpecialFloats";
/** /**
JSON type enumeration JSON type enumeration
@ -612,7 +612,7 @@ struct JSONValue
/// Implicitly calls $(D toJSON) on this JSONValue. /// Implicitly calls $(D toJSON) on this JSONValue.
/// $(I specialFloats) will enable encoding NaN/Inf as strings. /// $(I specialFloats) will enable encoding NaN/Inf as strings.
string toString(in SpecialFloats specialFloats = SpecialFloats.no) const string toString(in JsonSpecialFloats specialFloats = JsonSpecialFloats.no) const
{ {
return toJSON(&this, false, specialFloats); return toJSON(&this, false, specialFloats);
} }
@ -620,7 +620,7 @@ struct JSONValue
/// Implicitly calls $(D toJSON) on this JSONValue, like $(D toString), but /// Implicitly calls $(D toJSON) on this JSONValue, like $(D toString), but
/// also passes $(I true) as $(I pretty) argument. /// also passes $(I true) as $(I pretty) argument.
/// $(I specialFloats) will enable encoding NaN/Inf as strings. /// $(I specialFloats) will enable encoding NaN/Inf as strings.
string toPrettyString(in SpecialFloats specialFloats = SpecialFloats.no) const string toPrettyString(in JsonSpecialFloats specialFloats = JsonSpecialFloats.no) const
{ {
return toJSON(&this, true, specialFloats); return toJSON(&this, true, specialFloats);
} }
@ -629,7 +629,8 @@ struct JSONValue
/** /**
Parses a serialized string and returns a tree of JSON values. Parses a serialized string and returns a tree of JSON values.
*/ */
JSONValue parseJSON(T)(T json, int maxDepth = -1) if(isInputRange!T) JSONValue parseJSON(T)(T json, int maxDepth = -1, JsonSpecialFloats specialFloats = JsonSpecialFloats.no)
if(isInputRange!T)
{ {
import std.ascii : isWhite, isDigit, isHexDigit, toUpper, toLower; import std.ascii : isWhite, isDigit, isHexDigit, toUpper, toLower;
import std.utf : toUTF8; import std.utf : toUTF8;
@ -769,6 +770,22 @@ JSONValue parseJSON(T)(T json, int maxDepth = -1) if(isInputRange!T)
return str.data.length ? str.data : ""; return str.data.length ? str.data : "";
} }
bool tryGetSpecialFloat(string str, out double val) {
switch(str) {
case JSONFloatLiteral.nan:
val = double.nan;
return true;
case JSONFloatLiteral.inf:
val = double.infinity;
return true;
case JSONFloatLiteral.negativeInf:
val = -double.infinity;
return true;
default:
return false;
}
}
void parseValue(JSONValue* value) void parseValue(JSONValue* value)
{ {
depth++; depth++;
@ -822,8 +839,17 @@ JSONValue parseJSON(T)(T json, int maxDepth = -1) if(isInputRange!T)
break; break;
case '"': case '"':
auto str = parseString();
// if special float parsing is enabled, check if string represents NaN/Inf
if (specialFloats && tryGetSpecialFloat(str, value.store.floating)) {
// found a special float, its value was placed in value.store.floating
value.type_tag = JSON_TYPE.FLOAT;
break;
}
value.type_tag = JSON_TYPE.STRING; value.type_tag = JSON_TYPE.STRING;
value.store.str = parseString(); value.store.str = str;
break; break;
case '0': .. case '9': case '0': .. case '9':
@ -923,6 +949,15 @@ JSONValue parseJSON(T)(T json, int maxDepth = -1) if(isInputRange!T)
return root; return root;
} }
/**
Parses a serialized string and returns a tree of JSON values.
*/
JSONValue parseJSON(T)(T json, JsonSpecialFloats specialFloats)
if(isInputRange!T)
{
return parseJSON!T(json, -1, specialFloats);
}
/** /**
Takes a tree of JSON values and returns the serialized string. Takes a tree of JSON values and returns the serialized string.
@ -930,9 +965,9 @@ Any Object types will be serialized in a key-sorted order.
If $(D pretty) is false no whitespaces are generated. If $(D pretty) is false no whitespaces are generated.
If $(D pretty) is true serialized string is formatted to be human-readable. If $(D pretty) is true serialized string is formatted to be human-readable.
If $(D specialFloats) is SpecialFloats.yes, encode special floats (NaN/Infinity) as strings. If $(D specialFloats) is JsonSpecialFloats.yes, encode special floats (NaN/Infinity) as strings.
*/ */
string toJSON(in JSONValue* root, in bool pretty = false, in SpecialFloats specialFloats = SpecialFloats.no) string toJSON(in JSONValue* root, in bool pretty = false, in JsonSpecialFloats specialFloats = JsonSpecialFloats.no)
{ {
auto json = appender!string(); auto json = appender!string();
@ -1444,15 +1479,41 @@ EOF";
// handling of special float values (NaN, Inf, -Inf) // handling of special float values (NaN, Inf, -Inf)
unittest unittest
{ {
import std.math : isNaN, isInfinity;
import std.exception : assertThrown; import std.exception : assertThrown;
// when passing SpecialFloats.yes, encode NaN/Inf as strings // expected representations of NaN and Inf
assert(JSONValue(float.nan).toString(SpecialFloats.yes) == q{"NaN"}); enum {
assert(JSONValue(double.infinity).toString(SpecialFloats.yes) == q{"Infinite"}); nanString = '"' ~ JSONFloatLiteral.nan ~ '"',
assert(JSONValue(-real.infinity).toString(SpecialFloats.yes) == q{"-Infinite"}); infString = '"' ~ JSONFloatLiteral.inf ~ '"',
negativeInfString = '"' ~ JSONFloatLiteral.negativeInf ~ '"',
}
// when passing SpecialFloats.no, thow on converting NaN/Inf // when passing JsonSpecialFloats.yes, encode NaN/Inf as strings
assert(JSONValue(float.nan).toString(JsonSpecialFloats.yes) == nanString);
assert(JSONValue(double.infinity).toString(JsonSpecialFloats.yes) == infString);
assert(JSONValue(-real.infinity).toString(JsonSpecialFloats.yes) == negativeInfString);
// when passing JsonSpecialFloats.no, thow on converting NaN/Inf
assertThrown!JSONException(JSONValue(float.nan).toString); assertThrown!JSONException(JSONValue(float.nan).toString);
assertThrown!JSONException(JSONValue(double.infinity).toString); assertThrown!JSONException(JSONValue(double.infinity).toString);
assertThrown!JSONException(JSONValue(-real.infinity).toString); assertThrown!JSONException(JSONValue(-real.infinity).toString);
// when parsing json with JsonSpecialFloats.yes, decode special strings as floats
JSONValue jvNan = parseJSON(nanString, JsonSpecialFloats.yes);
JSONValue jvInf = parseJSON(infString, JsonSpecialFloats.yes);
JSONValue jvNegInf = parseJSON(negativeInfString, JsonSpecialFloats.yes);
assert(jvNan.floating.isNaN);
assert(jvInf.floating.isInfinity && jvInf.floating > 0);
assert(jvNegInf.floating.isInfinity && jvNegInf.floating < 0);
// when parsing json with JsonSpecialFloats.no, decode special strings as strings
jvNan = parseJSON(nanString);
jvInf = parseJSON(infString);
jvNegInf = parseJSON(negativeInfString);
assert(jvNan.str == JSONFloatLiteral.nan);
assert(jvInf.str == JSONFloatLiteral.inf);
assert(jvNegInf.str == JSONFloatLiteral.negativeInf);
} }