diff --git a/src/objects/include/monetaryamount.hpp b/src/objects/include/monetaryamount.hpp index 084a3a32..f4e79bd3 100644 --- a/src/objects/include/monetaryamount.hpp +++ b/src/objects/include/monetaryamount.hpp @@ -130,7 +130,7 @@ class MonetaryAmount { [[nodiscard]] bool isCloseTo(MonetaryAmount otherAmount, double relativeDifference) const; [[nodiscard]] constexpr CurrencyCode currencyCode() const { - // We do not want to expose private nb decimals bits to outside world + // We do not want to expose private nb decimals bits return _curWithDecimals.withNoDecimalsPart(); } @@ -170,17 +170,21 @@ class MonetaryAmount { [[nodiscard]] constexpr bool operator==(const MonetaryAmount &) const = default; - [[nodiscard]] constexpr bool operator==(AmountType amount) const { return _amount == amount && nbDecimals() == 0; } - friend constexpr bool operator==(AmountType amount, MonetaryAmount rhs) { return rhs == amount; } - - [[nodiscard]] constexpr auto operator<=>(AmountType amount) const { - return _amount <=> amount * ipow10(static_cast(nbDecimals())); + /// Note: for comparison with numbers (integrals or double), only the amount is compared. + /// To be consistent with operator<=>, the currency will be ignored for equality. + /// TODO: check if this special behavior be problematic in some cases + [[nodiscard]] constexpr bool operator==(std::signed_integral auto amount) const { + return _amount == static_cast(amount) && nbDecimals() == 0; } - [[nodiscard]] friend constexpr auto operator<=>(AmountType amount, MonetaryAmount other) { - return amount * ipow10(static_cast(other.nbDecimals())) <=> other._amount; + [[nodiscard]] constexpr bool operator==(double amount) const { return amount == toDouble(); } + + [[nodiscard]] constexpr auto operator<=>(std::signed_integral auto amount) const { + return _amount <=> static_cast(amount) * ipow10(static_cast(nbDecimals())); } + [[nodiscard]] constexpr auto operator<=>(double amount) const { return toDouble() <=> amount; } + [[nodiscard]] constexpr MonetaryAmount abs() const noexcept { return {true, _amount < 0 ? -_amount : _amount, _curWithDecimals}; } @@ -199,8 +203,15 @@ class MonetaryAmount { MonetaryAmount &operator-=(MonetaryAmount other) { return *this = *this + (-other); } [[nodiscard]] MonetaryAmount operator*(AmountType mult) const; + [[nodiscard]] friend MonetaryAmount operator*(MonetaryAmount rhs, std::signed_integral auto mult) { + return static_cast(mult) * rhs; + } + [[nodiscard]] friend MonetaryAmount operator*(std::signed_integral auto mult, MonetaryAmount rhs) { + return rhs * static_cast(mult); + } - [[nodiscard]] friend MonetaryAmount operator*(AmountType mult, MonetaryAmount rhs) { return rhs * mult; } + [[nodiscard]] MonetaryAmount operator*(double mult) const { return *this * MonetaryAmount(mult); } + [[nodiscard]] friend MonetaryAmount operator*(double mult, MonetaryAmount rhs) { return rhs * mult; } /// Multiplication involving 2 MonetaryAmounts *must* have at least one 'Neutral' currency. /// This is to remove ambiguity on the resulting currency: @@ -210,15 +221,19 @@ class MonetaryAmount { /// - XXXXXXX * YYYYYYY -> ??????? (exception will be thrown in this case) [[nodiscard]] MonetaryAmount operator*(MonetaryAmount mult) const; - MonetaryAmount &operator*=(AmountType mult) { return *this = *this * mult; } + MonetaryAmount &operator*=(std::signed_integral auto mult) { return *this = *this * mult; } MonetaryAmount &operator*=(MonetaryAmount mult) { return *this = *this * mult; } + MonetaryAmount &operator*=(double mult) { return *this = *this * mult; } + + [[nodiscard]] MonetaryAmount operator/(std::signed_integral auto div) const { return *this / MonetaryAmount(div); } - [[nodiscard]] MonetaryAmount operator/(AmountType div) const { return *this / MonetaryAmount(div); } + [[nodiscard]] MonetaryAmount operator/(double div) const { return *this / MonetaryAmount(div); } [[nodiscard]] MonetaryAmount operator/(MonetaryAmount div) const; - MonetaryAmount &operator/=(AmountType div) { return *this = *this / div; } + MonetaryAmount &operator/=(std::signed_integral auto div) { return *this = *this / div; } MonetaryAmount &operator/=(MonetaryAmount div) { return *this = *this / div; } + MonetaryAmount &operator/=(double div) { return *this = *this / div; } [[nodiscard]] constexpr MonetaryAmount toNeutral() const noexcept { return {true, _amount, _curWithDecimals.toNeutral()}; @@ -333,7 +348,8 @@ class MonetaryAmount { } /// Private constructor to set fields directly without checks. - /// We add a dummy bool parameter to differentiate it from the public constructor + /// We add a dummy bool parameter to differentiate it from the public constructor. + /// The number of decimals will be set from within the given curWithDecimals constexpr MonetaryAmount(bool, AmountType amount, CurrencyCode curWithDecimals) noexcept : _amount(amount), _curWithDecimals(curWithDecimals) {} diff --git a/src/objects/src/monetaryamount.cpp b/src/objects/src/monetaryamount.cpp index 8d44a40d..bc56137e 100644 --- a/src/objects/src/monetaryamount.cpp +++ b/src/objects/src/monetaryamount.cpp @@ -4,6 +4,7 @@ #include #include #include +#include #include #include #include @@ -370,7 +371,8 @@ MonetaryAmount MonetaryAmount::operator+(MonetaryAmount other) const { MonetaryAmount MonetaryAmount::operator*(AmountType mult) const { AmountType amount = _amount; auto nbDecs = nbDecimals(); - if (mult < -1 || mult > 1) { // for * -1, * 0 and * -1 result is trivial without overflow + // for * -1, * 0 and * -1 result is trivial without overflow + if (mult < -1 || mult > 1) { // Beware of overflows, they can come faster than we think with multiplications. const auto nbDigitsMult = ndigits(mult); const auto nbDigitsAmount = ndigits(_amount); diff --git a/src/objects/test/monetaryamount_test.cpp b/src/objects/test/monetaryamount_test.cpp index 85c0ef16..d50686ed 100644 --- a/src/objects/test/monetaryamount_test.cpp +++ b/src/objects/test/monetaryamount_test.cpp @@ -139,6 +139,14 @@ TEST(MonetaryAmountTest, IntegralComparison) { EXPECT_GE(7, MonetaryAmount("7")); } +TEST(MonetaryAmountTest, DoubleComparison) { + EXPECT_GT(MonetaryAmount("2.00 EUR"), 1.9); + EXPECT_LT(1.9, MonetaryAmount("2.00 EUR")); + + EXPECT_LE(-4.1, MonetaryAmount("-4.0000 EUR")); + EXPECT_GE(MonetaryAmount("-4.0000 EUR"), -4.3); +} + TEST(MonetaryAmountTest, OverflowProtectionDecimalPart) { // OK to truncate decimal part EXPECT_LT(MonetaryAmount("94729475.1434000003456523423654", "EUR") - MonetaryAmount("94729475.1434", "EUR"), @@ -172,6 +180,11 @@ TEST(MonetaryAmountTest, Multiply) { EXPECT_THROW(res = MonetaryAmount(1, "EUR") * MonetaryAmount(2, "ETH"), exception); } +TEST(MonetaryAmountTest, MultiplyDouble) { + EXPECT_EQ(MonetaryAmount("-4.98", CurrencyCode("ETH")) * 1.2, MonetaryAmount("-5.976", CurrencyCode("ETH"))); + EXPECT_EQ(15.98 * MonetaryAmount("0.2547"), MonetaryAmount("4.070106")); +} + TEST(MonetaryAmountTest, OverflowProtectionMultiplication) { for (CurrencyCode cur : {CurrencyCode("ETH"), CurrencyCode("Magic4Life")}) { EXPECT_EQ(MonetaryAmount("-9472902.80094504728", cur) * 3, MonetaryAmount("-28418708.4028351416", cur)); @@ -214,6 +227,12 @@ TEST(MonetaryAmountTest, Divide) { MonetaryAmount("0.00000000108420217")); } +TEST(MonetaryAmountTest, DivideDouble) { + EXPECT_EQ(MonetaryAmount("-4.98", CurrencyCode("ETH")) / -5.5, + MonetaryAmount("0.90545454545454545", CurrencyCode("ETH"))); + EXPECT_EQ(MonetaryAmount(658) / 0.012, MonetaryAmount("54833.3333333333333")); +} + TEST(MonetaryAmountTest, OverflowProtectionDivide) { for (CurrencyCode cur : {CurrencyCode(), CurrencyCode("ETH")}) { EXPECT_EQ(MonetaryAmount("0.00353598978800261", cur) / MonetaryAmount("19.65", cur),