Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve short formatter #77

Merged
merged 5 commits into from
Dec 5, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@
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.']],
],
]);
Loading