Skip to content
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

Closed
chiphogg opened this issue Feb 15, 2023 · 3 comments · Fixed by #339
Closed

Indicate relative magnitudes in common unit labels #105

chiphogg opened this issue Feb 15, 2023 · 3 comments · Fixed by #339
Labels
⬇️ affects: code (implementation) Affects implementation details of the code 📁 kind: enhancement New feature or request 💪 effort: small
Milestone

Comments

@chiphogg
Copy link
Contributor

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).

  • PRO: Doesn't put any unit on special footing.
  • PRO: Clearly indicates the common unit's magnitude in terms of each constituent unit.
  • CON: A little verbose.

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.

@chiphogg
Copy link
Contributor Author

Related: mpusz/mp-units#438.

@chiphogg chiphogg added the 📁 kind: enhancement New feature or request label Feb 15, 2023
@chiphogg chiphogg added ⬇️ affects: code (implementation) Affects implementation details of the code 📝 status: blocked Blocked on another issue or an external dependency 💪 effort: small labels Mar 26, 2023
@chiphogg chiphogg added this to the 0.4.0 milestone Jun 21, 2023
@chiphogg
Copy link
Contributor Author

Note: in discussions relating to the standard units project, we ended up converging on an EQUIV syntax:

Old:
COM([1 / 15625] km / h == [1 / 25146] mi / h)

New:
EQUIV{[1 / 15625] km / h, [1 / 25146] mi / h}

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:

EQUIV{[1 / 15625 km / h], [1 / 25146 mi / h]}

@chiphogg
Copy link
Contributor Author

Actually, we want to include the unscaled unit label inside of the square brackets, so it will be more like this:

EQUIV{[1 / 15625 km / h], [1 / 25146 mi / h]}

For even better clarity, if the magnitude label has a /, we should enclose it in parentheses:

EQUIV{[(1 / 15625) km / h], [(1 / 25146) mi / h]}

@chiphogg chiphogg modified the milestones: On deck, 0.4.0 Nov 24, 2024
chiphogg added a commit that referenced this issue Nov 27, 2024
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.
chiphogg added a commit that referenced this issue Nov 29, 2024
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.
chiphogg added a commit that referenced this issue Nov 29, 2024
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.
chiphogg added a commit that referenced this issue Dec 2, 2024
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.
@chiphogg chiphogg removed the 📝 status: blocked Blocked on another issue or an external dependency label Dec 2, 2024
chiphogg added a commit that referenced this issue Dec 2, 2024
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.
chiphogg added a commit that referenced this issue Dec 2, 2024
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.
chiphogg added a commit that referenced this issue Dec 2, 2024
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.
chiphogg added a commit that referenced this issue Dec 2, 2024
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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
⬇️ affects: code (implementation) Affects implementation details of the code 📁 kind: enhancement New feature or request 💪 effort: small
Projects
None yet
Development

Successfully merging a pull request may close this issue.

1 participant