1
0
Fork 0
mirror of https://github.com/archtechx/money.git synced 2025-12-12 11:24:03 +00:00

Merge pull request #16 from gauravmak/from_string_implementation

Money instance creation from a formatted string
This commit is contained in:
Samuel Štancl 2022-04-05 15:26:05 +02:00 committed by GitHub
commit 545610efb0
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 140 additions and 6 deletions

View file

@ -143,7 +143,7 @@ $money->decimals(); // 100.0
### Formatting money ### 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 ```php
$money = Money::fromDecimal(40.25, USD::class); $money = Money::fromDecimal(40.25, USD::class);
@ -166,6 +166,33 @@ $money = Money::fromDecimal(40.25, USD::class);
$money->formatted(['decimalSeparator' => ',', 'prefix' => '$ ', 'suffix' => ' USD']); $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 ### 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: 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: 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 ```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. This is mostly useful for currencies like the Czech Crown which generally don't use cents, but **can** use them in specific cases.

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

View file

@ -36,7 +36,7 @@ final class Money implements JsonSerializable, Arrayable, Wireable
public static function fromDecimal(float $decimal, Currency|string $currency = null): self public static function fromDecimal(float $decimal, Currency|string $currency = null): self
{ {
return new static( return new static(
(int) round($decimal * 10 ** currency($currency)->mathDecimals()), (int) round($decimal * pow(10, currency($currency)->mathDecimals())),
currency($currency) currency($currency)
); );
} }
@ -154,7 +154,7 @@ final class Money implements JsonSerializable, Arrayable, Wireable
/** Get the decimal representation of the value. */ /** Get the decimal representation of the value. */
public function decimal(): float public function decimal(): float
{ {
return $this->value / 10 ** $this->currency->mathDecimals(); return $this->value / pow(10, $this->currency->mathDecimals());
} }
/** Format the value. */ /** 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. */ /** Get the string representation of the Money instance. */
public function __toString(): string public function __toString(): string
{ {
@ -228,7 +246,7 @@ final class Money implements JsonSerializable, Arrayable, Wireable
return $this return $this
->divideBy($this->currency->rate()) ->divideBy($this->currency->rate())
->divideBy(10 ** $mathDecimalDifference) ->divideBy(pow(10, $mathDecimalDifference))
->value(); ->value();
} }
@ -240,7 +258,7 @@ final class Money implements JsonSerializable, Arrayable, Wireable
$mathDecimalDifference = $newCurrency->mathDecimals() - currencies()->getDefault()->mathDecimals(); $mathDecimalDifference = $newCurrency->mathDecimals() - currencies()->getDefault()->mathDecimals();
return new static( return new static(
(int) round($this->valueInDefaultCurrency() * $newCurrency->rate() * 10 ** $mathDecimalDifference, 0), (int) round($this->valueInDefaultCurrency() * $newCurrency->rate() * pow(10, $mathDecimalDifference), 0),
$currency $currency
); );
} }

View file

@ -4,6 +4,9 @@ declare(strict_types=1);
namespace ArchTech\Money; namespace ArchTech\Money;
use ArchTech\Money\Exceptions\CannotExtractCurrencyException;
use Exception;
class PriceFormatter class PriceFormatter
{ {
/** Format a decimal per the currency's specifications. */ /** Format a decimal per the currency's specifications. */
@ -22,4 +25,44 @@ class PriceFormatter
return $currency->prefix() . $decimal . $currency->suffix(); 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'.");
}
} }

View file

@ -1,6 +1,7 @@
<?php <?php
use ArchTech\Money\Currencies\USD; use ArchTech\Money\Currencies\USD;
use ArchTech\Money\Exceptions\CannotExtractCurrencyException;
use ArchTech\Money\Money; use ArchTech\Money\Money;
use ArchTech\Money\Tests\Currencies\CZK; use ArchTech\Money\Tests\Currencies\CZK;
use ArchTech\Money\Tests\Currencies\EUR; use ArchTech\Money\Tests\Currencies\EUR;
@ -147,6 +148,36 @@ test('money can be formatted without rounding', function () {
)->toBe('10,34 Kč'); )->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 () { test('converting money to a string returns the formatted string', function () {
expect( expect(
(string) Money::fromDecimal(10.00, USD::class) (string) Money::fromDecimal(10.00, USD::class)