diff --git a/include/internal/json_converter.hpp b/include/internal/json_converter.hpp index 0b632269..e36a6a86 100644 --- a/include/internal/json_converter.hpp +++ b/include/internal/json_converter.hpp @@ -93,26 +93,65 @@ namespace csv { return out; } - inline void append_json_number_without_leading_zeros( - std::string& out, - csv::string_view value - ) noexcept { + CSV_CONST inline bool is_ascii_space(char c) noexcept { + return c == ' ' || (c >= '\t' && c <= '\r'); + } + + /** Append a numeric field as a JSON number. + * + * data_type() accepts surrounding whitespace, a '+' sign, leading zeros, + * and a '.' without a digit on one side, none of which JSON allows. + */ + inline void append_json_number(std::string& out, csv::string_view value) noexcept { size_t first = 0; - if (first < value.size() && (value[first] == '+' || value[first] == '-')) { + size_t last = value.size(); + while (first < last && is_ascii_space(value[first])) { + ++first; + } + + while (last > first && is_ascii_space(value[last - 1])) { + --last; + } + + if (first < last && (value[first] == '+' || value[first] == '-')) { if (value[first] == '-') { out += '-'; } ++first; } - while (first + 1 < value.size() + while (first + 1 < last && value[first] == '0' && value[first + 1] >= '0' && value[first + 1] <= '9') { ++first; } - out.append(value.data() + first, value.size() - first); + if (first < last && value[first] == '.') { + out += '0'; + } + + for (size_t i = first; i < last; ++i) { + out += value[i]; + if (value[i] == '.' && (i + 1 == last || value[i + 1] < '0' || value[i + 1] > '9')) { + out += '0'; + } + } + } + + /** JSON has no hexadecimal literal, so write 0x-prefixed integers in decimal. */ + inline bool append_json_hex_integer(std::string& out, csv::string_view value) { + if (value.find_first_of("xX") == csv::string_view::npos) { + return false; + } + + long long parsed = 0; + if (!classify_scalar::parse_hex(value.data(), value.data() + value.size(), parsed)) { + return false; + } + + out += std::to_string(parsed); + return true; } class JsonConverter { @@ -235,7 +274,9 @@ namespace csv { void append_json_value(std::string& out, csv::string_view value) const { const DataType type = internals::data_type(value); if (type >= DataType::CSV_INT8 && type <= DataType::CSV_DOUBLE) { - append_json_number_without_leading_zeros(out, value); + if (type > DataType::CSV_INT64 || !append_json_hex_integer(out, value)) { + append_json_number(out, value); + } } else if (type == DataType::CSV_NULL) { out += "null"; } else { diff --git a/tests/test_csv_row_json.cpp b/tests/test_csv_row_json.cpp index e1ac8dc2..0fc3a479 100644 --- a/tests/test_csv_row_json.cpp +++ b/tests/test_csv_row_json.cpp @@ -118,6 +118,25 @@ TEST_CASE("CSVRow JSON serialization removes numeric leading zeros", "[csv_row_t REQUIRE(row.to_json_array() == "[69,-42,9,0,0.5,1e2]"); } +TEST_CASE("CSVRow JSON serialization writes valid numbers for every numeric field", "[csv_row_to_json]") { + CSVRow row = make_csv_row( + { ".5", "-.5", "+.5", "5.", "1.e5", " 08 ", " +2", "10.25" }, + { "no_int", "neg_no_int", "pos_no_int", "no_frac", "no_frac_exp", "padded_zero", "padded_sign", "plain" } + ); + + REQUIRE(row.to_json_array() == "[0.5,-0.5,0.5,5.0,1.0e5,8,2,10.25]"); +} + +TEST_CASE("CSVRow JSON serialization writes hexadecimal integers in decimal", "[csv_row_to_json]") { + // Classification and hex conversion must agree on whitespace and a leading '+'. + CSVRow row = make_csv_row( + { "0x1F", "-0X10", "0x7FFFFFFFFFFFFFFF", "10", " \t+0X2A\r\n" }, + { "hex", "neg_hex", "max_hex", "decimal", "padded_hex" } + ); + + REQUIRE(row.to_json_array() == "[31,-16,9223372036854775807,10,42]"); +} + // Reported in: https://github.com/vincentlaucsb/csv-parser/issues/68 TEST_CASE("CSVRow to_json() with Wrong Columns", "[csv_json_wrong_cols]") { std::stringstream csv_string(R"(A,B,C,