Add support for writing NaN/Infinity if SIMDJSON_ENABLE_NAN_INF is enabled (#2711)

* Add tests for dumping out NaN/Infinity to the json builder

* If SIMDJSON_ENABLE_NAN_INF is set, write out NaN and Infinity

* Add tests for dumping out NaN/Infinity when serializing DOM

* If SIMDJSON_ENABLE_NAN_INF, use 'NaN' and 'Infinity' for nan/inf

* Fix bug where 'Inf' as a root atom + spaces of padding parses incorrectly

* Update FracturedJson printers so tables containing NaN/Infinity are aligned
This commit is contained in:
Alecto Irene Perez
2026-05-07 13:11:03 -04:00
committed by GitHub
parent 0a23ebbfef
commit 376ef7e8e9
6 changed files with 388 additions and 24 deletions
+162 -20
View File
@@ -1,5 +1,8 @@
#include "simdjson.h"
#include "test_builder.h"
#include <array>
#include <cmath>
#include <limits>
#include <map>
#include <string>
#include <string_view>
@@ -17,28 +20,27 @@ struct Car {
std::vector<double> tire_pressure;
}; // Car
#if SIMDJSON_SUPPORTS_CONCEPTS
struct Car2549 {
std::string make;
std::string model;
int64_t year;
std::vector<float> tire_pressure;
std::string make;
std::string model;
int64_t year;
std::vector<float> tire_pressure;
};
namespace simdjson {
// we intentionally pass by non-const reference to car.
template <typename builder_type>
void tag_invoke(serialize_tag, builder_type& builder, Car2549& car) {
builder.start_object();
builder.append_key_value("make", car.make);
builder.append_comma();
builder.append_key_value("model", car.model);
builder.append_comma();
builder.append_key_value("year", car.year);
builder.append_comma();
builder.append_key_value("tire_pressure", car.tire_pressure);
builder.end_object();
}
// we intentionally pass by non-const reference to car.
template <typename builder_type>
void tag_invoke(serialize_tag, builder_type &builder, Car2549 &car) {
builder.start_object();
builder.append_key_value("make", car.make);
builder.append_comma();
builder.append_key_value("model", car.model);
builder.append_comma();
builder.append_key_value("year", car.year);
builder.append_comma();
builder.append_key_value("tire_pressure", car.tire_pressure);
builder.end_object();
}
} // namespace simdjson
static_assert(simdjson::require_custom_serialization<Car2549>);
@@ -160,6 +162,140 @@ bool append_float() {
TEST_SUCCEED();
}
#if SIMDJSON_ENABLE_NAN_INF
bool append_nan() {
TEST_START();
simdjson::builder::string_builder sb;
sb.append(std::numeric_limits<double>::quiet_NaN());
std::string_view p;
ASSERT_SUCCESS(sb.view().get(p));
ASSERT_EQUAL(p, "NaN");
TEST_SUCCEED();
}
bool append_positive_infinity() {
TEST_START();
simdjson::builder::string_builder sb;
sb.append(std::numeric_limits<double>::infinity());
std::string_view p;
ASSERT_SUCCESS(sb.view().get(p));
ASSERT_EQUAL(p, "Infinity");
TEST_SUCCEED();
}
bool append_negative_infinity() {
TEST_START();
simdjson::builder::string_builder sb;
sb.append(-std::numeric_limits<double>::infinity());
std::string_view p;
ASSERT_SUCCESS(sb.view().get(p));
ASSERT_EQUAL(p, "-Infinity");
TEST_SUCCEED();
}
bool append_float_nan_inf() {
TEST_START();
{
simdjson::builder::string_builder sb;
sb.append(std::numeric_limits<float>::quiet_NaN());
std::string_view p;
ASSERT_SUCCESS(sb.view().get(p));
ASSERT_EQUAL(p, "NaN");
}
{
simdjson::builder::string_builder sb;
sb.append(std::numeric_limits<float>::infinity());
std::string_view p;
ASSERT_SUCCESS(sb.view().get(p));
ASSERT_EQUAL(p, "Infinity");
}
{
simdjson::builder::string_builder sb;
sb.append(-std::numeric_limits<float>::infinity());
std::string_view p;
ASSERT_SUCCESS(sb.view().get(p));
ASSERT_EQUAL(p, "-Infinity");
}
TEST_SUCCEED();
}
bool nan_inf_in_array() {
TEST_START();
simdjson::builder::string_builder sb;
sb.start_array();
sb.append(1.5);
sb.append_comma();
sb.append(std::numeric_limits<double>::quiet_NaN());
sb.append_comma();
sb.append(std::numeric_limits<double>::infinity());
sb.append_comma();
sb.append(-std::numeric_limits<double>::infinity());
sb.append_comma();
sb.append(2.5);
sb.end_array();
std::string_view p;
ASSERT_SUCCESS(sb.view().get(p));
ASSERT_EQUAL(p, "[1.5,NaN,Infinity,-Infinity,2.5]");
TEST_SUCCEED();
}
bool nan_inf_in_object() {
TEST_START();
simdjson::builder::string_builder sb;
sb.start_object();
sb.append_key_value("a", std::numeric_limits<double>::quiet_NaN());
sb.append_comma();
sb.append_key_value("b", std::numeric_limits<double>::infinity());
sb.append_comma();
sb.append_key_value("c", -std::numeric_limits<double>::infinity());
sb.end_object();
std::string_view p;
ASSERT_SUCCESS(sb.view().get(p));
ASSERT_EQUAL(p, "{\"a\":NaN,\"b\":Infinity,\"c\":-Infinity}");
TEST_SUCCEED();
}
bool nan_inf_roundtrip() {
TEST_START();
simdjson::builder::string_builder sb;
sb.start_array();
sb.append(std::numeric_limits<double>::quiet_NaN());
sb.append_comma();
sb.append(std::numeric_limits<double>::infinity());
sb.append_comma();
sb.append(-std::numeric_limits<double>::infinity());
sb.end_array();
std::string_view p;
ASSERT_SUCCESS(sb.view().get(p));
simdjson::padded_string output{p};
simdjson::dom::parser parser;
simdjson::dom::element doc;
ASSERT_SUCCESS(parser.parse(output).get(doc));
simdjson::dom::array arr;
ASSERT_SUCCESS(doc.get_array().get(arr));
std::array<double, 3> expected{
std::numeric_limits<double>::quiet_NaN(),
std::numeric_limits<double>::infinity(),
-std::numeric_limits<double>::infinity(),
};
size_t index = 0;
for (auto val : arr) {
double parsed;
ASSERT_SUCCESS(val.get_double().get(parsed));
if (std::isnan(expected[index])) {
ASSERT_TRUE(std::isnan(parsed));
} else {
ASSERT_EQUAL(parsed, expected[index]);
}
index++;
}
ASSERT_EQUAL(index, expected.size());
TEST_SUCCEED();
}
#endif // SIMDJSON_ENABLE_NAN_INF
bool append_null() {
TEST_START();
simdjson::builder::string_builder sb;
@@ -459,14 +595,15 @@ bool car_test() {
bool issue2549() {
TEST_START();
simdjson::builder::string_builder sb;
Car2549 c = { "Toyota", "Corolla", 2017, {1.0f,2.0f,3.0f} };
Car2549 c = {"Toyota", "Corolla", 2017, {1.0f, 2.0f, 3.0f}};
sb.start_object();
sb.append_key_value("car", c);
sb.end_object();
std::string_view p;
auto result = sb.view().get(p);
ASSERT_SUCCESS(result);
ASSERT_EQUAL(p, "{\"car\":{\"make\":\"Toyota\",\"model\":\"Corolla\",\"year\":2017,\"tire_pressure\":[1.0,2.0,3.0]}}");
ASSERT_EQUAL(p, "{\"car\":{\"make\":\"Toyota\",\"model\":\"Corolla\","
"\"year\":2017,\"tire_pressure\":[1.0,2.0,3.0]}}");
TEST_SUCCEED();
}
@@ -661,6 +798,11 @@ bool run() {
issue2549() && car_test_template() && serialize_optional() &&
#endif
append_char() && append_integer() && append_float() && append_null() &&
#if SIMDJSON_ENABLE_NAN_INF
append_nan() && append_positive_infinity() &&
append_negative_infinity() && append_float_nan_inf() &&
nan_inf_in_array() && nan_inf_in_object() && nan_inf_roundtrip() &&
#endif
clear() && escape_and_append() && escape_and_append_with_quotes() &&
append_raw() && raw_with_length() && string_convertion() &&
buffer_growth() && unicode_validation() && true;
+163 -2
View File
@@ -1,7 +1,9 @@
#include "simdjson.h"
#include "test_macros.h"
#include "test_main.h"
#include <array>
#include <cmath>
#include <limits>
#include <string>
using namespace simdjson;
@@ -25,8 +27,10 @@ bool parse_nan() {
bool parse_infinity() {
TEST_START();
for (auto json_str :
{"infinity", "Infinity", "INFINITY", "inf", "Inf", "INF"}) {
for (auto json_str : {"infinity", "Infinity", "INFINITY", "inf", "Inf", "INF",
// Check that 'Inf' parses correctly even when padded to
// the same length as 'Infinity'
"inf ", "Inf ", "INF "}) {
dom::parser parser;
dom::element doc;
ASSERT_SUCCESS(
@@ -280,6 +284,154 @@ bool reject_truncated_atoms() {
TEST_SUCCEED();
}
// DOM printer (to_string / minify / prettify) tests. When NaN/Infinity
// parsing is enabled, the writer must emit the same literals on output so
// that round-tripping through the parser preserves the value.
bool print_nan() {
TEST_START();
dom::parser parser;
dom::element doc;
ASSERT_SUCCESS(parser.parse("NaN"_padded).get(doc));
ASSERT_EQUAL(simdjson::to_string(doc), "NaN");
ASSERT_EQUAL(simdjson::minify(doc), "NaN");
TEST_SUCCEED();
}
bool print_infinity() {
TEST_START();
dom::parser parser;
dom::element doc;
ASSERT_SUCCESS(parser.parse("Infinity"_padded).get(doc));
ASSERT_EQUAL(simdjson::to_string(doc), "Infinity");
ASSERT_EQUAL(simdjson::minify(doc), "Infinity");
TEST_SUCCEED();
}
bool print_negative_infinity() {
TEST_START();
dom::parser parser;
dom::element doc;
ASSERT_SUCCESS(parser.parse("-Infinity"_padded).get(doc));
ASSERT_EQUAL(simdjson::to_string(doc), "-Infinity");
ASSERT_EQUAL(simdjson::minify(doc), "-Infinity");
TEST_SUCCEED();
}
bool print_nan_in_array() {
TEST_START();
dom::parser parser;
dom::element doc;
ASSERT_SUCCESS(
parser.parse("[1.5, NaN, Infinity, -Infinity, 2.5]"_padded).get(doc));
ASSERT_EQUAL(simdjson::to_string(doc), "[1.5,NaN,Infinity,-Infinity,2.5]");
TEST_SUCCEED();
}
bool print_nan_in_object() {
TEST_START();
dom::parser parser;
dom::element doc;
ASSERT_SUCCESS(
parser.parse(R"({"a": NaN, "b": Infinity, "c": -Infinity})"_padded)
.get(doc));
ASSERT_EQUAL(simdjson::to_string(doc),
"{\"a\":NaN,\"b\":Infinity,\"c\":-Infinity}");
TEST_SUCCEED();
}
bool print_roundtrip() {
TEST_START();
dom::parser parser;
dom::element doc;
ASSERT_SUCCESS(parser.parse("[NaN, Infinity, -Infinity]"_padded).get(doc));
std::string serialized = simdjson::to_string(doc);
dom::parser parser2;
dom::element doc2;
ASSERT_SUCCESS(parser2.parse(padded_string(serialized)).get(doc2));
dom::array arr;
ASSERT_SUCCESS(doc2.get_array().get(arr));
std::array<double, 3> expected{
std::numeric_limits<double>::quiet_NaN(),
std::numeric_limits<double>::infinity(),
-std::numeric_limits<double>::infinity(),
};
size_t index = 0;
for (auto val : arr) {
double parsed;
ASSERT_SUCCESS(val.get_double().get(parsed));
if (std::isnan(expected[index])) {
ASSERT_TRUE(std::isnan(parsed));
} else {
ASSERT_EQUAL(parsed, expected[index]);
}
index++;
}
ASSERT_EQUAL(index, expected.size());
TEST_SUCCEED();
}
// FracturedJson aligns values into columns in table mode. The column width
// is driven by the estimator for unseen elements and by measure_value_length
// for the chosen cells; if either undercounts NaN/Infinity, column 1's
// padding won't match column 2's emitted width and rows visibly misalign.
// Force table mode with min_table_rows = 2 and max_inline_length = 0.
bool table_aligns_nan() {
TEST_START();
dom::parser parser;
dom::element doc;
ASSERT_SUCCESS(
parser.parse(R"([{"a": 1, "b": 1},{"a": NaN, "b": 1}])"_padded).get(doc));
fractured_json_options opts;
opts.min_table_rows = 2;
opts.max_inline_length = 0;
ASSERT_EQUAL(simdjson::fractured_json(doc, opts),
"[\n"
" { \"a\": 1 , \"b\": 1 },\n"
" { \"a\": NaN, \"b\": 1 }\n"
"]");
TEST_SUCCEED();
}
bool table_aligns_inf() {
TEST_START();
dom::parser parser;
dom::element doc;
ASSERT_SUCCESS(
parser.parse(R"([{"a": 1, "b": 1},{"a": Infinity, "b": 1}])"_padded)
.get(doc));
fractured_json_options opts;
opts.min_table_rows = 2;
opts.max_inline_length = 0;
ASSERT_EQUAL(simdjson::fractured_json(doc, opts),
"[\n"
" { \"a\": 1 , \"b\": 1 },\n"
" { \"a\": Infinity, \"b\": 1 }\n"
"]");
TEST_SUCCEED();
}
bool table_aligns_neg_inf() {
TEST_START();
dom::parser parser;
dom::element doc;
ASSERT_SUCCESS(
parser.parse(R"([{"a": 1, "b": 1},{"a": -Infinity, "b": 1}])"_padded)
.get(doc));
fractured_json_options opts;
opts.min_table_rows = 2;
opts.max_inline_length = 0;
ASSERT_EQUAL(simdjson::fractured_json(doc, opts),
"[\n"
" { \"a\": 1 , \"b\": 1 },\n"
" { \"a\": -Infinity, \"b\": 1 }\n"
"]");
TEST_SUCCEED();
}
bool run() {
return parse_nan() //
&& parse_infinity() //
@@ -291,6 +443,15 @@ bool run() {
&& reject_trailing_junk() //
&& reject_similar_prefix() //
&& reject_truncated_atoms() //
&& print_nan() //
&& print_infinity() //
&& print_negative_infinity() //
&& print_nan_in_array() //
&& print_nan_in_object() //
&& print_roundtrip() //
&& table_aligns_nan() //
&& table_aligns_inf() //
&& table_aligns_neg_inf() //
;
}