diff --git a/indra/llcommon/CMakeLists.txt b/indra/llcommon/CMakeLists.txt index 1ec792fb39..b963bfb69e 100644 --- a/indra/llcommon/CMakeLists.txt +++ b/indra/llcommon/CMakeLists.txt @@ -135,6 +135,7 @@ target_sources( alsavehistory.h alscratcharena.h alsimd.h + alunitconversion.h alwatchedfile.h always_return.h apply.h @@ -317,6 +318,7 @@ if(AL_BUILD_TESTS) tests/alscratcharena_test.cpp tests/alserialworker_test.cpp tests/alsimd_test.cpp + tests/alunitconversion_test.cpp tests/alwatchedfile_test.cpp tests/apply_test.cpp tests/bitpack_test.cpp diff --git a/indra/llcommon/alunitconversion.h b/indra/llcommon/alunitconversion.h new file mode 100644 index 0000000000..a37ebc403f --- /dev/null +++ b/indra/llcommon/alunitconversion.h @@ -0,0 +1,156 @@ +/** + * @file alunitconversion.h + * @brief A conversion between units as one affine map, worked out at compile time. + * + * $LicenseInfo:firstyear=2026&license=viewerlgpl$ + * Alchemy Viewer Source Code + * Copyright (C) 2026, Rye + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; + * version 2.1 of the License only. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * $/LicenseInfo$ + */ + +#pragma once + +#include +#include +#include + +/// A conversion between units: x -> (x * mMultiplier + mOffset) / mDivisor. +/// +/// Built by applying * / + - to ALUnitConversion(), left to right, as they would apply to a value: Fahrenheit from +/// Celsius is ALUnitConversion() * 9 / 5 + 32. While every factor is an integer the terms are integers, kept in +/// lowest terms, so that an integer conversion is exact and divides once; a floating point factor makes them doubles. +template +struct ALUnitConversion +{ + T mMultiplier = 1; + T mOffset = 0; + T mDivisor = 1; +}; + +/// The type of a conversion's terms once a term of type T meets one of type U. +template +using ALUnitConversionTerm = std::conditional_t || std::is_floating_point_v, double, std::intmax_t>; + +template +constexpr ALUnitConversion al_unit_conversion_cast(ALUnitConversion conversion) +{ + return { (TO)conversion.mMultiplier, (TO)conversion.mOffset, (TO)conversion.mDivisor }; +} + +/// conversion with its integer terms divided by their greatest common divisor, and its divisor positive. +template +constexpr ALUnitConversion al_lowest_terms(ALUnitConversion conversion) +{ + if constexpr (std::is_integral_v) + { + if (conversion.mDivisor < 0) + { + conversion = { -conversion.mMultiplier, -conversion.mOffset, -conversion.mDivisor }; + } + const T divisor = std::gcd(std::gcd(conversion.mMultiplier, conversion.mOffset), conversion.mDivisor); + if (divisor > 1) + { + conversion = { conversion.mMultiplier / divisor, conversion.mOffset / divisor, conversion.mDivisor / divisor }; + } + } + return conversion; +} + +template + requires std::is_arithmetic_v +constexpr ALUnitConversion> operator * (ALUnitConversion conversion, FACTOR factor) +{ + auto result = al_unit_conversion_cast>(conversion); + result.mMultiplier *= factor; + result.mOffset *= factor; + return al_lowest_terms(result); +} + +template + requires std::is_arithmetic_v +constexpr ALUnitConversion> operator / (ALUnitConversion conversion, FACTOR factor) +{ + auto result = al_unit_conversion_cast>(conversion); + result.mDivisor *= factor; + return al_lowest_terms(result); +} + +template + requires std::is_arithmetic_v +constexpr ALUnitConversion> operator + (ALUnitConversion conversion, FACTOR offset) +{ + auto result = al_unit_conversion_cast>(conversion); + result.mOffset += offset * result.mDivisor; + return al_lowest_terms(result); +} + +template + requires std::is_arithmetic_v +constexpr ALUnitConversion> operator - (ALUnitConversion conversion, FACTOR offset) +{ + auto result = al_unit_conversion_cast>(conversion); + result.mOffset -= offset * result.mDivisor; + return al_lowest_terms(result); +} + +/// The conversion applying first, then second. +template +constexpr ALUnitConversion> al_compose_unit_conversions(ALUnitConversion first, ALUnitConversion second) +{ + // second((x * m1 + c1) / d1) = (x * m1 * m2 + c1 * m2 + c2 * d1) / (d1 * d2) + const auto a = al_unit_conversion_cast>(first); + const auto b = al_unit_conversion_cast>(second); + return al_lowest_terms(ALUnitConversion>{ a.mMultiplier * b.mMultiplier, + a.mOffset * b.mMultiplier + b.mOffset * a.mDivisor, + a.mDivisor * b.mDivisor }); +} + +/// The conversion undoing conversion: x = (y * mDivisor - mOffset) / mMultiplier. +template +constexpr ALUnitConversion al_invert_unit_conversion(ALUnitConversion conversion) +{ + return al_lowest_terms(ALUnitConversion{ conversion.mDivisor, -conversion.mOffset, conversion.mMultiplier }); +} + +/// Applies CONVERSION::value, a constant ALUnitConversion, to value. An integer value under an integer conversion is +/// worked in 64 bits and divided once, so that a factor T cannot hold, such as the 86400000000 microseconds of a day, +/// still divides it; otherwise the conversion is worked in T, or in double for an integer T. A multiplier or divisor of +/// 1 and an offset of 0 are not applied at all, so a floating point value is not rounded, nor -0 made +0, by them. +template +constexpr T al_apply_unit_conversion(T value) +{ + constexpr auto conversion = CONVERSION::value; + using term_t = std::remove_cv_t; + using work_t = std::conditional_t && std::is_integral_v, + std::conditional_t, std::intmax_t, std::uintmax_t>, + std::conditional_t, T, double>>; + + work_t result = (work_t)value; + if constexpr (conversion.mMultiplier != 1) + { + result *= (work_t)conversion.mMultiplier; + } + if constexpr (conversion.mOffset != 0) + { + result += (work_t)conversion.mOffset; + } + if constexpr (conversion.mDivisor != 1) + { + result /= (work_t)conversion.mDivisor; + } + return (T)result; +} diff --git a/indra/llcommon/llunits.h b/indra/llcommon/llunits.h index 122d4557bf..4e9e111658 100644 --- a/indra/llcommon/llunits.h +++ b/indra/llcommon/llunits.h @@ -69,7 +69,7 @@ LL_DECLARE_DERIVED_UNIT(Minutes, "min", Seconds, / 60); LL_DECLARE_DERIVED_UNIT(Hours, "h", Minutes, / 60); LL_DECLARE_DERIVED_UNIT(Days, "d", Hours, / 24); LL_DECLARE_DERIVED_UNIT(Milliseconds, "ms", Seconds, * 1000); -LL_DECLARE_DERIVED_UNIT(Microseconds, "\x09\x3cs", Milliseconds, * 1000); +LL_DECLARE_DERIVED_UNIT(Microseconds, "\xC2\xB5s", Milliseconds, * 1000); LL_DECLARE_DERIVED_UNIT(Nanoseconds, "ns", Microseconds, * 1000); } diff --git a/indra/llcommon/llunittype.h b/indra/llcommon/llunittype.h index bb1408609a..87297d068d 100644 --- a/indra/llcommon/llunittype.h +++ b/indra/llcommon/llunittype.h @@ -1,5 +1,5 @@ /** - * @file llunit.h + * @file llunittype.h * @brief Unit conversion classes * * $LicenseInfo:firstyear=2001&license=viewerlgpl$ @@ -30,208 +30,198 @@ #include "stdtypes.h" #include "llpreprocessor.h" #include "llerror.h" +#include "alunitconversion.h" + +#include + +#include +#include #ifdef LL_WINDOWS #pragma warning(push) #pragma warning(disable : 4244) // possible loss of data on conversions #endif -//lightweight replacement of type traits for simple type equality check +// the storage type an operation on two storage types yields template -struct LLIsSameType -{ - static const bool value = false; -}; +using LLResultTypeAdd = decltype(std::declval() + std::declval()); -template -struct LLIsSameType -{ - static const bool value = true; -}; +template +using LLResultTypeSubtract = decltype(std::declval() - std::declval()); -// workaround for decltype() not existing and typeof() not working inline in gcc 4.2 template -struct LLResultTypeAdd -{ - typedef LL_TYPEOF(S() + T()) type_t; -}; +using LLResultTypeMultiply = decltype(std::declval() * std::declval()); template -struct LLResultTypeSubtract -{ - typedef LL_TYPEOF(S() - T()) type_t; -}; +using LLResultTypeDivide = decltype(std::declval() / std::declval()); +// the storage type a conversion between two storage types is worked in template -struct LLResultTypeMultiply +using LLResultTypePromote = std::common_type_t; + +// LLUnit and LLUnitImplicit, whatever their storage type and units +template +concept ALUnitType = requires { typename T::is_unit_t; }; + +template +concept ALImplicitUnitType = ALUnitType && requires { typename T::is_implicit_unit_t; }; + +template +concept ALExplicitUnitType = ALUnitType && !ALImplicitUnitType; + +// a plain value, such as an F32, or anything else that is not a unit +template +concept ALUnitlessType = !ALUnitType; + +// the unit UNITS is defined from, through as many units as it takes: Bytes for Gigabits +template> +struct ALRootUnit { - typedef LL_TYPEOF(S() * T()) type_t; + using type = typename ALRootUnit::type; }; -template -struct LLResultTypeDivide +template +struct ALRootUnit { - typedef LL_TYPEOF(S() / T(1)) type_t; + using type = UNITS; }; -template -struct LLResultTypePromote +// units of one dimension, which convert into each other +template +concept ALCompatibleUnits = std::is_same_v::type, typename ALRootUnit::type>; + +// the conversion from the root unit of UNITS, through each unit it is defined from, to UNITS +template +constexpr auto al_unit_conversion_from_root() { - typedef LL_TYPEOF((true) ? S() : T()) type_t; + if constexpr (std::is_same_v) + { + return ALUnitConversion(); + } + else + { + return al_compose_unit_conversions(al_unit_conversion_from_root(), UNITS::sConversion); + } +} + +// the conversion from FROM_UNITS to TO_UNITS, as one map through their root unit +template +struct ALUnitConversionBetween +{ + static constexpr auto value = al_compose_unit_conversions(al_invert_unit_conversion(al_unit_conversion_from_root()), + al_unit_conversion_from_root()); }; template struct LLUnit { - typedef LLUnit self_t; - typedef STORAGE_TYPE storage_t; - typedef void is_unit_t; + using self_t = LLUnit; + using storage_t = STORAGE_TYPE; + using unit_t = UNITS; + using is_unit_t = void; // value initialization - LL_FORCE_INLINE explicit LLUnit(storage_t value = storage_t()) + LL_FORCE_INLINE constexpr explicit LLUnit(storage_t value = storage_t()) : mValue(value) {} - - LL_FORCE_INLINE static self_t convert(self_t v) - { - return v; - } - - template - LL_FORCE_INLINE static self_t convert(LLUnit v) - { - self_t result; - result.mValue = (STORAGE_TYPE)v.value(); - return result; - } - - template - LL_FORCE_INLINE static self_t convert(LLUnit v) - { - self_t result; - STORAGE_TYPE divisor = ll_convert_units(v, result); - result.mValue /= divisor; - return result; - } - template - LL_FORCE_INLINE static self_t convert(LLUnit v) - { - typedef typename LLResultTypePromote::type_t result_storage_t; - LLUnit result; - result_storage_t divisor = ll_convert_units(v, result); - result.value(result.value() / divisor); - return self_t(result.value()); + requires ALCompatibleUnits + [[nodiscard]] LL_FORCE_INLINE static constexpr self_t convert(LLUnit v) + { + if constexpr (std::is_same_v) + { + return self_t((storage_t)v.value()); + } + else + { + // worked in the promoted storage type, so that an integer unit converted to a floating point one keeps + // its fraction until it is stored + using result_storage_t = LLResultTypePromote; + return self_t((storage_t)al_apply_unit_conversion>((result_storage_t)v.value())); + } } - // unit initialization and conversion template - LL_FORCE_INLINE LLUnit(LLUnit other) + requires ALCompatibleUnits + LL_FORCE_INLINE constexpr LLUnit(LLUnit other) : mValue(convert(other).mValue) {} - LL_FORCE_INLINE storage_t value() const + // nor does a unit of another dimension pass for this one through the storage type + template + requires (!ALCompatibleUnits) + LLUnit(LLUnit other) = delete; + + [[nodiscard]] LL_FORCE_INLINE constexpr storage_t value() const { return mValue; } - LL_FORCE_INLINE void value(const storage_t& value) + LL_FORCE_INLINE constexpr void value(const storage_t& value) { mValue = value; } template - storage_t valueInUnits() const + requires ALCompatibleUnits + [[nodiscard]] constexpr storage_t valueInUnits() const { return LLUnit(*this).value(); } template - void valueInUnits(const storage_t& value) const + requires ALCompatibleUnits + constexpr void valueInUnits(const storage_t& value) { *this = LLUnit(value); } - LL_FORCE_INLINE operator storage_t() const + LL_FORCE_INLINE constexpr operator storage_t() const { return value(); } - /*LL_FORCE_INLINE self_t& operator= (storage_t v) + LL_FORCE_INLINE constexpr self_t& operator += (self_t other) { - value(v); + mValue += other.mValue; return *this; - }*/ - - LL_FORCE_INLINE void operator += (self_t other) - { - mValue += convert(other).mValue; } - LL_FORCE_INLINE void operator -= (self_t other) - { - mValue -= convert(other).mValue; - } + template + requires (!ALCompatibleUnits) + self_t& operator += (const OTHER& other) = delete; - LL_FORCE_INLINE void operator *= (const storage_t& multiplicand) + LL_FORCE_INLINE constexpr self_t& operator -= (self_t other) { - mValue *= multiplicand; - } - - LL_FORCE_INLINE void operator *= (const self_t& multiplicand) - { - // spurious use of dependent type to stop gcc from triggering the static assertion before instantiating the template - LL_BAD_TEMPLATE_INSTANTIATION(STORAGE_TYPE, "Multiplication of unit types not supported."); - } - - LL_FORCE_INLINE void operator /= (const storage_t& divisor) - { - mValue /= divisor; - } - - void operator /= (const self_t& divisor) - { - // spurious use of dependent type to stop gcc from triggering the static assertion before instantiating the template - LL_BAD_TEMPLATE_INSTANTIATION(STORAGE_TYPE, "Illegal in-place division of unit types."); + mValue -= other.mValue; + return *this; } - template - LL_FORCE_INLINE bool operator == (const LLUnit& other) const - { - return mValue == convert(other).value(); - } + template + requires (!ALCompatibleUnits) + self_t& operator -= (const OTHER& other) = delete; - template - LL_FORCE_INLINE bool operator != (const LLUnit& other) const + LL_FORCE_INLINE constexpr self_t& operator *= (const storage_t& multiplicand) { - return mValue != convert(other).value(); - } - - template - LL_FORCE_INLINE bool operator < (const LLUnit& other) const - { - return mValue < convert(other).value(); + mValue *= multiplicand; + return *this; } - template - LL_FORCE_INLINE bool operator <= (const LLUnit& other) const - { - return mValue <= convert(other).value(); - } + // a product of units is a new kind of unit + template + self_t& operator *= (const OTHER& multiplicand) = delete; - template - LL_FORCE_INLINE bool operator > (const LLUnit& other) const + LL_FORCE_INLINE constexpr self_t& operator /= (const storage_t& divisor) { - return mValue > convert(other).value(); + mValue /= divisor; + return *this; } - template - LL_FORCE_INLINE bool operator >= (const LLUnit& other) const - { - return mValue >= convert(other).value(); - } + // a unit divided by a unit is a plain value, not this unit + template + self_t& operator /= (const OTHER& divisor) = delete; protected: storage_t mValue; @@ -256,161 +246,101 @@ std::istream& operator >>(std::istream& s, LLUnit& unit) template struct LLUnitImplicit : public LLUnit { - typedef LLUnitImplicit self_t; - typedef typename LLUnit::storage_t storage_t; - typedef LLUnit base_t; + using self_t = LLUnitImplicit; + using storage_t = typename LLUnit::storage_t; + using base_t = LLUnit; + using is_implicit_unit_t = void; - LL_FORCE_INLINE LLUnitImplicit(storage_t value = storage_t()) + LL_FORCE_INLINE constexpr LLUnitImplicit(storage_t value = storage_t()) : base_t(value) {} template - LL_FORCE_INLINE LLUnitImplicit(LLUnit other) + requires ALCompatibleUnits + LL_FORCE_INLINE constexpr LLUnitImplicit(LLUnit other) : base_t(other) {} - // unlike LLUnit, LLUnitImplicit is *implicitly* convertable to a POD value (F32, S32, etc) - // this allows for interoperability with legacy code - LL_FORCE_INLINE operator storage_t() const - { - return base_t::value(); - } - - using base_t::operator +=; - LL_FORCE_INLINE void operator += (storage_t value) - { - base_t::mValue += value; - } - - // this overload exists to explicitly catch use of another implicit unit - // without ambiguity between conversion to storage_t vs conversion to base_t - template - LL_FORCE_INLINE void operator += (LLUnitImplicit other) - { - base_t::mValue += base_t::convert(other).value(); - } - - using base_t::operator -=; - LL_FORCE_INLINE void operator -= (storage_t value) - { - base_t::mValue -= value; - } - - // this overload exists to explicitly catch use of another implicit unit - // without ambiguity between conversion to storage_t vs conversion to base_t - template - LL_FORCE_INLINE void operator -= (LLUnitImplicit other) - { - base_t::mValue -= base_t::convert(other).value(); - } - template - LL_FORCE_INLINE bool operator == (LLUnit other) const - { - return base_t::mValue == base_t::convert(other).value(); - } + requires (!ALCompatibleUnits) + LLUnitImplicit(LLUnit other) = delete; - template - LL_FORCE_INLINE bool operator == (LLUnitImplicit other) const + // unlike LLUnit, LLUnitImplicit is also *implicitly* constructed from a POD value (F32, S32, etc) + // and takes part in arithmetic with one; this allows for interoperability with legacy code + LL_FORCE_INLINE constexpr operator storage_t() const { - return base_t::mValue == base_t::convert(other).value(); - } - - template - LL_FORCE_INLINE bool operator == (STORAGE_T other) const - { - return base_t::mValue == other; - } - - template - LL_FORCE_INLINE bool operator != (LLUnit other) const - { - return base_t::mValue != convert(other).value(); - } - - template - LL_FORCE_INLINE bool operator != (LLUnitImplicit other) const - { - return base_t::mValue != base_t::convert(other).value(); - } - - template - LL_FORCE_INLINE bool operator != (STORAGE_T other) const - { - return base_t::mValue != other; + return base_t::value(); } - template - LL_FORCE_INLINE bool operator < (LLUnit other) const + LL_FORCE_INLINE constexpr self_t& operator += (base_t other) { - return base_t::mValue < base_t::convert(other).value(); + base_t::operator += (other); + return *this; } - template - LL_FORCE_INLINE bool operator < (LLUnitImplicit other) const + LL_FORCE_INLINE constexpr self_t& operator += (storage_t value) { - return base_t::mValue < base_t::convert(other).value(); + base_t::mValue += value; + return *this; } - template - LL_FORCE_INLINE bool operator < (STORAGE_T other) const + // this overload exists to explicitly catch use of another unit + // without ambiguity between conversion to storage_t vs conversion to base_t + template + requires ALCompatibleUnits + LL_FORCE_INLINE constexpr self_t& operator += (const OTHER& other) { - return base_t::mValue < other; + base_t::mValue += base_t::convert(other).value(); + return *this; } - template - LL_FORCE_INLINE bool operator <= (LLUnit other) const - { - return base_t::mValue <= base_t::convert(other).value(); - } + template + requires (!ALCompatibleUnits) + self_t& operator += (const OTHER& other) = delete; - template - LL_FORCE_INLINE bool operator <= (LLUnitImplicit other) const + LL_FORCE_INLINE constexpr self_t& operator -= (base_t other) { - return base_t::mValue <= base_t::convert(other).value(); + base_t::operator -= (other); + return *this; } - template - LL_FORCE_INLINE bool operator <= (STORAGE_T other) const + LL_FORCE_INLINE constexpr self_t& operator -= (storage_t value) { - return base_t::mValue <= other; + base_t::mValue -= value; + return *this; } - template - LL_FORCE_INLINE bool operator > (LLUnit other) const + // this overload exists to explicitly catch use of another unit + // without ambiguity between conversion to storage_t vs conversion to base_t + template + requires ALCompatibleUnits + LL_FORCE_INLINE constexpr self_t& operator -= (const OTHER& other) { - return base_t::mValue > base_t::convert(other).value(); + base_t::mValue -= base_t::convert(other).value(); + return *this; } - template - LL_FORCE_INLINE bool operator > (LLUnitImplicit other) const - { - return base_t::mValue > base_t::convert(other).value(); - } + template + requires (!ALCompatibleUnits) + self_t& operator -= (const OTHER& other) = delete; - template - LL_FORCE_INLINE bool operator > (STORAGE_T other) const + LL_FORCE_INLINE constexpr self_t& operator *= (const storage_t& multiplicand) { - return base_t::mValue > other; + base_t::operator *= (multiplicand); + return *this; } - template - LL_FORCE_INLINE bool operator >= (LLUnit other) const - { - return base_t::mValue >= base_t::convert(other).value(); - } + template + self_t& operator *= (const OTHER& multiplicand) = delete; - template - LL_FORCE_INLINE bool operator >= (LLUnitImplicit other) const + LL_FORCE_INLINE constexpr self_t& operator /= (const storage_t& divisor) { - return base_t::mValue >= base_t::convert(other).value(); + base_t::operator /= (divisor); + return *this; } - template - LL_FORCE_INLINE bool operator >= (STORAGE_T other) const - { - return base_t::mValue >= other; - } + template + self_t& operator /= (const OTHER& divisor) = delete; }; template @@ -429,427 +359,329 @@ std::istream& operator >>(std::istream& s, LLUnitImplicit& return s; } -template -LL_FORCE_INLINE S2 ll_convert_units(LLUnit in, LLUnit& out) -{ - S2 divisor(1); - - LL_STATIC_ASSERT((LLIsSameType::value - || !LLIsSameType::value - || !LLIsSameType::value), - "conversion requires compatible units"); +// what an operation on two units yields: in the first one's units, and implicit when either of them is +template +using ALUnitResult = std::conditional_t || ALImplicitUnitType, + LLUnitImplicit, + LLUnit>; - if (LLIsSameType::value) - { - // T1 and T2 same type, just assign - out.value((S2)in.value()); - } - else if (T1::sLevel > T2::sLevel) - { - // reduce T1 - LLUnit new_in; - divisor *= (S2)ll_convert_units(in, new_in); - divisor *= (S2)ll_convert_units(new_in, out); - } - else - { - // reduce T2 - LLUnit new_out; - divisor *= (S2)ll_convert_units(in, new_out); - divisor *= (S2)ll_convert_units(new_out, out); - } - return divisor; -} - -template -struct LLStorageType -{ - typedef T type_t; -}; - -template -struct LLStorageType > -{ - typedef STORAGE_TYPE type_t; -}; - -// all of these operators need to perform type promotion on the storage type of the units, so they -// cannot be expressed as operations on identical types with implicit unit conversion -// e.g. typeof(S32Bytes(x) + F32Megabytes(y)) <==> F32Bytes +// what an operation on a unit and a plain value yields: the unit's kind, in STORAGE_TYPE +template +using ALUnitLike = std::conditional_t, + LLUnitImplicit, + LLUnit>; // -// operator + +// Operators between two units take any mix of LLUnit and LLUnitImplicit, and convert the second into the first's +// units. An operation the units do not support is deleted rather than left out: a unit converts to its storage type, +// so an operator left out would be taken by the storage type's own and drop the units without a word. // -template -LL_FORCE_INLINE LLUnit::type_t, UNITS1> operator + (LLUnit first, LLUnit second) -{ - LLUnit::type_t, UNITS1> result(first); - result += second; - return result; -} -template -LLUnit operator + (LLUnit first, UNITLESS second) +// +// operator + and -: two units of one dimension, or an implicit unit and a plain value +// +template + requires ALCompatibleUnits +[[nodiscard]] LL_FORCE_INLINE constexpr ALUnitResult> operator + (const UNIT1& first, const UNIT2& second) { - LL_BAD_TEMPLATE_INSTANTIATION(STORAGE_TYPE, "operator + requires compatible unit types"); - return LLUnit(0); + using result_t = ALUnitResult>; + return result_t(result_t::convert(first).value() + result_t::convert(second).value()); } -template -LLUnit operator + (UNITLESS first, LLUnit second) -{ - LL_BAD_TEMPLATE_INSTANTIATION(STORAGE_TYPE, "operator + requires compatible unit types"); - return LLUnit(0); -} +template + requires (!ALCompatibleUnits) +void operator + (const UNIT1& first, const UNIT2& second) = delete; -template -LL_FORCE_INLINE LLUnitImplicit::type_t, UNITS1> operator + (LLUnitImplicit first, LLUnitImplicit second) +template +[[nodiscard]] LL_FORCE_INLINE constexpr LLUnitImplicit, typename UNIT::unit_t> operator + (const UNIT& first, UNITLESS_TYPE second) { - LLUnitImplicit::type_t, UNITS1> result(first); + LLUnitImplicit, typename UNIT::unit_t> result(first); result += second; return result; } -template -LL_FORCE_INLINE LLUnitImplicit::type_t, UNITS1> operator + (LLUnit first, LLUnitImplicit second) +template +[[nodiscard]] LL_FORCE_INLINE constexpr LLUnitImplicit, typename UNIT::unit_t> operator + (UNITLESS_TYPE first, const UNIT& second) { - LLUnitImplicit::type_t, UNITS1> result(first); + LLUnitImplicit, typename UNIT::unit_t> result(first); result += second; return result; } -template -LL_FORCE_INLINE LLUnitImplicit::type_t, UNITS1> operator + (LLUnitImplicit first, LLUnit second) -{ - LLUnitImplicit::type_t, UNITS1> result(first); - result += LLUnitImplicit(second); - return result; -} +// an explicit unit and a plain value do not add +template +void operator + (const UNIT& first, const UNITLESS_TYPE& second) = delete; + +template +void operator + (const UNITLESS_TYPE& first, const UNIT& second) = delete; -template -LL_FORCE_INLINE LLUnitImplicit::type_t>::type_t, UNITS> operator + (LLUnitImplicit first, UNITLESS_TYPE second) +template + requires ALCompatibleUnits +[[nodiscard]] LL_FORCE_INLINE constexpr ALUnitResult> operator - (const UNIT1& first, const UNIT2& second) { - LLUnitImplicit::type_t>::type_t, UNITS> result(first); - result += second; - return result; + using result_t = ALUnitResult>; + return result_t(result_t::convert(first).value() - result_t::convert(second).value()); } -template -LL_FORCE_INLINE LLUnitImplicit::type_t, STORAGE_TYPE>:: - type_t, UNITS> operator + (UNITLESS_TYPE first, LLUnitImplicit second) +template + requires (!ALCompatibleUnits) +void operator - (const UNIT1& first, const UNIT2& second) = delete; + +template +[[nodiscard]] LL_FORCE_INLINE constexpr LLUnitImplicit, typename UNIT::unit_t> operator - (const UNIT& first, UNITLESS_TYPE second) { - LLUnitImplicit::type_t, STORAGE_TYPE>::type_t, UNITS> result(first); - result += second; + LLUnitImplicit, typename UNIT::unit_t> result(first); + result -= second; return result; } -// -// operator - -// -template -LL_FORCE_INLINE LLUnit::type_t, UNITS1> operator - (LLUnit first, LLUnit second) +template +[[nodiscard]] LL_FORCE_INLINE constexpr LLUnitImplicit, typename UNIT::unit_t> operator - (UNITLESS_TYPE first, const UNIT& second) { - LLUnit::type_t, UNITS1> result(first); + LLUnitImplicit, typename UNIT::unit_t> result(first); result -= second; return result; } -template -LLUnit operator - (LLUnit first, UNITLESS second) -{ - LL_BAD_TEMPLATE_INSTANTIATION(STORAGE_TYPE, "operator - requires compatible unit types"); - return LLUnit(0); -} +template +void operator - (const UNIT& first, const UNITLESS_TYPE& second) = delete; -template -LLUnit operator - (UNITLESS first, LLUnit second) -{ - LL_BAD_TEMPLATE_INSTANTIATION(STORAGE_TYPE, "operator - requires compatible unit types"); - return LLUnit(0); -} +template +void operator - (const UNITLESS_TYPE& first, const UNIT& second) = delete; -template -LL_FORCE_INLINE LLUnitImplicit::type_t, UNITS1> operator - (LLUnitImplicit first, LLUnitImplicit second) +// +// operator *: a unit and a plain value +// +template +[[nodiscard]] LL_FORCE_INLINE constexpr ALUnitLike> operator * (const UNIT& first, UNITLESS_TYPE second) { - LLUnitImplicit::type_t, UNITS1> result(first); - result -= second; - return result; + return ALUnitLike>(first.value() * second); } -template -LL_FORCE_INLINE LLUnitImplicit::type_t, UNITS1> operator - (LLUnit first, LLUnitImplicit second) +template +[[nodiscard]] LL_FORCE_INLINE constexpr ALUnitLike> operator * (UNITLESS_TYPE first, const UNIT& second) { - LLUnitImplicit::type_t, UNITS1> result(first); - result -= second; - return result; + return ALUnitLike>(first * second.value()); } -template -LL_FORCE_INLINE LLUnitImplicit::type_t, UNITS1> operator - (LLUnitImplicit first, LLUnit second) -{ - LLUnitImplicit::type_t, UNITS1> result(first); - result -= LLUnitImplicit(second); - return result; -} +// a product of units is a new kind of unit +template +void operator * (const UNIT1& first, const UNIT2& second) = delete; -template -LL_FORCE_INLINE LLUnitImplicit::type_t>::type_t, UNITS> operator - (LLUnitImplicit first, UNITLESS_TYPE second) +// +// operator /: a unit by a plain value, or by a unit of its dimension, which leaves a plain value +// +template +[[nodiscard]] LL_FORCE_INLINE constexpr ALUnitLike> operator / (const UNIT& first, UNITLESS_TYPE second) { - LLUnitImplicit::type_t>::type_t, UNITS> result(first); - result -= second; - return result; + return ALUnitLike>(first.value() / second); } -template -LL_FORCE_INLINE LLUnitImplicit::type_t, STORAGE_TYPE>::type_t, UNITS> operator - (UNITLESS_TYPE first, LLUnitImplicit second) +template + requires ALCompatibleUnits +[[nodiscard]] LL_FORCE_INLINE constexpr LLResultTypeDivide operator / (const UNIT1& first, const UNIT2& second) { - LLUnitImplicit::type_t, STORAGE_TYPE>::type_t, UNITS> result(first); - result -= second; - return result; + using first_t = LLUnit; + return (LLResultTypeDivide)(first.value() / first_t::convert(second).value()); } +template + requires (!ALCompatibleUnits) +void operator / (const UNIT1& first, const UNIT2& second) = delete; + // -// operator * +// comparisons between two units of one dimension, or between an implicit unit and a plain value // -template -LLUnit operator * (LLUnit, LLUnit) +// != is rewritten from ==. The relational operators are not rewritten from <=>, which MSVC compiles to a chain of +// branches for a floating point storage type. +// +template + requires ALCompatibleUnits +[[nodiscard]] LL_FORCE_INLINE constexpr bool operator == (const UNIT1& first, const UNIT2& second) { - // spurious use of dependent type to stop gcc from triggering the static assertion before instantiating the template - LL_BAD_TEMPLATE_INSTANTIATION(STORAGE_TYPE1, "multiplication of unit types results in new unit type - not supported."); - return LLUnit(); + return first.value() == LLUnit::convert(second).value(); } -template -LL_FORCE_INLINE LLUnit::type_t>::type_t, UNITS> operator * (LLUnit first, UNITLESS_TYPE second) +template + requires ALCompatibleUnits +[[nodiscard]] LL_FORCE_INLINE constexpr bool operator < (const UNIT1& first, const UNIT2& second) { - return LLUnit::type_t>::type_t, UNITS>(first.value() * second); + return first.value() < LLUnit::convert(second).value(); } -template -LL_FORCE_INLINE LLUnit::type_t, STORAGE_TYPE>::type_t, UNITS> operator * (UNITLESS_TYPE first, LLUnit second) +template + requires ALCompatibleUnits +[[nodiscard]] LL_FORCE_INLINE constexpr bool operator <= (const UNIT1& first, const UNIT2& second) { - return LLUnit::type_t, STORAGE_TYPE>::type_t, UNITS>(first * second.value()); + return first.value() <= LLUnit::convert(second).value(); } -template -LLUnitImplicit operator * (LLUnitImplicit, LLUnitImplicit) +template + requires ALCompatibleUnits +[[nodiscard]] LL_FORCE_INLINE constexpr bool operator > (const UNIT1& first, const UNIT2& second) { - // spurious use of dependent type to stop gcc from triggering the static assertion before instantiating the template - LL_BAD_TEMPLATE_INSTANTIATION(STORAGE_TYPE1, "multiplication of unit types results in new unit type - not supported."); - return LLUnitImplicit(); + return first.value() > LLUnit::convert(second).value(); } -template -LL_FORCE_INLINE LLUnitImplicit::type_t>::type_t, UNITS> operator * (LLUnitImplicit first, UNITLESS_TYPE second) +template + requires ALCompatibleUnits +[[nodiscard]] LL_FORCE_INLINE constexpr bool operator >= (const UNIT1& first, const UNIT2& second) { - return LLUnitImplicit::type_t, UNITS>(first.value() * second); + return first.value() >= LLUnit::convert(second).value(); } -template -LL_FORCE_INLINE LLUnitImplicit::type_t, STORAGE_TYPE>::type_t, UNITS> operator * (UNITLESS_TYPE first, LLUnitImplicit second) -{ - return LLUnitImplicit::type_t, STORAGE_TYPE>::type_t, UNITS>(first * second.value()); -} +template + requires (!ALCompatibleUnits) +bool operator == (const UNIT1& first, const UNIT2& second) = delete; +template + requires (!ALCompatibleUnits) +bool operator < (const UNIT1& first, const UNIT2& second) = delete; -// -// operator / -// +template + requires (!ALCompatibleUnits) +bool operator <= (const UNIT1& first, const UNIT2& second) = delete; -template -LL_FORCE_INLINE LLUnit::type_t>::type_t, UNITS> operator / (LLUnit first, UNITLESS_TYPE second) -{ - return LLUnit::type_t>::type_t, UNITS>(first.value() / second); -} +template + requires (!ALCompatibleUnits) +bool operator > (const UNIT1& first, const UNIT2& second) = delete; + +template + requires (!ALCompatibleUnits) +bool operator >= (const UNIT1& first, const UNIT2& second) = delete; -template -LL_FORCE_INLINE typename LLResultTypeDivide::type_t operator / (LLUnit first, LLUnit second) +template +[[nodiscard]] LL_FORCE_INLINE constexpr bool operator == (const UNIT& first, const UNITLESS_TYPE& second) { - return first.value() / first.convert(second).value(); + return first.value() == second; } -template -LL_FORCE_INLINE LLUnitImplicit::type_t>::type_t, UNITS> operator / (LLUnitImplicit first, UNITLESS_TYPE second) +template +[[nodiscard]] LL_FORCE_INLINE constexpr bool operator < (const UNIT& first, const UNITLESS_TYPE& second) { - return LLUnitImplicit::type_t>::type_t, UNITS>(first.value() / second); + return first.value() < second; } -template -LL_FORCE_INLINE typename LLResultTypeDivide::type_t operator / (LLUnitImplicit first, LLUnitImplicit second) +template +[[nodiscard]] LL_FORCE_INLINE constexpr bool operator <= (const UNIT& first, const UNITLESS_TYPE& second) { - return (typename LLResultTypeDivide::type_t)(first.value() / first.convert(second).value()); + return first.value() <= second; } -template -LL_FORCE_INLINE typename LLResultTypeDivide::type_t operator / (LLUnit first, LLUnitImplicit second) +template +[[nodiscard]] LL_FORCE_INLINE constexpr bool operator > (const UNIT& first, const UNITLESS_TYPE& second) { - return (typename LLResultTypeDivide::type_t)(first.value() / first.convert(second).value()); + return first.value() > second; } -template -LL_FORCE_INLINE typename LLResultTypeDivide::type_t operator / (LLUnitImplicit first, LLUnit second) +template +[[nodiscard]] LL_FORCE_INLINE constexpr bool operator >= (const UNIT& first, const UNITLESS_TYPE& second) { - return (typename LLResultTypeDivide::type_t)(first.value() / first.convert(second).value()); + return first.value() >= second; } template struct LLGetUnitLabel { - static const char* getUnitLabel() { return ""; } + static constexpr const char* getUnitLabel() { return ""; } }; template struct LLGetUnitLabel > { - static const char* getUnitLabel() { return T::getUnitLabel(); } + static constexpr const char* getUnitLabel() { return T::getUnitLabel(); } }; -template -struct LLUnitLinearOps +// formats the value as its storage type would be, then the unit's label: fmt::format("{:.1f}", F32Seconds(2.f)) is "2.0s" +template +struct fmt::formatter> : fmt::formatter { - typedef LLUnitLinearOps self_t; - - LLUnitLinearOps(T val) - : mValue(val), - mDivisor(1) - {} - - template - self_t operator * (OTHER_T other) - { - return mValue * other; - } - - template - self_t operator / (OTHER_T other) - { - mDivisor *= other; - return *this; - } - - template - self_t operator + (OTHER_T other) - { - mValue += other * mDivisor; - return *this; - } - - template - self_t operator - (OTHER_T other) + template + auto format(const LLUnit& unit, FORMAT_CONTEXT& ctx) const { - mValue -= other * mDivisor; - return *this; + auto out = fmt::formatter::format(unit.value(), ctx); + for (const char* label = UNITS::getUnitLabel(); *label; ++label) + { + *out++ = *label; + } + return out; } - - T mValue; - T mDivisor; }; -template -struct LLUnitInverseLinearOps +template +struct fmt::formatter> : fmt::formatter> { - typedef LLUnitInverseLinearOps self_t; - - LLUnitInverseLinearOps(T val) - : mValue(val), - mDivisor(1), - mMultiplicand(1) - {} - - template - self_t operator * (OTHER_T other) - { - mDivisor *= other; - return *this; - } - - template - self_t operator / (OTHER_T other) - { - mValue *= other; - mMultiplicand *= other; - return *this; - } +}; - template - self_t operator + (OTHER_T other) +// +// unit definitions +// +// A unit is a type naming its base unit and its label, and, for a unit defined from another, sConversion: the +// ALUnitConversion taking a value in the base unit to one in this unit. +// +// struct Kilobytes : ALDerivedUnit +// { +// static constexpr const char* getUnitLabel() { return "KB"; } +// static constexpr auto sConversion = ALUnitConversion() / 1024; +// }; +// +// Units of one dimension convert into each other through the base unit they share, as one map worked out at compile +// time. LL_DECLARE_BASE_UNIT and LL_DECLARE_DERIVED_UNIT write these out. +// +template +struct ALUnitDefinition +{ + template + [[nodiscard]] static constexpr LLUnit fromValue(T value) { - mValue -= other * mMultiplicand; - return *this; + return LLUnit(value); } - template - self_t operator - (OTHER_T other) + template + [[nodiscard]] static constexpr LLUnit fromValue(LLUnit value) { - mValue += other * mMultiplicand; - return *this; + return LLUnit(value); } - - T mValue; - T mDivisor; - T mMultiplicand; }; -#define LL_DECLARE_BASE_UNIT(base_unit_name, unit_label) \ -struct base_unit_name \ -{ \ - static const int sLevel = 0; \ - typedef base_unit_name base_unit_t; \ - static const char* getUnitLabel() { return unit_label; } \ - template \ - static LLUnit fromValue(T value) { return LLUnit(value); } \ - template \ - static LLUnit fromValue(LLUnit value) \ - { return LLUnit(value); } \ -} - +template +struct ALBaseUnit : ALUnitDefinition +{ + using base_unit_t = SELF; +}; -#define LL_DECLARE_DERIVED_UNIT(unit_name, unit_label, base_unit_name, conversion_operation) \ -struct unit_name \ -{ \ - static const int sLevel = base_unit_name::sLevel + 1; \ - typedef base_unit_name base_unit_t; \ - static const char* getUnitLabel() { return unit_label; } \ - template \ - static LLUnit fromValue(T value) { return LLUnit(value); } \ - template \ - static LLUnit fromValue(LLUnit value) \ - { return LLUnit(value); } \ -}; \ - \ -template \ -LL_FORCE_INLINE S2 ll_convert_units(LLUnit in, LLUnit& out) \ -{ \ - typedef typename LLResultTypePromote::type_t result_storage_t; \ - LLUnitInverseLinearOps result = \ - LLUnitInverseLinearOps(in.value()) conversion_operation; \ - out = LLUnit((S2)result.mValue); \ - return result.mDivisor; \ -} \ - \ -template \ -LL_FORCE_INLINE S2 ll_convert_units(LLUnit in, LLUnit& out) \ -{ \ - typedef typename LLResultTypePromote::type_t result_storage_t; \ - LLUnitLinearOps result = \ - LLUnitLinearOps(in.value()) conversion_operation; \ - out = LLUnit((S2)result.mValue); \ - return result.mDivisor; \ -} +template +struct ALDerivedUnit : ALUnitDefinition +{ + using base_unit_t = BASE_UNIT; +}; -#define LL_DECLARE_UNIT_TYPEDEFS(ns, unit_name) \ - typedef LLUnit F32##unit_name; \ - typedef LLUnitImplicit F32##unit_name##Implicit;\ - typedef LLUnit F64##unit_name; \ - typedef LLUnitImplicit F64##unit_name##Implicit;\ - typedef LLUnit S32##unit_name; \ - typedef LLUnitImplicit S32##unit_name##Implicit;\ - typedef LLUnit S64##unit_name; \ - typedef LLUnitImplicit S64##unit_name##Implicit;\ - typedef LLUnit U32##unit_name; \ - typedef LLUnitImplicit U32##unit_name##Implicit;\ - typedef LLUnit U64##unit_name; \ - typedef LLUnitImplicit U64##unit_name##Implicit +#define LL_DECLARE_BASE_UNIT(base_unit_name, unit_label) \ +struct base_unit_name : ALBaseUnit \ +{ \ + static constexpr const char* getUnitLabel() { return unit_label; } \ +} + +#define LL_DECLARE_DERIVED_UNIT(unit_name, unit_label, base_unit_name, conversion_operation) \ +struct unit_name : ALDerivedUnit \ +{ \ + static constexpr const char* getUnitLabel() { return unit_label; } \ + static constexpr auto sConversion = ALUnitConversion() conversion_operation; \ +} + +#define LL_DECLARE_UNIT_TYPEDEFS(ns, unit_name) \ + using F32##unit_name = LLUnit; \ + using F32##unit_name##Implicit = LLUnitImplicit; \ + using F64##unit_name = LLUnit; \ + using F64##unit_name##Implicit = LLUnitImplicit; \ + using S32##unit_name = LLUnit; \ + using S32##unit_name##Implicit = LLUnitImplicit; \ + using S64##unit_name = LLUnit; \ + using S64##unit_name##Implicit = LLUnitImplicit; \ + using U32##unit_name = LLUnit; \ + using U32##unit_name##Implicit = LLUnitImplicit; \ + using U64##unit_name = LLUnit; \ + using U64##unit_name##Implicit = LLUnitImplicit #ifdef LL_WINDOWS #pragma warning(pop) diff --git a/indra/llcommon/tests/alunitconversion_test.cpp b/indra/llcommon/tests/alunitconversion_test.cpp new file mode 100644 index 0000000000..c6dfd97af9 --- /dev/null +++ b/indra/llcommon/tests/alunitconversion_test.cpp @@ -0,0 +1,123 @@ +/** + * @file alunitconversion_test.cpp + * @brief Tests of ALUnitConversion. + * + * $LicenseInfo:firstyear=2026&license=viewerlgpl$ + * Alchemy Viewer Source Code + * Copyright (C) 2026, Rye + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; + * version 2.1 of the License only. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * $/LicenseInfo$ + */ + +#include "linden_common.h" + +#include "../alunitconversion.h" + +#include + +#include +#include +#include + +namespace +{ + template + constexpr bool has_terms(ALUnitConversion conversion, T multiplier, T offset, T divisor) + { + return conversion.mMultiplier == multiplier && conversion.mOffset == offset && conversion.mDivisor == divisor; + } + + struct ToFahrenheit + { + static constexpr auto value = ALUnitConversion() * 9 / 5 + 32; + }; + + struct FromFahrenheit + { + static constexpr auto value = al_invert_unit_conversion(ToFahrenheit::value); + }; + + // microseconds to days, by a divisor no 32 bit integer holds + struct ToDays + { + static constexpr auto value = ALUnitConversion() / 86400000000LL; + }; + + struct ToDegrees + { + static constexpr auto value = ALUnitConversion() * 57.29578f; + }; + + struct Doubled + { + static constexpr auto value = ALUnitConversion() * 2; + }; + + TEST_CASE("ALUnitConversion: operations build the map left to right in lowest terms", "[ALUnitConversion]") + { + STATIC_REQUIRE(has_terms(ALUnitConversion(), 1, 0, 1)); + STATIC_REQUIRE(has_terms(ALUnitConversion() * 9 / 5 + 32, 9, 160, 5)); + STATIC_REQUIRE(has_terms(ALUnitConversion() / 4 * 2, 1, 0, 2)); + STATIC_REQUIRE(has_terms(ALUnitConversion() * 8 / 1024, 1, 0, 128)); + STATIC_REQUIRE(has_terms(ALUnitConversion() / 2 - 3, 1, -6, 2)); + } + + TEST_CASE("ALUnitConversion: a floating point factor makes the terms doubles", "[ALUnitConversion]") + { + STATIC_REQUIRE(std::is_same_v>); + STATIC_REQUIRE(std::is_same_v>); + STATIC_REQUIRE(std::is_same_v>); + STATIC_REQUIRE(has_terms(ALUnitConversion() * 9 / 5 + 32.5, 9.0, 162.5, 5.0)); + } + + TEST_CASE("ALUnitConversion: compositions and inverses reduce to one map", "[ALUnitConversion]") + { + STATIC_REQUIRE(has_terms(FromFahrenheit::value, 5, -160, 9)); + STATIC_REQUIRE(has_terms(al_compose_unit_conversions(ToFahrenheit::value, FromFahrenheit::value), 1, 0, 1)); + + constexpr auto kilobytes = ALUnitConversion() / 1024; + STATIC_REQUIRE(has_terms(al_compose_unit_conversions(kilobytes, kilobytes), 1, 0, 1048576)); + STATIC_REQUIRE(has_terms(al_compose_unit_conversions(al_invert_unit_conversion(kilobytes), ALUnitConversion() * 8), 8192, 0, 1)); + + // an offset in the first conversion is scaled by the second, one in the second is not + STATIC_REQUIRE(has_terms(al_compose_unit_conversions(ALUnitConversion() + 1, ALUnitConversion() * 3 + 2), 3, 5, 1)); + } + + TEST_CASE("ALUnitConversion: an integer conversion is exact and divides once", "[ALUnitConversion]") + { + STATIC_REQUIRE(al_apply_unit_conversion(100) == 212); + STATIC_REQUIRE(al_apply_unit_conversion(212) == 100); + // (0 * 5 - 160) / 9 is -17.8, which truncates toward zero + STATIC_REQUIRE(al_apply_unit_conversion(0) == -17); + STATIC_REQUIRE(al_apply_unit_conversion(std::uint32_t(4294967295u)) == 0u); + STATIC_REQUIRE(al_apply_unit_conversion(std::int64_t(172800000000)) == 2); + } + + TEST_CASE("ALUnitConversion: a floating point conversion is worked in the value's own type", "[ALUnitConversion]") + { + STATIC_REQUIRE(al_apply_unit_conversion(37.f) == (37.f * 9.f + 160.f) / 5.f); + STATIC_REQUIRE(al_apply_unit_conversion(2.f) == 2.f * 57.29578f); + STATIC_REQUIRE(al_apply_unit_conversion(2.0) == 2.0 * (double)57.29578f); + // an integer value under a floating point conversion is worked in double, then truncated + STATIC_REQUIRE(al_apply_unit_conversion(2) == 114); + } + + TEST_CASE("ALUnitConversion: a conversion with no offset keeps negative zero", "[ALUnitConversion]") + { + CHECK(std::signbit(al_apply_unit_conversion(-0.f))); + CHECK(std::signbit(al_apply_unit_conversion(-0.0))); + } +} diff --git a/indra/llcommon/tests/llunits_test.cpp b/indra/llcommon/tests/llunits_test.cpp index 036aaef30c..f7f9c06a54 100644 --- a/indra/llcommon/tests/llunits_test.cpp +++ b/indra/llcommon/tests/llunits_test.cpp @@ -30,6 +30,11 @@ #include "llunits.h" #include +#include + +#include +#include + namespace LLUnits { namespace @@ -38,12 +43,15 @@ namespace LLUnits LL_DECLARE_BASE_UNIT(Quatloos, "Quat"); LL_DECLARE_DERIVED_UNIT(Latinum, "Lat", Quatloos, / 4); LL_DECLARE_DERIVED_UNIT(Solari, "Sol", Latinum, * 16); + // a multiplication after a division, which must keep the division's divisor + LL_DECLARE_DERIVED_UNIT(Ducats, "Duc", Quatloos, / 4 * 2); } // anonymous namespace } LL_DECLARE_UNIT_TYPEDEFS(LLUnits, Quatloos); LL_DECLARE_UNIT_TYPEDEFS(LLUnits, Latinum); LL_DECLARE_UNIT_TYPEDEFS(LLUnits, Solari); +LL_DECLARE_UNIT_TYPEDEFS(LLUnits, Ducats); namespace LLUnits { @@ -377,4 +385,247 @@ namespace int_celcius = int_fahrenheit; CHECK(int_celcius.value() == 100); } + + // a conversion's operations after a division are applied over the division's divisor + TEST_CASE("LLUnit: a conversion that multiplies after dividing keeps its divisor", "[LLUnit]") + { + F32Ducats ducats = F32Quatloos(8.f); + CHECK(ducats.value() == 4.f); + + F32Quatloos quatloos = F32Ducats(4.f); + CHECK(quatloos.value() == 8.f); + + S32Ducats int_ducats = S32Quatloos(8); + CHECK(int_ducats.value() == 4); + } + + // comparisons + TEST_CASE("LLUnit: comparisons convert the right operand into the storage of the left", "[LLUnit]") + { + // 1.5 truncates to 1 in S32 storage, so which operand is converted decides each answer + S32Quatloos int_quatloos(1); + F32QuatloosImplicit float_quatloos(1.5f); + + CHECK(int_quatloos == float_quatloos); + CHECK_FALSE(int_quatloos != float_quatloos); + CHECK_FALSE(int_quatloos < float_quatloos); + CHECK(int_quatloos >= float_quatloos); + + CHECK(float_quatloos != int_quatloos); + CHECK(float_quatloos > int_quatloos); + } + + constexpr F32Quatloos accumulate_quatloos() + { + F32Quatloos quatloos; + quatloos += S32Latinum(1); + quatloos *= 2.f; + quatloos -= F32Quatloos(1.f); + quatloos /= 7.f; + quatloos.valueInUnits(quatloos.value()); + return quatloos; + } + + constexpr F32QuatloosImplicit accumulate_implicit_quatloos() + { + F32QuatloosImplicit quatloos; + quatloos += 2; + quatloos += F32Quatloos(4.f); + quatloos += F32LatinumImplicit(1.f); + quatloos -= 4; + quatloos -= F32LatinumImplicit(1.f); + return quatloos; + } + + // constant expressions + TEST_CASE("LLUnit: conversions and arithmetic are constant expressions", "[LLUnit]") + { + constexpr F32Quatloos quatloos(1024.f); + STATIC_REQUIRE(F32Solari(quatloos).value() == 4096.f); + STATIC_REQUIRE(F32Latinum(quatloos) == S32Latinum(256)); + STATIC_REQUIRE(F32Quatloos(S32Solari(1)) == F32Quatloos(0.25f)); + STATIC_REQUIRE(quatloos.valueInUnits() == 256.f); + STATIC_REQUIRE(U32Kilobytes(U32Gigabytes(1)).value() == 1024 * 1024); + STATIC_REQUIRE(S32Fahrenheit(S32Celcius(100)).value() == 212); + STATIC_REQUIRE(S32Celcius(S32Fahrenheit(212)).value() == 100); + STATIC_REQUIRE(F32Ducats(F32Quatloos(8.f)).value() == 4.f); + STATIC_REQUIRE(Ratio::fromValue(0.5f) == F32Percent(50.f)); + + STATIC_REQUIRE(F32Seconds(1.f) + F32Milliseconds(500.f) == F32Seconds(1.5f)); + STATIC_REQUIRE(F32Seconds(1.f) - F32Milliseconds(250.f) == F32Milliseconds(750.f)); + STATIC_REQUIRE(F32Seconds(2.f) * 3 == F32Seconds(6.f)); + STATIC_REQUIRE(3 * F32Seconds(2.f) == F32Seconds(6.f)); + STATIC_REQUIRE(F32Seconds(2.f) / 4 == F32Seconds(0.5f)); + STATIC_REQUIRE(F32Seconds(1.f) / F32Milliseconds(250.f) == 4.f); + STATIC_REQUIRE(accumulate_quatloos() == F32Quatloos(4.f)); + + STATIC_REQUIRE(F32QuatloosImplicit(1.f) + 3 == 4); + STATIC_REQUIRE(10 - F32QuatloosImplicit(4.f) == 6); + STATIC_REQUIRE(2 * F32QuatloosImplicit(4.f) / 4 == 2); + STATIC_REQUIRE(F32QuatloosImplicit(4.f) / F32LatinumImplicit(1.f) == 1); + STATIC_REQUIRE(accumulate_implicit_quatloos() == 2); + + constexpr F32 plain = F32QuatloosImplicit(4.f); + STATIC_REQUIRE(plain == 4.f); + } + + TEST_CASE("LLUnit: comparisons are constant expressions", "[LLUnit]") + { + STATIC_REQUIRE(S32Quatloos(1) < S32Latinum(1)); + STATIC_REQUIRE(S32Latinum(1) > S32Quatloos(1)); + STATIC_REQUIRE(S32Quatloos(4) <= S32Latinum(1)); + STATIC_REQUIRE(S32Quatloos(4) >= F32Latinum(1.f)); + STATIC_REQUIRE(F32Quatloos(1.f) != F32Latinum(1.f)); + STATIC_REQUIRE(F32QuatloosImplicit(4.f) == F32Latinum(1.f)); + + STATIC_REQUIRE(F32QuatloosImplicit(1.f) < 2); + STATIC_REQUIRE(F32QuatloosImplicit(2.f) >= 2); + STATIC_REQUIRE(1 == F32QuatloosImplicit(1.f)); + STATIC_REQUIRE(1 != F32QuatloosImplicit(2.f)); + STATIC_REQUIRE(2 > F32QuatloosImplicit(1.f)); + } + + TEST_CASE("LLUnit: unit labels and levels are constant expressions", "[LLUnit]") + { + STATIC_REQUIRE(std::string_view(Quatloos::getUnitLabel()) == "Quat"); + STATIC_REQUIRE(std::string_view(LLGetUnitLabel::getUnitLabel()) == "Sol"); + STATIC_REQUIRE(std::string_view(LLGetUnitLabel::getUnitLabel()).empty()); + STATIC_REQUIRE(std::string_view(Microseconds::getUnitLabel()) == "\xC2\xB5s"); + } + + // result types + TEST_CASE("LLUnit: arithmetic promotes storage as the storage types promote", "[LLUnit]") + { + STATIC_REQUIRE(std::is_same_v); + STATIC_REQUIRE(std::is_same_v); + STATIC_REQUIRE(std::is_same_v); + STATIC_REQUIRE(std::is_same_v); + STATIC_REQUIRE(std::is_same_v); + STATIC_REQUIRE(std::is_same_v); + + STATIC_REQUIRE(std::is_same_v); + STATIC_REQUIRE(std::is_same_v); + STATIC_REQUIRE(std::is_same_v); + STATIC_REQUIRE(std::is_same_v); + STATIC_REQUIRE(std::is_same_v); + STATIC_REQUIRE(std::is_same_v); + } + + // conversions through the root unit + TEST_CASE("LLUnit: a conversion between two derived units applies both offsets", "[LLUnit]") + { + // Fahrenheit to Kelvin goes through Celsius, with an offset each way + F32Kelvin kelvin = F32Fahrenheit(212.f); + CHECK(value_near(kelvin.value(), 373.15f, 0.01f)); + + F32Fahrenheit fahrenheit = F32Kelvin(273.15f); + CHECK(value_near(fahrenheit.value(), 32.f, 0.01f)); + } + + TEST_CASE("LLUnit: an integer conversion divides by a divisor its storage cannot hold", "[LLUnit]") + { + // a day is 86400000000 microseconds, more than a U32 holds + CHECK(U32Days(U32Microseconds(U32_MAX)).value() == 0); + CHECK(U32Microseconds(U32Seconds(4000)).value() == 4000000000u); + CHECK(S64Microseconds(S32Days(1)).value() == 86400000000); + } + + TEST_CASE("LLUnit: adding an explicit unit to an implicit one keeps the explicit one's fraction", "[LLUnit]") + { + // 0.375 latinum is 1.5 quatloos, which S32 storage would truncate to 1 + F32 sum = S32QuatloosImplicit(1) + F32Latinum(0.375f); + CHECK(sum == 2.5f); + + F32 difference = S32QuatloosImplicit(1) - F32Latinum(0.375f); + CHECK(difference == -0.5f); + } + + // what the units reject, asked of a concept, since an expression that does not compile cannot be tested otherwise + template + concept CanAdd = requires (const A& a, const B& b) { a + b; }; + + template + concept CanSubtract = requires (const A& a, const B& b) { a - b; }; + + template + concept CanMultiply = requires (const A& a, const B& b) { a * b; }; + + template + concept CanDivide = requires (const A& a, const B& b) { a / b; }; + + template + concept CanOrder = requires (const A& a, const B& b) { a < b; }; + + template + concept CanEqual = requires (const A& a, const B& b) { a == b; }; + + template + concept CanAddTo = requires (A& a, const B& b) { a += b; }; + + template + concept CanMultiplyBy = requires (A& a, const B& b) { a *= b; }; + + TEST_CASE("LLUnit: units of different dimensions do not mix", "[LLUnit]") + { + STATIC_REQUIRE(CanAdd); + STATIC_REQUIRE_FALSE(CanAdd); + STATIC_REQUIRE_FALSE(CanAdd); + STATIC_REQUIRE_FALSE(CanSubtract); + STATIC_REQUIRE_FALSE(CanDivide); + STATIC_REQUIRE_FALSE(CanOrder); + STATIC_REQUIRE_FALSE(CanEqual); + STATIC_REQUIRE_FALSE(CanAddTo); + STATIC_REQUIRE_FALSE(CanAddTo); + + STATIC_REQUIRE(std::is_constructible_v); + STATIC_REQUIRE_FALSE(std::is_constructible_v); + STATIC_REQUIRE_FALSE(std::is_constructible_v); + STATIC_REQUIRE_FALSE(std::is_convertible_v); + } + + TEST_CASE("LLUnit: an explicit unit takes a plain value only as a factor", "[LLUnit]") + { + STATIC_REQUIRE_FALSE(CanAdd); + STATIC_REQUIRE_FALSE(CanAdd); + STATIC_REQUIRE_FALSE(CanSubtract); + STATIC_REQUIRE(CanMultiply); + STATIC_REQUIRE(CanMultiply); + STATIC_REQUIRE(CanDivide); + STATIC_REQUIRE(CanMultiplyBy); + STATIC_REQUIRE_FALSE(std::is_convertible_v); + + STATIC_REQUIRE(CanAdd); + STATIC_REQUIRE(CanSubtract); + STATIC_REQUIRE(std::is_convertible_v); + } + + TEST_CASE("LLUnit: a product of units is not a unit", "[LLUnit]") + { + STATIC_REQUIRE_FALSE(CanMultiply); + STATIC_REQUIRE_FALSE(CanMultiply); + STATIC_REQUIRE_FALSE(CanMultiplyBy); + STATIC_REQUIRE_FALSE(CanMultiplyBy); + } + + TEST_CASE("LLUnit: compound assignments return the unit", "[LLUnit]") + { + F32QuatloosImplicit quatloos(1.f); + (quatloos += 1) *= 3; + CHECK(quatloos == 6); + STATIC_REQUIRE(std::is_same_v); + STATIC_REQUIRE(std::is_same_v); + + F32Quatloos explicit_quatloos(1.f); + STATIC_REQUIRE(std::is_same_v); + } + + // formatting + TEST_CASE("LLUnit: fmt formats the value as its storage type then the label", "[LLUnit]") + { + CHECK(fmt::format("{}", F32Seconds(1.5f)) == "1.5s"); + CHECK(fmt::format("{:.2f}", F32Seconds(1.5f)) == "1.50s"); + CHECK(fmt::format("{:>4}", S32Bytes(12)) == " 12B"); + CHECK(fmt::format("{}", F32QuatloosImplicit(2.f)) == "2Quat"); + CHECK(fmt::format("{}", U32Microseconds(3)) == "3\xC2\xB5s"); + } }