diff --git a/README.md b/README.md index 78f821a..b511d93 100644 --- a/README.md +++ b/README.md @@ -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 @@ -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 @@ -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 diff --git a/config/filament-money-field.php b/config/filament-money-field.php index ad9d7bd..ac74b37 100644 --- a/config/filament-money-field.php +++ b/config/filament-money-field.php @@ -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), /* |--------------------------------------------------------------------------- diff --git a/src/Forms/Components/MoneyInput.php b/src/Forms/Components/MoneyInput.php index da70d1c..7a27214 100644 --- a/src/Forms/Components/MoneyInput.php +++ b/src/Forms/Components/MoneyInput.php @@ -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; diff --git a/src/HasMoneyAttributes.php b/src/HasMoneyAttributes.php index dae7d4e..238f33a 100644 --- a/src/HasMoneyAttributes.php +++ b/src/HasMoneyAttributes.php @@ -14,6 +14,8 @@ trait HasMoneyAttributes protected string $locale; + protected ?int $decimals = null; + protected ?string $monetarySeparator = null; public function getCurrency(): Currency @@ -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; } diff --git a/src/MoneyFormatter.php b/src/MoneyFormatter.php index 7a13a12..4282f13 100644 --- a/src/MoneyFormatter.php +++ b/src/MoneyFormatter.php @@ -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); @@ -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); @@ -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); + } } diff --git a/tests/FormInputTest.php b/tests/FormInputTest.php index a94c30f..59865df 100644 --- a/tests/FormInputTest.php +++ b/tests/FormInputTest.php @@ -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']); + } } diff --git a/tests/MoneyFormatterTest.php b/tests/MoneyFormatterTest.php index 1dcf50f..8a40a1f 100644 --- a/tests/MoneyFormatterTest.php +++ b/tests/MoneyFormatterTest.php @@ -239,7 +239,7 @@ public function testMoneyParserDecimal(): void ); } - public function testInternationalCurrencySymbol() + public function testInternationalCurrencySymbol(): void { config(['filament-money-field.intl_currency_symbol' => true]); @@ -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) ); } }