Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
57 changes: 49 additions & 8 deletions include/internal/json_converter.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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 {
Expand Down
19 changes: 19 additions & 0 deletions tests/test_csv_row_json.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Loading