diff --git a/README.md b/README.md index 96ba4ca..3083b4e 100644 --- a/README.md +++ b/README.md @@ -438,8 +438,11 @@ container. `4.9406564584124654e-324`. Refused as too large or too small: any number whose numerator or denominator reaches 4096 bits, wherever it appears, as `go/constant` rounds a fraction that grows that large - (`2^4095`, `1e-1300 + 1`); and a power computed as a double whose base - or result is outside the normal range of a double (`1e-400^0.5`). + (`2^4095`, `1e-1300 + 1`); and a power computed as a double whose + result is outside the normal range of a double (`2^1500.5`). The base + of such a power may be outside that range: square roots taken from its + exact value bring it inside first, so `(2^1200)^0.5` is + `4.149515568880993e+180` and `1e-400^0.5` is `1e-200`. - **Failure is an exit.** If the chat client exits or the connection to it drops, the bot exits with an error and the container's restart policy starts both again. `SIGTERM` stops the bot, which stops the diff --git a/docs/TODO.md b/docs/TODO.md index 4683084..3de5c1a 100644 --- a/docs/TODO.md +++ b/docs/TODO.md @@ -28,7 +28,8 @@ with no deprecation warning. # Completed Steps - 2026-09-29 Exact results past the range of a double, such as `2^1200`, - are written to 17 significant digits instead of being refused + are written to 17 significant digits instead of being refused, and a + fractional power of such a number, such as `(2^1200)^0.5`, is answered - 2026-09-29 `POST`, `GET` and `DELETE` on a chat's webhooks, under `/api/v1/chats/{id}/webhooks`, kept in `$DATA_DIR/webhooks.json` - 2026-09-29 `GET` and `POST /api/v1/chats/{id}/messages`: a chat's diff --git a/internal/calc/calc.go b/internal/calc/calc.go index b193687..17801fb 100644 --- a/internal/calc/calc.go +++ b/internal/calc/calc.go @@ -52,7 +52,9 @@ const ( // significantDigits significant digits, the most the shortest form of a // double takes. It is rounded to them from a float of floatPrecision // bits, the bits a numerator or denominator can hold and 64 more for the -// digits, so that the float rounds as the exact result would. +// digits, so that the float rounds as the exact result would. The square +// roots of a power's base past that range are taken in such a float too: +// see nonNegativePower. const ( significantDigits = 17 floatPrecision = bitLimit + 64 @@ -404,11 +406,25 @@ func nonNegativePower(x, y, n constant.Value) (constant.Value, error) { xf, _ := constant.Float64Val(x) yf, _ := constant.Float64Val(y) - f := math.Pow(xf, yf) - // Neither x nor x^y is zero. If either is not a normal double, it - // has lost digits, or all of them. - if !normal(xf) || !normal(f) { + // x^y is (√x)^(2y). An x outside the normal range of a double, such + // as 2^1200, would lose digits as a double, or all of them, so square + // roots taken from its exact value bring it into that range first. As + // x is between 2^-4096 and 2^4096 (see exact), three at most are + // needed. + r, _ := constant.Val(x).(*big.Rat) + root := new(big.Float).SetPrec(floatPrecision).SetRat(r) + + for !normal(xf) { + root.Sqrt(root) + xf, _ = root.Float64() + yf *= 2 + } + + // x^y is not zero. If it is not a normal double, it has lost digits, + // or all of them. + f := math.Pow(xf, yf) + if !normal(f) { return nil, ErrOutOfRange } diff --git a/internal/calc/calc_test.go b/internal/calc/calc_test.go index f5b1b3f..e43bb5a 100644 --- a/internal/calc/calc_test.go +++ b/internal/calc/calc_test.go @@ -130,6 +130,21 @@ func TestEvaluatePowers(t *testing.T) { // A power computed in float64 carries its rounding into the exact // arithmetic after it, past the range of a double as within it. "2^0.5 * 1e400": "1.4142135623730951e+400", + // A fractional power of an exact number outside the range of a + // double, taken from its exact value, up to the edges of that + // range. + "(2^1200)^0.5": "4.149515568880993e+180", + "(2^1024)^0.5": "1.3407807929942597e+154", + "(2^-1200)^0.5": "2.409919865102884e-181", + "1e400^0.5": "1e+200", + "1e-400^0.5": "1e-200", + "1e-310^0.5": "1e-155", + "(2^1200)^-0.5": "2.409919865102884e-181", + "(2^1200)^0.5 / 2^600": "1", + "1e400^-0.001": "0.39810717055349726", + "1e-400^0.001": "0.39810717055349726", + "(2^2047)^0.5": "1.2711610061536464e+308", + "(2^-2044)^0.5": "2.2250738585072014e-308", }) } @@ -252,15 +267,17 @@ func TestEvaluateOutOfRange(t *testing.T) { "2^5000": calc.ErrOutOfRange, "(-2)^5001": calc.ErrOutOfRange, "0.5^-5000": calc.ErrOutOfRange, - // Powers computed in float64 whose base or result is not a - // normal double, and so has lost digits, or all of them. + // Powers computed in float64 whose result is not a normal double, + // and so has lost digits, or all of them, whatever the size of + // the base. "2^-1073.5 * 2^1073": calc.ErrOutOfRange, - "1e400^-0.001": calc.ErrOutOfRange, - "1e-400^0.001": calc.ErrOutOfRange, - "1e-310^0.5": calc.ErrOutOfRange, + "2^1500.5": calc.ErrOutOfRange, + "(2^1200)^0.9": calc.ErrOutOfRange, + "1e-400^0.9": calc.ErrOutOfRange, + "(2^2048)^0.5": calc.ErrOutOfRange, + "(2^-2046)^0.5": calc.ErrOutOfRange, "(0.5^1100)^4 / (0.5^1100)^4": calc.ErrOutOfRange, "(1/3)^1e400": calc.ErrOutOfRange, - "(2^1200)^0.5": calc.ErrOutOfRange, // go/constant holds numbers of this size rounded. A sum of them // can lose the answer (this one would be 0), and so can a // remainder or the sign of -1 to such a power. @@ -343,6 +360,10 @@ func TestEvaluateBoundsWork(t *testing.T) { {in: "3^2583", want: "2.5363018640659988e+1232"}, {in: "2^-4094", want: "3.8299909843808741e-1233"}, {in: "-1/3^2583", want: "-3.9427483540814775e-1233"}, + // Fractional powers of numbers just below the limit, whose bases + // take the most square roots to bring into the range of a double. + {in: "(2^-4094)^0.125", want: "8.869511863657883e-155"}, + {in: "(1/3^2583)^0.5", err: calc.ErrOutOfRange}, } for _, c := range cases {