diff --git a/README.md b/README.md index f192ed8..5d9d93b 100644 --- a/README.md +++ b/README.md @@ -143,7 +143,7 @@ $money->decimals(); // 100.0 ### Formatting money -You can format money using the `->formatted()` method: +You can format money using the `->formatted()` method. It takes [display decimals](#display-decimals) into consideration. ```php $money = Money::fromDecimal(40.25, USD::class); @@ -166,6 +166,22 @@ $money = Money::fromDecimal(40.25, USD::class); $money->formatted(['decimalSeparator' => ',', 'prefix' => '$ ', 'suffix' => ' USD']); ``` +There is also a `->rawFormatted()` if you wish to use [math decimals](#math-decimals) instead of [display decimals](#display-decimals). +```php +$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: +```php +$money = money(1000); +$formatted = $money->formatted(); // $10.00 +$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). + ### Rounding money Some currencies, such as the Czech Crown (CZK), generally display final prices in full crowns, but use cents for the intermediate math operations. For example: @@ -414,7 +430,7 @@ For the Czech Crown (CZK), the display decimals will be `0`, but the math decima For the inverse of what was just explained above, you can use the `rawFormatted()` method. This returns the formatted value, **but uses the math decimals for the display decimals**. Meaning, the value in the example above will be displayed including cents: ```php -money(123456, new CZK)->rawFormatted(); // 1 235,56 Kč +money(123456, new CZK)->rawFormatted(); // 1 234,56 Kč ``` This is mostly useful for currencies like the Czech Crown which generally don't use cents, but **can** use them in specific cases. diff --git a/src/Money.php b/src/Money.php index 886fdcd..9eb22ab 100644 --- a/src/Money.php +++ b/src/Money.php @@ -171,6 +171,14 @@ 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 + { + $decimal = PriceFormatter::resolve($formatted, currency($currency), variadic_array($overrides)); + + return static::fromDecimal($decimal, currency($currency)); + } + /** Get the string representation of the Money instance. */ public function __toString(): string { diff --git a/src/PriceFormatter.php b/src/PriceFormatter.php index 28fcd23..de243cc 100644 --- a/src/PriceFormatter.php +++ b/src/PriceFormatter.php @@ -22,4 +22,19 @@ class PriceFormatter return $currency->prefix() . $decimal . $currency->suffix(); } + + /** Extract the decimal from the formatter string as per the currency's specifications. */ + public static function resolve(string $formatted, Currency $currency, array $overrides = []): float + { + $currency = Currency::fromArray( + array_merge(currency($currency)->toArray(), $overrides) + ); + + $formatted = ltrim($formatted, $currency->prefix()); + $formatted = rtrim($formatted, $currency->suffix()); + + $removeNonDigits = preg_replace('/[^\d'.preg_quote($currency->decimalSeparator()).']/', '', $formatted); + + return (float) str_replace($currency->decimalSeparator(), '.', $removeNonDigits); + } } diff --git a/tests/Pest/MoneyTest.php b/tests/Pest/MoneyTest.php index a860ee6..4ab046a 100644 --- a/tests/Pest/MoneyTest.php +++ b/tests/Pest/MoneyTest.php @@ -147,6 +147,18 @@ test('money can be formatted without rounding', function () { )->toBe('10,34 Kč'); }); +test('money can be created from a formatted string', function () { + $money = Money::fromFormatted('$10.40'); + expect($money->value())->toBe(1040); +}); + +test('money can be created from a raw formatted string', function () { + currencies()->add([CZK::class]); + + $money = Money::fromFormatted('1 234,56 Kč', CZK::class); + expect($money->value())->toBe(123456); +}); + test('converting money to a string returns the formatted string', function () { expect( (string) Money::fromDecimal(10.00, USD::class)