diff --git a/README.md b/README.md index 5d9d93b..7cf6a33 100644 --- a/README.md +++ b/README.md @@ -172,7 +172,7 @@ $money = Money::new(123456, CZK::class); $money->rawFormatted(); // 1 235,56 Kč ``` -And converting the formatted value back to the Money instance is also possible: +And converting the formatted value back to the Money instance is also possible. The package tries to extract the currency from the provided string. ```php $money = money(1000); $formatted = $money->formatted(); // $10.00 @@ -180,7 +180,18 @@ $fromFormatted = Money::fromFormatted($formatted); $fromFormatted->is($money); // true ``` -Note: `fromFormatted()` misses the cents if the [math decimals](#math-decimals) are greater than [display decimals](#display-decimals). +Optional overrides for the [currency specification](#currency-logic) are accepted too. +```php +$money = money(1000); +$formatted = $money->formatted(); // $10.00 +$fromFormatted = Money::fromFormatted($formatted, USD::class, ['decimalSeparator' => ',', 'prefix' => '$ ', 'suffix' => ' USD']); +$fromFormatted->is($money); // true +``` + +Notes: +1) If currency is not specified and none of the currencies match the prefix and suffix, an exception will be thrown. +2) If currency is not specified and multiple currencies use the same prefix and suffix, an exception will be thrown. +3) `fromFormatted()` misses the cents if the [math decimals](#math-decimals) are greater than [display decimals](#display-decimals). ### Rounding money diff --git a/src/Money.php b/src/Money.php index 9eb22ab..0f66604 100644 --- a/src/Money.php +++ b/src/Money.php @@ -174,6 +174,10 @@ final class Money implements JsonSerializable, Arrayable, Wireable /** Create a Money instance from a formatted string. */ public static function fromFormatted(string $formatted, Currency|string $currency = null, mixed ...$overrides): self { + $currency = isset($currency) + ? currency($currency) + : PriceFormatter::extractCurrency($formatted); + $decimal = PriceFormatter::resolve($formatted, currency($currency), variadic_array($overrides)); return static::fromDecimal($decimal, currency($currency)); diff --git a/src/PriceFormatter.php b/src/PriceFormatter.php index d4e2d69..709a10a 100644 --- a/src/PriceFormatter.php +++ b/src/PriceFormatter.php @@ -23,7 +23,7 @@ class PriceFormatter return $currency->prefix() . $decimal . $currency->suffix(); } - /** Extract the decimal from the formatter string as per the currency's specifications. */ + /** Extract the decimal from the formatted string as per the currency's specifications. */ public static function resolve(string $formatted, Currency $currency, array $overrides = []): float { $currency = Currency::fromArray( @@ -41,4 +41,25 @@ class PriceFormatter return (float) str_replace($currency->decimalSeparator(), '.', $removeNonDigits); } + + /** Tries to extract the currency from the formatted string. */ + public static function extractCurrency(string $formatted): Currency + { + $possibleCurrency = null; + + foreach (currencies()->all() as $currency) { + if ( + str_starts_with($formatted, $currency->prefix()) + && str_ends_with($formatted, $currency->suffix()) + ) { + if ($possibleCurrency) { + throw new \Exception('Multiple currencies are using the same prefix and suffix. Please specify the currency of the formatted string.'); + } + + $possibleCurrency = $currency; + } + } + + return $possibleCurrency ?? throw new \Exception('None of the currencies are using the prefix and suffix that would match with the formatted string.'); + } } diff --git a/tests/Pest/MoneyTest.php b/tests/Pest/MoneyTest.php index 4ab046a..586b8b7 100644 --- a/tests/Pest/MoneyTest.php +++ b/tests/Pest/MoneyTest.php @@ -159,6 +159,24 @@ test('money can be created from a raw formatted string', function () { expect($money->value())->toBe(123456); }); +test('an exception is thrown if none of the currencies match the prefix and suffix', function () { + $money = money(1000); + $formatted = $money->formatted(); + + currencies()->remove(USD::class); + + pest()->expectException(\Exception::class); + Money::fromFormatted($formatted); +}); + +test('an exception is thrown if multiple currencies are using the same prefix and suffix', function () { + currencies()->add(['code' => 'USD2', 'name' => 'USD2', 'prefix' => '$']); + $money = money(1000); + + pest()->expectException(\Exception::class); + Money::fromFormatted($money->formatted()); +}); + test('converting money to a string returns the formatted string', function () { expect( (string) Money::fromDecimal(10.00, USD::class)