moneyphp / money

PHP implementation of Fowler's Money pattern.
http://moneyphp.org
MIT License
4.6k stars 440 forks source link

Question about Japanese Yen cent amount #684

Closed driesvints closed 2 years ago

driesvints commented 2 years ago

I just discovered that Japanese Yen requires three number of cents rather than the regular two of other currencies.

Meaning the following:

new Money(12100, 'JPY');

Will result in ¥12,100 rather than the expected ¥121,00.

This is an extremely important gotcha in my eyes. I might have glanced over it in the docs? Also, I realise that this probably is due to my lack of knowing how much cents there are for each currency but I was wondering if moneyphp helps with this somehow? If not, is there a complete list somewhere with all supported currencies and how many cents they have (or a built-in API)?

Thanks for any help! 🙏

gmponos commented 2 years ago

I think JPY does not have cents.

The comma separator there is for thousands.

UlrichEckhardt commented 2 years ago

Yes, there are 1/1000 of a yen as monetary unit. See https://en.wikipedia.org/wiki/Japanese_yen, search for sen and rin.

gmponos commented 2 years ago

Well ISO does not have.. https://github.com/moneyphp/iso-currencies/blob/master/resources/current.yaml#L364

So if he uses ISO converter/formatter or something like that then it does not.

gmponos commented 2 years ago

Also from the WIKI above

The sen and the rin were eventually taken out of circulation at the end of 1953

driesvints commented 2 years ago

I see. Thank you for responding @gmponos & @UlrichEckhardt. I'm wondering what the correct way to handle this in MoneyPHP is? Should we always rely on the value of minorUnit?

driesvints commented 2 years ago

I'm btw working with Paddle which returns values as floats/strings.

driesvints commented 2 years ago

I've got something working:

// With $plan being an object with a price (string) and currency (string)
// Example: "121.0" & "JPY"
// Other example: "12.24" & "EUR"
// This is how Paddle gives us these.

$currency = new Currency($plan->currency);
$subUnit = (new ISOCurrencies)->subunitFor($currency);
$factor = 1;

for ($i = 0; $i < $subUnit; $i++) {
    $factor *= 10;
}

$amount = bcmul((string) $plan->price, (string) $factor);

$money = new Money($amount, $currency);

$numberFormatter = new NumberFormatter('en', NumberFormatter::CURRENCY);
$moneyFormatter = new IntlMoneyFormatter($numberFormatter, new ISOCurrencies());

return $moneyFormatter->format($money);

And that returns ¥121 and €12.24 properly. Dirty but working 😅

driesvints commented 2 years ago

Does anyone here have any thoughts about the above code? Does that seem okay to you all? Maybe @frederikbosch?

frederikbosch commented 2 years ago

@driesvints The number of subunits returned by the ISOCurrencies class is based on the list of ISO 4217 currencies as provided by the official ISO Maintenance Agency. It comes from this package, also in the moneyphp organization.

new Money expects you to pass the value in the smallest unit but Money itself is not aware of the subunit. This is a value that only lives in implementations of the Currencies interface.

So if you are using ISOCurrencies - as @gmponos says - it will return is 0 subunits for JPY, while it returns 2 for EUR. You are free to overwrite this behavior in your implementation of Currencies. It is quite easy actually. The following uses the ISO list but uses a fixed subunits, as parameter of the constructor.

final class FixedSubunitCurrencies implements Currencies 
{
    private Currencies $delegated;
    private int $subunits;

    public function __construct(Currencies $delegated, int $subunits)
    {
         $this->delegated = $delegated;
         $this->subunits = $subunits;
    }

    public function contains(Currency $currency): bool
    {
          return $this->delegated->contains($currency);
    }

    public function subunitFor(Currency $currency): int
    {
         return $this->subunits;
    }

    public function getIterator(): Traversable
    {
          return $this->delegated->getIterator();
    }
}

$currencies = new FixedSubunitCurrencies(new ISOCurrencies(), 2);
$currencies->subunitFor(); // always 2

Of course it all depends what you want to achieve, but I think you should be able to solve this issue without the factor calculation and/or an additional bcmul call.

driesvints commented 2 years ago

@frederikbosch thank you for those insights! But this is more of an issue where I have the values already formatted up front (from Paddle) and need to convert them to the proper integer value before instantiating the Money object. So I think I still need to do calculations.

frederikbosch commented 2 years ago

@driesvints Why not use a parser?

driesvints commented 2 years ago

@frederikbosch thank you for pointing me into that direction! I ended up writing the below function to parse the given amount before passing it to a formatter:


    /**
     * Parse the given amount from Paddle.
     *
     * @param  string  $amount
     * @param  string  $currency
     * @return string
     */
    protected function parseAmount($amount, $currency)
    {
        $currencies = new ISOCurrencies();

        $numberFormatter = new NumberFormatter(
            config('cashier.currency_locale'), NumberFormatter::DECIMAL
        );

        $moneyParser = new IntlLocalizedDecimalParser($numberFormatter, $currencies);

        $money = $moneyParser->parse($amount, new Currency(strtoupper($currency ?? config('cashier.currency'))));

        return $money->getAmount();
    }

That did the trick 🎉