Giter VIP home page Giter VIP logo

money's Introduction

Brick

Incubator for PHP components under development.

Build Status Coverage Status License

Once a component reaches sufficient maturity, it will be moved to its own repository and composer package, and enter a beta phase. For this reason, this repository will never contain releases.

The incubator can be included in your project using Composer. Just define the following requirement in your composer.json file:

{
    "require": {
        "brick/brick": "dev-master"
    }
}

Feel free to use the incubator for your own use/research, but please note that the API can, and will, change at any time. Components can also disappear at any time.

money's People

Contributors

antonkomarev avatar axlon avatar benmorel avatar ekateiva avatar grahamcampbell avatar jiripudil avatar joelvh avatar ncatalani avatar peter279k avatar raphaelstolt avatar rdarcy1 avatar solodkiy avatar trebi avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

money's Issues

Add explanation to README for import-currencies.php

It's not really clear, at first sight, what the import-currencies.php is for, so a mention in the README.md would be helpful for project users.

It also should be prolly moved into a dedicated bin directory, so it doesn't get confused with Git repository configuration files like .travis.yml or .gitignore.

A Composer script for it would also make it easier, clearer, and well documented for the library users.

Ability to set default context and rounding mode

I would like to open a discussion about the possibility of implementing a way to override the context, and rounding mode of money methods.

These are hard coded all the way, and I personally find it pretty annoying, and ugly that I have to specify what I would like to do.
99.99% of the time I use my own context, and a RoundingMode::HALF_UP.

This is because in real use case scenarios the money object is always constrained by the database schema.

For example: We use 6 decimals because we work with really small, really preciese values.
Let's say the price of a single sheet of paper is 0.123456 EUR. Rounding that to 0.12 and storing it is a no-go, considering you literally need millions of these printed sometimes, and that could make a huge difference - a loss.

Calculations are always done in Rational, and cast to Money at the very end, to avoid rounding as much as possible of course, but the end is always the database scale and precision.

Long story short: DefaultContext is not viable for everyone.

public static function of($amount, $currency, ?Context $context = null, int $roundingMode = RoundingMode::UNNECESSARY) : Money
    {
        if (! $currency instanceof Currency) {
            $currency = Currency::of($currency);
        }

        if ($context === null) {
            $context = new DefaultContext();
        }

        $amount = BigNumber::of($amount);

        return self::create($amount, $currency, $context, $roundingMode);
    }

