brick / money

A money and currency library for PHP
MIT License
1.61k stars 96 forks source link

There are equals method? #17

Closed solodkiy closed 4 years ago

solodkiy commented 6 years ago

I need a simple bool method what answer me are the two money object completely equal (amount and currency) or not. isEqualTo looks like this method, but it throws MoneyMismatchException exception if currencies are different. What I should use for this problem?

BenMorel commented 6 years ago

Very good point. This method throws an exception because there's no way to tell whether 2 amounts in different currencies are equal, without an exchange rate; but your use case is valid as well, one might want to check whether amount and currency are equal.

We have at least 3 options here:

@Solodkiy Can you tell me a bit more about your use case: in what situation do you need to check that both the amount and currency are equal?

@jiripudil @jkuchar @VasekPurchart Do you have an opinion here?

solodkiy commented 6 years ago

I write system to analyse transactions from the bank. For it I need to compare transactions with each other. Transactions are equals if their amount, and other data equals too.

I used moneyphp/money before. This lib use first option: equals always return bool, compare can throw exception. https://github.com/moneyphp/money/blob/master/src/Money.php#L128 But for backward compatibility I suggest create new method named equals (i think this is the most common name for this operation) and deprecate current isEqualTo.

There's no way to tell whether 2 amounts in different currencies are equal, without an exchange rate

In Money object you don't need to think about exchange rate at all. You have special class MoneyComparator fort this task.

solodkiy commented 6 years ago

Small research about naming:

"function equals(" 367,814 code results https://github.com/search?l=PHP&p=6&q=%22function+equals%28%22&type=Code&utf8=%E2%9C%93

"function isequal(" 27,040 code results https://github.com/search?l=PHP&q=%22function+isequal%28%22&type=Code&utf8=%E2%9C%93

BenMorel commented 6 years ago

I know equals() might be used more (probably mimicking Java), but I like isEqualTo, which is consistent in naming and behaviour with isGreaterThan(), isLessThan(), etc.

So my worry with isEqualTo() returning false for different currencies is introducing an inconsistency.

At first glance I'd say that adding an equals() method that behaves as you want might be the best solution, but I wouldn't deprecate isEqualTo() in this case.

Still, I'm a bit worried about having two equal(s) methods, which might be confusing for users. What about another, distinctive name, like isSameAs()?

VasekPurchart commented 6 years ago

Still, I'm a bit worried about having to equal(s) methods, which might be confusing for users. What about another, distinctive name, like isSameAs()?

I was thinking (but did not manage to reply yet) the same - two methods with similar names and vastly different behaviours would be really confusing. And I think that I came up with isSameAs() as well.

BenMorel commented 6 years ago

Another acceptable name would be isIdenticalTo().

isSameAs() is not bad, but I'm afraid that "same" could be confused with "same object reference" (just like assertSame() in PHPUnit).

Bonus question: should this method check that the contexts are identical as well? I would say no, but then the naming is, once again, confusing.

BenMorel commented 6 years ago

I actually thought about a name that might be more suitable: matches(): https://github.com/brick/money/blob/master/src/AbstractMoney.php#L206-L220

Is everyone happy with this name, before I tag a release?

solodkiy commented 6 years ago

I still think it can confuse people. But it solve my problem.

jkuchar commented 6 years ago

To be honest I would not ever expect $money->isEqualTo() to do anything with currency conversion. When I would do that I would like to use special comparator which would compare money with more knowledge it has.

When I write it on paper it what feels natural to me:

20 CZK = 20 CZK βœ”οΈ SAME
20 CZK = 20 EUR ❌ NOT SAME
25 CZK ~ 1 EUR NOT SAME; ALMOST SAME
25 CZK = 1 EUR ❌ NOT SAME (I would never compare money in different currencies for equality; they are always almost the same, but not exactly)

The thing here is that when comparing different currencies, environment (outside world) is stepping in and it is up to model how precise they want to compare them (you have have realtime forex provider, than it does not make much sense), they will be equal one time and few milliseconds later they will not.

I would think of equality as logical equality and identity as it works generally with value objects (identical when all fields are the same)

