RemyDuijkeren / NodaMoney

NodaMoney provides a library that treats Money as a first class citizen and handles all the ugly bits like currencies and formatting.
Apache License 2.0
207 stars 45 forks source link

Doing money calculation and perform rounding at the end #27

Open RemyDuijkeren opened 9 years ago

RemyDuijkeren commented 9 years ago

By default the Money instance will round every math operation (+. -, *. /, etc) that might have fractions of cents using a RoundingMode.

This will cause issues with multiple operations and money loss. If you multiple by a couple of numbers, do some division, add a few things, do some more division, you'll end up losing multiple cents along the way. This happens frequently in financial applications and many others as well.

There needs to be a way to perform multiple calculations on a Money instance without any rounding and then round at the end. Perhaps a MoneyCalculater (or building pattern) of some sort that can provide multiple operations without rounding and in the end provide back a Money instance that is rounded.

The best is probably to use a delegate like Func<Money, Money>, Action<Money> or a custom delegate.

See an example like https://github.com/gigs2go/joda-money-calculator/tree/master/joda-money-calculator or the builder pattern in JavaMoney.

proxio-david commented 8 years ago

Is there a reason for not keeping the exact value in Money, and using that in all math operator overloads, and then only round when converting to another type (Amount-getter, ToString etc)?

RemyDuijkeren commented 8 years ago

Rounding money isn't just a matter of presentation and doing this in a view or when converting to a different type. Whether money value is rounded is important domain logic, and is intrinsic to the Money type.

Money is based on a real agreed system and can't represent values out of its range. For Euro's to lowest positive value is a cent, €0.01, and you can't split this. That is what this type does for you, it protects you from passing unrounded money, which isn't valid. Your domain model should always return or store rounded values for Money.

An invoice is a good example where using the unrounded value underneath for the calculation, but presenting it rounded, would get you in trouble (you are missing a cent).

itemdesc quantity unit price item total rounded item total
item 1 1,4 €12,54 €17,556 €17,56
item 2 0,8 €3,56 €2,848 €2,85
item 3 1 €3,78 €3,78 €3,78
total €24,184 €24,19
rounded total €24,18 €24,19

Compare this with for example the Int type. When I do the following calculation 7 / 3, the result would be an int with the value 2. An int type can't represent a floating point so it's rounded. You wouldn't expect that the result of the division is hidden somewhere in the Int type and would effect the next calculation. If you want result as a floating point than you should use a floating point type.

So rounding should be the default, but of course there are exceptions where you want to do calculations and only do the rounding in the end or in certain in-between-steps. You are basically want to convert Money to a decimal type, do calculations and convert it back to Money again. The solution should be explicit that you want to work with unrounded values.

proxio-david commented 8 years ago

Thank you for the explanation.

TomasJuocepis commented 7 years ago

I am considering using NodaMoney instead of developing my own money library; however, I see it's lacking in features in terms of manipulating money - i.e. working with unrounded amounts - although the foundation of what's already build seems solid.

I suggest introducing a new type - let's say UnroundedMoney - to make the domain richer but concise. You can have functionality such as: UnroundedMoney.Round() -> Money Money * double -> UnroundedMoney

RemyDuijkeren commented 7 years ago

Hi. I'm not against such a type, but I don't know if this is the way to go.

Do you have clear use cases how and where unrounded money would be used? The problem is I think that everyone has different policies, to come up with a generic solution.

TomasJuocepis commented 7 years ago

Two common cases I'm familiar with are accrual calculations and distributions. Accrual calculation in particular is a type of calculation that often needs to be persisted as well as used outside its immediate scope of where it was originally calculated. It's not essential to have a type for this but there is some value in having more precise function signatures and data types. Who knows, it might save someone from silly bugs like forgetting to include a money variable in their multi-step calculation or including money twice in multiplication. Another case would be to keep track of post-rounding remainders - in some cases remainders need to be persisted and carried around - and they're not part of some unfinished intermediary calculation.

RemyDuijkeren commented 7 years ago

Accrual calculations and distributions is not a clear enough use case. It think each company has it's own rules about how to do this. What is the rule you need and how do you apply it in a scenario?

