aboutsummaryrefslogtreecommitdiffstats
path: root/core
diff options
context:
space:
mode:
authoreunhhu <[email protected]>2026-09-28 10:01:54 +0100
committereunhhu <[email protected]>2026-09-28 10:01:54 +0100
commit52a3623f4371b632c0df326c91ff97f44f921094 (patch)
treec857deb741f7b29f43880b5a1aa68e5828fe9427 /core
parentcdeee266d3d0f22cd245c6febf69e71a00b1adb6 (diff)
downloadpure-52a3623f4371b632c0df326c91ff97f44f921094.tar.gz
pure-52a3623f4371b632c0df326c91ff97f44f921094.zip
Read and write numbers exactly without strtod or printf in the common cases
Diffstat (limited to 'core')
-rw-r--r--core/module-text.c29
-rw-r--r--core/test-module.c36
2 files changed, 65 insertions, 0 deletions
diff --git a/core/module-text.c b/core/module-text.c
index 68c03b0..4ce1769 100644
--- a/core/module-text.c
+++ b/core/module-text.c
@@ -72,6 +72,35 @@ size_t pure_format_number(double value, char out[32]) {
out[length] = 0;
return length;
}
+ /* The fewest decimals k whose digits, the value * 10^k rounded to the
+ * nearest integer m, read back as the value: %g's digits. m and 10^k are
+ * exact (m < 2^53, k <= 22), so m / 10^k is the correctly rounded value of
+ * those digits, as reading them is; the product's rounding error (fma)
+ * tells the nearest m, and a half between two goes to %g. Else, and for
+ * what %g would write with an exponent, the fewest %g digits that do. */
+ static const double tens[] = {1e0, 1e1, 1e2, 1e3, 1e4, 1e5, 1e6, 1e7, 1e8, 1e9, 1e10, 1e11,
+ 1e12, 1e13, 1e14, 1e15, 1e16};
+ double magnitude = fabs(value);
+ if (magnitude >= 1e-4 && magnitude < 1e15)
+ for (int k = 1; k <= 16; ++k) {
+ double scaled = magnitude * tens[k];
+ if (scaled >= 9007199254740992.0) break;
+ uint64_t m = (uint64_t)scaled;
+ double beyond = (scaled - (double)m) + fma(magnitude, tens[k], -scaled);
+ if (beyond == 0.5) break;
+ m += beyond > 0.5;
+ if ((double)m / tens[k] != magnitude) continue;
+ char digits[24];
+ size_t count = 0, length = 0;
+ do { digits[count++] = (char)('0' + m % 10); m /= 10; } while (m);
+ while (count <= (size_t)k) digits[count++] = '0'; /* a leading zero before the point */
+ if (value < 0) out[length++] = '-';
+ while (count > (size_t)k) out[length++] = digits[--count];
+ out[length++] = '.';
+ while (count) out[length++] = digits[--count];
+ out[length] = 0;
+ return length;
+ }
for (int precision = 1; precision <= 17; ++precision) {
snprintf(out, 32, "%.*g", precision, value);
if (strtod(out, NULL) == value) break;
diff --git a/core/test-module.c b/core/test-module.c
index d5b7eee..8ca031c 100644
--- a/core/test-module.c
+++ b/core/test-module.c
@@ -3987,7 +3987,43 @@ static void timers(void) {
CHECK(load(bad, &module_b) == PURE_INVALID && strstr(error, "view with an effect"));
}
+/* A number is written with the fewest digits that read back as it, the
+ * digits %g gives (with its exponent's leading zeros dropped): the same
+ * text as that search over precisions, for values of every kind. */
+static void number_texts(void) {
+ uint32_t seed = 0x6e756d73u;
+ size_t checked = 0;
+ for (unsigned i = 0; i < 300000; ++i) {
+ double value;
+ uint64_t bits = (uint64_t)next_random(&seed) << 32 | next_random(&seed);
+ switch (i % 5) {
+ case 0: value = (double)((int64_t)(bits % 2000001) - 1000000) / 100.0; break; /* money */
+ case 1: value = (double)(bits % 20000001) / pow(10, (double)(bits >> 40 & 7)); break; /* decimals */
+ case 2: memcpy(&value, &bits, sizeof(value)); if (!isfinite(value)) value = 0.75; break;
+ case 3: value = (double)(bits % 100000) / 7.0; break;
+ default: value = ldexp((double)(bits >> 11), (int)(bits % 80) - 60); break;
+ }
+ char got[32], want[32];
+ pure_format_number(value, got);
+ if (value == 0) strcpy(want, "0");
+ else if (value == floor(value) && fabs(value) < 1e15) snprintf(want, sizeof(want), "%.0f", value);
+ else {
+ for (int precision = 1; precision <= 17; ++precision) {
+ snprintf(want, sizeof(want), "%.*g", precision, value);
+ if (strtod(want, NULL) == value) break;
+ }
+ char *exponent = strchr(want, 'e');
+ if (exponent)
+ while (exponent[2] == '0' && exponent[3]) memmove(exponent + 2, exponent + 3, strlen(exponent + 2));
+ }
+ CHECK(!strcmp(got, want) && strtod(got, NULL) == value);
+ ++checked;
+ }
+ printf("{\"number_texts\":%zu}\n", checked);
+}
+
int main(void) {
+ number_texts();
behaviour();
faults();
rejections();