Skip to content

Commit

Permalink
[Feature] - Allow MonetaryAmount operations with doubles
Browse files Browse the repository at this point in the history
  • Loading branch information
sjanel committed Mar 7, 2024
1 parent 4d12e3e commit fbeaec4
Show file tree
Hide file tree
Showing 3 changed files with 51 additions and 14 deletions.
42 changes: 29 additions & 13 deletions src/objects/include/monetaryamount.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}

Expand Down Expand Up @@ -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<uint8_t>(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<AmountType>(amount) && nbDecimals() == 0;
}

[[nodiscard]] friend constexpr auto operator<=>(AmountType amount, MonetaryAmount other) {
return amount * ipow10(static_cast<uint8_t>(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<AmountType>(amount) * ipow10(static_cast<uint8_t>(nbDecimals()));
}

[[nodiscard]] constexpr auto operator<=>(double amount) const { return toDouble() <=> amount; }

[[nodiscard]] constexpr MonetaryAmount abs() const noexcept {
return {true, _amount < 0 ? -_amount : _amount, _curWithDecimals};
}
Expand All @@ -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<AmountType>(mult) * rhs;
}
[[nodiscard]] friend MonetaryAmount operator*(std::signed_integral auto mult, MonetaryAmount rhs) {
return rhs * static_cast<AmountType>(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:
Expand All @@ -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()};
Expand Down Expand Up @@ -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) {}

Expand Down
4 changes: 3 additions & 1 deletion src/objects/src/monetaryamount.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
#include <cassert>
#include <cmath>
#include <compare>
#include <concepts>
#include <cstdint>
#include <cstdlib>
#include <iomanip>
Expand Down Expand Up @@ -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);
Expand Down
19 changes: 19 additions & 0 deletions src/objects/test/monetaryamount_test.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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"),
Expand Down Expand Up @@ -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));
Expand Down Expand Up @@ -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),
Expand Down

0 comments on commit fbeaec4

Please sign in to comment.