tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

ProportionValue.h (9927B)


      1 /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
      2 /* vim: set ts=8 sts=2 et sw=2 tw=80: */
      3 /* This Source Code Form is subject to the terms of the Mozilla Public
      4 * License, v. 2.0. If a copy of the MPL was not distributed with this
      5 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
      6 
      7 #ifndef ProportionValue_h
      8 #define ProportionValue_h
      9 
     10 #include <algorithm>
     11 #include <limits>
     12 
     13 namespace mozilla {
     14 
     15 // Class storing a proportion value between 0 and 1, effectively 0% to 100%.
     16 // The public interface deals with doubles, but internally the value is encoded
     17 // in an integral type, so arithmetic operations are fast.
     18 // It also supports an invalid value: Use MakeInvalid() to construct, it infects
     19 // any operation, and gets converted to a signaling NaN.
     20 class ProportionValue {
     21 public:
     22  using UnderlyingType = uint32_t;
     23 
     24  // Default-construct at 0%.
     25  constexpr ProportionValue()
     26      // This `noexcept` is necessary to avoid a build error when encapsulating
     27      // `ProportionValue` in `std::Atomic`:
     28      // "use of deleted function
     29      // 'constexpr std::atomic<mozilla::ProportionValue>::atomic()"
     30      // because the default `std::atomic<T>::atomic()` constructor is marked:
     31      // `noexcept(std::is_nothrow_default_constructible_v<T>)`
     32      // and therefore this default constructor here must be explicitly marked
     33      // `noexcept` as well.
     34      noexcept
     35      : mIntegralValue(0u) {}
     36 
     37  // Construct a ProportionValue with the given value, clamped to 0..1.
     38  // Note that it's constexpr, so construction from literal numbers should incur
     39  // no runtime costs.
     40  // If `aValue` is NaN, behavior is undefined! Use `MakeInvalid()` instead.
     41  constexpr explicit ProportionValue(double aValue)
     42      : mIntegralValue(UnderlyingType(std::clamp(aValue, 0.0, 1.0) * scMaxD)) {}
     43 
     44  [[nodiscard]] static constexpr ProportionValue MakeInvalid() {
     45    return ProportionValue(scInvalidU, Internal{});
     46  }
     47 
     48  [[nodiscard]] constexpr double ToDouble() const {
     49    return IsInvalid() ? std::numeric_limits<double>::signaling_NaN()
     50                       : (double(mIntegralValue) * scInvMaxD);
     51  }
     52 
     53  // Retrieve the underlying integral value, for storage or testing purposes.
     54  [[nodiscard]] constexpr UnderlyingType ToUnderlyingType() const {
     55    return mIntegralValue;
     56  };
     57 
     58  // Re-construct a ProportionValue from an underlying integral value.
     59  [[nodiscard]] static constexpr ProportionValue FromUnderlyingType(
     60      UnderlyingType aUnderlyingType) {
     61    return ProportionValue(
     62        (aUnderlyingType <= scMaxU) ? aUnderlyingType : scInvalidU, Internal{});
     63  }
     64 
     65  [[nodiscard]] constexpr bool IsExactlyZero() const {
     66    return mIntegralValue == 0u;
     67  }
     68 
     69  [[nodiscard]] constexpr bool IsExactlyOne() const {
     70    return mIntegralValue == scMaxU;
     71  }
     72 
     73  [[nodiscard]] constexpr bool IsValid() const {
     74    // Compare to the maximum value, not just exactly scInvalidU, to catch any
     75    // kind of invalid state.
     76    return mIntegralValue <= scMaxU;
     77  }
     78  [[nodiscard]] constexpr bool IsInvalid() const {
     79    // Compare to the maximum value, not just exactly scInvalidU, to catch any
     80    // kind of invalid state.
     81    return mIntegralValue > scMaxU;
     82  }
     83 
     84  // Strict comparisons based on the underlying integral value. Use
     85  // `CompareWithin` instead to make fuzzy comparisons.
     86  // `ProportionValue::MakeInvalid()`s are equal, and greater than anything
     87  // else; Best to avoid comparisons, and first use IsInvalid() instead.
     88 #define OPERATOR_COMPARISON(CMP)                                  \
     89  [[nodiscard]] constexpr friend bool operator CMP(               \
     90      const ProportionValue& aLHS, const ProportionValue& aRHS) { \
     91    return aLHS.mIntegralValue CMP aRHS.mIntegralValue;           \
     92  }
     93  OPERATOR_COMPARISON(==)
     94  OPERATOR_COMPARISON(!=)
     95  OPERATOR_COMPARISON(<)
     96  OPERATOR_COMPARISON(<=)
     97  OPERATOR_COMPARISON(>)
     98  OPERATOR_COMPARISON(>=)
     99 #undef OPERATOR_COMPARISON
    100 
    101  // Arithmetic operations + - *, all working on the underlying integral values
    102  // (i.e, no expensive floating-point operations are used), and always clamping
    103  // to 0..1 range. Invalid values are poisonous.
    104 
    105  [[nodiscard]] constexpr ProportionValue operator+(
    106      ProportionValue aRHS) const {
    107    return ProportionValue(
    108        (IsInvalid() || aRHS.IsInvalid())
    109            ? scInvalidU
    110            // Adding fixed-point values keep the same scale, so there is no
    111            // adjustment needed for that. [0,1]+[0,1]=[0,2], so we only need to
    112            // ensure that the result is capped at max 1, aka scMaxU:
    113            // a+b<=max <=> b<=max-a, so b is at maximum max-a.
    114            : (mIntegralValue +
    115               std::min(aRHS.mIntegralValue, scMaxU - mIntegralValue)),
    116        Internal{});
    117  }
    118 
    119  [[nodiscard]] constexpr ProportionValue operator-(
    120      ProportionValue aRHS) const {
    121    return ProportionValue(
    122        (IsInvalid() || aRHS.IsInvalid())
    123            ? scInvalidU
    124            // Subtracting fixed-point values keep the same scale, so there is
    125            // no adjustment needed for that. [0,1]-[0,1]=[-1,1], so we only
    126            // need to ensure that the value is positive:
    127            // a-b>=0 <=> b<=a, so b is at maximum a.
    128            : (mIntegralValue - std::min(aRHS.mIntegralValue, mIntegralValue)),
    129        Internal{});
    130  }
    131 
    132  [[nodiscard]] constexpr ProportionValue operator*(
    133      ProportionValue aRHS) const {
    134    // Type to hold the full result of multiplying two maximum numbers.
    135    using DoublePrecisionType = uint64_t;
    136    static_assert(sizeof(DoublePrecisionType) >= 2 * sizeof(UnderlyingType));
    137    return ProportionValue(
    138        (IsInvalid() || aRHS.IsInvalid())
    139            ? scInvalidU
    140            // Multiplying fixed-point values doubles the scale (2^31 -> 2^62),
    141            // so we need to adjust the result by dividing it by one scale
    142            // (which is optimized into a binary right-shift).
    143            : (UnderlyingType((DoublePrecisionType(mIntegralValue) *
    144                               DoublePrecisionType(aRHS.mIntegralValue)) /
    145                              DoublePrecisionType(scMaxU))),
    146        Internal{});
    147  }
    148 
    149  // Explicitly forbid divisions, they make little sense, and would almost
    150  // always return a clamped 100% (E.g.: 50% / 10% = 0.5 / 0.1 = 5 = 500%).
    151  [[nodiscard]] constexpr ProportionValue operator/(
    152      ProportionValue aRHS) const = delete;
    153 
    154  // Division by a positive integer value, useful to split an interval in equal
    155  // parts (with maybe some spare space at the end, because it is rounded down).
    156  // Division by 0 produces an invalid value.
    157  [[nodiscard]] constexpr ProportionValue operator/(uint32_t aDivisor) const {
    158    return ProportionValue((IsInvalid() || aDivisor == 0u)
    159                               ? scInvalidU
    160                               : (mIntegralValue / aDivisor),
    161                           Internal{});
    162  }
    163 
    164  // Multiplication by a positive integer value, useful as inverse of the
    165  // integer division above. But it may be lossy because the division is rounded
    166  // down, therefore: PV - u < (PV / u) * u <= PV.
    167  // Clamped to 100% max.
    168  [[nodiscard]] constexpr ProportionValue operator*(
    169      uint32_t aMultiplier) const {
    170    return ProportionValue(IsInvalid()
    171                               ? scInvalidU
    172                               : ((aMultiplier > scMaxU / mIntegralValue)
    173                                      ? scMaxU
    174                                      : (mIntegralValue * aMultiplier)),
    175                           Internal{});
    176  }
    177 
    178 private:
    179  // Tagged constructor for internal construction from the UnderlyingType, so
    180  // that it is never ambiguously considered in constructions from one number.
    181  struct Internal {};
    182  constexpr ProportionValue(UnderlyingType aIntegralValue, Internal)
    183      : mIntegralValue(aIntegralValue) {}
    184 
    185  // Use all but 1 bit for the fractional part.
    186  // Valid values can go from 0b0 (0%) up to 0b1000...00 (scMaxU aka 100%).
    187  static constexpr unsigned scFractionalBits = sizeof(UnderlyingType) * 8 - 1;
    188  // Maximum value corresponding to 1.0 or 100%.
    189  static constexpr UnderlyingType scMaxU = UnderlyingType(1u)
    190                                           << scFractionalBits;
    191  // This maximum value corresponding to 1.0 can also be seen as the scaling
    192  // factor from any [0,1] `double` value to the internal integral value.
    193  static constexpr double scMaxD = double(scMaxU);
    194  // The inverse can be used to convert the internal value back to [0,1].
    195  static constexpr double scInvMaxD = 1.0 / scMaxD;
    196 
    197  // Special value outside [0,max], used to construct invalid values.
    198  static constexpr UnderlyingType scInvalidU = ~UnderlyingType(0u);
    199 
    200  // Internal integral value, guaranteed to always be <= scMaxU, or scInvalidU.
    201  // This is effectively a fixed-point value using 1 bit for the integer part
    202  // and 31 bits for the fractional part.
    203  // It is roughly equal to the `double` value [0,1] multiplied by scMaxD.
    204  UnderlyingType mIntegralValue;
    205 };
    206 
    207 namespace literals {
    208 inline namespace ProportionValue_literals {
    209 
    210 // User-defined literal for integer percentages, e.g.: `10_pc`, `100_pc`
    211 // (equivalent to `ProportionValue{0.1}` and `ProportionValue{1.0}`).
    212 // Clamped to [0, 100]_pc.
    213 [[nodiscard]] constexpr ProportionValue operator""_pc(
    214    unsigned long long int aPercentage) {
    215  return ProportionValue{
    216      double(std::clamp<unsigned long long int>(aPercentage, 0u, 100u)) /
    217      100.0};
    218 }
    219 
    220 // User-defined literal for non-integer percentages, e.g.: `12.3_pc`, `100.0_pc`
    221 // (equivalent to `ProportionValue{0.123}` and `ProportionValue{1.0}`).
    222 // Clamped to [0.0, 100.0]_pc.
    223 [[nodiscard]] constexpr ProportionValue operator""_pc(long double aPercentage) {
    224  return ProportionValue{
    225      double(std::clamp<long double>(aPercentage, 0.0, 100.0)) / 100.0};
    226 }
    227 
    228 }  // namespace ProportionValue_literals
    229 }  // namespace literals
    230 
    231 }  // namespace mozilla
    232 
    233 #endif  // ProportionValue_h