Skip to content

Commit

Permalink
feature #5902 Add new options to configure the decimal and thousands …
Browse files Browse the repository at this point in the history
…separators (javiereguiluz)

This PR was squashed before being merged into the 4.x branch.

Discussion
----------

Add new options to configure the decimal and thousands separators

Commits
-------

464ff0d Add new options to configure the decimal and thousands separators
  • Loading branch information
javiereguiluz committed Sep 5, 2023
2 parents 68442ac + 464ff0d commit 90e24f3
Show file tree
Hide file tree
Showing 12 changed files with 271 additions and 17 deletions.
14 changes: 14 additions & 0 deletions doc/crud.rst
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,20 @@ Date, Time and Number Formatting Options
// NumberField and IntegerField can override this value with their
// own setNumberFormat() methods, which works in the same way
->setNumberFormat('%.2d');

// Sets the character used to separate each thousand group in a number
// e.g. if separator is ',' then 12345 is formatted as 12,345
// By default, EasyAdmin doesn't add any thousands separator to numbers;
// NumberField and IntegerField can override this value with their
// own setThousandsSeparator() methods, which works in the same way
->setThousandsSeparator(',')

// Sets the character used to separate the decimal part of a non-integer number
// e.g. if separator is '.' then 1/10 is formatted as 0.1
// by default, EasyAdmin displays the default decimal separator used by PHP;
// NumberField and IntegerField can override this value with their
// own setDecimalSeparator() methods, which works in the same way
->setDecimalSeparator('.')
;
}

Expand Down
10 changes: 10 additions & 0 deletions doc/fields/IntegerField.rst
Original file line number Diff line number Diff line change
Expand Up @@ -37,4 +37,14 @@ argument of the ``sprintf()`` function::
// the following example would format 123 as '+00123'
yield IntegerField::new('...')->setNumberFormat('%+06d');

setThousandsSeparator
~~~~~~~~~~~~~~~~~~~~~

By default, the integer value doesn't separate each thousands group in any way
(e.g. ``12345`` is displayed like that, instead of ``12,345``). Use this option
to set the character to use to separate each thousands group::

// this would display '12345' as '12 345'
yield IntegerField::new('...')->setThousandsSeparator(' ');

.. _`IntegerType`: https://symfony.com/doc/current/reference/forms/types/integer.html
20 changes: 20 additions & 0 deletions doc/fields/NumberField.rst
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,16 @@ Basic Information
Options
-------

setDecimalSeparator
~~~~~~~~~~~~~~~~~~~

The numeric values show the default decimal separator used by PHP (e.g. 1/10 is
shown as ``0.1``). Use this option to set a different character to separate the
decimal part of the number::

// this would display '12345.67' as '12345,67'
yield NumberField::new('...')->setDecimalSeparator(',');

setNumberFormat
~~~~~~~~~~~~~~~

Expand Down Expand Up @@ -71,5 +81,15 @@ default ``<input type="number">`` element::

yield NumberField::new('...')->setStoredAsString();

setThousandsSeparator
~~~~~~~~~~~~~~~~~~~~~

By default, the numeric value doesn't separate each thousands group in any way
(e.g. ``12345.67`` is displayed like that, instead of ``12,345.67``). Use this option
to set the character to use to separate each thousands group::

// this would display '12345.67' as '12 345.67'
yield NumberField::new('...')->setThousandsSeparator(' ');

.. _`NumberType`: https://symfony.com/doc/current/reference/forms/types/number.html
.. _`PHP NumberFormatter class`: https://www.php.net/manual/en/class.numberformatter.php
14 changes: 14 additions & 0 deletions src/Config/Crud.php
Original file line number Diff line number Diff line change
Expand Up @@ -232,6 +232,20 @@ public function setNumberFormat(string $format): self
return $this;
}

public function setThousandsSeparator(string $separator): self
{
$this->dto->setThousandsSeparator($separator);

return $this;
}

public function setDecimalSeparator(string $separator): self
{
$this->dto->setDecimalSeparator($separator);

return $this;
}

/**
* @param array $sortFieldsAndOrder ['fieldName' => 'ASC|DESC', ...]
*/
Expand Down
22 changes: 22 additions & 0 deletions src/Dto/CrudDto.php
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,8 @@ final class CrudDto
private string $dateIntervalFormat = '%%y Year(s) %%m Month(s) %%d Day(s)';
private ?string $timezone = null;
private ?string $numberFormat = null;
private ?string $thousandsSeparator = null;
private ?string $decimalSeparator = null;
private array $defaultSort = [];
private ?array $searchFields = [];
private bool $autofocusSearch = false;
Expand Down Expand Up @@ -302,6 +304,26 @@ public function setNumberFormat(string $numberFormat): void
$this->numberFormat = $numberFormat;
}

