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