Skip to content

Commit

Permalink
Merge pull request #77 from pelmered/feature/improve-short-formatter
Browse files Browse the repository at this point in the history
Improve short formatter
  • Loading branch information
pelmered authored Dec 5, 2024
2 parents 0ab4bc4 + 71de314 commit 1ea3b62
Show file tree
Hide file tree
Showing 7 changed files with 203 additions and 73 deletions.
10 changes: 9 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 0 additions & 1 deletion src/FilamentMoneyFieldServiceProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

namespace Pelmered\FilamentMoneyField;

use Illuminate\Database\Schema\Blueprint;
use Spatie\LaravelPackageTools\Package;
use Spatie\LaravelPackageTools\PackageServiceProvider;

Expand Down
6 changes: 3 additions & 3 deletions src/Forms/Components/MoneyInput.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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())
);
Expand All @@ -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())
);
Expand Down
38 changes: 31 additions & 7 deletions src/MoneyFormatter.php
Original file line number Diff line number Diff line change
Expand Up @@ -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 '';

Check notice on line 51 in src/MoneyFormatter.php

View check run for this annotation

OtterWise Otto / Code Coverage

src/MoneyFormatter.php:51

Line 51 is not covered by tests.
}
$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('/^(?<number>[0-9]+)(?<suffix>[A-Z])$/', $abbreviated, $matches1);
preg_match('/^(?<number>[0-9.]+)(?<suffix>[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('/(?<number>[0-9\.,]+)/', $formatted, $matches2);
preg_match('/(?<number>[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(
Expand Down
6 changes: 4 additions & 2 deletions tests/MoneyColumnTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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'));
});
202 changes: 150 additions & 52 deletions tests/MoneyFormatterTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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',
],
]);
13 changes: 6 additions & 7 deletions tests/ValidationRulesTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
'same value' => [
['total' => 100],
['total' => new MinValueRule(10000, new MoneyInput('total'))],
true
true,
],
'higher value' => [
['amount' => 200],
Expand All @@ -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(
Expand All @@ -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],
Expand All @@ -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.']],
],
]);

0 comments on commit 1ea3b62

Please sign in to comment.