public function getThousandsSeparator(): ?string
{
return $this->thousandsSeparator;
}

public function setThousandsSeparator(string $separator): void
{
$this->thousandsSeparator = $separator;
}

public function getDecimalSeparator(): ?string
{
return $this->decimalSeparator;
}

public function setDecimalSeparator(string $separator): void
{
$this->decimalSeparator = $separator;
}

public function getDefaultSort(): array
{
return $this->defaultSort;
Expand Down
17 changes: 12 additions & 5 deletions src/Field/Configurator/IntegerConfigurator.php
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,17 @@ public function configure(FieldDto $field, EntityDto $entityDto, AdminContext $c
return;
}

if (null !== $numberFormat = $field->getCustomOption(NumberField::OPTION_NUMBER_FORMAT)) {
$field->setFormattedValue(sprintf($numberFormat, $value));
} elseif (null !== $numberFormat = $context->getCrud()->getNumberFormat()) {
$field->setFormattedValue(sprintf($numberFormat, $value));
}
$numberFormat = $field->getCustomOption(NumberField::OPTION_NUMBER_FORMAT)
?? $context->getCrud()->getNumberFormat()
?? null;
$thousandsSeparator = $field->getCustomOption(NumberField::OPTION_THOUSANDS_SEPARATOR)
?? $context->getCrud()->getThousandsSeparator()
?? null;

$field->setFormattedValue(match (true) {
null !== $numberFormat => sprintf($numberFormat, $value),
null !== $thousandsSeparator => number_format($value, 0, '.', $thousandsSeparator),
default => $value,
});
}
}
36 changes: 27 additions & 9 deletions src/Field/Configurator/NumberConfigurator.php
Original file line number Diff line number Diff line change
Expand Up @@ -32,26 +32,44 @@ public function configure(FieldDto $field, EntityDto $entityDto, AdminContext $c
return;
}

$scale = $field->getCustomOption(NumberField::OPTION_NUM_DECIMALS);
$numDecimals = $field->getCustomOption(NumberField::OPTION_NUM_DECIMALS);
$roundingMode = $field->getCustomOption(NumberField::OPTION_ROUNDING_MODE);
$isStoredAsString = true === $field->getCustomOption(NumberField::OPTION_STORED_AS_STRING);

$field->setFormTypeOptionIfNotSet('input', $isStoredAsString ? 'string' : 'number');
$field->setFormTypeOptionIfNotSet('rounding_mode', $roundingMode);
$field->setFormTypeOptionIfNotSet('scale', $scale);
$field->setFormTypeOptionIfNotSet('scale', $numDecimals);

