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

    This 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 / yr
    

    the 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 semantics
    

    Current behavior

    Compound units preserve the ratio-dimensionless scale, so:

    (ppb / yr) * yr -> ppb
    

    with 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 whether raw() or value() was used internally).

    Example class of issue:

    dimensionless(1.0) / 50_pct// not necessarily consistent with1.0 / 50_pct
    

    Current 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