Skip to content

Commit

Permalink
Merge pull request #39 from pelmered/feature/decimals-support
Browse files Browse the repository at this point in the history
Add support for decimals and significant digits
  • Loading branch information
pelmered authored Jun 3, 2024
2 parents 71e8bc6 + 5d81fd3 commit d290585
Show file tree
Hide file tree
Showing 7 changed files with 236 additions and 20 deletions.
33 changes: 31 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ MONEY_DEFAULT_CURRENCY=SEK
php artisan vendor:publish --provider="Pelmered\FilamentMoneyField\FilamentMoneyFieldServiceProvider" --tag="config"
```

## Additional Configuration
## Global Configuration

### If you want to use the formatting mask on the `MoneyInput` component

Expand All @@ -90,6 +90,33 @@ Possible options: `after`, `before`, `none`.
MONEY_UNIT_PLACEMENT=after // Defaults to before
```

### Decimals and significant digits

The number of decimals and significant digits can be set in the config file. Defaults to 2.

```env
//with input 123456
MONEY_DECIMAL_DIGITS=0 // Gives 0 decimals, e.g. $1,235
MONEY_DECIMAL_DIGITS=2 // Gives 2 decimals, e.g. $1,234.56
```

For significant digits, use negative values. For example -2 will give you 2 significant digits.

```env
//with input 12345678
MONEY_DECIMAL_DIGITS=-2 // Gives 2 significant digits, e.g. $120,000
MONEY_DECIMAL_DIGITS=-4 // Gives 4 significant digits, e.g. $123,400
```

This can also be set on a per-field basis.

```php
MoneyInput::make('price')->decimalDigits(0);
MoneyEntry::make('price')->decimalDigits(2);
MoneyColumn::make('price')->decimalDigits(-2);
```


## Usage