$formatterAttributes = ['rounding_mode' => $this->getRoundingModeAsString($roundingMode)];
if (null !== $scale) {
$formatterAttributes['fraction_digit'] = $scale;
if (null !== $numDecimals) {
$formatterAttributes['fraction_digit'] = $numDecimals;
}

if (null !== $numberFormat = $field->getCustomOption(NumberField::OPTION_NUMBER_FORMAT)) {
$field->setFormattedValue(sprintf($numberFormat, $value));
} elseif (null !== $numberFormat = $context->getCrud()->getNumberFormat()) {
$numberFormat = $field->getCustomOption(NumberField::OPTION_NUMBER_FORMAT)
?? $context->getCrud()->getNumberFormat()
?? null;

if (null !== $numberFormat) {
$field->setFormattedValue(sprintf($numberFormat, $value));
} else {
$field->setFormattedValue($this->intlFormatter->formatNumber($value, $formatterAttributes));

return;
}

$thousandsSeparator = $field->getCustomOption(NumberField::OPTION_THOUSANDS_SEPARATOR)
?? $context->getCrud()->getThousandsSeparator()
?? null;
if (null !== $thousandsSeparator) {
$formatterAttributes['grouping_separator'] = $thousandsSeparator;
}

$decimalSeparator = $field->getCustomOption(NumberField::OPTION_DECIMAL_SEPARATOR)
?? $context->getCrud()->getDecimalSeparator()
?? null;
if (null !== $decimalSeparator) {
$formatterAttributes['decimal_separator'] = $decimalSeparator;
}

$field->setFormattedValue($this->intlFormatter->formatNumber($value, $formatterAttributes));
}

private function getRoundingModeAsString(int $mode): string
Expand Down
11 changes: 10 additions & 1 deletion src/Field/IntegerField.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ final class IntegerField implements FieldInterface
use FieldTrait;

public const OPTION_NUMBER_FORMAT = 'numberFormat';
public const OPTION_THOUSANDS_SEPARATOR = 'thousandsSeparator';

/**
* @param TranslatableInterface|string|false|null $label
Expand All @@ -27,7 +28,8 @@ public static function new(string $propertyName, $label = null): self
->setFormType(IntegerType::class)
->addCssClass('field-integer')
->setDefaultColumns('col-md-4 col-xxl-3')
->setCustomOption(self::OPTION_NUMBER_FORMAT, null);
->setCustomOption(self::OPTION_NUMBER_FORMAT, null)
->setCustomOption(self::OPTION_THOUSANDS_SEPARATOR, null);
}

// this format is passed directly to the first argument of `sprintf()` to format the integer before displaying it
Expand All @@ -37,4 +39,11 @@ public function setNumberFormat(string $sprintfFormat): self

return $this;
}

public function setThousandsSeparator(string $separator): self
{
$this->setCustomOption(self::OPTION_THOUSANDS_SEPARATOR, $separator);

return $this;
}
}
20 changes: 19 additions & 1 deletion src/Field/NumberField.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ final class NumberField implements FieldInterface
public const OPTION_ROUNDING_MODE = 'roundingMode';
public const OPTION_STORED_AS_STRING = 'storedAsString';
public const OPTION_NUMBER_FORMAT = 'numberFormat';
public const OPTION_THOUSANDS_SEPARATOR = 'thousandsSeparator';
public const OPTION_DECIMAL_SEPARATOR = 'decimalSeparator';

/**
* @param TranslatableInterface|string|false|null $label
Expand All @@ -33,7 +35,9 @@ public static function new(string $propertyName, $label = null): self
->setCustomOption(self::OPTION_NUM_DECIMALS, null)
->setCustomOption(self::OPTION_ROUNDING_MODE, \NumberFormatter::ROUND_HALFUP)
->setCustomOption(self::OPTION_STORED_AS_STRING, false)
->setCustomOption(self::OPTION_NUMBER_FORMAT, null);
->setCustomOption(self::OPTION_NUMBER_FORMAT, null)
->setCustomOption(self::OPTION_THOUSANDS_SEPARATOR, null)
->setCustomOption(self::OPTION_DECIMAL_SEPARATOR, null);
}

public function setNumDecimals(int $num): self
Expand Down Expand Up @@ -83,4 +87,18 @@ public function setNumberFormat(string $sprintfFormat): self

return $this;
}

public function setThousandsSeparator(string $separator): self
{
$this->setCustomOption(self::OPTION_THOUSANDS_SEPARATOR, $separator);

return $this;
}

public function setDecimalSeparator(string $separator): self
{
$this->setCustomOption(self::OPTION_DECIMAL_SEPARATOR, $separator);

return $this;
}
}
56 changes: 55 additions & 1 deletion src/Intl/IntlFormatter.php
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,36 @@ final class IntlFormatter
'before_suffix' => \NumberFormatter::PAD_BEFORE_SUFFIX,
'after_suffix' => \NumberFormatter::PAD_AFTER_SUFFIX,
];
private const NUMBER_TEXT_ATTRIBUTES = [
'positive_prefix' => \NumberFormatter::POSITIVE_PREFIX,
'positive_suffix' => \NumberFormatter::POSITIVE_SUFFIX,
'negative_prefix' => \NumberFormatter::NEGATIVE_PREFIX,
'negative_suffix' => \NumberFormatter::NEGATIVE_SUFFIX,
'padding_character' => \NumberFormatter::PADDING_CHARACTER,
'currency_code' => \NumberFormatter::CURRENCY_CODE,
'default_ruleset' => \NumberFormatter::DEFAULT_RULESET,
'public_rulesets' => \NumberFormatter::PUBLIC_RULESETS,
];
private const NUMBER_SYMBOLS = [
'decimal_separator' => \NumberFormatter::DECIMAL_SEPARATOR_SYMBOL,
'grouping_separator' => \NumberFormatter::GROUPING_SEPARATOR_SYMBOL,
'pattern_separator' => \NumberFormatter::PATTERN_SEPARATOR_SYMBOL,
'percent' => \NumberFormatter::PERCENT_SYMBOL,
'zero_digit' => \NumberFormatter::ZERO_DIGIT_SYMBOL,
'digit' => \NumberFormatter::DIGIT_SYMBOL,
'minus_sign' => \NumberFormatter::MINUS_SIGN_SYMBOL,
'plus_sign' => \NumberFormatter::PLUS_SIGN_SYMBOL,
'currency' => \NumberFormatter::CURRENCY_SYMBOL,
'intl_currency' => \NumberFormatter::INTL_CURRENCY_SYMBOL,
'monetary_separator' => \NumberFormatter::MONETARY_SEPARATOR_SYMBOL,
'exponential' => \NumberFormatter::EXPONENTIAL_SYMBOL,
'permill' => \NumberFormatter::PERMILL_SYMBOL,
'pad_escape' => \NumberFormatter::PAD_ESCAPE_SYMBOL,
'infinity' => \NumberFormatter::INFINITY_SYMBOL,
'nan' => \NumberFormatter::NAN_SYMBOL,
'significant_digit' => \NumberFormatter::SIGNIFICANT_DIGIT_SYMBOL,
'monetary_grouping_separator' => \NumberFormatter::MONETARY_GROUPING_SEPARATOR_SYMBOL,
];

private array $dateFormatters = [];
private array $numberFormatters = [];
Expand Down Expand Up @@ -171,8 +201,24 @@ private function createNumberFormatter(?string $locale, string $style, array $at
$locale = \Locale::getDefault();
}

$textAttrs = [];
foreach ($attrs as $name => $value) {
if (isset(self::NUMBER_TEXT_ATTRIBUTES[$name])) {
$textAttrs[$name] = $value;
unset($attrs[$name]);
}
}

$symbols = [];
foreach ($attrs as $name => $value) {
if (isset(self::NUMBER_SYMBOLS[$name])) {
$symbols[$name] = $value;
unset($attrs[$name]);
}
}

ksort($attrs);
$hash = sprintf('%s|%s|%s', $locale, $style, json_encode($attrs, \JSON_THROW_ON_ERROR));
$hash = $locale.'|'.$style.'|'.json_encode($attrs).'|'.json_encode($textAttrs).'|'.json_encode($symbols);

if (!isset($this->numberFormatters[$hash])) {
$this->numberFormatters[$hash] = new \NumberFormatter($locale, self::NUMBER_STYLES[$style]);
Expand Down Expand Up @@ -200,6 +246,14 @@ private function createNumberFormatter(?string $locale, string $style, array $at
$this->numberFormatters[$hash]->setAttribute(self::NUMBER_ATTRIBUTES[$name], $value);
}

foreach ($textAttrs as $name => $value) {
$this->numberFormatters[$hash]->setTextAttribute(self::NUMBER_TEXT_ATTRIBUTES[$name], $value);
}

foreach ($symbols as $name => $value) {
$this->numberFormatters[$hash]->setSymbol(self::NUMBER_SYMBOLS[$name], $value);
}

return $this->numberFormatters[$hash];
}

Expand Down
36 changes: 36 additions & 0 deletions tests/Config/CrudTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,40 @@ public function testSetFormThemes()

$this->assertSame(['common/base_form_theme.html.twig', 'admin/form/my_theme.html.twig'], $crudConfig->getAsDto()->getFormThemes());
}

public function testDefaultThousandsSeparator()
{
$crudConfig = Crud::new();

$this->assertNull($crudConfig->getAsDto()->getThousandsSeparator());
}

/**
* @testWith [",", ".", " ", "-", ""]
*/
public function testSetThousandsSeparator(string $separator)
{
$crudConfig = Crud::new();
$crudConfig->setThousandsSeparator($separator);

$this->assertSame($separator, $crudConfig->getAsDto()->getThousandsSeparator());
}

public function testDefaultDecimalSeparator()
{
$crudConfig = Crud::new();

$this->assertNull($crudConfig->getAsDto()->getDecimalSeparator());
}

/**
* @testWith [",", ".", " ", "-", ""]
*/
public function testSetDecimalSeparator(string $separator)
{
$crudConfig = Crud::new();
$crudConfig->setDecimalSeparator($separator);

$this->assertSame($separator, $crudConfig->getAsDto()->getDecimalSeparator());
}
}
Loading

0 comments on commit 90e24f3

Please sign in to comment.