From 3d538649917d79a1bc605f84b907a574dfe08c5c Mon Sep 17 00:00:00 2001 From: "Yukihiro \"Matz\" Matsumoto" Date: Fri, 6 Mar 2026 14:00:15 +0900 Subject: [PATCH] fp_uscale.c: add shortest representation for Float#to_s Use uscale-based shortest() to compute the minimal decimal string that uniquely identifies each double. This guarantees perfect round-trip (parse(to_s(x)) == x) while keeping output concise (e.g. 0.1 prints as "0.1", not "0.10000000000000001"). Co-authored-by: Claude --- src/fp_uscale.c | 173 ++++++++++++++++++++++++++++++++++++++++++++++-- src/numeric.c | 7 +- 2 files changed, 169 insertions(+), 11 deletions(-) diff --git a/src/fp_uscale.c b/src/fp_uscale.c index 0ade7bc89..071f71c9f 100644 --- a/src/fp_uscale.c +++ b/src/fp_uscale.c @@ -908,6 +908,124 @@ static void fixed_width(double f, int n, uint64_t *d, int *p) *p = -(*p); } +/* + * skewed returns floor(log10(3/4 * 2^e)) + * Used for shortest-width printing at powers of 2 + */ +static inline int skewed(int e) +{ + return (e * 631305 - 261663) >> 21; +} + +/* + * trim_zeros removes trailing zeros from x * 10^p + */ +static void trim_zeros(uint64_t *x, int *p) +{ + const uint64_t inv5 = 0xcccccccccccccccdULL; + const uint64_t inv5p2 = 0x8f5c28f5c28f5c29ULL; + const uint64_t inv5p4 = 0xd288ce703afb7e91ULL; + const uint64_t inv5p8 = 0xc767074b22e90e21ULL; + uint64_t d; + + /* cut 1 zero, or else return */ + d = (*x * inv5); + d = (d >> 1) | (d << 63); + if (d <= UINT64_MAX / 10) { + *x = d; + (*p)++; + } + else { + return; + } + + /* cut 8 zeros */ + d = (*x * inv5p8); + d = (d >> 8) | (d << 56); + if (d <= UINT64_MAX / 100000000ULL) { + *x = d; + *p += 8; + } + + /* cut 4 zeros */ + d = (*x * inv5p4); + d = (d >> 4) | (d << 60); + if (d <= UINT64_MAX / 10000) { + *x = d; + *p += 4; + } + + /* cut 2 zeros */ + d = (*x * inv5p2); + d = (d >> 2) | (d << 62); + if (d <= UINT64_MAX / 100) { + *x = d; + *p += 2; + } + + /* cut 1 zero */ + d = (*x * inv5); + d = (d >> 1) | (d << 63); + if (d <= UINT64_MAX / 10) { + *x = d; + (*p)++; + } +} + +/* + * shortest computes the shortest formatting of f + * Returns d and p such that d * 10^p equals f when parsed + */ +static void shortest(double f, uint64_t *d, int *p) +{ + const int min_exp = -1085; + uint64_t m, min, max; + int e, z, odd, lp; + scaler pre; + uint64_t dmin, dmax; + + unpack64(f, &m, &e); + z = 11; + + if (m == (1ULL << 63) && e > min_exp) { + *p = -skewed(e + z); + min = m - (1ULL << (z - 2)); + } + else { + if (e < min_exp) { + z = 11 + (min_exp - e); + } + *p = -log10_pow2(e + z); + min = m - (1ULL << (z - 1)); + } + max = m + (1ULL << (z - 1)); + odd = (int)((m >> z) & 1); + + lp = log2_pow10(*p); + pre = prescale(e, *p, lp); + + dmin = ur_ceil(ur_nudge(uscale(min, pre), +odd)); + dmax = ur_floor(ur_nudge(uscale(max, pre), -odd)); + + /* check if a multiple of 10 is in range */ + *d = dmax / 10; + if (*d * 10 >= dmin) { + int new_p = -(*p - 1); + trim_zeros(d, &new_p); + *p = new_p; + return; + } + + /* multiple valid values: pick the rounded one */ + if (dmin < dmax) { + *d = ur_round(uscale(m, pre)); + } + else { + *d = dmin; + } + *p = -(*p); +} + static double parse_decimal(uint64_t d, int p) { int b, lp, e; @@ -1033,10 +1151,16 @@ mrb_format_float(mrb_float f, char *buf, size_t buf_size, char fmt, int prec, ch } } - if (prec < 0) prec = 6; - e_char = 'E' | (fmt & 0x20); - fmt |= 0x20; - if (fmt == 'g' && prec == 0) prec = 1; + { + int use_shortest = (prec == -2); + if (prec < 0) prec = 6; + e_char = 'E' | (fmt & 0x20); + fmt |= 0x20; + if (fmt == 'g') { + if (use_shortest) fmt = 'S'; /* shortest mode */ + else if (prec == 0) prec = 1; + } + } if (f == 0.0) { /* zero */ @@ -1074,7 +1198,46 @@ mrb_format_float(mrb_float f, char *buf, size_t buf_size, char fmt, int prec, ch int p, nd, exp; char digs[20]; - if (fmt == 'g') { + if (fmt == 'S') { + /* shortest representation for to_s */ + int i; + shortest((double)f, &d, &p); + nd = count_digits(d); + exp = p + nd - 1; + format_base10(digs, nd, d); + +#ifdef MRB_USE_FLOAT32 + if (exp < -4 || exp >= 7) { +#else + if (exp < -4 || exp >= 15) { +#endif + /* e format */ + *s++ = digs[0]; + if (nd > 1) { + *s++ = '.'; + for (i = 1; i < nd; i++) *s++ = digs[i]; + } + s += emit_exp(s, e_char, exp); + } + else { + /* f format */ + if (exp < 0) { + *s++ = '0'; + *s++ = '.'; + for (i = 0; i < -(exp + 1); i++) *s++ = '0'; + for (i = 0; i < nd; i++) *s++ = digs[i]; + } + else { + for (i = 0; i <= exp && i < nd; i++) *s++ = digs[i]; + for (; i <= exp; i++) *s++ = '0'; + if (nd > exp + 1) { + *s++ = '.'; + for (i = exp + 1; i < nd; i++) *s++ = digs[i]; + } + } + } + } + else if (fmt == 'g') { /* g/G format */ int fprec; fixed_width((double)f, prec, &d, &p); diff --git a/src/numeric.c b/src/numeric.c index 7c6b08469..d3f370d7f 100644 --- a/src/numeric.c +++ b/src/numeric.c @@ -418,13 +418,8 @@ mrb_value mrb_float_to_str(mrb_state *mrb, mrb_value flo, const char *fmt) { char buf[25]; -#ifdef MRB_USE_FLOAT32 - const int prec = 7; -#else - const int prec = 15; -#endif - mrb_format_float(mrb_float(flo), buf, sizeof(buf), 'g', prec, '\0'); + mrb_format_float(mrb_float(flo), buf, sizeof(buf), 'g', -2, '\0'); for (char *p = buf; *p; p++) { if (*p == '.') goto exit; if (*p == 'e') {