From e28f1905acce77bbffdfac51b6059e5b1bca2284 Mon Sep 17 00:00:00 2001 From: Yarchik Date: Thu, 23 Jul 2026 17:31:22 +0100 Subject: [PATCH] Fix ln() rounding up on a result just below a rounding tie MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit For an argument whose natural log lands just below a rounding tie (the digit after the rounding position is followed by a long run of 9s, i.e. `...d 4999...`), ln() rounded up instead of down, violating the documented "correctly rounded" guarantee. naturalLogarithm/naturalExponential drive checkRoundingDigits with the `repeating` flag. In that branch the argument-reduction reconstruction can land the working value on a false exact-tie form (`...5000` / `...0000`), but the repeating branch only detected a trailing `...9999` run — unlike the non-repeating branch, which also detects `...5000`/`...0000` ties. So the near-tie was treated as resolved and finalise() rounded the false tie up. Mirror the tie/zero detection the non-repeating branch already has. Verified: a near-tie sweep of ln and exp across the five half-rounding modes (20000 cases) vs an 80-digit oracle now reports 0 mis-roundings (several were wrong before), and the full test suite passes. --- decimal.js | 6 ++++-- decimal.mjs | 6 ++++-- test/modules/ln.js | 9 +++++++++ 3 files changed, 17 insertions(+), 4 deletions(-) diff --git a/decimal.js b/decimal.js index 1bb9a4a..ab82d1c 100644 --- a/decimal.js +++ b/decimal.js @@ -2595,11 +2595,13 @@ if (i == 0) rd = rd / 1000 | 0; else if (i == 1) rd = rd / 100 | 0; else if (i == 2) rd = rd / 10 | 0; - r = (repeating || rm < 4) && rd == 9999 || !repeating && rm > 3 && rd == 4999; + r = (repeating || rm < 4) && rd == 9999 || !repeating && rm > 3 && rd == 4999 + || rd == 5000 || rd == 0; } else { r = ((repeating || rm < 4) && rd + 1 == k || (!repeating && rm > 3) && rd + 1 == k / 2) && - (d[di + 1] / k / 1000 | 0) == mathpow(10, i - 3) - 1; + (d[di + 1] / k / 1000 | 0) == mathpow(10, i - 3) - 1 + || (rd == k / 2 || rd == 0) && (d[di + 1] / k / 1000 | 0) == 0; } } diff --git a/decimal.mjs b/decimal.mjs index 522effa..957ba3b 100644 --- a/decimal.mjs +++ b/decimal.mjs @@ -2591,11 +2591,13 @@ function checkRoundingDigits(d, i, rm, repeating) { if (i == 0) rd = rd / 1000 | 0; else if (i == 1) rd = rd / 100 | 0; else if (i == 2) rd = rd / 10 | 0; - r = (repeating || rm < 4) && rd == 9999 || !repeating && rm > 3 && rd == 4999; + r = (repeating || rm < 4) && rd == 9999 || !repeating && rm > 3 && rd == 4999 + || rd == 5000 || rd == 0; } else { r = ((repeating || rm < 4) && rd + 1 == k || (!repeating && rm > 3) && rd + 1 == k / 2) && - (d[di + 1] / k / 1000 | 0) == mathpow(10, i - 3) - 1; + (d[di + 1] / k / 1000 | 0) == mathpow(10, i - 3) - 1 + || (rd == k / 2 || rd == 0) && (d[di + 1] / k / 1000 | 0) == 0; } } diff --git a/test/modules/ln.js b/test/modules/ln.js index 0df6d16..8f3cc45 100644 --- a/test/modules/ln.js +++ b/test/modules/ln.js @@ -31,6 +31,15 @@ T('ln', function () { t('NaN', String(Math.log(NaN)), 40, 4); t('2.7182818284590452353602874713526624977572', '1', 39, 4); + // Correctly-rounded ln where the result lands just below a rounding tie + // (...d 4999...): these rounded up instead of down before the guard-digit + // fix in checkRoundingDigits. + t('4.25914212183600678912449997353532582823393815', '1.4490677601509316343', 20, 4); + t('1.6852358445909596506369573334700403721376594', '0.52190552112613867979', 20, 4); + t('147.901327174696092190385346999167895276772254', '4.9965453431362365587', 20, 4); + t('7.65606302212512952930223042181358382147654373', '2.0354978858468052277', 20, 4); + t('24.7812098757520159815273195615677780988161707', '3.2100856996964392732', 20, 4); + t('91247532.65728', '18.3290865106890306', 19, 1); t('727579403.9', '20.40523369730819818291393006', 28, 3); t('419065154.52076499608', '19.8535369658470908', 19, 2);