### InfoList
Expand Down Expand Up @@ -131,7 +158,9 @@ MoneyInput::make('price')
->currency('SEK')
->locale('sv_SE')
->minValue(0) // Do not allow negative values.
->maxValue(10000); // Add min and max value (in minor units, i.e. cents) to the input field. In this case no values over 100
->maxValue(10000) // Add min and max value (in minor units, i.e. cents) to the input field. In this case no values over 100
->step(100) // Step value for the input field. In this case only multiples of 100 are allowed.
->decimals(0)
```

### Table column
Expand Down
2 changes: 1 addition & 1 deletion config/filament-money-field.php
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@
| The currency code to use if not set on the field.
|
*/
'fraction_digits' => env('MONEY_FRACTION_DIGITS', 2),
'decimal_digits' => env('MONEY_DECIMAL_DIGITS', 2),

/*
|---------------------------------------------------------------------------
Expand Down
4 changes: 2 additions & 2 deletions src/Forms/Components/MoneyInput.php
Original file line number Diff line number Diff line change
Expand Up @@ -35,12 +35,12 @@ protected function setUp(): void
return $state;
}

return MoneyFormatter::formatAsDecimal((int) $state, $currency, $locale);
return MoneyFormatter::formatAsDecimal((int) $state, $currency, $locale, $this->decimals);
});

$this->dehydrateStateUsing(function (MoneyInput $component, $state): ?string {
$currency = $component->getCurrency();
$state = MoneyFormatter::parseDecimal($state, $currency, $component->getLocale());
$state = MoneyFormatter::parseDecimal($state, $currency, $component->getLocale(), $this->decimals);

if (! is_numeric($state)) {
return null;
Expand Down
10 changes: 10 additions & 0 deletions src/HasMoneyAttributes.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ trait HasMoneyAttributes

protected string $locale;

protected ?int $decimals = null;

protected ?string $monetarySeparator = null;

public function getCurrency(): Currency
Expand Down Expand Up @@ -54,5 +56,13 @@ public function locale(string|Closure|null $locale = null): static
return $this;
}

public function decimals(int|Closure $decimals): static
{
$this->decimals = $this->evaluate($decimals);

return $this;
}

// This should typically be provided by the Filament\Support\Concerns\EvaluatesClosures trait in Filament
abstract protected function evaluate(string|Closure|null $value): mixed;
}
50 changes: 38 additions & 12 deletions src/MoneyFormatter.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,35 +16,45 @@ public static function format(
null|int|string $value,
Currency $currency,
string $locale,
int $outputStyle = NumberFormatter::CURRENCY
int $outputStyle = NumberFormatter::CURRENCY,
?int $decimals = null,
): string {
if ($value === '' || ! is_numeric($value)) {
return '';
}

$numberFormatter = self::getNumberFormatter($locale, $outputStyle);
$numberFormatter = self::getNumberFormatter($locale, $outputStyle, $decimals);
$moneyFormatter = new IntlMoneyFormatter($numberFormatter, new ISOCurrencies());

$money = new Money((int) $value, $currency);

return $moneyFormatter->format($money); // outputs $1.000,00
return $moneyFormatter->format($money); // Outputs something like "$1.234,56"
}

public static function formatAsDecimal(null|int|string $value, Currency $currency, string $locale): string
{
return static::format($value, $currency, $locale, NumberFormatter::DECIMAL); // outputs 1.000,00
public static function formatAsDecimal(
null|int|string $value,
Currency $currency,
string $locale,
?int $decimals = null,
): string {
return static::format($value, $currency, $locale, NumberFormatter::DECIMAL, $decimals);
}

public static function parseDecimal(?string $moneyString, Currency $currency, string $locale): string
{
public static function parseDecimal(
?string $moneyString,
Currency $currency,
string $locale,
?int $decimals = null
): string {
if (is_null($moneyString) || $moneyString === '') {
return '';
}

$numberFormatter = self::getNumberFormatter($locale, NumberFormatter::DECIMAL);
$numberFormatter = self::getNumberFormatter($locale, NumberFormatter::DECIMAL, $decimals);
$moneyParser = new IntlLocalizedDecimalParser($numberFormatter, new ISOCurrencies());

// Needed to fix some parsing issues with small numbers such as
// Remove grouping separator from the money string
// This is needed to fix some parsing issues with small numbers such as
// "2,00" with "," left as thousands separator in the wrong place
// See: https://github.com/pelmered/filament-money-field/issues/20
$formattingRules = self::getFormattingRules($locale);
Expand Down Expand Up @@ -74,12 +84,19 @@ public static function getFormattingRules(string $locale): MoneyFormattingRules
);
}

private static function getNumberFormatter(string $locale, int $style): NumberFormatter
private static function getNumberFormatter(string $locale, int $style, ?int $decimals = null): NumberFormatter
{
$config = config('filament-money-field');

$numberFormatter = new NumberFormatter($locale, $style);
$numberFormatter->setAttribute(NumberFormatter::FRACTION_DIGITS, $config['fraction_digits']);

$decimals = self::getDecimals($decimals);

if ($decimals < 0) {
$numberFormatter->setAttribute(NumberFormatter::MAX_SIGNIFICANT_DIGITS, abs($decimals));
} else {
$numberFormatter->setAttribute(NumberFormatter::FRACTION_DIGITS, $decimals);
}

if ($config['intl_currency_symbol']) {
$intlCurrencySymbol = $numberFormatter->getSymbol(NumberFormatter::INTL_CURRENCY_SYMBOL);
Expand All @@ -95,4 +112,13 @@ private static function getNumberFormatter(string $locale, int $style): NumberFo

return $numberFormatter;
}

private static function getDecimals(?int $decimals = null): int
{
if (! is_null($decimals)) {
return $decimals;
}

return (int) config('filament-money-field.decimal_digits', 2);
}
}
27 changes: 27 additions & 0 deletions tests/FormInputTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -200,4 +200,31 @@ public function testResolveLabelClosures(): void
$field = $component->getComponent('data.price');
$this->assertEquals('Custom Label in Closure', $field->getLabel());
}

public function testSetDecimalsOnField(): void
{
$field = (new MoneyInput('price'))->decimals(1);
$component = ComponentContainer::make(FormTestComponent::make())
->statePath('data')
->components([
$field,
])->fill([$field->getName() => 2345345]);
$this->assertEquals('2345345', $component->getState()['price']);

$field = (new MoneyInput('price'))->decimals(3);
$component = ComponentContainer::make(FormTestComponent::make())
->statePath('data')
->components([
$field,
])->fill([$field->getName() => 2345345]);
$this->assertEquals('2345345', $component->getState()['price']);

$field = (new MoneyInput('price'))->decimals(-2);
$component = ComponentContainer::make(FormTestComponent::make())
->statePath('data')
->components([
$field,
])->fill([$field->getName() => 2345345]);
$this->assertEquals('2345345', $component->getState()['price']);
}
}
130 changes: 127 additions & 3 deletions tests/MoneyFormatterTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -239,7 +239,7 @@ public function testMoneyParserDecimal(): void
);
}

public function testInternationalCurrencySymbol()
public function testInternationalCurrencySymbol(): void
{
config(['filament-money-field.intl_currency_symbol' => true]);

Expand All @@ -249,13 +249,137 @@ public function testInternationalCurrencySymbol()
);
}

public function testInternationalCurrencySymbolSuffix()
public function testInternationalCurrencySymbolSuffix(): void
{
config(['filament-money-field.intl_currency_symbol' => true]);

self::assertSame(
self::replaceNonBreakingSpaces('1 000,00 SEK'),
MoneyFormatter::format(100000, new Currency('EUR'), 'sv_SE')
MoneyFormatter::format(100000, new Currency('SEK'), 'sv_SE')
);
}

public function testGlobalDecimals(): void
{
config(['filament-money-field.decimal_digits' => null]);
self::assertSame(
self::replaceNonBreakingSpaces('$1,000'),
MoneyFormatter::format(100020, new Currency('USD'), 'en_US')
);

config(['filament-money-field.decimal_digits' => 0]);
self::assertSame(
self::replaceNonBreakingSpaces('$1,000'),
MoneyFormatter::format(100020, new Currency('USD'), 'en_US')
);

config(['filament-money-field.decimal_digits' => 2]);
self::assertSame(
self::replaceNonBreakingSpaces('$1,000.11'),
MoneyFormatter::format(100011, new Currency('USD'), 'en_US')
);

config(['filament-money-field.decimal_digits' => 4]);
self::assertSame(
self::replaceNonBreakingSpaces('$1,000.7700'),
MoneyFormatter::format(100077, new Currency('USD'), 'en_US')
);

config(['filament-money-field.decimal_digits' => -2]);
self::assertSame(
self::replaceNonBreakingSpaces('$120,000'),
MoneyFormatter::format(12345678, new Currency('USD'), 'en_US')
);

config(['filament-money-field.decimal_digits' => -4]);
self::assertSame(
self::replaceNonBreakingSpaces('$1,235,000'),
MoneyFormatter::format(123456789, new Currency('USD'), 'en_US')
);
}

public function testGlobalDecimalsSek(): void
{
config(['filament-money-field.decimal_digits' => 0]);
self::assertSame(
self::replaceNonBreakingSpaces('1 000 kr'),
MoneyFormatter::format(100020, new Currency('SEK'), 'sv_SE')
);

config(['filament-money-field.decimal_digits' => 2]);
self::assertSame(
self::replaceNonBreakingSpaces('1 000,11 kr'),
MoneyFormatter::format(100011, new Currency('SEK'), 'sv_SE')
);

config(['filament-money-field.decimal_digits' => 4]);
self::assertSame(
self::replaceNonBreakingSpaces('1 000,7700 kr'),
MoneyFormatter::format(100077, new Currency('SEK'), 'sv_SE')
);

config(['filament-money-field.decimal_digits' => -2]);
self::assertSame(
self::replaceNonBreakingSpaces('120 000 kr'),
MoneyFormatter::format(12345678, new Currency('SEK'), 'sv_SE')
);

config(['filament-money-field.decimal_digits' => -4]);
self::assertSame(
self::replaceNonBreakingSpaces('1 235 000 kr'),
MoneyFormatter::format(123456789, new Currency('SEK'), 'sv_SE')
);
}

public function testDecimalsAsParameter(): void
{
self::assertSame(
self::replaceNonBreakingSpaces('$1,234.56'),
MoneyFormatter::format(123456, new Currency('USD'), 'en_US')
);
self::assertSame(
self::replaceNonBreakingSpaces('$1,235'),
MoneyFormatter::format(123456, new Currency('USD'), 'en_US', decimals: 0)
);
self::assertSame(
self::replaceNonBreakingSpaces('$1,000.12'),
MoneyFormatter::format(100012, new Currency('USD'), 'en_US', decimals: 2)
);
self::assertSame(
self::replaceNonBreakingSpaces('$1,000.5500'),
MoneyFormatter::format(100055, new Currency('USD'), 'en_US', decimals: 4)
);
self::assertSame(
self::replaceNonBreakingSpaces('$1,200'),
MoneyFormatter::format(123456, new Currency('USD'), 'en_US', decimals: -2)
);
self::assertSame(
self::replaceNonBreakingSpaces('$123,500'),
MoneyFormatter::format(12345678, new Currency('USD'), 'en_US', decimals: -4)
);
}

public function testDecimalsAsParameterSek(): void
{
self::assertSame(
self::replaceNonBreakingSpaces('1 001 kr'),
MoneyFormatter::format(100060, new Currency('SEK'), 'sv_SE', decimals: 0)
);
self::assertSame(
self::replaceNonBreakingSpaces('1 000,12 kr'),
MoneyFormatter::format(100012, new Currency('SEK'), 'sv_SE', decimals: 2)
);
self::assertSame(
self::replaceNonBreakingSpaces('1 000,5500 kr'),
MoneyFormatter::format(100055, new Currency('SEK'), 'sv_SE', decimals: 4)
);
self::assertSame(
self::replaceNonBreakingSpaces('1 200 kr'),
MoneyFormatter::format(123456, new Currency('SEK'), 'sv_SE', decimals: -2)
);
self::assertSame(
self::replaceNonBreakingSpaces('123 500 kr'),
MoneyFormatter::format(12345678, new Currency('SEK'), 'sv_SE', decimals: -4)
);
}
}

0 comments on commit d290585

Please sign in to comment.