Post-rounding remainders? Could we solve this by adding a new extension method like so: public static IEnumerable<Money> SafeDivide(this Money money, int shares, MidpointRounding rounding, out Money remainder)

bugproof commented 5 years ago

This issue makes this library kinda useless it's like doing round(x) * round(y) instead of round(x * y)

Also, I'm not sure if the default rounding MidpointRounding.ToEven is a good choice for money. I think rules are different for rounding depending on the country.

For example, 1.345m with MidpointRounding.ToEven will be 1.34 with MidpointRounding.AwayFromZero will be 1.35

@remyvd have you come up with any idea yet?

ptjhuang commented 5 years ago

I've spent copious number of hours explaining to clients why two seemingly identical numbers on the screen result in an out of balance error message, and why things are 1 cent off. Had we enforced this constraint of respecting the domain's invariant (e.g. users, the tax office, the accountants, don't report on fractional cents), I will have a few years longer to live.

I wouldn't use the Money type wherever I see Decimal - that would miss the intent of this library. I would take time to identify where the data needs to interface with the Accounting world at large and use it.

In a version of an accounting domain, extending on the invoice line items example above, each line item wouldn't need Money to represent line total. The invoice total, definitely. Perhaps subtotals of types of line items calls for it too. Because they interface with general ledger via journal entries, and the recipient of the invoice. Both accountants and the recipient would be interested in the totals having the legal tender precision, and currency. As for the breakdown, use whatever precision desired, as long as it adds to the total - they don't interface with the accounting world.

I like it the way it is, non-opinionated.

Bouke commented 5 years ago

Money is based on a real agreed system and can't represent values out of its range. For Euro's to lowest positive value is a cent, €0.01, and you can't split this.

It's not uncommon for prices to be defined in fractions of cents. I think it would be great for this library to support that, and make rounding amounts opt-in. For my use-case I need greater precision on the line items, and also be able to serialise with great precision. So having this library take care of calculations and returning a rounded value wouldn't work for me. I would simply want to opt-out of rounding.

Take for example an Azure VM (B1S), which is currently priced at €0.0034/hour for my region. I wouldn't be able to use this library to perform any calculations based on that rate. Another example would be cash payments in the Netherlands: amounts are rounded to the nearest €0.05. When you have to pay €9.99 with a €10 note, you get no change. However I wouldn't want to use a software library that only allows me to use multiples of €0.05; I'd still need the €0.01 granularity on the line items.

ptjhuang commented 5 years ago

I understand what you're saying @Bouke, but I think we're talking about different things here. If you've only used 1 hour of that VM, Azure will not make you pay 0.0034 euro when they invoice you. They'll either not charge you anything, or charge you 0.01 euro. When it comes to doing calculations on what 18 hours of use would cost me, I certainly would NOT be using this library - all it's extra features over and above what Decimal offers are useless in this calculation. But when it comes to working out how much I've left to pay an invoice in EUR from Azure using USD, I would think so.

RemyDuijkeren commented 5 years ago

This issue makes this library kinda useless it's like doing round(x) * round(y) instead of round(x * y)

For multiplying it does round(x * y). For adding or subtracting amounts of money it does round(x) + round(y) because of the explained example above.

Also, I'm not sure if the default rounding MidpointRounding.ToEven is a good choice for money. I think rules are different for rounding depending on the country.

MidpointRounding.ToEven is also called the 'bankers rounding' and is used a lot. But I agree it can differ, so I think this should probably move to a property of currency.

@Bouke I think the difference lies where the money type is being used for: (Total) Amount or (Unit) Price. A Price is money/unit, which if multiplied by the unit result into money (=(Total) Amount). So: unit price = amount unit money/unit = money

(Total) Amounts should I think always be rounded to prevent errors, but I understand that sometimes (Unit) Prices needs more precision. Maybe adding a type Price<unit> which allows more precision could help?

The example you give is a special type of price, an hourrate: money/hour or money/TimeSpan. This could be new Price<TimeSpan>(0.0034m, "EUR") or a type Hourrate that extends from Price. Hourrate could also have businessrules about rounding like @ptjhuang mentioned.You could do for example: *TimeSpan Hourrate = (Total) Amount**

Do you see other places, besides price, where you need more precision?

Bouke commented 2 years ago

For my use-case I simply need the tuple of amount+currency, any business rules regarding the calculations involving this tuple will be up to my domain code. What I want from a money library is the ability to represent this tuple and perform exact calculations. Having the ability to represent "money in hand" is something else, which is less relevant to me, but I can see the use-case for that. Rounding and distribution would be relevant to such a type indeed.

Regarding having special types to reflect price could be interesting. But what would that mean, do I have to multiply the Price by unit and get the (rounded) Amount? (Price<TimeSpan> * TimeSpan = Money) That wouldn't help me, I'd still need the non-rounded amounts, to store in a database, and I'll round them at some point later on.

JobaDiniz commented 2 years ago

I was looking up a library for Money & Currency and this seemed to address what I was looking for, however, rounding numbers within the library is not something it will work for several cases.

For instance, in the stock exchange domain, you can't round anything. You can surely show in the screen rounded numbers, but in your domain/database/code you can't, otherwise investors will lose money.

Investors can buy fractions of stocks, for example, one can buy 0.0004 fraction stocks at $ 287.93 of MSFT, which will result in $ 0.115172, which can be rounded in the screen to $ 0.11, but never rounded in the database or codebase.

I've created a FinancialDecimal type which could replace decimal all over the library, and let the consumer decide whether they want to round or not:

FinancialDecimal

/// <summary>
    /// Describes a <see cref="decimal"/> in a financial world, where decimals values can be rounded to X fractional decimals points.
    /// When calculating financial values, it's important to not round decimals. However, when comparing these values, rounding is necessary.
    /// The intent of <see cref="FinancialDecimal"/> is to hold true/raw decimal values, and give an opportunity to round them when needed,
    /// instead of always using <see cref="Math.Round(decimal)"/> all over the codebase.
    /// </summary>
    public struct FinancialDecimal
    {
        public static FinancialDecimal Zero = new(0);
        private readonly decimal value;

        public FinancialDecimal(decimal value) => this.value = value;
        /// <summary>
        /// Rounds a decimal value to a specified number of fractional digits, and rounds midpoint values to the nearest even number.
        /// </summary>
        /// <param name="digits">The number of decimal places in the return value.</param>
        public decimal Round(int digits) => Math.Round(value, digits);
        public static implicit operator decimal(FinancialDecimal average) => average.value;
        public static implicit operator FinancialDecimal(decimal value) => new(value);

        public override string ToString() => value.ToString();
    }

Usage

var value = new FinancialDecimal(20.34345m);
var rounded = value.Round(2);
RemyDuijkeren commented 2 years ago

Money has strict rules in the real world. For Euro, the lowest is one cent. This isn't a "money in hand" rule. You just can't represent money lower than one cent. You can do a calculation, but in the end, it needs to be rounded, if you want to represent it in money.

But what would that mean, do I have to multiply the Price by unit and get the (rounded) Amount? (Price<TimeSpan> * TimeSpan = Money) That wouldn't help me, I'd still need the non-rounded amounts, to store in a database, and I'll round them at some point later on.

A price is a ratio, which can be allowed to have a higher precision (same as the ExchangeRate in this lib). When calculating a line-item total, the result should be rounded. Why do you think you need the non-rounded amounts? I'm very hesitant to add an unrounded money type, which goes against accounting rules, if there is not a good use case.

If you need the intermediate result and want to store it, the decimal type is perfect for that. You can still create a tuple if you need to keep the currency close to the calculated value: (Currency, decimal) result = ("EUR", 0.0034).

For instance, in the stock exchange domain

That is an extremely specific and complex domain with his own rules. It could be that this library is not a suitable candidate for that domain.

I've created a FinancialDecimal type which could replace decimal all over the library, and let the consumer decide whether they want to round or not

I don't understand what this solves. Just use decimal in your domain, it's the perfect type for it. If you want to have a handy method to Rond(), you can create an extension method on Decimal.

Btw with this lib, you can also do:

var value = 20.34345m;
Money money = value;