diff --git a/README.md b/README.md index 3f3a1c2..683f3a5 100644 --- a/README.md +++ b/README.md @@ -137,7 +137,15 @@ MoneyColumn::make('price') ->locale('sv_SE'), MoneyColumn::make('price') - ->short(), // Short fromat, e.g. $1.23M instead of $1,234,567.89 + ->short(), // Short format, e.g. $1.23M instead of $1,234,567.89 + +MoneyColumn::make('price') + ->decimals(4) + ->short(), // $1.2345M + +MoneyColumn::make('price') + ->decimals(-3) // 3 significant digits + ->short(), // $1.23K or $23.1M ``` ### InfoList diff --git a/src/FilamentMoneyFieldServiceProvider.php b/src/FilamentMoneyFieldServiceProvider.php index 6416f7d..611321e 100644 --- a/src/FilamentMoneyFieldServiceProvider.php +++ b/src/FilamentMoneyFieldServiceProvider.php @@ -2,7 +2,6 @@ namespace Pelmered\FilamentMoneyField; -use Illuminate\Database\Schema\Blueprint; use Spatie\LaravelPackageTools\Package; use Spatie\LaravelPackageTools\PackageServiceProvider; diff --git a/src/Forms/Components/MoneyInput.php b/src/Forms/Components/MoneyInput.php index 3540c09..e201785 100644 --- a/src/Forms/Components/MoneyInput.php +++ b/src/Forms/Components/MoneyInput.php @@ -51,7 +51,7 @@ protected function setUp(): void $this->dehydrateStateUsing(function (MoneyInput $component, null|int|string $state): ?string { $currency = $component->getCurrency(); - $state = MoneyFormatter::parseDecimal($state, $currency, $component->getLocale(), $this->getDecimals()); + $state = MoneyFormatter::parseDecimal((string) $state, $currency, $component->getLocale(), $this->getDecimals()); if (! is_numeric($state)) { return null; @@ -114,7 +114,7 @@ public function minValue(mixed $value): static $this->rule( static function (MoneyInput $component) { - return new MinValueRule($component->getMinValue(), $component); + return new MinValueRule((int) $component->getMinValue(), $component); }, static fn (MoneyInput $component): bool => filled($component->getMinValue()) ); @@ -128,7 +128,7 @@ public function maxValue(mixed $value): static $this->rule( static function (MoneyInput $component) { - return new MaxValueRule($component->getMaxValue(), $component); + return new MaxValueRule((int) $component->getMaxValue(), $component); }, static fn (MoneyInput $component): bool => filled($component->getMaxValue()) ); diff --git a/src/MoneyFormatter.php b/src/MoneyFormatter.php index bba8398..1fdaee5 100644 --- a/src/MoneyFormatter.php +++ b/src/MoneyFormatter.php @@ -41,34 +41,58 @@ public static function formatAsDecimal( return static::format($value, $currency, $locale, NumberFormatter::DECIMAL, $decimals); } + public static function numberFormat( + null|int|string $value, + Currency $currency, + string $locale, + int $decimals = 2, + ): string { + if (! is_numeric($value)) { + return ''; + } + $numberFormatter = self::getNumberFormatter($locale, NumberFormatter::DECIMAL, $decimals); + + return (string) $numberFormatter->format((float) $value); // Outputs something like "1.234,56" + } + public static function formatShort( null|int|string $value, Currency $currency, string $locale, int $decimals = 2, + bool $showCurrencySymbol = true ): string { if (! is_numeric($value)) { return ''; } // No need to abbreviate if the value is less than 1000 - if ($value < 1000) { + if ($value < 100000) { return static::format($value, $currency, $locale, $decimals); } - $abbreviated = (string) Number::abbreviate((int) $value); + $abbreviated = (string) Number::abbreviate((int) $value / 100, 0, abs($decimals)); // Split the number and the suffix - preg_match('/^(?[0-9]+)(?[A-Z])$/', $abbreviated, $matches1); + preg_match('/^(?[0-9.]+)(?[A-Z])$/', $abbreviated, $matches1); + /** @var array{number: string, suffix: string} $matches1 */ + $abbreviatedNumber = $matches1['number']; + $suffix = $matches1['suffix']; + + $formattedNumber = static::numberFormat($abbreviatedNumber, $currency, $locale, decimals: $decimals); + + if (! $showCurrencySymbol) { + return $formattedNumber.$suffix; + } // Format the number - $formatted = static::format($matches1['number'], $currency, $locale); + $formattedCurrency = static::format($abbreviatedNumber, $currency, $locale, decimals: $decimals); // Find the formatted number - preg_match('/(?[0-9\.,]+)/', $formatted, $matches2); + preg_match('/(?[0-9\.,\s]+)/', $formattedCurrency, $matches2); + /** @var array{number: string} $matches2 */ - // Insert the suffix back - return substr_replace($formatted, $matches1['suffix'], strpos($formatted, $matches2['number']) + strlen($matches2['number']), 0); + return str_replace($matches2['number'], $formattedNumber.$suffix, $formattedCurrency); } public static function parseDecimal( diff --git a/tests/MoneyColumnTest.php b/tests/MoneyColumnTest.php index a2a9e9a..b49b3ee 100644 --- a/tests/MoneyColumnTest.php +++ b/tests/MoneyColumnTest.php @@ -28,12 +28,14 @@ $column = MoneyColumn::make('price')->short(); expect($column->formatState(250))->toEqual('$2.50'); expect($column->formatState(250056))->toEqual('$2.50K'); - expect($column->formatState(24604231))->toEqual('$0.25M'); + expect($column->formatState(24604231))->toEqual('$246.04K'); + expect($column->formatState(2460523122))->toEqual('$24.61M'); }); it('formats money column state to short format with sek', function () { $column = MoneyColumn::make('price')->currency('SEK')->locale('sv_SE')->short(); expect($column->formatState(651))->toEqual(replaceNonBreakingSpaces('6,51 kr')); expect($column->formatState(235235))->toEqual(replaceNonBreakingSpaces('2,35K kr')); - expect($column->formatState(23523562))->toEqual(replaceNonBreakingSpaces('0,24M kr')); + expect($column->formatState(23523562))->toEqual(replaceNonBreakingSpaces('235,24K kr')); + //expect($column->formatState(23523562))->toEqual(replaceNonBreakingSpaces('235,24K kr')); }); diff --git a/tests/MoneyFormatterTest.php b/tests/MoneyFormatterTest.php index af2c3e7..86c617d 100644 --- a/tests/MoneyFormatterTest.php +++ b/tests/MoneyFormatterTest.php @@ -192,58 +192,156 @@ function provideDecimalDataUsd(): array it('formats tointernational currency symbol as suffix', function () { config(['filament-money-field.intl_currency_symbol' => true]); - self::assertSame( - replaceNonBreakingSpaces('1 000,00 SEK'), - MoneyFormatter::format(100000, new Currency('SEK'), 'sv_SE') - ); + expect(MoneyFormatter::format(100000, new Currency('SEK'), 'sv_SE')) + ->toBe(replaceNonBreakingSpaces('1 000,00 SEK')); }); -it('formats with decimal parameter', function () { - self::assertSame( - replaceNonBreakingSpaces('$1,234.56'), - MoneyFormatter::format(123456, new Currency('USD'), 'en_US') - ); - self::assertSame( - replaceNonBreakingSpaces('$1,235'), - MoneyFormatter::format(123456, new Currency('USD'), 'en_US', decimals: 0) - ); - self::assertSame( - replaceNonBreakingSpaces('$1,000.12'), - MoneyFormatter::format(100012, new Currency('USD'), 'en_US', decimals: 2) - ); - self::assertSame( - replaceNonBreakingSpaces('$1,000.5500'), - MoneyFormatter::format(100055, new Currency('USD'), 'en_US', decimals: 4) - ); - self::assertSame( - replaceNonBreakingSpaces('$1,200'), - MoneyFormatter::format(123456, new Currency('USD'), 'en_US', decimals: -2) - ); - self::assertSame( - replaceNonBreakingSpaces('$123,500'), - MoneyFormatter::format(12345678, new Currency('USD'), 'en_US', decimals: -4) - ); -}); +it('formats with decimal parameter', function ($amount, $decimals, $expected) { + expect(MoneyFormatter::format($amount, new Currency('USD'), 'en_US', decimals: $decimals)) + ->toBe(replaceNonBreakingSpaces($expected)); +})->with([ + [123456, 2, '$1,234.56'], + [123456, 0, '$1,235'], + [100012, 2, '$1,000.12'], + [100055, 4, '$1,000.5500'], + [123456, -2, '$1,200'], + [12345678, -4, '$123,500'], +]); -it('formats with decimal parameter in sek', function () { - self::assertSame( - replaceNonBreakingSpaces('1 001 kr'), - MoneyFormatter::format(100060, new Currency('SEK'), 'sv_SE', decimals: 0) - ); - self::assertSame( - replaceNonBreakingSpaces('1 000,12 kr'), - MoneyFormatter::format(100012, new Currency('SEK'), 'sv_SE', decimals: 2) - ); - self::assertSame( - replaceNonBreakingSpaces('1 000,5500 kr'), - MoneyFormatter::format(100055, new Currency('SEK'), 'sv_SE', decimals: 4) - ); - self::assertSame( - replaceNonBreakingSpaces('1 200 kr'), - MoneyFormatter::format(123456, new Currency('SEK'), 'sv_SE', decimals: -2) - ); - self::assertSame( - replaceNonBreakingSpaces('123 500 kr'), - MoneyFormatter::format(12345678, new Currency('SEK'), 'sv_SE', decimals: -4) - ); -}); +it('formats with decimal parameter in sek', function ($amount, $decimals, $expected) { + + expect(MoneyFormatter::format($amount, new Currency('SEK'), 'sv_SE', decimals: $decimals)) + ->toBe(replaceNonBreakingSpaces($expected)); + +})->with([ + [100060, 0, '1 001 kr'], + [100012, 2, '1 000,12 kr'], + [100055, 4, '1 000,5500 kr'], + [123456, -2, '1 200 kr'], + [12345678, -4, '123 500 kr'], +]); + +it('formats to short format', function (mixed $input, string $expectedOutput) { + expect(MoneyFormatter::formatShort($input, new Currency('USD'), 'en_US')) + ->toBe(replaceNonBreakingSpaces($expectedOutput)); +})->with([ + 'invalid' => [ + 'invalid', + '', + ], + 'small 1' => [ + 123, + '$1.23', + ], + 'small 2' => [ + 12300, + '$123.00', + ], + 'thousands' => [ + 123456, + '$1.23K', + ], + 'millions' => [ + 1234567890, + '$12.35M', + ], + 'billions' => [ + 100000000, + '$1.00M', + ], +]); + +it('formats to short format with decimals', function (mixed $input, int $decimals, string $expectedOutput) { + expect(MoneyFormatter::formatShort($input, new Currency('USD'), 'en_US', decimals: $decimals)) + ->toBe(replaceNonBreakingSpaces($expectedOutput)); +})->with([ + 'thousands with 0 decimals' => [ + 123456, + 0, + '$1K', + ], + 'thousands with 2 decimals' => [ + 123456, + 2, + '$1.23K', + ], + 'thousands with 4 decimals' => [ + 123456, + 4, + '$1.2346K', + ], + 'thousands with -2 decimals' => [ + 123456, + -2, + '$1.2K', + ], + 'thousands with -4 decimals' => [ + 123456, + -4, + '$1.235K', + ], + 'millions' => [ + 1234567890, + 2, + '$12.35M', + ], + 'billions' => [ + 100000000, + 2, + '$1.00M', + ], +]); + +it('formats to short format with SEK', function (mixed $input, string $expectedOutput) { + expect(MoneyFormatter::formatShort($input, new Currency('SEK'), 'sv_SE')) + ->toBe(replaceNonBreakingSpaces($expectedOutput)); +})->with([ + 'thousands' => [ + 123456, + '1,23K kr', + ], + 'millions' => [ + 1234567890, + '12,35M kr', + ], + 'billions' => [ + 100100000, + '1,00M kr', + ], +]); + +it('formats to short format with USD and hidden currency symbol', function (mixed $input, string $expectedOutput) { + expect(MoneyFormatter::formatShort($input, new Currency('USD'), 'en_US', showCurrencySymbol: false)) + ->toBe(replaceNonBreakingSpaces($expectedOutput)); +})->with([ + 'thousands' => [ + 123456, + '1.23K', + ], + 'millions' => [ + 1234567890, + '12.35M', + ], + 'billions' => [ + 100000000, + '1.00M', + ], +]); + +it('formats to short format with SEK and hidden currency symbol', function (mixed $input, string $expectedOutput) { + expect(MoneyFormatter::formatShort($input, new Currency('SEK'), 'sv_SE', showCurrencySymbol: false)) + ->toBe(replaceNonBreakingSpaces($expectedOutput)); +})->with([ + 'thousands' => [ + 123456, + '1,23K', + ], + 'millions' => [ + 1234567890, + '12,35M', + ], + 'billions' => [ + 100000000, + '1,00M', + ], +]); diff --git a/tests/ValidationRulesTest.php b/tests/ValidationRulesTest.php index 9f3aa5c..5ed5f03 100644 --- a/tests/ValidationRulesTest.php +++ b/tests/ValidationRulesTest.php @@ -24,7 +24,7 @@ 'same value' => [ ['total' => 100], ['total' => new MinValueRule(10000, new MoneyInput('total'))], - true + true, ], 'higher value' => [ ['amount' => 200], @@ -35,17 +35,16 @@ ['total' => 100], ['total' => new MinValueRule(15000, new MoneyInput('total'))], false, - ['total' => ['The Total must be at least 150.00.']] + ['total' => ['The Total must be at least 150.00.']], ], 'invalid value' => [ ['totalAmount' => 'invalid'], ['totalAmount' => new MinValueRule(10000, new MoneyInput('totalAmount'))], false, - ['totalAmount' => ['The Total Amount must be a valid numeric value.']] + ['totalAmount' => ['The Total Amount must be a valid numeric value.']], ], ]); - it('validates max value', function ($data, $rules, bool $expected, $errors = null) { $validator = Validator::make( @@ -63,13 +62,13 @@ 'same value' => [ ['total' => 100], ['total' => new MaxValueRule(10000, new MoneyInput('total'))], - true + true, ], 'higher value' => [ ['amount' => 200], ['amount' => new MaxValueRule(11000, new MoneyInput('amount'))], false, - ['amount' => ['The Amount must be less than or equal to 110.00.']] + ['amount' => ['The Amount must be less than or equal to 110.00.']], ], 'lower value' => [ ['total' => 90], @@ -80,6 +79,6 @@ ['totalAmount' => 'invalid'], ['totalAmount' => new MaxValueRule(10000, new MoneyInput('totalAmount'))], false, - ['totalAmount' => ['The Total Amount must be a valid numeric value.']] + ['totalAmount' => ['The Total Amount must be a valid numeric value.']], ], ]);