UNITS v3.3.0 Release Notes
Release Date: 2026-01-07 // 5 months ago-
Corrected Semantics for Ratio-Dimensionless Units
This release fixes multiple inconsistencies in the handling of ratio-dimensionless units
(e.g.percent,ppm,ppb) that previously could lead to silent precision loss, unit-scale loss,
or incorrect results in compound expressions.These changes primarily affect arithmetic involving:
- integral-backed ratio-dimensionless units,
- compound units such as
ppb / yr, - accumulation over time or other dimensions.
🛠 1) Normalization and truncation behavior (fixed)
Previous behavior
Ratio-dimensionless units normalize to a fraction (e.g. 50% → 0.5), but that normalization could
happen in the unit’s underlying type. With integral underlying types, this could silently truncate.Example:
percent<int>(50).value() ==0// truncated from 0.5This truncation could then propagate into arithmetic and comparisons.
Current behavior
Ratio-dimensionless units normalize in floating-point space when needed, so
value()preserves the
fractional meaning regardless of underlying type:percent<int>(50).value() ==0.5
🛠 2) Ratio scale could be lost in compound units (fixed)
Previous behavior
Expressions that form compound units from ratio-dimensionless numerators could discard the ratio
scale during type formation. For example, in:ppb / yrthe result could behave as if it were effectively
1 / yr(dimensionally correct, numerically wrong).
This could surface when multiplying back by time:(ppb_per_year * yr)// could fail to round-trip to ppb semanticsCurrent behavior
Compound units preserve the ratio-dimensionless scale, so:
(ppb / yr) * yr -> ppbwith the correct magnitude.
🛠 3) Mixed “points space” vs “fraction space” outcomes (fixed)
Ratio-dimensionless units have two relevant representations:
- raw(): the stored “points” (e.g.
50_pct.raw() == 50) - value(): the normalized fraction (e.g.
50_pct.value() == 0.5)
Previous behavior
Semantically similar expressions could produce different results depending on which overloads were
selected (and thus whetherraw()orvalue()was used internally).Example class of issue:
dimensionless(1.0) / 50_pct// not necessarily consistent with1.0 / 50_pctCurrent behavior
Scalar interactions with ratio-dimensionless units consistently use normalized values where a scalar
interpretation is intended, while unit-unit operations preserve ratio semantics until explicitly converted.As a result, equivalent expressions remain equivalent in practice.
4) Compound assignment on ratio-dimensionless units is now defined
Previous behavior
Compound assignment operators (e.g.
+=,*=,/=) could inherit the same inconsistencies described
above, especially for integral-backed ratio-dimensionless units.Current behavior
Compound assignment behavior is explicitly defined so that:
- arithmetic is performed in the correct semantic domain (normalized when appropriate),
- the stored representation remains consistent and deterministic.
Summary of what changed for users
🚀 Before this release:
- Integral-backed ratio-dimensionless values could silently change meaning due to truncation.
- Compound unit formation could preserve dimensional correctness but lose numeric scale.
- Equivalent expressions could yield different results based on operand ordering and overload selection.
🚀 After this release:
- Ratio-dimensionless values are normalized consistently and without truncation.
- Ratio scale is preserved through compound unit arithmetic (e.g. rates like
ppb/yr). - Equivalent expressions are consistent in both type behavior and numeric results.
Previous changes from v3.2.0
-
👍 Non-type template parameter (NTTP) support