mirror of
https://github.com/archtechx/money.git
synced 2025-12-12 03:14:03 +00:00
Merge pull request #16 from gauravmak/from_string_implementation
Money instance creation from a formatted string
This commit is contained in:
commit
545610efb0
5 changed files with 140 additions and 6 deletions
31
README.md
31
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,33 @@ $money = Money::fromDecimal(40.25, USD::class);
|
|||
$money->formatted(['decimalSeparator' => ',', 'prefix' => '$ ', 'suffix' => ' USD']);
|
||||
```
|
||||
|
||||
There's also `->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 234,56 Kč
|
||||
```
|
||||
|
||||
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
|
||||
$fromFormatted = Money::fromFormatted($formatted);
|
||||
$fromFormatted->is($money); // true
|
||||
```
|
||||
|
||||
If you had passed overrides while [formatting the money instance](#formatting-money), the same can passed to this method.
|
||||
```php
|
||||
$money = money(1000);
|
||||
$formatted = $money->formatted(['prefix' => '$ ', 'suffix' => ' USD']); // $ 10.00 USD
|
||||
$fromFormatted = Money::fromFormatted($formatted, USD::class, ['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
|
||||
|
||||
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 +441,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.
|
||||
|
|
|
|||
15
src/Exceptions/CannotExtractCurrencyException.php
Normal file
15
src/Exceptions/CannotExtractCurrencyException.php
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ArchTech\Money\Exceptions;
|
||||
|
||||
use Exception;
|
||||
|
||||
class CannotExtractCurrencyException extends Exception
|
||||
{
|
||||
public function __construct(string $message)
|
||||
{
|
||||
parent::__construct($message);
|
||||
}
|
||||
}
|
||||
|
|
@ -36,7 +36,7 @@ final class Money implements JsonSerializable, Arrayable, Wireable
|
|||
public static function fromDecimal(float $decimal, Currency|string $currency = null): self
|
||||
{
|
||||
return new static(
|
||||
(int) round($decimal * 10 ** currency($currency)->mathDecimals()),
|
||||
(int) round($decimal * pow(10, currency($currency)->mathDecimals())),
|
||||
currency($currency)
|
||||
);
|
||||
}
|
||||
|
|
@ -154,7 +154,7 @@ final class Money implements JsonSerializable, Arrayable, Wireable
|
|||
/** Get the decimal representation of the value. */
|
||||
public function decimal(): float
|
||||
{
|
||||
return $this->value / 10 ** $this->currency->mathDecimals();
|
||||
return $this->value / pow(10, $this->currency->mathDecimals());
|
||||
}
|
||||
|
||||
/** Format the value. */
|
||||
|
|
@ -171,6 +171,24 @@ final class Money implements JsonSerializable, Arrayable, Wireable
|
|||
]));
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a Money instance from a formatted string.
|
||||
*
|
||||
* @param string $formatted The string formatted using the `formatted()` or `rawFormatted()` method.
|
||||
* @param Currency|string|null $currency The currency to use when passing the overrides. If not provided, the currency of the formatted string is used.
|
||||
* @param array ...$overrides The overrides used when formatting the money instance.
|
||||
*/
|
||||
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, variadic_array($overrides));
|
||||
|
||||
return static::fromDecimal($decimal, currency($currency));
|
||||
}
|
||||
|
||||
/** Get the string representation of the Money instance. */
|
||||
public function __toString(): string
|
||||
{
|
||||
|
|
@ -228,7 +246,7 @@ final class Money implements JsonSerializable, Arrayable, Wireable
|
|||
|
||||
return $this
|
||||
->divideBy($this->currency->rate())
|
||||
->divideBy(10 ** $mathDecimalDifference)
|
||||
->divideBy(pow(10, $mathDecimalDifference))
|
||||
->value();
|
||||
}
|
||||
|
||||
|
|
@ -240,7 +258,7 @@ final class Money implements JsonSerializable, Arrayable, Wireable
|
|||
$mathDecimalDifference = $newCurrency->mathDecimals() - currencies()->getDefault()->mathDecimals();
|
||||
|
||||
return new static(
|
||||
(int) round($this->valueInDefaultCurrency() * $newCurrency->rate() * 10 ** $mathDecimalDifference, 0),
|
||||
(int) round($this->valueInDefaultCurrency() * $newCurrency->rate() * pow(10, $mathDecimalDifference), 0),
|
||||
$currency
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,6 +4,9 @@ declare(strict_types=1);
|
|||
|
||||
namespace ArchTech\Money;
|
||||
|
||||
use ArchTech\Money\Exceptions\CannotExtractCurrencyException;
|
||||
use Exception;
|
||||
|
||||
class PriceFormatter
|
||||
{
|
||||
/** Format a decimal per the currency's specifications. */
|
||||
|
|
@ -22,4 +25,44 @@ class PriceFormatter
|
|||
|
||||
return $currency->prefix() . $decimal . $currency->suffix();
|
||||
}
|
||||
|
||||
/** 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(
|
||||
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);
|
||||
|
||||
if (! is_string($removeNonDigits)) {
|
||||
throw new Exception('The formatted string could not be resolved to a valid number.');
|
||||
}
|
||||
|
||||
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 CannotExtractCurrencyException("Multiple currencies are using the same prefix and suffix as '$formatted'. Please specify the currency of the formatted string.");
|
||||
}
|
||||
|
||||
$possibleCurrency = $currency;
|
||||
}
|
||||
}
|
||||
|
||||
return $possibleCurrency ?? throw new CannotExtractCurrencyException("None of the currencies are using the prefix and suffix that would match with the formatted string '$formatted'.");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
<?php
|
||||
|
||||
use ArchTech\Money\Currencies\USD;
|
||||
use ArchTech\Money\Exceptions\CannotExtractCurrencyException;
|
||||
use ArchTech\Money\Money;
|
||||
use ArchTech\Money\Tests\Currencies\CZK;
|
||||
use ArchTech\Money\Tests\Currencies\EUR;
|
||||
|
|
@ -147,6 +148,36 @@ 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('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(CannotExtractCurrencyException::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(CannotExtractCurrencyException::class);
|
||||
Money::fromFormatted($money->formatted());
|
||||
});
|
||||
|
||||
test('converting money to a string returns the formatted string', function () {
|
||||
expect(
|
||||
(string) Money::fromDecimal(10.00, USD::class)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue