Git Product home page Git Product logo

money's Introduction

Brick\Money

A money and currency library for PHP.

Build Status Coverage Status Latest Stable Version Total Downloads License

Introduction

Working with financial data is a serious matter, and small rounding mistakes in an application may lead to serious consequences in real life. That's why floating-point arithmetic is not suited for monetary calculations.

This library is based on brick/math and handles exact calculations on monies of any size.

Installation

This library is installable via Composer:

composer require brick/money

Requirements

This library requires PHP 8.1 or later.

For PHP 8.0 compatibility, you can use version 0.8. For PHP 7.4, you can use version 0.7. For PHP 7.1, 7.2 & 7.3, you can use version 0.5. Note that these PHP versions are EOL and not supported anymore. If you're still using one of these PHP versions, you should consider upgrading as soon as possible.

Although not required, it is recommended that you install the GMP or BCMath extension to speed up calculations.

Project status & release process

While this library is still under development, it is well tested and should be stable enough to use in production environments.

The current releases are numbered 0.x.y. When a non-breaking change is introduced (adding new methods, optimizing existing code, etc.), y is incremented.

When a breaking change is introduced, a new 0.x version cycle is always started.

It is therefore safe to lock your project to a given release cycle, such as 0.9.*.

If you need to upgrade to a newer release cycle, check the release history for a list of changes introduced by each further 0.x.0 version.

Creating a Money

From a regular currency value

To create a Money, call the of() factory method:

use Brick\Money\Money;

$money = Money::of(50, 'USD'); // USD 50.00
$money = Money::of('19.9', 'USD'); // USD 19.90

If the given amount does not fit in the currency's default number of decimal places (2 for USD), you can pass a RoundingMode:

$money = Money::of('123.456', 'USD'); // RoundingNecessaryException
$money = Money::of('123.456', 'USD', roundingMode: RoundingMode::UP); // USD 123.46

Note that the rounding mode is only used once, for the value provided in of(); it is not stored in the Money object, and any subsequent operation will still need to be passed a RoundingMode when necessary.

From minor units (cents)

Alternatively, you can create a Money from a number of "minor units" (cents), using the ofMinor() method:

use Brick\Money\Money;

$money = Money::ofMinor(1234, 'USD'); // USD 12.34

Basic operations

Money is an immutable class: its value never changes, so it can be safely passed around. All operations on a Money therefore return a new instance:

use Brick\Money\Money;

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

echo $money->plus('4.99'); // USD 54.99
echo $money->minus(1); // USD 49.00
echo $money->multipliedBy('1.999'); // USD 99.95
echo $money->dividedBy(4); // USD 12.50

You can add and subtract Money instances as well:

use Brick\Money\Money;

$cost = Money::of(25, 'USD');
$shipping = Money::of('4.99', 'USD');
$discount = Money::of('2.50', 'USD');

echo $cost->plus($shipping)->minus($discount); // USD 27.49

If the two Money instances are not of the same currency, an exception is thrown:

use Brick\Money\Money;

$a = Money::of(1, 'USD');
$b = Money::of(1, 'EUR');

$a->plus($b); // MoneyMismatchException

If the result needs rounding, a rounding mode must be passed as second parameter, or an exception is thrown:

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

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

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

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

$money->multipliedBy('1.2345'); // RoundingNecessaryException
$money->multipliedBy('1.2345', RoundingMode::DOWN); // USD 61.72

$money->dividedBy(3); // RoundingNecessaryException
$money->dividedBy(3, RoundingMode::UP); // USD 16.67

Money contexts

By default, monies have the official scale for the currency, as defined by the ISO 4217 standard (for example, EUR and USD have 2 decimal places, while JPY has 0) and increment by steps of 1 minor unit (cent); they internally use what is called the DefaultContext. You can change this behaviour by providing a Context instance. All operations on Money return another Money with the same context. Each context targets a particular use case:

Cash rounding

Some currencies do not allow the same increments for cash and cashless payments. For example, CHF (Swiss Franc) has 2 fraction digits and allows increments of 0.01 CHF, but Switzerland does not have coins of less than 5 cents, or 0.05 CHF.

You can deal with such monies using CashContext:

use Brick\Money\Money;
use Brick\Money\Context\CashContext;
use Brick\Math\RoundingMode;

$money = Money::of(10, 'CHF', new CashContext(step: 5)); // CHF 10.00
$money->dividedBy(3, RoundingMode::DOWN); // CHF 3.30
$money->dividedBy(3, RoundingMode::UP); // CHF 3.35

Custom scale

You can use custom scale monies by providing a CustomContext:

use Brick\Money\Money;
use Brick\Money\Context\CustomContext;
use Brick\Math\RoundingMode;

$money = Money::of(10, 'USD', new CustomContext(scale: 4)); // USD 10.0000
$money->dividedBy(7, RoundingMode::UP); // USD 1.4286

Auto scale

If you need monies that adjust their scale to fit the operation result, then AutoContext is for you:

use Brick\Money\Money;
use Brick\Money\Context\AutoContext;

$money = Money::of('1.10', 'USD', new AutoContext()); // USD 1.1
$money->multipliedBy('2.5'); // USD 2.75
$money->dividedBy(8); // USD 0.1375

Note that it is not advised to use AutoContext to represent an intermediate calculation result: in particular, it cannot represent the result of all divisions, as some of them may lead to an infinite repeating decimal, which would throw an exception. For these use cases, RationalMoney is what you need. Head on to the next section!

Advanced calculations

You may occasionally need to chain several operations on a Money, and only apply a rounding mode on the very last step; if you applied a rounding mode on every single operation, you might end up with a different result. This is where RationalMoney comes into play. This class internally stores the amount as a rational number (a fraction). You can create a RationalMoney from a Money, and conversely:

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

$money = Money::of('9.5', 'EUR') // EUR 9.50
  ->toRational() // EUR 950/100
  ->dividedBy(3) // EUR 950/300
  ->plus('17.795') // EUR 6288500/300000
  ->multipliedBy('1.196') // EUR 7521046000/300000000
  ->to($money->getContext(), RoundingMode::DOWN) // EUR 25.07

As you can see, the intermediate results are represented as fractions, and no rounding is ever performed. The final to() method converts it to a Money, applying a context and a rounding mode if necessary. Most of the time you want the result in the same context as the original Money, which is what the example above does. But you can really apply any context:

...
  ->to(new CustomContext(scale: 8), RoundingMode::UP); // EUR 25.07015334

Note: as you can see in the example above, the numbers in the fractions can quickly get very large. This is usually not a problem—there is no hard limit on the number of digits involved in the calculations—but if necessary, you can simplify the fraction at any time, without affecting the actual monetary value:

...
  ->multipliedBy('1.196') // EUR 7521046000/300000000
  ->simplified() // EUR 3760523/150000

Money allocation

You can easily split a Money into a number of parts:

use Brick\Money\Money;

$money = Money::of(100, 'USD');
[$a, $b, $c] = $money->split(3); // USD 33.34, USD 33.33, USD 33.33

You can also allocate a Money according to a list of ratios. Say you want to distribute a profit of 987.65 CHF to 3 shareholders, having shares of 48%, 41% and 11% of a company:

use Brick\Money\Money;

$profit = Money::of('987.65', 'CHF');
[$a, $b, $c] = $profit->allocate(48, 41, 11); // CHF 474.08, CHF 404.93, CHF 108.64

It plays well with cash roundings, too:

use Brick\Money\Money;
use Brick\Money\Context\CashContext;

$profit = Money::of('987.65', 'CHF', new CashContext(step: 5));
[$a, $b, $c] = $profit->allocate(48, 41, 11); // CHF 474.10, CHF 404.95, CHF 108.60

Note that the ratios can be any (non-negative) integer values and do not need to add up to 100.

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.

