jinja : write floats in tojson as Python does (#30362)
tojson printed floats through a C++ stream (6 significant digits, no ".0"), so 48.858844 became 48.8588 and 1.0 became 1 in tool definitions, tool-call arguments and other template output. Write the shortest digits that read back as the same value, as json.dumps does. The string and safe filters of a float use the same code.
This commit is contained in:
1 parent
c5a997624a
commit
77390baa18
2 files changed
+90
-1
No files matched your search
+65
-1
@@ -8,6 +8,11 @@
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
#include <cctype>
|
||||
#include <cfloat>
|
||||
#include <cmath>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <vector>
|
||||
#include <optional>
|
||||
#include <algorithm>
|
||||
@@ -1517,6 +1522,65 @@ void global_from_json(context & ctx, const common_json & json_obj, bool mark_inp
|
||||
}
|
||||
}
|
||||
|
||||
// a float as Python writes it (repr, json.dumps): the shortest digits that read back as the same value
|
||||
static std::string float_to_json(double val) {
|
||||
if (!std::isfinite(val)) {
|
||||
return std::isnan(val) ? "nan" : val < 0 ? "-inf" : "inf"; // as before (json.dumps writes NaN and Infinity, str() writes nan and inf)
|
||||
}
|
||||
const double mag = std::fabs(val);
|
||||
|
||||
// at a power of 2 the gap to the value below is half the gap above, the nearest digits can miss while the next ones up read back
|
||||
int exp2 = 0;
|
||||
const bool is_pow2 = std::frexp(mag, &exp2) == 0.5;
|
||||
|
||||
// a normal double has at most one decimal of up to DBL_DIG digits that reads back as it, the search can start at DBL_DIG digits
|
||||
// 17 digits always read back
|
||||
std::string digits;
|
||||
int exp10 = 0;
|
||||
for (int prec = mag < DBL_MIN ? 0 : DBL_DIG - 1; digits.empty(); prec++) {
|
||||
char buf[32];
|
||||
snprintf(buf, sizeof(buf), "%.*e", prec, mag);
|
||||
const char * e = std::strchr(buf, 'e');
|
||||
// only the digits, the decimal point depends on the locale
|
||||
std::string d(buf, e - buf);
|
||||
d.erase(std::remove_if(d.begin(), d.end(), [](char c) { return c < '0' || c > '9'; }), d.end());
|
||||
exp10 = std::atoi(e + 1);
|
||||
const auto reads_back = [&](const std::string & cand) {
|
||||
return std::strtod((cand + "e" + std::to_string(exp10 - prec)).c_str(), nullptr) == mag;
|
||||
};
|
||||
if (prec == 16 || reads_back(d)) {
|
||||
digits = d;
|
||||
} else if (is_pow2 && d.back() != '9') {
|
||||
d.back()++;
|
||||
if (reads_back(d)) {
|
||||
digits = d;
|
||||
}
|
||||
}
|
||||
}
|
||||
while (digits.size() > 1 && digits.back() == '0') {
|
||||
digits.pop_back();
|
||||
}
|
||||
|
||||
// fixed notation for 1e-4 <= |val| < 1e16, else d.ddde+XX
|
||||
const int n = static_cast<int>(digits.size());
|
||||
const int point = exp10 + 1; // number of digits before the decimal point
|
||||
std::string out = std::signbit(val) ? "-" : "";
|
||||
if (point > -4 && point <= 16) {
|
||||
if (point <= 0) {
|
||||
out += "0." + std::string(-point, '0') + digits;
|
||||
} else if (point >= n) {
|
||||
out += digits + std::string(point - n, '0') + ".0";
|
||||
} else {
|
||||
out += digits.substr(0, point) + "." + digits.substr(point);
|
||||
}
|
||||
} else {
|
||||
char buf[16];
|
||||
snprintf(buf, sizeof(buf), "e%c%02d", exp10 < 0 ? '-' : '+', std::abs(exp10));
|
||||
out += digits.substr(0, 1) + (n > 1 ? "." + digits.substr(1) : "") + buf;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
// recursively convert value to JSON string
|
||||
// TODO: avoid circular references
|
||||
static void value_to_json_internal(std::ostringstream & oss, const value & val, int curr_lvl, int indent, const std::string_view item_sep, const std::string_view key_sep) {
|
||||
@@ -1534,7 +1598,7 @@ static void value_to_json_internal(std::ostringstream & oss, const value & val,
|
||||
} else if (is_val<value_int>(val)) {
|
||||
oss << val->as_int();
|
||||
} else if (is_val<value_float>(val)) {
|
||||
oss << val->as_float();
|
||||
oss << float_to_json(val->as_float());
|
||||
} else if (is_val<value_string>(val)) {
|
||||
oss << "\"";
|
||||
for (char c : val->as_string().str()) {
|
||||
|
||||
@@ -1026,6 +1026,31 @@ static void test_literals(testing & t) {
|
||||
"3.14"
|
||||
);
|
||||
|
||||
// Python writes the shortest digits that read back as the same value
|
||||
test_template(t, "tojson floats",
|
||||
"{{ data|tojson }}",
|
||||
{{"data", json::array({0.1, 1.0, 100.0, 8.3, 123456789.0, 48.858844, 0.30000000000000004, -0.0, 5.960464477539063e-08})}},
|
||||
"[0.1, 1.0, 100.0, 8.3, 123456789.0, 48.858844, 0.30000000000000004, -0.0, 5.960464477539063e-08]"
|
||||
);
|
||||
|
||||
test_template(t, "tojson floats, exponent",
|
||||
"{{ data|tojson }}",
|
||||
{{"data", json::array({1e15, 1e16, 0.0001, 0.00001, 1.5e10, -2.5e-7, 5e-324, 1.7976931348623157e308})}},
|
||||
"[1000000000000000.0, 1e+16, 0.0001, 1e-05, 15000000000.0, -2.5e-07, 5e-324, 1.7976931348623157e+308]"
|
||||
);
|
||||
|
||||
test_template(t, "tojson floats, nested with indent=2",
|
||||
"{{ data|tojson(indent=2) }}",
|
||||
{{"data", {{"total", 20.0}, {"items", json::array({{{"price", 9.99}, {"qty", 2}}})}}}},
|
||||
"{\n \"total\": 20.0,\n \"items\": [\n {\n \"price\": 9.99,\n \"qty\": 2\n }\n ]\n}"
|
||||
);
|
||||
|
||||
test_template(t, "float|string",
|
||||
"{{ a|string }} {{ b|string }} {{ (b * 10)|string }}",
|
||||
{{"a", 1.0}, {"b", 1e308}},
|
||||
"1.0 1e+308 inf"
|
||||
);
|
||||
|
||||
test_template(t, "string|tojson",
|
||||
"{{ 'hello' | tojson }}",
|
||||
json::object(),
|
||||
|
||||
Reference in new issue
Block a user