Could we make some changes here that would allow global configuration?
(This might be the rare case when I'm not against the use of a singleton for configuration)

Proposal:

public static function of($amount, $currency, ?Context $context = null, int $roundingMode = null) : Money
    {
        if (! $currency instanceof Currency) {
            $currency = Currency::of($currency);
        }

        if ($context === null) {
            $context = Configuration::getInstance()->getContext();
        }

        if ($roundingMode === null) {
            $roundingMode = Configuration::getInstance()->getRoundingMode();
        }

        $amount = BigNumber::of($amount);

        return self::create($amount, $currency, $context, $roundingMode);
    }

Format with thousand/million/billion/trillion suffix

Hi!

I did not find in the documentation whether such formatting can be done.

For example:

1k instead 1 000 or 1 тыс. instead 1 000 if ru_RU locale.
1.1k instead 1 100 or 1,1 тыс. instead 1 100 if ru_RU locale.

1m instead 1 000 000 or 1 млн instead 1 000 000 if ru_RU locale.
1.5m instead 1 500 000 or 1,5 млн instead 1 500 000 if ru_RU locale.
...etc

RoundingMode::AUTO?

Hi,

First off, I love this library, it's super intuitive and powerful.

One thing I have a problem with, however, is the rounding mode. Why isn't there an option to round to the nearest subunit (context sensitive of course)? It seems odd that I have to hardcode a RoundingMode or do the calculation myself.

For instance, your readme demostrates Rounding as such, where you're required to round either up or down:

$money->plus('0.999', RoundingMode::DOWN); // USD 50.99

$money->minus('0.999', RoundingMode::UP); // USD 49.01

To me, unless you have a specific use case where you need to deliberately favour a particular rounding, under no circumstance would '0.999' ever be considered '0.99'.

Perhaps I am missing something, and if so I'd appreciate the pointer in the right direction.

Cheers,
Mike

Laravel issue

I'm unable to use this in Laravel 9. It is quite strange as I can't use your namespace or anything from the package. Any idea on this?

Handle values up to 8 decimals?

is there a way to handle values up to 8 decimals?

This comes in handy when storing values in bitcoin where the smallest unit is 0.00000001

use Brick\Money\Money;

$money = Money::ofMinor(100000000, 'BTC'); // 0.00000001 BTC 

Will love to get a workaround for this.

Question: Correct way for working with MoneyBag

Hello.

I just want to understand the correct way of working with MoneyBag.

First, we have an instance of Money class

$money = Money::of(1.23,'EUR')

Second, we create a money bag and add money to it

$bag = new MoneyBag();
$bag->add($money);

And when we are going to use that MoneyBag we have a BigRational number

$amount = $bag->getAmount('EUR')

and how to use this BigRational for normal numbers? Let's say I want to convert it to int, float, etc

I always get

This rational number cannot be represented as an integer value without rounding.

And I don't quite understand where and how to add rounding in this case. Need an advice

Thank you.

Question: how is operator overloading seemingly working?

Hi,

I can't work out how this is actually working and gives correct results:

<?php

use Brick\Math\BigDecimal;

var_dump(BigDecimal::of(1) > BigDecimal::zero());
var_dump(BigDecimal::of(1) >= BigDecimal::zero());
var_dump(BigDecimal::of(1) <= BigDecimal::zero());
var_dump(BigDecimal::of(1) < BigDecimal::zero());

What magic is PHP doing here?
I'm stumped!

Conditional chaining

I have use case where i need to chain a lot of numbers based on true/false conditional. Idea is from method when in Laravel Framework.

https://github.com/laravel/framework/blob/5.8/src/Illuminate/Database/Concerns/BuildsQueries.php#L88

$salary = '3000.70';
$employerContributeTax = true;
$employerContributeInsurance = true;

$gross = Money::for($salary)
    ->when($employerContributeTax, function ($salary) {
        return $salary->minus($someNumber);
    })
    ->when($employerContributeInsurance, function ($salary) {
        return $salary->minus($someNumberAgain);
    });

Missing rounding on toBigInteger()?

Hi,
this:
Money::of(1914, 'PLN', null, RoundingMode::HALF_UP)->dividedBy(12, RoundingMode::HALF_UP)->getAmount()->toInt()
produce:
Brick\Math\Exception\RoundingNecessaryException
Rounding is necessary to represent the result of the operation at this scale.

Because it call \Brick\Math\BigDecimal::toBigInteger() without rounding mode. Why?
Does I do something wrong?

Version 0.5.1
PHP 7.4.8

Add a compareTo method

Add a new method compareTo, in order to get if a value is equals to (0), greater than (1), or less than (-1) the given value.

$money = Money::of(50, 'USD');
$money1 = Money::of(60, 'USD');
$money2 = Money::of(60, 'USD');

echo $money->compareTo($money1); // -1 (less than)
echo $money1->compareTo($money); // 1 (greter than)
echo $money->compareTo($money2); // 0 (equals)
echo $money->compareTo($money); // 0 (equals)

If you try to compare two different currencies an error should be shown

MoneyBag and custom currencies

Hi,

i wanted to create a MoneyBag with USD & BTC, but while addig the BTC i get a UnknownCurrencyException.

$moneyBag = new MoneyBag();
$moneyBag->add(Money::of('1234', 'USD'));
$moneyBag->add(Money::of('0.1234', new Currency('BTC', 0, 'Bitcoin', 8)));
UnknownCurrencyException "Unknown currency code: BTC"

Is this a normal/desired behavior or a bug?

$currency = Currency::of($currency);

At this line the ISOCurrencyProvider will be called and he doesn't know BTC.

Release

Can you make release on packagist?

Money contexts

Part of the list of things I want to work on before tagging a release, as mentioned in #2. Comments welcome.

Monies can have different scales: for example USD uses 2 decimals by default, but some apps may require additional precision and be able to represent 1.123456 USD. Additionally, some arithmetic operation on monies may require rounding: for example dividing 1.00 USD by 3.

When performing operations on monies, a MoneyContext controls the scale and the rounding. The available implementations are:

  • DefaultContext: adjusts the scale to the currency's default fraction digits (2 for EUR/USD/GBP/…, 0 for JPY), using rounding if necessary.
  • FixedContext: adjusts the scale to a given value, using rounding if necessary.
  • RetainContext: uses the scale of the left operand. This is the default context for arithmetic operations: plus(), minus(), multipliedBy(), dividedBy(). For example, 0.1234 USD + 1.00 USD equals 1.1234 USD, while 1.00 USD + 0.1234 USD uses rounding to fit the result in 2 decimals.
  • ExactContext: tries to adjust the scale to fit the result. For example, 0.99 USD / 2 would equal 0.495 USD, while 1.00 USD / 3 would throw an exception.

The first 3 contexts require a MoneyRounding implementation:

  • MathRounding uses brick/math's rounding modes directly
  • CashRounding uses rounding modes and steps, for example every 5 cents: 0.05, 0.10, 0.15, etc.

Contexts are very flexible and powerful, but it's a lot to digest when you're new to the library; even when you're comfortable with them, they're currently a pain to deal with.

For example, when dividing a money, you will most probably need rounding (unless you're really lucky that the results fits in the scale of your Money).

To use rounding, you need to pass a MoneyContext:

use Brick\Math\RoundingMode;
use Brick\Money\MoneyRounding\MathRounding;
use Brick\Money\MoneyContext\RetainContext;
use Brick\Money\Money;

$rounding = new MathRounding(RoundingMode::UP);
$context = new RetainContext($rounding);
echo Money::of('10.00', 'USD')->dividedBy(3, $context); // 3.34

This is too long. We need a shortcut for these simple use cases that we use daily.

Maybe these methods should accept not only a MoneyContext , but also a MoneyRounding, and even a RoundingMode constant directly.

This means that all three statements below would be equivalent:

$money->dividedBy(3, RoundingMode::UP);
$money->dividedBy(3, new MathRounding(RoundingMode::UP));
$money->dividedBy(3, new RetainContext(new MathRounding(RoundingMode::UP)));

So our example above could be simplified to:

use Brick\Math\RoundingMode;
use Brick\Money\Money;

echo Money::of('10.00', 'USD')->dividedBy(3, RoundingMode::UP); // 3.34

We may then argue that the constructors of the MoneyContext classes that accept a MoneyRounding should also accept a RoundingMode constant directly for convenience.

I hope that this could make it easy to use and less verbose for everyday use cases, while still allowing for more complex examples.

Thoughts on this? Alternative ideas? Are you happy with the overall context thing?

Note that depending on the implementation chosen in #3 Scales and Money classes, using contexts could be avoided entirely for standard (default scale) monies, which would only use rounding modes.

CurrencyProvider?

We have some currencies like BTC which are treated "equally" to other currencies in our database -- it would be nice if instead of needing to make a new Currency when making a Money, I could instead configure a currency provider globally (extending the ISO one) so I could instead just do Money::of('0.123', 'BTC').

Of course I feel dirty recommending any global state, but I'm not sure how else it could be implemented 🤔

CurrencyMismatchException pain

We are using money objects in single-currency environment. It is very uncomfortable that we need to check at every use of operation that requires more money objects for CurrencyMismatchException.

Then there are ShouldNeverHappend expceptions thrown everywhere, which does not make sense in our context.

I think that CurrencyMismatchException should be LogicException, which gives developer feedback that he has done something wrong and that he should not use it this way.

On the other hand, when someone "creates" currency on the fly from what someone set using GUI, it should make RuntimeException because it acts as validator of user input. "Hey, you cannot sum numbers of different currencies."

I'm not sure it it is responsibility of value-object to act as validator for user-input. What are your opinions?

Question: How to make single-currency usage more pleasant?

Same thing has happened with rounding on ::of() constructor. Should it throw Logic or Runtime exception? It is very uncomfortable to check for exceptions like these every time.

try {
  return Money::of(
    self::round($unitPrice->getBase(), $quantity, $unitPrice->getCurrency()),
    $unitPrice->getCurrency()
  );
} catch (RoundingNecessaryException $e) {
  throw new ShouldNeverHappenException($e);
}

Remove final definition from classes

Good afternoon,

I was wondering if we could safely drop the final definition from the classes within the package. I understand the value of defining certain classes as such, but I could see several valid use cases where extending the Money class, for example, would make sense.

As a current use case, I'd like to do this in a Laravel app to add the Arrayable and JsonSerializable interfaces to the Money class. I don't think this should be a part of this package, so it makes more sense to permit extension by package consumers.

Without the ability to extend, I'll have to throw together a factory and forward calls using magic methods. Yuk. Or make a fork. Also seems unnecessary.

If there is a particular reason why the classes are defined as final in the package, I'd appreciate a quick explanation so that I can decide how to proceed from there.

Thanks for your time.

Adding/Subtracting Money objects, but can't Multiply/Divide

Currently add/subtract support giving an AbstractMoney instance so that something like $first->add($second) is a valid call.

Why does multiply and divide not support this feature? They are effectively still mathematical operations and could be done on Abstracts also.

Is this something you are looking to do in a future release, if not, what are the reasons why it wouldn't work?

Thanks :)

Doctrine value object

Hi

Excellent library to work with money. I tried to implement it as a value object in doctrine and it seems there are some complicated challenges and I have to use another separate proxy class to work with value objects. Do you have any recommendation docs to work with ORMs?

Cheers

Advanced calculations amongst Money objects casts them to string

Hey Ben,

first off I wanna thank you for creating a lot of amazing packages, which I am using a lot! :)
Very helpful to me.

