-
Notifications
You must be signed in to change notification settings - Fork 23
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Indicate relative magnitudes in common unit labels #105
Comments
Related: mpusz/mp-units#438. |
Note: in discussions relating to the standard units project, we ended up converging on an
This was the brainchild of @JohelEGP. I like it a lot! Actually, we want to include the unscaled unit label inside of the square brackets, so it will be more like this:
|
For even better clarity, if the magnitude label has a
|
Think of this as "just enough of #85 to unblock #105". The fact that our common unit labels don't tell you the _size_ of the unit (#105) has really, really been bugging me for a long time. Recently, I realized we don't need to do all of #85 to get it! Instead, all we need to do is: 1. Build a _mechanism_ that we can easily _extend_. 2. Cover the most important use cases. This PR creates the `MagnitudeLabel` trait mechanism (also accessible via a function/value interface as `mag_label`). We enumerate the various categories of magnitudes that we can label, defaulting to "unsupported". The first two supported categories are _integers_ (that fit in `std::uintmax_t`), and _rationals_. We also add a trait, `has_exposed_slash`, looking forward to the obvious use case of auto-generating labels for scaled units. Those labels will have the form `"[M U]"` for a unit of label `"U"` scaled by a magnitude of label `"M"`. If `has_exposed_slash` is `true` for a given magnitude label, then we'll know to make this `"[(M) U]"` instead. Finally, we move a couple of `StringConstant`-ish utilities into `"string_constant.hh"`, so that we can use them in our implementation. Helps #85.
Think of this as "just enough of #85 to unblock #105". The fact that our common unit labels don't tell you the _size_ of the unit (#105) has really, really been bugging me for a long time. Recently, I realized we don't need to do all of #85 to get it! Instead, all we need to do is: 1. Build a _mechanism_ that we can easily _extend_. 2. Cover the most important use cases. This PR creates the `MagnitudeLabel` trait mechanism (also accessible via a function/value interface as `mag_label`). We enumerate the various categories of magnitudes that we can label, defaulting to "unsupported". The first two supported categories are _integers_ (that fit in `std::uintmax_t`), and _rationals_. We also add a trait, `has_exposed_slash`, looking forward to the obvious use case of auto-generating labels for scaled units. Those labels will have the form `"[M U]"` for a unit of label `"U"` scaled by a magnitude of label `"M"`. If `has_exposed_slash` is `true` for a given magnitude label, then we'll know to make this `"[(M) U]"` instead. Finally, we move a couple of `StringConstant`-ish utilities into `"string_constant.hh"`, so that we can use them in our implementation. Helps #85.
First, when multiplying a `ScaledUnit` by another magnitude, we now fold it into the existing magnitude of the scaled unit. Previously, we'd end up with `ScaledUnit<ScaledUnit<U, M1>, M2>`, and so on. We also now omit any "trivial" scaling factors, whether because we're scaling by `mag<1>()`, or (more commonly) whether we've applied a bunch of different scale factors and they all cancel out. (We did need to tweak a few cases that were relying on `U{} * mag<1>()` being meaningfully different from `U{}`.) Next, we now auto-generate labels for `ScaledUnit` specializations. For `ScaledUnit<U, M>`, if `U` has label `"U"`, and `M` has label `"M"`, we generate a label `"[M U]"` --- or, if `"M"` contains an exposed slash `"/"`, we'll generate the label `"[(M) U]"` for lack of ambiguity. This resolves the vast majority of `[UNLABELED_UNIT]` labels. The remaining work on #85 is simply to generate labels for a wider variety of magnitude label categories. Finally: we formerly had no way to decide ordering between units that are _both_ specializations of `ScaledUnit`, which _do_ have identical dimension _and_ magnitude, and yet are _not_ the same unit. (For example, something like `"[(1 / 4) ft]"` and `"[3 in]"`.) This may have been somewhat obscure in the past, but with the upcoming work on #105, it's about to become very common. We added a test case that exposes this, and then updated our ordering code to handle this case. Helps #85. Unblocks #105.
First, when multiplying a `ScaledUnit` by another magnitude, we now fold it into the existing magnitude of the scaled unit. Previously, we'd end up with `ScaledUnit<ScaledUnit<U, M1>, M2>`, and so on. We also now omit any "trivial" scaling factors, whether because we're scaling by `mag<1>()`, or (more commonly) whether we've applied a bunch of different scale factors and they all cancel out. (We did need to tweak a few cases that were relying on `U{} * mag<1>()` being meaningfully different from `U{}`.) Next, we now auto-generate labels for `ScaledUnit` specializations. For `ScaledUnit<U, M>`, if `U` has label `"U"`, and `M` has label `"M"`, we generate a label `"[M U]"` --- or, if `"M"` contains an exposed slash `"/"`, we'll generate the label `"[(M) U]"` for lack of ambiguity. This resolves the vast majority of `[UNLABELED_UNIT]` labels. The remaining work on #85 is simply to generate labels for a wider variety of magnitude label categories. Finally: we formerly had no way to decide ordering between units that are _both_ specializations of `ScaledUnit`, which _do_ have identical dimension _and_ magnitude, and yet are _not_ the same unit. (For example, something like `"[(1 / 4) ft]"` and `"[3 in]"`.) This may have been somewhat obscure in the past, but with the upcoming work on #105, it's about to become very common. We added a test case that exposes this, and then updated our ordering code to handle this case. Helps #85. Unblocks #105.
Formerly, the common unit for, say, `"m"` and `"in"`, would get a label something like `"COM[m, in]"`. This is correct, but useless. First, to know what the unit _actually is_, the end user has to do some math: they have to find the largest unit that evenly divides all inputs. And then there's no easy way to check whether they got the math right! Now, we do the math for you. The new label corresponding to the above example would be `"EQUIV{[(1 / 127) in], [(1 / 5000) m]}"`, or something like it. `EQUIV{...}` asserts to the reader that everything contained therein is quantity-equivalent. Pick any argument, and you'd get a true statement. Interestingly, it can happen that the number of units that should _show up in the label_ is **different** from the number of units _in the common unit_. If we have two different scaled versions of the same unit, then both should show up in `CommonUnit<...>`, but only one should show up in the label `"EQUIV{...}"`, because the scaled versions will necessarily be identical after they're re-scaled to fit the common unit. Therefore, we remove redundancies, and if only one unit remains, we drop the `"EQUIV{...}"` shell. Helps #105. We still need to do a little better. First, if the _label_ has only one unit, then we should just use that _instead of_ the common unit! And second, we need to handle the common _point_ units.
Formerly, the common unit for, say, `"m"` and `"in"`, would get a label something like `"COM[m, in]"`. This is correct, but useless. First, to know what the unit _actually is_, the end user has to do some math: they have to find the largest unit that evenly divides all inputs. And then there's no easy way to check whether they got the math right! Now, we do the math for you. The new label corresponding to the above example would be `"EQUIV{[(1 / 127) in], [(1 / 5000) m]}"`, or something like it. `EQUIV{...}` asserts to the reader that everything contained therein is quantity-equivalent. Pick any argument, and you'd get a true statement. Interestingly, it can happen that the number of units that should _show up in the label_ is **different** from the number of units _in the common unit_. If we have two different scaled versions of the same unit, then both should show up in `CommonUnit<...>`, but only one should show up in the label `"EQUIV{...}"`, because the scaled versions will necessarily be identical after they're re-scaled to fit the common unit. Therefore, we remove redundancies, and if only one unit remains, we drop the `"EQUIV{...}"` shell. Helps #105. We still need to do a little better. First, if the _label_ has only one unit, then we should just use that _instead of_ the common unit! And second, we need to handle the common _point_ units.
If we don't do this, it's a weird situation: we end up with a `CommonUnit<A, B, ...>` type, whose _label_ is just the label for a single _simple_ unit `X`... which is quantity-equivalent to `CommonUnit<A, B, ...>`. This means that when we print it, it will _look just like it is_ `X`, and people might get confused. With this PR, when we're in this situation, we simply make `CommonUnitT<A, B, ...>` return `X` itself! To do this, we _could_ try to piggyback on the label definition. I found that way introduces some really confusing circular dependencies. I find it is much simpler to simply count the _distinct unscaled units_ in the common unit, and do the simplification exactly when we get down to only one. Helps #105.
If we don't do this, it's a weird situation: we end up with a `CommonUnit<A, B, ...>` type, whose _label_ is just the label for a single _simple_ unit `X`... which is quantity-equivalent to `CommonUnit<A, B, ...>`. This means that when we print it, it will _look just like it is_ `X`, and people might get confused. With this PR, when we're in this situation, we simply make `CommonUnitT<A, B, ...>` (note the `T`, i.e., "compute the common unit") return `X` itself! To do this, we _could_ try to piggyback on the label definition. I found that way introduces some really confusing circular dependencies. I find it is much simpler to simply count the _distinct unscaled units_ in the common unit, and do the simplification exactly when we get down to only one. Helps #105.
A core part of Au's philosophy is "no preferred units". So when we subtract, say, MPH from KPH, the result is labeled as essentially "the common unit of MPH and KPH":
COM[km / h, mi / h]
.I think this is a mixed bag. It's good that we don't conjure up any unmentioned units.
COM[km / h, mi / h]
does unambiguously identify what unit is being used. But this isn't a commonly used unit, and it's clearly bad that the label doesn't give any clue as to its magnitude, relative to any other unit.I've found a new candidate solution. We can express the common unit in terms of each constituent unit --- and, for clarity, replace the
,
with==
. For example:COM([1 / 15625] km / h == [1 / 25146] mi / h)
.I think this is a clear improvement. Common-unit quantities generally only occur as the result of intermediate computations. They're not the kind of thing that end users are going to want to print, unmodified. But when they do end up being output, they'd better be unambiguous!
This solution depends on #85, because we need to be able to print magnitudes.
The text was updated successfully, but these errors were encountered: