This lets us remove a ton of special-casing throughout the codebase, and
just generally makes things a lot simpler.
We also remove the ability to take rational powers of `ratio`, including
`sqrt` and `cbrt` helpers, because these are intrinsically ill-defined.
Fixes#369.
For some reason, MSVC seems to want to instantiate these, even though
nobody ever asks for them (as evidenced by the fact that the builds
passed on other architectures).
This commit is huge, but hopefully the cognitive load is not too bad.
The bulk of this commit is just some fairly mechanical updates from
`ratio` to `Magnitude`. Other things to call out:
- `UnitRatio` goes away. We don't need this concept, because Magnitude
can't even _represent_ anything that doesn't satisfy it.
- I commented out some formatting test cases where the precise
expression changes, but the number is completely equivalent. We will
need to decide how we want to handle Magnitude formatting as a
separate, follow-on task. But at least Magnitude gives us all the
tools we'll need to do so!
We provide two new functions, `numerator(m)` and `denominator(m)`, for a
Magnitude `m`. They fulfill the following conditions:
1. `numerator(m)` and `denominator(m)` are always integer Magnitudes.
2. If `m` is rational, then `m == numerator(m) / denominator(m)`.
If `m` is _not_ rational, then the numerator and denominator are not
especially meaningful (there is no uniquely defined "leftover irrational
part"). However, we choose a convention that matches how humans would
write a mixed number. For example, sqrt(27/16) would have a numerator
of 3, denominator of 4, and a "leftover part" of sqrt(3), matching the
"human" way of writing this as [(3 * sqrt(3)) / 4]. This has no use
yet, but it may later be useful in printing the Magnitude of an
anonymous Unit for end users.
To further reduce friction for the upcoming migration, we provide an
implicit conversion from a Magnitude to a `ratio`. We restrict this
operation to rational Magnitudes, and guard this with a `static_assert`.
I verified that we hit GCC 10's constexpr limit with
`wheel_factorizer<1>`, but pass with `wheel_factorizer<4>`. I hope this
number is enough smaller than the square root of the previous value that
the other compilers will be able to handle it. If not: we'll go lower.
Certain existing units in the library require very large prime
numbers---so large, in fact, that our naive trial division hits the
_iteration limit_ for `constexpr` loops. We don't want to force users
to provide a compiler option override, so we'd better find another way.
The solution is to use the "wheel factorization" algorithm:
https://en.wikipedia.org/wiki/Wheel_factorization
This lets us skip most composite numbers in our trial division. The
implementation presented here is configurable in terms of the size of
the "basis" of primes we use. Bigger bases let us skip more primes, but
at the cost of storing more numbers. Fortunately, it turns out that N=3
was good enough for our purposes.
I used `int` before, because it was simple, and I thought _surely_ it
would be enough. I mean, who's going to make a unit whose magnitude has
a prime factor bigger than 32 bits?
This brings us to the "Dalton", a unit whose ratio-magnitude numerator
is 16605390666050. The prime factorization of 16605390666050 is
(2 * 5 * 5 * 53 * 6266185157), and this last number is bigger than 2^31
by a factor of 3 or so.
Fortunately, we should have done this from the beginning anyway, because
otherwise there would be numbers we could represent in `ratio` which we
couldn't represent in `Magnitude`, and this should never be the case.
```
test\unit_test\runtime\magnitude_test.cpp(143): error C2672:
'units::check_same_type_and_value': no matching overloaded function
found
```
Maybe it's confused by accessing the static member variable template
using dot-notation on an _instance_? If so, let's see how it likes the
member-function syntax.
For each Magnitude `m`, we now support:
- `is_rational(m)` tells whether it represents a rational number.
- `is_integral(m)` tells whether it represents an integer (and
`is_integral(m)` implies `is_rational(m)`).
- `m.value<T>` represents the value of `m` in the type `T`.
If `T` is integral, we only support `m.value<T>` when `is_integral(m)`.
We also perform all intermediate computations in the widest type of the
same category (floating point, signed, or unsigned). This means we can,
for example, give first-class support to embedded users who may have
hardware support only for `float`: we can ensure they get the most
accurate `float` value we can compute, without burdening them with a
dependency on `long double` in their runtime code.
We are not yet ready to replace `ratio` as the definition of unit
magnitudes, but we're a step closer. The next step will be to decompose
a Magnitude into numerator, denominator, and irrational parts, giving us
full bidirectional convertibility with existing `ratio` instances. Then
we can migrate over in a controlled fashion.