Now, I'm unsure if this is by design or considered a bug/improvement. But I tried doing advanced calculations between multiple AbstractMoney objects, and it results in them being cast to string which ofc breaks the entire calculation.

Here is an example that calculates the return on ad spend for advertising campaigns:

// $this->revenue & $this->spend are both Money objects.

/**
     * Calculate Return on Ad Spend as Percentage Value
     *
     *
     * @return int
     */
    private function returnOnAdSpend(): int
    {
        $revenue = $this->revenue->toRational();

        // prevent division by zero
        if ($revenue->isNegativeOrZero() && $this->spend->isNegativeOrZero()) {
            return 0;
        }

        $returnOnAdSpend = $revenue->dividedBy($this->spend)->multipliedBy(100);
dd($returnOnAdSpend); // App\Exceptions\MoneyException : The given value "USD 285.00" does not represent a valid number.
        return $returnOnAdSpend->getAmount()->toInt();
    }

Now if I change the $returnOnAdSpend calculation to this:

$returnOnAdSpend = $revenue->dividedBy($this->spend->getMinorAmount()->toInt())->multipliedBy(100);
dd($returnOnAdSpend);

I get this:

Brick\Money\RationalMoney {#2581
  -amount: Brick\Math\BigRational {#2550
    -numerator: Brick\Math\BigInteger {#2574
      -value: "1900000"
    }
    -denominator: Brick\Math\BigInteger {#2602
      -value: "330000"
    }
  }
  -currency: Brick\Money\Currency {#2588
    -currencyCode: "USD"
    -numericCode: 840
    -name: "US Dollar"
    -defaultFractionDigits: 2
  }
}

Am I using it wrong? What is going on? :)

edit:
Thought it would be good to mention that I'm using Brick/Money in a Laravel 7 app, and also have created a custom cast to cast values to/from Money:

<?php


namespace App\Casts;


use Brick\Money\Money;
use Brick\Money\AbstractMoney;
use Illuminate\Contracts\Database\Eloquent\CastsAttributes;

class MoneyCast implements CastsAttributes
{

    /**
     * Cast the given value.
     *
     * @param  \Illuminate\Database\Eloquent\Model  $model
     * @param  string  $key
     * @param  int  $value
     * @param  array  $attributes
     *
     * @return Money
     * @throws \Brick\Money\Exception\UnknownCurrencyException
     */
    public function get($model, $key, $value, $attributes)
    {
        return Money::ofMinor($value, $this->loadCurrency());
    }


    /**
     * Prepare the given value for storage.
     *
     * @param  \Illuminate\Database\Eloquent\Model  $model
     * @param  string  $key
     * @param  Money|float  $value
     * @param  array  $attributes
     *
     * @return int
     */
    public function set($model, $key, $value, $attributes)
    {
        if ($value instanceof AbstractMoney) {

            return $value->getMinorAmount()->toInt();

        }

        return Money::of($value, $this->loadCurrency())->getMinorAmount()->toInt();
    }


    private function loadCurrency()
    {
        return $model->account->currency ?? config('settings.currency');
    }

}

Non decimal currencies are not properly supported