Money bags (mixed currencies)

You may sometimes need to add monies in different currencies together. MoneyBag comes in handy for this:

use Brick\Money\Money;
use Brick\Money\MoneyBag;

$eur = Money::of('12.34', 'EUR');
$jpy = Money::of(123, 'JPY');

$moneyBag = new MoneyBag();
$moneyBag->add($eur);
$moneyBag->add($jpy);

You can add any kind of money to a MoneyBag: a Money, a RationalMoney, or even another MoneyBag.

Note that unlike other classes, MoneyBag is mutable: its value changes when you call add() or subtract().

What can you do with a MoneyBag? Well, you can convert it to a Money in the currency of your choice, using a CurrencyConverter. Keep reading!

Currency conversion

This library ships with a CurrencyConverter that can convert any kind of money (Money, RationalMoney or MoneyBag) to a Money in another currency:

use Brick\Money\CurrencyConverter;

$exchangeRateProvider = ...;
$converter = new CurrencyConverter($exchangeRateProvider); // optionally provide a Context here

$money = Money::of('50', 'USD');
$converter->convert($money, 'EUR', roundingMode: RoundingMode::DOWN);

The converter performs the most precise calculation possible, internally representing the result as a rational number until the very last step.

To use the currency converter, you need an ExchangeRateProvider. Several implementations are provided, among which:

ConfigurableProvider

This provider starts with a blank state, and allows you to add exchange rates manually:

use Brick\Money\ExchangeRateProvider\ConfigurableProvider;

$provider = new ConfigurableProvider();
$provider->setExchangeRate('EUR', 'USD', '1.0987');
$provider->setExchangeRate('USD', 'EUR', '0.9123');

PDOProvider

This provider reads exchange rates from a database table:

use Brick\Money\ExchangeRateProvider\PDOProvider;
use Brick\Money\ExchangeRateProvider\PDOProviderConfiguration;

$pdo = new \PDO(...);

$configuration = new PDOProviderConfiguration(
    tableName: 'exchange_rates',
    exchangeRateColumnName: 'exchange_rate',
    sourceCurrencyColumnName: 'source_currency_code',
    targetCurrencyColumnName: 'target_currency_code',
);

$provider = new PDOProvider($pdo, $configuration);

PDOProvider also supports fixed source or target currency, and dynamic WHERE conditions. Check the PDOProviderConfiguration class for more information.

BaseCurrencyProvider

This provider builds on top of another exchange rate provider, for the quite common case where all your available exchange rates are relative to a single currency. For example, the exchange rates provided by the European Central Bank are all relative to EUR. You can use them directly to convert EUR to USD, but not USD to EUR, let alone USD to GBP.

This provider will combine exchange rates to get the expected result:

use Brick\Money\ExchangeRateProvider\ConfigurableProvider;
use Brick\Money\ExchangeRateProvider\BaseCurrencyProvider;

$provider = new ConfigurableProvider();
$provider->setExchangeRate('EUR', 'USD', '1.1');
$provider->setExchangeRate('EUR', 'GBP', '0.9');

$provider = new BaseCurrencyProvider($provider, 'EUR');
$provider->getExchangeRate('EUR', 'USD'); // 1.1
$provider->getExchangeRate('USD', 'EUR'); // 10/11
$provider->getExchangeRate('GBP', 'USD'); // 11/9

Notice that exchange rate providers can return rational numbers!

Write your own provider

Writing your own provider is easy: the ExchangeRateProvider interface has just one method, getExchangeRate(), that takes the currency codes and returns a number.

Custom currencies

Money supports ISO 4217 currencies by default. You can also use custom currencies by creating a Currency instance. Let's create a Bitcoin currency:

use Brick\Money\Currency;
use Brick\Money\Money;

$bitcoin = new Currency(
    'XBT',     // currency code
    0,         // numeric currency code, useful when storing monies in a database; set to 0 if unused
    'Bitcoin', // currency name
    8          // default scale
);

