Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,29 @@ change is recorded here.

## [Unreleased]

### Added

- **`MaximumDecimalPlaces`** (`rounding.hpp`), 38: the most decimal places rounding takes either way, the scale
`Rational::from_decimal` and `_r` span. A place outside -38 to 38 is `ArithmeticError::Overflow`.

### Changed

- **Rounding reaches 38 decimal places in each direction**, up from 18, the largest power of ten in a 64-bit integer:
`checked_round` and `round` with `DecimalPlaces` or `SignificantDigits`, `checked_round_to_declared`,
`rational_from_double`, the rounding nodes, `rounded_output`, `rounded_sqrt`, `rounded_ln`, `rounded_log10` and
`rounded_exp`. 15 significant digits of a value below 1e-4 -- `-2.12345678901234e-5_r` is
`-212345678901234 / 10^19` -- now round instead of reporting `Overflow`. A place inside the range still reports
`Overflow` wherever an intermediate does not fit, never a wrong or clamped value: an irrational square root rounds
at no more than 19 places, and a logarithm the kernel computes at no more than 36 (ln 1 and log10 of a power of
ten are answered without the kernel, wherever their value fits: ln 1 at every places, log10 10 and log10 1/10 at
38). The integer kernel behind the rounded logarithms and exponential works in 448 bits, up
from 384, and `rounded_exp` computes every argument from -89 up, where it used to answer below -43 without the
kernel. Each rounded logarithm or exponential the kernel computes costs about 10 to 15 percent more
constant-evaluation steps than before, so a constant expression close to the compiler's budget may need room.
- **Text output still stops at 18 decimal places** either way, as its own limit: `decimal_text`, `number_text`, a
trace's and `render()`'s numbers and `std::format` refuse more, as before, though the value was rounded further.
`checked_decimal_text` at fewer than -18 places is `Overflow` on its own, as it was through `checked_round`.

## [0.5.0] - 2026-10-07

The fifth release. A tutorial on the documentation site builds a compressive-strength test step by step, and the README
Expand Down
5 changes: 3 additions & 2 deletions docs/dimensions.md
Original file line number Diff line number Diff line change
Expand Up @@ -629,8 +629,9 @@ value these fields state converts to one exactly.