As I was helping a friend designing a money library in javascript (dinero.js), I tried to find how non decimal currencies were solved in PHP libraries, as it's the language I'm most familiar. I ended up stumbling upon yours.

Problem is point 4 of the "Falsehoods programmers believe about prices" list.

We struggled on supporting non decimal currencies and I realised brick\money currently doesn't support them at all.

As stated here, 1 Ariary (unit) = 5 iraimbilanja (minor unit).

However brick\money gives 1 Ariary (unit) = 100 iraimbilanja (minor unit)

\Brick\Money\Money::of(1, 'MGA')->getMinorAmount(); // 100

Non decimal currencies has some major implications, formatters for example won't be able to represent the whole amount in a decimal fashion.

Here's some content that could contribute to your own solution:
https://v2.dinerojs.com/docs/core-concepts/currency#currency-base
dinerojs/dinero.js#294 (comment)

No support for adding CurrencyProviders

Hi,

Although you do support custom currencies through new Currency, the project does not support the expectation that the Money class may not rely on the ISOCurrencyProvider, and could instead rely on a CurrencyProviderInterface.

I propose making ISOCurrencyProvider an implementation of CurrencyProviderInterface, and setting it to default to maintain the existing functionality, and then enable users to set their own Concrete CurrencyProvider to allow for additional options.

This would cover the case where cryptocurrencies (popularity rising as they are!) can be built using the interface and be validated correctly, instead of having to manually override the string currency with a new Currency() every time. This is for sure necessary as a developer when you want to dynamically load a list of available crypto's. I wouldn't expect the CryptoCurrencyProvider to be a part of this project, but at least the Interface to support additional non standard currencies.

I would be more than happy to pick this work up and submit a PR. I am writing it anyway for my local project but without the reliance on the Interface :)

Thanks.

json serialization

Using laravel, it can automatically serialize array or object into json. For example

class IndexController extends Controller
{
  public function index()
  {
    return ['amount' => 0.1, 'currency' => 'AUD'];
  }
}

becomes

{"amount":0.1,"currency":"AUD"}

However, if laravel is returning brick/money, it returns an empty object:

class IndexController extends Controller
{
  public function index()
  {
    return new Money::of(0.1,'AUD');
  }
}

becomes

{}

How do you make it to serialize into {"amount":0.1,"currency":"AUD"} response instead?

Other way I can think of is to implement JsonSerializable and implement jsonSerialize() function:

final class Money extends AbstractMoney implements JsonSerializable
{
    ...

    public function jsonSerialize(): array
    {
        return [
            'amount' => $this->getAmount()->toFloat(),
            'currency' => $this->getCurrency()->getCurrencyCode(),
        ];
    }
}

As for test:

public function testJSONSerialize(): void
{
    $money = Money::of('5.50', 'USD');
    $got = $money->jsonSerialize();

    $expected = [
        'amount' => 5.50,
        'currency' => 'USD'
    ];

    $this->assertEquals($expected, $got);
}

Also it needed ext-json in composer

"require": {
    "ext-json": "*"
},

Implementation / performance

Part of the list of things I want to work on before tagging a release, as mentioned in #2. Comments welcome.

The library is fully based on Brick\Math's BigDecimal. This is good, as it uses precise arithmetic.
However, I feel like it's a bit overkill when you typically deal with monies of reasonable size, that could be easily represented with integers: you can fit up to ~20 million USD with 2 decimals on 32-bit, and an unfathomable amount on 64-bit.

It's interesting to note that the Java Money reference implementation provides two classes: Money based on BigDecimal, and FastMoney based on long.

Sure, we don't have the same speed requirements as a Java application: we won't use this library for stuff like real-time trading, but using GMP or BCMath under the hood when you're just adding/subtracting everyday monies is questionable.

Note that even if we had an integer-based implementation, integer arithmetic could be used only for the simplest calculations: we'd still need to use BigDecimal for things like division, some multiplication, and probably rounding.

My current view on this is: if the library is kept as it is today (a single Money class), I don't think it's worth having two implementations. However, as suggested in #3 Scales and Money classes, if we followed the Money/BigMoney route, we could consider going with int for Money, and with BigDecimal for BigMoney.

allocateWithRemainder() performs no allocation

Prior to this change, Money::of('1', 'USD')->allocateWithRemainder(400, 0, 40, 20, 2) returned 86, 0, 8, 4, 0, 2 (as cents wrapped in Money instances of course), and I distributed the 2 cents remainder by adding one to the 86 figure, one to the 8 figure.

After the change, Money::of('1', 'USD')->allocateWithRemainder(400, 0, 40, 20, 2) returns 0, 0, 0, 0, 0, 100.

I understand the reason behind this, however allocateWithRemainder() is no longer helpful in cases like the above, as it does no allocation at all.

Is there anything within the library that can help me with that? Or should I resort to allocating the 100 cents myself (probably by weight)?

Thanks!

There are equals method?

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?

The allocate method using integer ratios causes avoidable discrepancies

Consider

use Brick\Money\Money;

$profit = Money::ofMinor('1000', 'GBP');
[$a, $b] = $profit->allocate(37.5, 62.5); // GBP 37.40, GBP 62.60
[$c, $d] = $profit->allocate(62.5, 37.5); // GBP 62.70, GBP 37.30

In both cases it would be preferable if the values to come out were GBP 62.50 and 37.50, regardless of the order.

If the allocate method were to take floats (or perhaps preferably rational numbers) it could implement a test to see if the allocation yields a remainder. If so, convert to integer.

[feature request] Allow changing default rounding mode

Some methods like of(), dividedBy(), multipliedBy(), in some cases will throw an exception if there's no rounding mode explicitly specified so I have to call them with RoundingMode::HALF_EVEN (which is the default for the app).

The corresponding method calls look like