BenMorel commented 6 years ago

@jkuchar Are you suggesting that isEqualTo()'s current behaviour should be changed to return false instead of throwing an exception on different currencies?

I'm not really against the idea, but I have two remarks:

isIdenticalTo() should compare value, currency and context and return only TRUE / FALSE

Unless someone has a valid use case, I currently don't see the point of adding a method that compares everything, including the context. This is not what's been asked here.

BenMorel commented 6 years ago

No feedback here? I'd like to move forward with this issue.

jiripudil commented 6 years ago

It would start behaving differently from isGreaterThan() etc., which makes me a bit uncomfortable

Me too, but I have only yet used this library in a single-currency environment, in which the current and proposed behaviour is essentially the same. Hence my view might be skewed, but I'm in favour of:

But then again, I lack first-hand experience with multiple currencies. I'd love to hear from someone who's been using this library in a multi-currency setup.

jkuchar commented 6 years ago

I'm quite busy right now, I will get back to this this weekend.

mtangoo commented 6 years ago

I'm not experienced but will throw my 2 cents. AFAICS, I see two ways:

  1. Extend isEqualTo to add boolean flag for strict comparison (that is throw exception if they aren't equal) or simple true/false when aren't equal or different currency.
  2. Add new method and leave the former intact

I favor the second option as it will not break compatibility and will make things easier and explicit. As to the wording, I think isSameAs is quiet tempting. Because from application point of view having same value and currency means "same money". If that is not fitting well with the rest of community then am fine with is isIdenticalTo`

VasekPurchart commented 6 years ago

I am for:

BenMorel commented 6 years ago

@jkuchar Looking forward to your feedback ;)

mtangoo commented 6 years ago

@BenMorel what is the progress so far?

BenMorel commented 6 years ago

@mtangoo Waiting for more feedback... I don't have a strong opinion at the moment, and I would welcome some additional thoughts from you guys, in particular @jkuchar who's gonna get back to this after his week-end... :wink:

mtangoo commented 6 years ago

Ok. I think all have been said and all I have to do is wait too...!

eblanshey commented 5 years ago

To add some personal feedback:

BenMorel commented 5 years ago

I agree that is makes sense, at least by default, to keep isEqualTo()'s behaviour aligned with all other comparison methods:

It would be very surprising to see isEqualTo() return false on different currencies, while isGreaterThanOrEqualTo() throws. Their naming similarity should imply a similar behaviour, IMO.

It would be equally surprising to see isEqualTo() behave differently from compareTo() === 0, IMHO still.

It's also comforting to see that Java's MonetaryAmount.isEqualTo() does the same:

@throws MonetaryException if the amount's currency is not equals to the currency of this instance.

So let's summarize, once again, our options:

Option 1: add a boolean parameter to isEqualTo()

public function isEqualTo(bool $throwOnDifferentCurrencies = true) : bool

This way, it would behave by default as it currently does, but one would be able to ask it to return false instead of throwing an exception when comparing different currencies.

Option 2: add a new method

We'd keep the current method as is, and add a new one. Suggested names so far (in their order of appearance in this thread):

Let me add a few more ideas to this list:

If we really want to convey the meaning of the method in the name, maybe we should be this verbose?

Open question:


None of these options is a BC break, so they're equal on this point.

I, however, somehow dislike option 1: changing the throwing behaviour of a method with a parameter is a smell to me (I can't explain why, though, so feel free to prove me wrong on this point).

On the other hand, none of the names from option 2 found favour in my sight so far.

Maybe we should cast a vote, but we're not many here so I'm not sure what this is worth :)

jkuchar commented 5 years ago

@BenMorel Thanks for nice summary.

I'm really happy that his issue is still open, despite my long weekend (joke). I would like to add my two cents that come up to my mind while I was reading this whole thread again.

Is comparison and equality the same thing? Therefore should they behave the same?

Let's dive into definitions a little.

image

Interesting is that part which says

wiki: inequality If the values in question are elements of an ordered set, such as the integers or the real numbers, they can be compared in size.

(I would consider Money object as pair of currency and decimal value)

This means - if we start with all values Money(currency, decimal value) object can represent we will end up with unoredered (generic) set. If we define equivalency classes by grouping values by currency, we come up with ordered sets for all possible values for each currency (one orderedset can be e.g. Money<CZK, decimal value>. This also means that we have defiend equivalence classes where are all values for each currency. This subset must be always ordered set as it contains all values for once currency only and value is always a decimal number.

This implies that isEqualTo (equality realation) is always defined for every two Money objects. This is because it does not depend from which class of equivalence value come from.

However if we are comparing values (greater then, lower then, ...) we need to have both values coming from the same class of equivalence, otherwise greatherThen and lowerThen is not defined. This is typically implemented by returning neutral/unknown value (unit in Haskell, null in native-PHP or exception in Java).

Summary

As there is a fundamentail difference between equality and inequality operations, so it makes sense to me to respect nature of these operations so:

One more exceptional question...

Should exception be checked (extending \RuntimeException) or unchecked (extending \LogicException)?

As it does not make sense to me to have try...catch block for every isGreatherThen() and because isGreatherThen() is just NOT defined on values with different values I would consider this as invalid usage.

Sidenote on Contexts

As Contexts do not affect value of Money object directly, it affects behaviour of operations. It is not important for equality and inquality relations, therefor I haven't mentioned it in text above.

This also implies that I would consider 1 CZK (cash) and 1.00 CZK (digital transfers) the same and definitely comparable (greather, lower then)

eblanshey commented 5 years ago

Interesting point that equality and comparison is not the same, though I'm not sure most people would intuitively understand it that way without reading the docs. It would be acceptable to make isEqualTo only return true/false, and the others throw an exception.

I know this thread related to "equals" specifically, but this morning I started thinking that if isEqualTo added a boolean value to also check currencies, undoubtedly someone in the future will create an issue asking why only isEqualTo allows comparing different currencies, and not any of the other methods like isGreaterThan. The same could be said of using a separate method to compare currencies, like isSameValueAs -- someone would ask for a isValueGreaterThan.

I, however, somehow dislike option 1: changing the throwing behaviour of a method with a parameter is a smell to me (I can't explain why, though, so feel free to prove me wrong on this point).

Agreed. Option two would make the most sense, in addition to other comparisons methods that do market price conversions (if you believe that is within the scope of this project--not sure you want to go that route.)

BenMorel commented 5 years ago

@jkuchar Thanks for your reply, I hope you enjoyed your weekend! πŸ™‚

You definitely pushed the discussion to the next level here.

Is comparison and equality the same thing? Therefore should they behave the same?

If I understand you correctly, the interesting part in the quote from Wikipedia is actually "It does not say that one is greater than the other, or even that they can be compared in size" (emphasis mine).

So your reasoning is that 1 USD and 1 EUR cannot be compared in size, but can still be compared for equality (not equal).

This reasoning definitely makes sense, however my worries from my previous post still hold; the most prominent one being that if you stop throwing exceptions on different currencies in isEqualTo(), you behave differently from isGreaterThanOrEqualTo(), despite the naming suggesting (to me, at least) that one operation is a subset of the other! I believe that this can be really confusing for consumers of the API.

Also, when I think about the way I use Money in my own applications, most of the time I do want an exception if I accidentally compare two monies with different currencies for equality! So even though your suggestion would solve the op's use case, it would leave me without a method for my use case, so back to square one, we'd still need another method or a boolean parameter!

In summary, I don't think changing the behaviour of isEqualTo() alone, is a good solution.

One more exceptional question...

You're most probably right about exception management. An unchecked exception would make sense when comparing monies with different currencies, as this likely represents an error in your program, and less likely represents an exception you'd catch in a normal program flow.

This is a topic of its own though, especially with the lack of definition of checked and unchecked exceptions in PHP, therefore I encourage you to open another issue if you want to discuss this further.

Sidenote on Contexts

Fully agree that we shouldn't be comparing the context; I had to ask the question though.

@eblanshey I appreciate your feedback as well!

undoubtedly someone in the future will create an issue asking why only isEqualTo allows comparing different currencies, and not any of the other methods like isGreaterThan.

I don't think someone could argue that for inequality methods, as you have no way to say which one is greather than the other, without currency conversion, which is outside the scope of the Money object itself.

Option two would make the most sense

I still believe that indeed, even though @jkuchar's comment made me think about it for a little while.

in addition to other comparisons methods that do market price conversions (if you believe that is within the scope of this project--not sure you want to go that route.)

It's definitely in the scope of this project, and is already there, see MoneyComparator!

BenMorel commented 5 years ago

The more I think about it, the more I like the original suggestion of isSameAs(). It conveys the meaning quite well, with just 2 drawbacks:

Sigh...

BenMorel commented 5 years ago

isSameValueAs() would do, provided that by "value" we understand the (amount, currency) pair: is that an appropriate word?

solodkiy commented 5 years ago

Maybe something like IsAmountAndCurrencySame? I still think that two similar named methods will be confusing, and we need maximize difference if we want to take this path. Value is not that obvious for my opinion.

solodkiy commented 5 years ago

Also I think that the best decision is have one equals method, without exeption. Agree with @jkuchar. Bool flags is not great too.

BenMorel commented 5 years ago

IsAmountAndCurrencySame?

isSameAmountAndCurrencyAs() would be acceptable, I guess.

Also I think that the best decision is have one equals method, without exeption.

As I said above, my own expectation in my apps is to get an exception if I accidentally compare two monies with different currencies (my use case is probably very different from yours), so we need a method for that, too. What would you suggest then?

solodkiy commented 5 years ago

Can you tell more about cases when you need this compare? Do you catch this exceptions? What do you do in catch block? What will happen with your app if method reurn false without exeption?

Maybe assertion will cover your case? $m->assertCurrencyEqual(Money $m2) : void | exception You can place this kind of assertion just bellow code where you getting this objects and get an exception even faster.

solodkiy commented 5 years ago

Strange thought: we can resolve logic conflict between equals and great/less methods if we remove great/less from money class and left it only in MoneyComporator. (In this case we need something like SimpleExchangeRateProvider which compare only similar currencies and throw exceptions otherwise) roll-safe-think-about-it

eblanshey commented 5 years ago

Sorry, I misunderstood the issue here. In addition to the isEqualTo method comparing currencies I thought OP also wanted to be able to compare the values using exchange rates Β―_(ツ)_/Β― Don't ask how I derived that.

Considering just comparing if currency and value are the same, I'm personally indifferent as long as it is well documented.. Everyone else has their own intuitive idea of how it should work and you can't please everyone :)

BenMorel commented 5 years ago

Can you tell more about cases when you need this compare? Do you catch this exceptions?

I never catch these exceptions. When I do isEqualTo()/isGreater|LessThan(), I assume that I'm dealing with monies in the same currency. If by mistake, my code allows a money with a different currency to be compared, this means that I made a mistake somewhere in my code. One reason could be that I forgot to perform a currency conversion to bring all my monies in the same currency. Or that I didn't check the currency from a Money coming from an external input. In any case, I would not want isEqualTo() to just return false, I need it to throw, so that my script fails and I get a notification that something needs to be fixed.

Maybe assertion will cover your case? $m->assertCurrencyEqual(Money $m2)

I wanted to suggest the same thing for your use case:

if ($money->isSameCurrencyAs($other) && $money->isEqualTo($other)) { ...

This would also solve your problem, but I didn't suggest it because I thought that calling two methods when one could do would make the code unnecessarily verbose. So the same goes for your suggestion: I don't want to pollute my code with additional assertCurrencyEquals() calls, when the comparison method can do the check for me transparently.

Strange thought: we can resolve logic conflict between equals and great/less methods if we remove great/less from money class and left it only in MoneyComporator.

I don't like this idea at all: when your app mostly deals with monies in a single currency; this adds a level of complexity that you really don't need!

BenMorel commented 5 years ago

Considering just comparing if currency and value are the same, I'm personally indifferent as long as it is well documented.. Everyone else has their own intuitive idea of how it should work and you can't please everyone :)

Sure, as long as it's documented it's fine. It looks like the only way to please everyone here, naming conventions aside, is to create two methods.

Let's just find a name that is meaningful enough to keep confusion to a minimum?

jkuchar commented 5 years ago

@BenMorel I think about the way I use Money in my own applications, most of the time I do want an exception if I accidentally compare two monies with different currencies for equality! So even though your suggestion would solve the op's use case, it would leave me without a method for my use case, so back to square one, we'd still need another method or a boolean parameter!

I do not like this idea as you are bringing your domain-specific usage into general-purpose library. So I’m still for strictly distinguishing between comparision and equality. (see general equals method later in this post)

Values from different classes of equivalence as simply un-equal-able without addition knowledge (currency convertor). So I would be strict on Money object and provide whole new API for manipulating Money objects multi currency environment. (but do not want to discuss it here as it can bring lot of complexity here as converting currency pairs in different directions does not have to lead into the same result as spread exists in the market and it can depend on amount size and hevily on time and this breaks consistency rule of java equals).

Β @Solodkiy IsAmountAndCurrencySameAs()

It is descriptive, but I do not like this as it just describes implementation not the domain concept. It is qualitatively the same thing as

$100czk->getAmount()->equals($5eur->getAmount()) && $100czk->getCurrency()->equals($5eur->getCurrency())

@BenMorel In any case, I would not want isEqualTo() to just return false, I need it to throw, so that my script fails and I get a notification that something needs to be fixed.

I still do not get what kind of usage that is. Please, could you elaborate on this more? (maybe I have partly answered this question later)

API overview

As there is still not clear attitude what outcome should be - I’m going to evaluate on Money API more.

I will work with there two objects:

$czk100 = Money::of(100, Currency::ofCountry('czk'));
$eur5 = Money::of(5, Currency::ofCountry('czk'));

These methods does not allow cross-currency operations:

$czk100->plus($eur5); // : ❌ usage exception
$czk100->minus($eur5); // : ❌ usage exception
$czk100->multipliedBy($eur5); // : ❌ usage exception
$czk100->dividedBy($eur5); // : ❌ usage exception

$czk100->isGreaterThan($eur5); // : ❌ usage exception
$czk100->isLessThan($eur5); // : ❌ usage exception
$czk100->isGreaterThanOrEqualTo($eur5); // : ❌ usage exception
$czk100->isLessThanOrEqualTo($eur5); // : ❌ usage exception

Equals method

And equals is different:

$czk100->equals($czk100); // true
$czk100->equals($eur5); // false

$czk100->equals('hello!'); // false
$czk100->equals(new \DateTimeImmutable()); // false

from Java docs

Indicates whether some other object is "equal to" this one.

The equals method implements an equivalence relation on non-null object references:

  • It is reflexive: for any non-null reference value x, x.equals(x) should return true.
  • It is symmetric: for any non-null reference values x and y, x.equals(y) should return true if and only if y.equals(x) returns true.
  • It is transitive: for any non-null reference values x, y, and z, if x.equals(y) returns true and y.equals(z) returns true, then x.equals(z) should return true.
  • It is consistent: for any non-null reference values x and y, multiple invocations of x.equals(y) consistently return true or consistently return false, provided no information used in equals comparisons on the objects is modified.
  • For any non-null reference value x, x.equals(null) should return false.

The equals method for class Object implements the most discriminating possible equivalence relation on objects; that is, for any non-null reference values x and y, this method returns true if and only if x and y refer to the same object (x == y has the value true).

Parameters:

obj - the reference object with which to compare.

Returns:

true if this object is the same as the obj argument; false otherwise.

According to Java docs: There is no possibility of throwing exception as all objects are sort-of equal-able. For example Date(2018-11-27) is not equal to Money(100, CZK). And I definitely cannot say that Date(2018-11-27) > Money(100, CZK)

Problem we are solving: Is isEqualTo() domain-specific or general method?

There are basically two valid point of view on this.

1. General equals method

Take Java equals() method in general: everything is comparable, so it will assign for every two objects if they are equal or not. This is cool as it can be used accross the system and will still hold the same interface. According to Java docs there exists no objects that we do not know if they are equal or not - therefore equals should not throw an exception.

Problem is that there is no standard-way of doing this in PHP.

Also read this stack overflow summary. Because every object MUST be equal or not equal to each other, that is why Java has equals method on Object class.

2. Money-specific equals method

Same usage as equals in β‘΄. Difference is that is defined only for two Money objects with the same currency. There are now three results - equal, not equal and incomparable. This is quite close like if we would have separate class for each currency and have generics for that e.g. Money<CZK>. That would feel much more natural - as generic parameters could create β€žnew” object type for each currency.

If someone accidentally uses this method for collections it can do strange things. E.g. when Money is as key and someone is checking if the key is equal to key which is already saved. One get and exception. Unexpected. As there is no standard way of defining equals in PHP I use my helper for that.

BenMorel commented 5 years ago

@jkuchar Thanks for your detailed post, lots of interesting insight, in particular Java's definition of equals().

I do not like this idea as you are bringing your domain-specific usage into general-purpose library.

That's not my intention. I was attempting to explain that, even though the current behaviour of isEqualTo() does not make sense in the OP's domain, it does to some extent in mine, so we need to find a solution that's good for everyone, which is the whole intent of a general-purpose library.

Values from different classes of equivalence as simply un-equal-able without addition knowledge (currency convertor). So I would be strict on Money object (...)

That's exactly what the library does at the moment: throwing exception when comparing different currencies, whether this is for inequality or equality.

(...) and provide whole new API for manipulating Money objects multi currency environment.

Did you see above that we already have a MoneyComparator implementation?

In any case, I would not want isEqualTo() to just return false, I need it to throw, so that my script fails and I get a notification that something needs to be fixed.

I still do not get what kind of usage that is. Please, could you elaborate on this more?

Simple: some parts of my code deal with multi-currencies, and some other parts deal with the assumption that all monies are of the same currency. If this assumption is broken by a defect in my code, leading to a leak of a Money with a different currency, I'd like to get an exception when I perform an isEqualTo(), instead of just getting false and letting the defect potentially go unnoticed.

  1. General equals method

A similar suggestion has been made in brick/date-time#9, with no feedback from the OP so far. I do not quite like this approach. In Java this makes sense, as this is a language requirement that all objects must be comparable to each other. In a specific PHP library however, I don't see any value in providing a generic equals() method where you could, to take an extreme example, compare a Money to a LocalDate.

I like to build libraries around use cases: what's a legitimate use case for such a comparison? If there is none, then this "feature" should not be built into the library IMO.

  1. Money-specific equals method There are now three results - equal, not equal and incomparable.

Exactly, and we have so far 2 equally valid use cases, that only differ in how they handle equality on 2 monies with different currencies:

I still think that isEqualTo()'s behaviour should stay aligned with that of other is*() methods, and stay conceptually equivalent to compareTo() === 0, so that:

I would not break this consistency, but I'd rather add a new method for the OP's use case, method for which we don't seem to agree on a good name.

What do you suggest, exactly, @jkuchar?

jkuchar commented 5 years ago

I haven't been suggesting anything in particular. I have just been trying to put all arguments that I had on my mind. (yep, sometime contradicting each other πŸ˜ƒ) I just ended with that there are really two use-cases.

Β  > (...) and provide whole new API for manipulating Money objects multi currency environment. Did you see above that we already have a MoneyComparator implementation?

Yep, it totaly makes sense to make these copariosons external as they are more conserned with coverting then comparing values.


After thinking again about this I agree that:

For me it would makes sense to implement \Ds\Hashable from phpds (polyfill available if ext not available). They are value objects therefore it perfectly makes sense to use them in collections. Implementhing equal and hashCode from \Hashable allows such usage.

BenMorel commented 5 years ago

Implementing comparisons for collections is yet-another-use-case I think, as now it's questionable whether you should compare contexts as well?

eblanshey commented 5 years ago

I don't have a strong opinion, but to add an option to the discussion: if you want to go the "two separate methods" route but don't want to be verbose, you can use PHPunit's method: it has "assertEquals" and "assertSame" for strict checking. Perhaps keep "isEqualTo" as it is, and make "isSameAs" always return true or false.

BenMorel commented 5 years ago

I've already suggested isSameAs(), but raised doubts regarding the possible confusion with the === operator (which is PHPUnit's meaning for same), and the fact that it doesn't compare contexts, only amount and currency.

mtangoo commented 5 years ago

looks like discussion is't moving to conclusion...

BenMorel commented 5 years ago

I guess pretty much everything has been said: we just need to find a reasonable name for the new method. I've listed some ideas above. Suggestions / votes welcome.

mtangoo commented 5 years ago

Suggestions / votes welcome.

Mine goes to isAmountAndCurrencyEqualTo()

jkuchar commented 5 years ago

My ordered list of favorites:

  1. is() - $czk100->is($eur5) === false
    • does not feel like it converts currencies; I would never say 100 CZK is 5 EUR; I can say they are equal in some cases
    • I like that it does not introduce new concept
    • pretty standard method name
    • can be general-purpose - $czk100->is(anything excluding Money) === false
  2. equals() - $czk100->equals($eur5) === false
    • Java-style
    • should have general signature equals($other)
    • ready for implementing \Ds\Hashable
    • can lead to feeling that it can convert to different currency
  3. isAmountAndCurrencyEqualTo() - $czk100->isAmountAndCurrencyEqualTo($eur5) === false
    • self-explaining
    • does not respect conventions -> no way to use it as general-purpose comparison
    • chatty
BenMorel commented 5 years ago

Any other votes out there, @Solodkiy, @VasekPurchart, @jiripudil, @eblanshey?

jiripudil commented 5 years ago

While isAmountAndCurrencyEqualTo() is on the verbose side, it communicates its intent without leaving much space for ambiguity, so :+1: for it from me

BenMorel commented 5 years ago

I tend to lean towards isAmountAndCurrencyEqualTo(), too. Let's wait a couple days to give others a chance to give their opinion, and we'll move forward.

bmeynell commented 5 years ago

Is Money considered a VO? If so, I would expect an equals() method to compare equivalence of the values represented by the VO (e.g, Amount and Currency). Such is done by another money library cited earlier in this thread.

If considered a VO, the context property could be indicative of code smell. However, I've seen a context object used elsewhere, such as in Python's Decimal/Fixed Point Arithmetic module.

BenMorel commented 5 years ago

I'm not sure whether our Money objects can be considered VO as per Fowler's definition. They are immutable, don't have an identity, and can be compared by value, but comparison should exclude context indeed, so they probably don't qualify as VO.

We can't really compare brick/money with moneyphp, as the latter does not support, AFAIK, custom scales, cash roundings, etc., so they don't need a context at all, and more naturally qualify as VO.

We can't just have an equals() method in addition to isEqualTo() as it's too close and would be confusing. Suggestion has already been made above to change isEqualTo() to return false when currencies are different, but I'm not very comfortable with this idea for the reasons I already exposed above.

I have the same feeling regarding moneyphp's API: their inequality methods (lessThan(), greaterThan(), ...) delegate to compare() and throw on different currencies, while the equality method equals() does not throw on different currencies, just returns false. It therefore behaves differently from compare() === 0. Is that a good or a bad thing? I honestly don't know.

BenMorel commented 5 years ago

Poll

OK let's do a real poll on 3 options, as I don't want to introduce a bias by asking to vote on a new method name only.

I'd like everyone here to vote please. If you don't care, post "I don't care".

You can vote for just 1, 2 or all 3 options, in your order of preference. If you vote for option 3, add one or more method names in your order of preference.

Vote template:

- 1st choice: option x
- 2nd choice: option y (optional)
- 3rd choice: option z (optional)

Note that I'm not planning to rename isEqualTo() to equals() in any case, as I want to keep the same conventions throughout the project.