Conversion is built on `formula::Rational` and the `checked_` arithmetic
functions, so it inherits their overflow behaviour and rounding limits
exactly -- including the ±18-decimal-place ceiling on `DecimalPlaces` and the
numerator-magnitude-dependent limits on rounding described in
exactly -- including the ±38-decimal-place limit on rounding,
`MaximumDecimalPlaces`, and the numerator-magnitude-dependent limits on
rounding described in
[`docs/numbers.md`](numbers.md#limits). Nothing in `dimension.hpp` or
`unit.hpp` widens or narrows those limits; a `Unit`'s magnitude and offset are
exactly the integer pairs `Rational` already knows how to handle exactly.
14 changes: 8 additions & 6 deletions docs/display.md
Original file line number Diff line number Diff line change
Expand Up @@ -485,10 +485,12 @@ std::println("not measured: {}", absentText.view());
std::println("two places: {}\n", twoPlaces.view());
```

**When a number cannot be spelled.** `decimal_text` throws
`ArithmeticException` for more than 18 places, and where rounding to whole
tens or thousands overflows. `number_text` throws it where its rounding
overflows so, and for a padded or approximating style in a unit whose
**When a number cannot be spelled.** Text is written at no more than 18
places either way -- its own limit, though `round` itself reaches 38
([Numbers](numbers.md#limits)). `decimal_text` throws
`ArithmeticException` for more than 18 places, or fewer than -18, and where
rounding to whole tens or thousands overflows. `number_text` throws it
where its rounding overflows so, and for a padded or approximating style in a unit whose
declared decimals lie outside -18 to 18: a `Rational` in any unit you pass,
or a `Measured` whose unit has a symbol or is dimensionless. A `Measured` in
a dimensioned unit with no symbol is shown in the coherent unit, at that
Expand Down Expand Up @@ -653,8 +655,8 @@ before the call stack of the evaluation:

```
test\negative\format_places_without_mode.cpp(17): error C7595: 'std::basic_format_string<char,formula::Rational>::basic_format_string': call to immediate function is not a constant expression
include\formula-cpp/format.hpp(347): note: failure was caused by call of undefined function or one not declared 'constexpr'
include\formula-cpp/format.hpp(347): note: see usage of 'formula::detail::formula_number_format_needs_a_rounding_mode'
include\formula-cpp/format.hpp(348): note: failure was caused by call of undefined function or one not declared 'constexpr'
include\formula-cpp/format.hpp(348): note: see usage of 'formula::detail::formula_number_format_needs_a_rounding_mode'
```

clang and g++ name the same function, in their own words.
Expand Down
24 changes: 15 additions & 9 deletions docs/expressions.md
Original file line number Diff line number Diff line change
Expand Up @@ -480,7 +480,7 @@ STATIC_REQUIRE(**formula::checked_evaluate_si<formula::Rational>(formula::log10(
| Function | Exact at | Elsewhere, in `Rational` |
|---|---|---|
| `ln(x)` | x = 1: 0 | `Inexact` |
| `log10(x)` | x = 10^k, k from -18 to 18: k -- `1000` and `1/1000` alike | `Inexact` |
| `log10(x)` | x = 10^k, k from -38 to 38: k -- `1000` and `1/1000` alike | `Inexact` |
| `exp(x)` | x = 0: 1 | `Inexact`, however large |

`Inexact` is the exact layer refusing to approximate, as it refuses
Expand Down Expand Up @@ -510,16 +510,22 @@ CHECK(lnAt<DecimalPlaces { 4 }, RoundingMode::HalfEven>(Rational { 2 }) == Ratio
`"rounded_transcendental: ln 2 to 4 dp in every mode and of 1/2 with the directions paired the other way"`,
whose `lnAt` helper evaluates `rounded_ln<Places, Mode>` at the given ratio.)

The places are the method's own, and at most 18; the result must fit a
`Rational` there, which any logarithm does: the `log10` of 10^18 - 1 is
reported to all 18 places. The integer kernel takes every argument a
The places are the method's own, from -38 to 38 as for `round`; the result
must fit a `Rational` there, which any logarithm does up to 36 places: the
`log10` of 10^18 - 1 is reported to 18 places, and `ln` 2 to 30, as
0.693147180559945309417232121458. The integer kernel takes every argument a
`Rational` holds: `ln` of 2^127 - 1 is 88.029691931113054295 at 18 places,
under `Floor`. `exp` of more than 88.7 is `Overflow`, since e^x is then past
the largest `Rational`; below that it answers wherever the result fits the
declared places -- e^45 to 18 places, e^88 to whole units -- and is
under `Floor`. A logarithm's computation is up to 2^-120 wide, at least
1.6 · 10^-37, so at 37 or 38 places every logarithm the kernel computes is
`Overflow`. The exact points below -- `ln` 1, `log10` of a power of ten --
are answered without the kernel, wherever their value fits: `ln` 1 at every
places, `log10` of 10 and of 1/10 at 38.
`exp` of more than 88.7 is `Overflow`, since e^x is then past the largest
`Rational`; below that it answers wherever the result fits the declared
places -- e^1 to 37 places, e^45 to 18, e^88 to whole units -- and is
`Overflow` where it does not, as e^88.5 is at every places. Two kinds of
argument never reach the kernel: a power of ten, 10^-38 up to 10^38, is
answered exactly, so `log10` of 10^30 is 30; and `exp` of less than -43 is
answered exactly, so `log10` of 10^30 is 30; and `exp` of less than -89 is
0, or one unit under `Ceiling` and `AwayFromZero`, whatever its width.
Only ln 1, log10 10^k and
exp 0 can tie, and the mode breaks the tie as `rounded<>` does: `log10` of
Expand All @@ -539,7 +545,7 @@ guess. `rounded<...>(ln(x))` is not
| argument failed | its error | its error | its error |
| argument zero or below | `DomainError` | -- | `DomainError` (the logarithms) |
| not a point where the value is rational | `Inexact` | `Inexact` | the rounded decimal |
| result too large at the declared places, or places outside -18 to 18 | -- | -- | `Overflow` |
| result too large at the declared places, or places outside -38 to 38 | -- | -- | `Overflow` |
| rounding not decidable | -- | -- | `Overflow` |
| `checked_evaluate_si<double>` | `std::log`, `std::log10`; `DomainError` for zero, below and NaN | `std::exp`; too large is `+inf` | does not compile, as `rounded_sqrt` does not |

Expand Down
44 changes: 27 additions & 17 deletions docs/numbers.md
Original file line number Diff line number Diff line change
Expand Up @@ -313,12 +313,17 @@ the answer.
`Rational`'s numerator and denominator are `formula::Int128`, signed 128-bit
integers: each holds up to 2^127 − 1, and a numerator down to −2^127 -- up to
39 decimal digits. `from_decimal`'s exponent, the `_r` literal and
`parse_decimal_text` reach 10^-38 to 10^38. `DecimalPlaces` and the
decimal-place form of `round` stay limited to ±18 places; an out-of-range
`DecimalPlaces` reports `Overflow`, while an out-of-range `SignificantDigits`
(fewer than 1) reports `DomainError` -- both mean "argument outside the
domain of the operation", but a caller switching on the code should expect
either one.
`parse_decimal_text` reach 10^-38 to 10^38, and so does rounding: `round`
and `checked_round` take places from -38 to 38, `MaximumDecimalPlaces`,
whether named as `DecimalPlaces` or reached through `SignificantDigits` -- 15
significant digits of 2.12345678901234e-5 are rounded at 19 places. A place
outside that range reports `Overflow`, while an out-of-range
`SignificantDigits` (fewer than 1) reports `DomainError` -- both mean
"argument outside the domain of the operation", but a caller switching on the
code should expect either one. A place inside the range still reports
`Overflow` where the scaled value does not fit, as below. Text is another
matter: a number is written at no more than 18 places either way
([Displaying numbers](display.md)), however many it was rounded to.

Rounding to `N` decimal places scales the value by `10^N`. Common factors of
two cancel against the denominator first, so what must fit in `Rational::Int`
Expand All @@ -330,17 +335,22 @@ is

which for a power-of-two denominator is `|numerator| * 5^N`. **The limit is set
by the numerator's magnitude**, not by the denominator and not by the size of
the value: `1 / 2^121` rounds correctly at all 18 places, while a 100-bit
numerator over the same denominator is refused at 18.
the value: `1 / 2^121` rounds correctly at all 38 places, while a 100-bit
numerator over the same denominator rounds at 11 places and is refused at 12.
Even a small value can be refused well inside the range: 2/3 at 38 places
forms 2 · 10^38, past 2^127.

A `double` below 2^53 in magnitude has a numerator of at most 53 bits, and
rounding it at up to 18 places forms at most 2^53 · 5^18 · 2^18, below 2^113,
so such a value from `from_double_exact` or `rational_from_double` rounds at
every place from 0 to 18: 0,45 as a `double` is exactly
`8106479329266893 / 2^54`, and rounds to 18 places as 0.450000000000000011.
Past that the limit returns: a whole `double` such as 1e21 has a numerator of
its own magnitude and nothing to cancel, so it is refused at 18 places, 1e38
even at 1, and `2^-100` at -18 places multiplies its denominator past 2^127.
rounding it at `N` places forms at most 2^53 · 10^N, below 2^127 up to
`N` = 22, so such a value from `from_double_exact` or `rational_from_double`
rounds at every place from 0 to 22; 2^53 - 1 is refused at 23. A fraction
whose denominator is at least 2^N cancels all of the 2^N and forms at most
2^53 · 5^N, which fits up to `N` = 31: 0,45 as a `double` is exactly
`8106479329266893 / 2^54`, rounds to 18 places as 0.450000000000000011 and
to 31 as 0.4500000000000000111022302462516, and is refused at 32. Past 2^53
the limit returns: a whole `double` such as 1e21 has a numerator of its own
magnitude and nothing to cancel, so it is refused at 18 places, 1e38 even at
1, and `2^-100` at -9 places multiplies its denominator past 2^127.
`from_decimal(45, -2)` is `9/20` -- the same nominal value, and what a method
that writes 0,45 means.

Expand All @@ -351,8 +361,8 @@ over `2^147`, is refused.

For an exact decimal, prefer `from_decimal`: its numerator is whatever you
passed -- usually a handful of significant digits -- so the limit above does not
bite. Its *denominator* need not be small at all: `from_decimal(1, -18)` is
`1/10^18`, and it still rounds correctly at every decimal place from 0 to 18.
bite. Its *denominator* need not be small at all: `from_decimal(1, -38)` is
`1/10^38`, and it still rounds correctly at every decimal place from 0 to 38.
That is the clearest demonstration that the denominator is not what constrains
rounding to decimals.

Expand Down
8 changes: 4 additions & 4 deletions docs/numeric-headroom.md
Original file line number Diff line number Diff line change
Expand Up @@ -440,10 +440,10 @@ different denominator for every point is built to overflow.

Only the integer `Rational` stores changed. These stay as they were:

- **Rounding's decimal places**, which stop at ±18
([Numbers](numbers.md#limits)). Widening them is a separate decision.
`from_decimal`'s exponents and the `_r` literal have since widened to
10^±38 and a 128-bit mantissa.
- **Text output's decimal places**, which stop at 18 either way
([Displaying numbers](display.md)). Rounding's own places, `from_decimal`'s
exponents and the `_r` literal have since widened to ±38 places, 10^±38 and
a 128-bit mantissa ([Numbers](numbers.md#limits)).
- **The 64-bit fields** of `Unit`, `Band` and `Breakpoint`: `band` and
`breakpoint` refuse a `Rational` bound or key that does not fit them.

Expand Down
Loading
Loading