You can now use this Currency instead of a currency code:

$money = Money::of('0.123', $bitcoin); // XBT 0.12300000

Formatting

Formatting requires the intl extension.

Money objects can be formatted according to a given locale:

$money = Money::of(5000, 'USD');
echo $money->formatTo('en_US'); // $5,000.00
echo $money->formatTo('fr_FR'); // 5 000,00 $US

Alternatively, you can format Money objects with your own instance of NumberFormatter, which gives you room for customization:

$formatter = new \NumberFormatter('en_US', \NumberFormatter::CURRENCY);
$formatter->setSymbol(\NumberFormatter::CURRENCY_SYMBOL, 'US$');
$formatter->setSymbol(\NumberFormatter::MONETARY_GROUPING_SEPARATOR_SYMBOL, '·');
$formatter->setAttribute(\NumberFormatter::MIN_FRACTION_DIGITS, 2);

$money = Money::of(5000, 'USD');
echo $money->formatWith($formatter); // US$5·000.00

Important note: because formatting is performed using NumberFormatter, the amount is converted to floating point in the process; so discrepancies can appear when formatting very large monetary values.

Storing the monies in the database

Persisting the amount

  • As an integer: in many applications, monies are only ever used with their default scale (e.g. 2 decimal places for USD, 0 for JPY). In this case, the best practice is to store minor units (cents) as an integer field:

    $integerAmount = $money->getMinorAmount()->toInt();

    And later retrieve it as:

    Money::ofMinor($integerAmount, $currencyCode);

    This approach works well with all currencies, without having to worry about the scale. You only have to worry about not overflowing an integer (which would throw an exception), but this is unlikely to happen unless you're dealing with huge amounts of money.

  • As a decimal: for most other cases, storing the amount string as a decimal type is advised:

    $decimalAmount = (string) $money->getAmount();

    And later retrieve it as:

    Money::of($decimalAmount, $currencyCode);

Persisting the currency

  • As a string: if you only deal with ISO currencies, or custom currencies having a 3-letter currency code, you can store the currency in a CHAR(3). Otherwise, you'll most likely need a VARCHAR. You may also use an ENUM if your application uses a fixed list of currencies.

    $currencyCode = $money->getCurrency()->getCurrencyCode();

    When retrieving the currency: you can use ISO currency codes directly in Money::of() and Money::ofMinor(). For custom currencies, you'll need to convert them to Currency instances first.

  • As an integer: if you only deal with ISO currencies, or custom currencies with a numeric code, you may store the currency code as an integer:

    $numericCode = $money->getCurrency()->getNumericCode();

    When retrieving the currency: you can use numeric codes of ISO currencies directly in Money::of() and Money::ofMinor(). For custom currencies, you'll need to convert them to Currency instances first.

  • Hardcoded: if your application only ever deals with one currency, you may very well hardcode the currency code and not store it in your database at all.

Using an ORM

If you're using an ORM such as Doctrine, it is advised to store the amount and currency separately, and perform conversion in the getters/setters:

class Entity
{
    private int $price;
    private string $currencyCode;

    public function getPrice() : Money
    {
        return Money::ofMinor($this->price, $this->currencyCode);
    }

    public function setPrice(Money $price) : void
    {
        $this->price = $price->getMinorAmount()->toInt();
        $this->currencyCode = $price->getCurrency()->getCurrencyCode();
    }
}

FAQ

How does this project compare with moneyphp/money?

Please see this discussion.

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

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!

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?

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!

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);
}

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?

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.

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

GitHub Sponsors?

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

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.

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.

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);
    });

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.

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?

Release

Can you make release on packagist?

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.

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');
    }

}

[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'.

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

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?

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.

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

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.

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

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.

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!

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.

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": "*"
},

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);
    }

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.

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

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

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

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.

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.

Tag a release

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

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)

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.

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.

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 :)

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!

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

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

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!

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 🤔

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.