$amount = Money::of($float, $currency, roundingMode: RoundingMode::HALF_EVEN)
     ->multipliedBy($percent, RoundingMode::HALF_EVEN);
     ->dividedBy($anotherPercent, RoundingMode::HALF_EVEN)
;

As you see, the same rounding mode repeats many times which violates the DRY principle and makes the code too busy.
And it turns out, in my project, there's almost a hundred mentions of this rounding mode.
It would be great if there was a way to set a default project-wide rounding mode so that there's no need to repeat it every time as in the example above.

Maybe some static method e.g. Money::setDefaultRoundingMode()

class Money
{
    private static $defaultRoundingMode = RoundingMode::UNNECESSARY;

    public static function setDefaultRoundingMode(int $roundingMode)
    {
        self::$defaultRoundingMode = $roundingMode;
    }

    public static function create(BigNumber $amount, Currency $currency, Context $context, int $roundingMode = null) : Money
    {
        $amount = $context->applyTo($amount, $currency, $roundingMode ?? self::$defaultRoundingMode);

        return new Money($amount, $currency, $context);
    }   
}

I do realize that the aim of this library is to always return exact results, unless you're explicitly instructing it not to. But when the library is used for financial apps, where proper rounding is a must, it just becomes a PITA to always specify the same rounding mode.

It would have been also an option to extend the Money class with one having default rounding mode other than 'unneccessary', but the class is 'final'.

Simple rounding test failed

Hello!

I am trying to check the corrections of rounding.
This simple test must be passed:

1/7/13713 = 1

But result discourages:

$brickRoundings=[
    RoundingMode::UP,
    RoundingMode::DOWN,
    RoundingMode::CEILING,
    RoundingMode::FLOOR,
    RoundingMode::HALF_UP,
    RoundingMode::HALF_DOWN,
    RoundingMode::HALF_CEILING,
    RoundingMode::HALF_FLOOR,
    RoundingMode::HALF_EVEN,
];
$money = Brick::of(1, 'EUR');
foreach ($brickRoundings as $brickRounding) {
    $brickResult=$money->dividedBy(7, $brickRounding)->dividedBy(13, $brickRounding)
        ->multipliedBy(7, $brickRounding)->multipliedBy(13, $brickRounding);
    echo 'brick: ' . $brickResult->formatTo('en_US').PHP_EOL;
}
brick: €1.82
brick: €0.91
brick: €1.82
brick: €0.91
brick: €0.91
brick: €0.91
brick: €0.91
brick: €0.91
brick: €0.91

the result is not 1 while even Windows 10 calculator app provides the correct answer =1. Excel, for sure also ..
also PHP:

<?php
echo 1/7/13*7*13;
// =1

Would you please explain: why I cannot get the correct answer = 1?

thank you!

Get rid of symbol

Hello,

I've created custom Formatter like this:

class MoneyFormatter extends \NumberFormatter
{
    public function __construct($locale, $style, $pattern = null)
    {
        parent::__construct($locale, $style, $pattern);

        $this->setSymbol(\NumberFormatter::CURRENCY_SYMBOL, '');
        $this->setSymbol(\NumberFormatter::MONETARY_GROUPING_SEPARATOR_SYMBOL, ' ');
        $this->setAttribute(\NumberFormatter::FRACTION_DIGITS, 2);
    }
}

I'm using it like this:

public static function convertMoneyMinorToMajor($amount, $currency = 'PLN')
{
    $formatter = new MoneyFormatter('pl_PL', \NumberFormatter::CURRENCY);
    return Money::ofMinor($amount, $currency)->formatWith($formatter);
}

Example results:

"1 000,00 " "20,00 " "500,00 " "50,00 " "500,00 " "500,00 " "500,00 "

As you can see I set CURRENCY_SYMBOL as "" (nothing) but now formatter adds space at the end and I can't get rid of it. Do you know how to remove this without doing str_replace?

Maybe there is another way to get rid of currency symbol without making custom formatter? I just want numeric value as a result.

Currency conversion precision

Hi!

Coming over from other packages, I've got to say this is definitely what I needed for this use case which deals with multiple currencies and down-to-the-last-cent precision work needed. However, one thing that bugs me is that I lose precision using the CurrencyConverter class. In the class, you are using BigRational to store and represent the money before returning it as a new Money instance.

Would it be possible instead to return it as as RationalMoney or even BigRational instead of a Money instance to be able to keep precision? Maybe a boolean parameter? This gives developers freedom to apply a context and/or rounding even after conversion to another currency.

My use case is I store the rational equivalents (as a string with it's currency through RationalMoney, maybe a different or query efficient approach can be suggested?) on the database to keep it precise, only using context and rounding upon display. So if there is another subsequent operation on the money object, precision will not be lost.

Tag a release

Hi, I like your library and I would like to use it. Do you think you could tag v0.1? Thanks!

Custom currencies

Part of the list of things I want to work on before tagging a release, as mentioned in #2. Comments welcome.

The library ships with ISO currencies, but allows you to add custom currencies (bitcoins, etc.). This is done through a class called DefaultCurrencyProvider; Money uses it to resolve a currency code such as USD to a Currency instance, to get its default scale among other things.

My issue with the current implementation is that it's a singleton: if your code adds a custom currency, it affects third-party code using Money as well, and vice versa. ISO currencies can't be overridden, but conflicts between custom currencies are possible.

The only way I can see around this, would be to not allow currency codes in Money factory methods, and force a Currency instance to be provided.
This used to be this way, but this was very unfriendly to use. You want to be able to write:

Money::of('9.90', 'USD');

and not:

Money::of('9.90', ISOCurrency::of('USD'));

So at some point, you have to resolve the currency code from somewhere, and I could not find a better idea than making this a global, singleton configuration.

Now that I'm thinking about it, another option could be to allow a currency code string to be provided for ISO currencies only; custom currencies would need to be provided as a Currency object. This would remove the currency provider bloat entirely. Thoughts?

Side question: the Currency object has this thing called the default fraction digits (default scale, for example 2 for USD/EUR/etc.); do cryptocurrencies have a somewhat official, default scale like traditional currencies?

Formatting the Number part in toString or add formatting functions

Hi,
Is there a way to format string echo-ed? I meanis echo Money::of(5000, 'TZS') should display TZS 5,000.00 instead of current TZS 5000.00. It makes reading big number easier.

PHP have already number formatting function and it wouldn't be very hard to add its.

I'm not sure if every currency have corresponding separator or even if there is such a thing as universal separator. If indeed there is separator per currency we can check the currency and apply number_format accordingly. If not, then we can add a format($separator=',') function to money class that works the same as toString() but formats the number part of the result.

What do you think?

ExchangeRateProvider method parameters

In the ExchangeRateProvider interface, we have a getExchangeRate(string $sourceCurrencyCode, string $targetCurrencyCode) method. Can we change the parameter type to getExchangeRate(Currency $sourceCurrency, Currency $targetCurrency)? I'm guessing most databases use an int ID rather than a CODE string. In our case, we need to add some unnecessary relationships when fetching from the database. Also, the internal database may use an internal incremental ID instead of the ISO ID.

Feature: implement splitWithRemainder() & allocateWithRemainder()

Same as split() and allocate(), but:

  • don't spread the remainder on the first monies
  • add another Money at the end of the array with the remainder

Taking the examples from the README:

$money = Money::of(100, 'USD');
[$a, $b, $c, $r] = $money->splitWithRemainder(3); // USD 33.33, USD 33.33, USD 33.33, USD 0.01
$profit = Money::of('987.65', 'CHF');
[$a, $b, $c, $r] = $profit->allocateWithRemainder(48, 41, 11); // CHF 474.08, CHF 404.93, CHF 108.64, CHF 0.01
$profit = Money::of('987.65', 'CHF', new CashContext(5));
[$a, $b, $c, $r] = $profit->allocateWithRemainder(48, 41, 11); // CHF 474.05, CHF 404.90, CHF 108.60, CHF 0.10

Scales & contexts

This is kind of a merger of other issues, in particular #3 (Scales and Money classes) and #4 (Money contexts), that happen to be tightly related.

I figured we've reached a point where we have a common view of a few things, and several conflicting ideas on others. I'll try to summarize the current status of the brainstorming.

I hope this issue will help us find some common ground.

What we (seem to) agree on

  • There should be a single Money class (no Money/BigMoney split)
  • A Money is composed of a BigDecimal and a Currency
    • the BigDecimal can have any scale, as required by the application
    • the Currency is composed of a currency code, and a default scale; this scale can be used to provide a default scale in factory methods, and dictates how some methods such as getAmountMinor() behave, independently of the scale of the Money itself
  • A Money should never embed a rounding mode, the caller of an operation on a Money should always be in control of rounding
  • Operations on a Money should be able to round by steps, to support cash increments of 1.00 units (step 100, CZK) or 0.05 units (step 5, CHF); steps for ISO currencies will not be hardcoded in the library, and will be left up to the developer
  • Some use cases can require several consecutive operations on monies, where you do not want to apply any kind of scaling or rounding until the very last step; we should therefore provide a calculator based on BigRational, that doesn't take any scale, step or rounding mode until the getResult() method is called

What we disagree on

Actually there is just one main friction point: operations. Should all operations require a full context (target scale, step, and rounding mode), or should a Money instance dictate what the result scale & step are?

I tried to summarize your points of view here, feel free to correct me if I'm wrong:

  • @jkuchar and I like the idea of storing the step in the Money itself. All operations on a Money would yield another Money with the same scale & step capabilities:

    $money = Money::of(50, 'USD'); // USD 50.00
    $money = $money->dividedBy(3, RoundingMode::HALF_UP); // USD 16.67
    
    $money = Money::of(50, 'CZK', /* step */ 100); // CZK 50.00
    $money = $money->dividedBy(3, RoundingMode::DOWN); // CZK 16.00
    $money = $money->dividedBy(3, RoundingMode::DOWN); // CZK 5.00

    The rationale behind this is that usually, an application deals with a fixed scale for a given currency (e.g. the currency's default scale for an online shop, or a higher scale for a Forex trading website), and the need to juggle scales in the middle of a series of operations seems very unusual. I personally feel like the need for a sudden change of scale might be an indicator that you're trying to do something that would be a better fit for a BigRational-based calculator.

    Note that we could allow an optional context to be provided, to allow overriding the current scale & step. This would just not be mandatory.

    Critics include the fact that plus() may throw an exception when adding an amount whose scale is larger than that of the left operand, instead of returning a Money with an adjusted scale (as BigDecimal would do), and that the result depends on the order of the operands (USD 1.20 + USD 0.1 = USD 1.30 while USD 0.1 + USD 1.20 = USD 1.3, and USD 1.21 + USD 0.1 = USD 1.31 while USD 0.1 + USD 1.21 = Exception). I replied here.

  • @jiripudil is not against this behaviour, but suggests that we'd throw an exception when adding together monies of different scales & steps. I replied here.

  • Finally, @VasekPurchart sees Money as an "anemic" value object, that ideally would not contain any operations. He's not fully against having operations on Money though, but in this case suggests that all operations would have to provide the full context: scale, step, and rounding mode. (Note: this is pretty much what we have today and what I was trying to get away from).

What others are doing

This is just an overview of what I could investigate in a reasonable timeframe. If you know other libraries that you think deserve to be mentioned, please let me know.

moneyphp (PHP)

This PHP library offers a single class, Money, that only stores amounts in integer form, so in "minor units". Currencies are only defined by a currency code. No scale is involved. Multiplication and division take an optional rounding mode, defaulting to HALF_UP.

Joda Money (Java)

This popular library offers two implementations, Money and BigMoney:

  • Money always has the default scale for the currency. The result of an operation always has the same scale: plus() must add an amount that is compatible with this scale or you get an exception, and dividedBy() must provide a rounding mode.
  • BigMoney has a flexible scale. BigMoney.plus() adjusts the scale of the result, effectively acting like a BigDecimal with a Currency: USD 25.95 + 3.021 = USD 28.971. BigMoney.dividedBy() returns a BigMoney with the same scale as the left operand: USD 1.00 / 3, rounded DOWN = USD 0.33. You cannot choose the scale of the result.

Java 9 Money API (JSR 354) (Java)

This is the new money interface that is now part of Java from version 9 onwards. Java 9 is due to be released tomorrow, 21 September 2017; you can hardly dream of a fresher API! It's been created by a team of experts, several of them working for Credit Suisse.

This API defines a MonetaryAmount interface that Money classes must implement. MonetaryAmount instances embed a MonetaryContext that defines "the numeric capabilities, e.g. the supported precision and maximal scale, as well as the common implementation flavor."

According to the source code documentation, operations like add(), multiply() and divide() take a single argument, the operand. The result's scale is adjusted just like a BigDecimal would do, but an exception can be thrown if the scale of the result exceeds the max scale defined by the context.

The reference implementation, Moneta, provides several classes:

  • Money, based on BigDecimal
  • FastMoney, based on long
  • RoundedMoney, based on BigDecimal like Money, but additionally embedding a custom rounding implementation, that is applied to every operation result. The resulting monies, in turn, embed this rounding for further calculations.

I gave Moneta a try:

CurrencyUnit usDollar = Monetary.getCurrency("USD");
BigDecimal amount = new BigDecimal("100.00");
System.out.println(Money.of(amount, usDollar).divide(3));
System.out.println(FastMoney.of(amount, usDollar).divide(3));
INFO: Using custom MathContext: precision=256, roundingMode=HALF_EVEN
USD 33.33333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333
USD 33.33333

These defaults are plain nonsense if you ask me.

Python Money Class (Python)

Monies have the scale of the number they were instantiated with. Adding two monies of different scales will return a Money with the maximum of the two scales. Dividing a Money will return a number with a fixed number of decimals, rounded:

>>> m = Money("100.0", "USD");
>>> m
USD 100.0
>>> m + Money("1.23", "USD");
USD 101.23
>>> m/3
USD 33.33333333333333333333333333

Ping @martinknor, @fortis and @fprochazka again, now is the time to make your voice heard before the first beta release!

Issues with comparison - returns false although both are the same

Hey Ben,

I'm running into an issue where I'm unsure if we're looking at a bug.
When comparing a Money to an int, it returns false as in it's not the same, although it is.

I have these dumps in my phpunit tests:

# $this->min_bid = Money obj of 0.06 USD
# $this->optimized_bid = int 6

dump($this->min_bid);
dump($this->optimized_bid);
dump($this->min_bid->isGreaterThanOrEqualTo($this->optimized_bid));

This is the output:

Brick\Money\Money {#3601
  -amount: Brick\Math\BigDecimal {#3612
    -value: "6"
    -scale: 2
  }
  -currency: Brick\Money\Currency {#3594
    -currencyCode: "USD"
    -numericCode: 840
    -name: "US Dollar"
    -defaultFractionDigits: 2
  }
  -context: Brick\Money\Context\DefaultContext {#3608}
}
6
false

I'm getting the same result when comparing with isEqual.
This might be me misunderstanding how this is supposed to work, but to me the result is unexpected.

is 6 equal to 6? yes, or not? lol

GitHub Sponsors?

Hi @BenMorel! I would like to ask if you have considered adding support for GitHub Sponsors / Patreon / Bitcoin address? Thanks! :-)

Scales and Money classes

Part of the list of things I want to work on before tagging a release, as mentioned in #2. Comments welcome.

Currently, a single class, Money, allows to work with default scales (1.50 USD) but also with arbitrary scales (1.123456 USD). I like this flexibility, but it comes at a price:

  • You never know what kind of money you're dealing with. Say you write a function that accepts a Money and performs some basic calculations on it:

    function addDeliveryFee(Money $orderTotal) : Money
    {
        $deliveryFee = Money::of('1.90', 'USD');
        return $orderTotal->plus($deliveryFee);
    }

    You're expecting that everyone will call this function with a typical money like 1.23 USD, but suddenly someone calls your function with 1.2345 USD. Because Money retains the left operand scale by default, your function will return 3.1345 USD. Probably not what you want. You may want to fail in some way here, such as throwing an exception.

    Sure, you could check $money->getAmount()->getScale() , but doing so in every function that accepts a Money? Let's be honest: you don't want to do that.

  • I personally store all my (default scale) monies in the database as integers, representing "minor units" (cents). For example, 1.23 USD is stored as 123, while 500 JPY is just stored as 500.
    To do that, I call $money->getAmountMinor() that gives me 123 for 1.23 USD. The problem is, by doing so I'm assuming that I'm dealing with a standard scale money; if for any reason I get a high precision money such as 1.2345 USD, getAmountMinor() will return 12345, and if I blindly store this in my database to later retrieve it as a standard scale money, I would end up with 123.45 USD! Same problem as above then, we may need a way to enforce a default scale money at some point for safety reasons.

To be honest, this kind of problem never occurred to me so far, as I have full control over my code from A to Z, and usually only deal with default scale monies. Still, it has always made me feel uncomfortable, and I'm afraid that it could lead to potential issues in third-party libraries dealing with monies. Should we expect them to do their own scale check, or to trust that the caller will give a money with the correct scale? Or should we provide a way to enforce at least default scale monies?

I can see 2 options here:

  • leave it as it is, and maybe at least provide a convenience method such as isDefaultScale() so that methods not trusting the caller may more easily do their own check;

  • use type safety to have a way to ensure that you're dealing with a standard scale money. In this scenario, we would have 2 different classes for default scale monies and arbitrary scale monies, which could be implemented in many different ways:

    • use 2 completely distinct classes such as Money (default scale) and BigMoney (arbitrary scale). This is the approach used by Joda Money (Java); in their implementation, the 2 classes are not fully interchangeable. For example, while BigMoney::plus() accepts either a Money or a BigMoney, Money::plus() only accepts another Money.
    • keep Money as it is, and add a subclass called something like DefaultMoney (hate the name). If you instantiate a Money with a default scale, it would return a DefaultMoney; this way, if you write a function that expects a Money, it will accept a DefaultMoney, but if it expects a DefaultMoney, it will not accept a Money.
    • a mix of both: use separate Money/BigMoney classes as above, but have them implement a common interface. This is similar to point 1 because we would have 2 separate classes that do not extend each other, but similar to point 2 because we would have a fully shared interface so that Money::plus() and BigMoney::plus() will accept both a Money and a BigMoney (is this necessary, after all?)

When I started this library, I didn't like Joda's Money/BigMoney approach very much. I believed in a generic Money class that would handle all use cases. Now that I have it somehow, I realize that there might be more drawbacks than advantages.

In most projects, needless to say that we'll be using default scale monies way more often than arbitrary scale ones. So it does make sense to have a special case (class) for them. This way, one can be sure when dealing with a Money, that it's always the default scale.

As a final note, I also checked the approach introduced by the new Java Money API (JSR-354) and its reference implementation. It made me curious as they use a common interface (MonetaryAmount) for all monies. I thought this was good, until I realized that it is so generic that implementations have to store the context in the Money class itself. The consequence of this is that when you accept a Money, you get not only a value and a currency, but also how it's supposed to be rounded. I don't believe this is good, as I prefer to receive only a monetary amount, and decide how I will perform the calculations and roundings in every operation I write. It's also worth noting that for now, Joda Money, which is very popular in the Java world, doesn't implement JSR-354; this may change as Java 9 (the first version to include the Money API) is not out yet, but last time I checked, the lead developer was not keen on the spec.

Note that if I were to follow the Money/BigMoney path, this would affect these two other issues:

  • #4 Money contexts:
    • Money::plus() and Money::minus() would not require a MoneyContext anymore, because the constant scale would guarantee that no rounding is necessary
    • Money::multipliedBy() and Money::dividedBy() would still require a rounding mode, but not a full MoneyContext
  • #6 Implementation / performance: we could consider implementing Money using an integer, while BigMoney would still use BigDecimal.

I'm really looking forward to your opinion on this one, which I consider the most important issue of all.

Missing a few country currencies

Hi!

I noticed you are missing a few country currencies. The list is significant, see below:

Bulgarian Lev (OLD),BGL
Cyprus Pound,CYP
Czechoslovakian Koruna,CSK
Ecuador Sucre,ECS
Estonian Kroon,EEK
Finnish Markka,FIM
Central Pacific Franc,CFP
German Mark,DEM
Greek Drachma,GRD
Italian Lira,ITL
Irish Punt,IEP
Italian Lira,ITL
Latvian Lats,LVL
Lithuanian Litas,LTL
Luxembourg Franc,LUF
Malagasy Franc,MGF
Maltese Lira,MTL
Mauritanian Ouguiya,MRO
Mozambique Metical,MZM
Dutch Guilder,NLG
Central Pacific Franc,CFP
Portuguese Escudo,PTE
Romanian Leu,ROL
Italian Lira,ITL
Sao Tome/Principe Dobra,STD
Slovak Koruna,SKK
Slovenian Tolar,SIT
Spanish Peseta,ESP
Sudanese Pound,SDP
Sudanese Dinar,SDD
Suriname Guilder,SRG
Turkish Lira,TRL
Turkmenistan Manat,TMM
Uganda Shilling,UGS
Venezuelan Bolivar,VEB
Central Pacific Franc,CFP
Spanish Peseta,ESP
Yugoslav Dinar,YUN
Zambian Kwacha,ZMK
Zimbabwe Dollar,ZWD

Persisting into database

Hi,
Thank you for the library. I was reading the doc and its comprehensive except it does not provide any recommendation as to what should be the field type (database column data type that can work better with library). This will help avert confusion as reading the answers on the internet ends up confusing newbees. Also comment on fields like Postgresql Money data type would be helpful.

Cheers!

florianv/swap integration

I like this library, but one thing which is putting me off a bit compared to moneyphp/money is that this library doesn't have integration for most popular exchange rates provider florianv/swap. This is in contrast with moneyphp/money which comes with support bundled in.

So, hereby, this is a request for adding core support for florianv/swap as a new ExchangeRateProvider.

Issue with documentation of allocate()

The readme says the following:

When the allocation yields a remainder, both split() and allocate() spread it on the first monies in the list, until the total adds up to the original Money. This is the algorithm suggested by Martin Fowler in his book Patterns of Enterprise Application Architecture. You can see that in the first example, where the first money gets 33.34 dollars while the others get 33.33 dollars.

However it looks to me that split() spreads in the way described, but allocate appears to use the ratios to divide up the remainder:


$profit = Money::ofMinor('1000', 'GBP');
[$a, $b, $c] = $profit->allocate(45, 20, 34); // GBP 45.50, GBP 20.20, GBP 34.30

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.