diff --git a/composer.json b/composer.json index 2057279..537dd7a 100644 --- a/composer.json +++ b/composer.json @@ -18,7 +18,8 @@ "illuminate/filesystem": "^7.0|^8.0", "league/iso3166": "^3.0", "guzzlehttp/guzzle": "^6.3.1|^7.0.1", - "mpociot/vat-calculator": "^2.4" + "mpociot/vat-calculator": "^2.4", + "ext-intl": "*" }, "require-dev": { "phpunit/phpunit": "^8.5.8|^9.3.3" diff --git a/src/CountriesServiceProvider.php b/src/CountriesServiceProvider.php index 1953c21..f089bf4 100644 --- a/src/CountriesServiceProvider.php +++ b/src/CountriesServiceProvider.php @@ -124,6 +124,10 @@ protected function registerBindings() $this->app->singleton('country.helper', function ($app) { return new CountryHelper($app->config); }); + + $this->app->singleton('number.helper', function ($app) { + return new NumberHelper($app->config); + }); } /** @@ -137,6 +141,7 @@ protected function registerFacades() $this->app->alias('currency.helper', CurrencyHelper::class); $this->app->alias('i18n.taxrate', TaxRate::class); $this->app->alias('country.helper', CountryHelper::class); + $this->app->alias('number.helper', NumberHelper::class); }); } } diff --git a/src/CurrencyHelper.php b/src/CurrencyHelper.php index e09e55d..fa9a361 100644 --- a/src/CurrencyHelper.php +++ b/src/CurrencyHelper.php @@ -6,13 +6,6 @@ class CurrencyHelper extends LocalizedHelper { - /** - * Cached formatters. - * - * @var array - */ - protected $cachedFormatters = []; - /** * Return a value in the given currency formatted for the given locale. * @@ -28,8 +21,11 @@ public function toFormat( string $currencyCode = CurrencyCode::POUND_STERLING, string $locale = 'en' ): string { + /* + * Sometimes we can display up to 6 decimals of the monetary unit (ex: £0.106544) for energy prices. + */ return $this - ->getDefaultFormatter($locale) + ->getDefaultFormatter($locale, 6) ->formatCurrency($value, $currencyCode); } @@ -105,74 +101,15 @@ public function toStandardFormat( string $locale = 'en', int $precision = null ): string { + if (is_null($precision)) { + return $this->getDefaultFormatter($locale)->formatCurrency($value, $currencyCode); + } + return $this ->getFixedPrecisionFormatter($locale, $precision) ->formatCurrency($value, $currencyCode); } - /** - * Get the formatter cache key. - * - * @param array $params - * @return string - */ - protected function getFormatterCacheKey(array $params): string - { - return hash('sha256', serialize($params)); - } - - /** - * Gets a formatter or creates and returns it if not already. - * - * @param string $key - * @param callable $createHandler - * @return NumberFormatter - */ - protected function getFormatter(string $key, callable $createHandler): NumberFormatter - { - if (array_key_exists($key, $this->cachedFormatters)) { - return $this->cachedFormatters[$key]; - } - - return $this->cachedFormatters[$key] = $createHandler(); - } - - /** - * Create the default formatter for the given locale. - * - * @param string $locale - * @return NumberFormatter - */ - protected function getDefaultFormatter(string $locale): NumberFormatter - { - return $this->getFormatter( - $this->getFormatterCacheKey(func_get_args()), - function () use ($locale) { - $formatter = $this->getBaseFormatter($this->getSystemLocale($locale)); - - /* - * NumberFormatter will round up with 2 decimals only by default. - * Sometimes we can display up to 6 decimals of the monetary unit (ex: £0.106544) for energy prices. - */ - $formatter->setAttribute(NumberFormatter::MAX_FRACTION_DIGITS, 6); - - return $formatter; - } - ); - } - - /** - * Create a basic number formatter for the given locale in the given style. - * - * @param string $locale - * @param int|null $style - * @return NumberFormatter - */ - protected function getBaseFormatter(string $locale, ?int $style = null): NumberFormatter - { - return new NumberFormatter($locale, $style ?? NumberFormatter::CURRENCY); - } - /** * Create a number formatter for retrieving symbols. * @@ -183,7 +120,7 @@ protected function getBaseFormatter(string $locale, ?int $style = null): NumberF protected function getSymbolFormatter(string $locale, string $currencyCode): NumberFormatter { return $this->getFormatter( - $this->getFormatterCacheKey(func_get_args()), + $this->getFormatterCacheKey(__FUNCTION__, func_get_args()), function () use ($locale, $currencyCode) { return $this->getBaseFormatter( $this->getSystemLocale($locale) . "@currency=$currencyCode" @@ -193,47 +130,12 @@ function () use ($locale, $currencyCode) { } /** - * Create a number formatter with a given precision. + * Return the formatter style used by the number formatter. * - * @param string $locale - * @param int|null $precision - * @return NumberFormatter + * @return int */ - protected function getFixedPrecisionFormatter(string $locale, ?int $precision): NumberFormatter + protected function getFormatterStyle(): int { - return $this->getFormatter( - $this->getFormatterCacheKey(func_get_args()), - function () use ($locale, $precision) { - $formatter = $this->getBaseFormatter($this->getSystemLocale($locale)); - - if (is_int($precision)) { - $formatter->setAttribute(NumberFormatter::MAX_FRACTION_DIGITS, $precision); - $formatter->setAttribute(NumberFormatter::MIN_FRACTION_DIGITS, $precision); - } - - return $formatter; - } - ); - } - - /** - * Create a number formatter using a pattern. - * - * @param string $locale - * @param string $pattern - * @return NumberFormatter - */ - protected function getPatternedFormatter(string $locale, string $pattern): NumberFormatter - { - return $this->getFormatter( - $this->getFormatterCacheKey(func_get_args()), - function () use ($locale, $pattern) { - $formatter = $this->getBaseFormatter($this->getSystemLocale($locale)); - - $formatter->setPattern($pattern); - - return $formatter; - } - ); + return NumberFormatter::CURRENCY; } } diff --git a/src/Facades/NumberHelper.php b/src/Facades/NumberHelper.php new file mode 100755 index 0000000..889f361 --- /dev/null +++ b/src/Facades/NumberHelper.php @@ -0,0 +1,21 @@ +cachedFormatters)) { + return $this->cachedFormatters[$key]; + } + + $this->cachedFormatters[$key] = $createHandler(); + + return $this->cachedFormatters[$key] = $createHandler(); + } + + /** + * Get the formatter cache key. + * + * @param string $method + * @param array $params + * @return string + */ + protected function getFormatterCacheKey(string $method, array $params): string + { + return hash('sha256', $method . serialize($params)); + } + + /** + * Create a basic number formatter for the given locale in the given style. + * + * @param string $locale + * @return NumberFormatter + */ + protected function getBaseFormatter(string $locale): NumberFormatter + { + return new NumberFormatter($locale, $this->getFormatterStyle()); + } + + /** + * Create a number formatter with a fixed given precision. + * + * @param string $locale + * @param int $precision + * @return NumberFormatter + */ + protected function getFixedPrecisionFormatter(string $locale, int $precision): NumberFormatter + { + return $this->getDefaultFormatter($locale, $precision, $precision); + } + + /** + * Create the default formatter for the given locale. The precision can be variable based on the max and min values. + * If no precision is specified, the default one for the locale and NumberFormatter style will be used. + * + * @param string $locale + * @param int|null $maxPrecision + * @param int|null $minPrecision + * @return NumberFormatter + */ + protected function getDefaultFormatter( + string $locale, + int $maxPrecision = null, + int $minPrecision = null + ): NumberFormatter { + return $this->getFormatter( + $this->getFormatterCacheKey(__FUNCTION__, func_get_args()), + function () use ($locale, $maxPrecision, $minPrecision) { + $formatter = $this->getBaseFormatter($this->getSystemLocale($locale)); + + if (! is_null($maxPrecision)) { + $formatter->setAttribute(NumberFormatter::MAX_FRACTION_DIGITS, $maxPrecision); + } + + if (! is_null($minPrecision)) { + $formatter->setAttribute(NumberFormatter::MIN_FRACTION_DIGITS, $minPrecision); + } + + return $formatter; + } + ); + } + + /** + * Create a number formatter using a pattern. + * + * @param string $locale + * @param string $pattern + * @return NumberFormatter + */ + protected function getPatternedFormatter(string $locale, string $pattern): NumberFormatter + { + return $this->getFormatter( + $this->getFormatterCacheKey(__FUNCTION__, func_get_args()), + function () use ($locale, $pattern) { + $formatter = $this->getBaseFormatter($this->getSystemLocale($locale)); + + $formatter->setPattern($pattern); + + return $formatter; + } + ); + } } diff --git a/src/NumberHelper.php b/src/NumberHelper.php index 006651d..edeba90 100644 --- a/src/NumberHelper.php +++ b/src/NumberHelper.php @@ -7,19 +7,29 @@ class NumberHelper extends LocalizedHelper { /** - * Return a number formatted for the given locale. + * Return a number formatted for the given locale. Specify precision to define the exact number of decimal digits. * * @param float|int $value * @param string $locale + * @param int|null $precision * @return string */ - public function toFormat($value, string $locale = 'en'): string + public function toFormat($value, string $locale = 'en', int $precision = null): string { - $formatter = new NumberFormatter( - $this->getSystemLocale($locale), - NumberFormatter::DEFAULT_STYLE - ); + if (is_null($precision)) { + return $this->getDefaultFormatter($locale)->format($value); + } - return $formatter->format($value); + return $this->getFixedPrecisionFormatter($locale, $precision)->format($value); + } + + /** + * Return the formatter style used by the number formatter. + * + * @return int + */ + protected function getFormatterStyle(): int + { + return NumberFormatter::DEFAULT_STYLE; } } diff --git a/tests/Unit/NumberHelperTest.php b/tests/Unit/NumberHelperTest.php index 4ace80f..ac11643 100644 --- a/tests/Unit/NumberHelperTest.php +++ b/tests/Unit/NumberHelperTest.php @@ -12,20 +12,46 @@ class NumberHelperTest extends TestCase * * @return array */ - public function providerTestToFormat() + public function providerTestToFormat(): array { - $value = 1500.51254567890; + $nonBreakingSpace = "\xC2\xA0"; return [ - 'number in english format' => [ - $value, + 'number in english format with default precision' => [ + 2456.3487283384, 'en', - '1,500.513', + null, + '2,456.349', ], - 'number in norwegian format' => [ - $value, + 'number in english format with fixed precision' => [ + 2456.3487283384, + 'en', + 5, + '2,456.34873', + ], + 'number in english (IE) format with default precision' => [ + 1569.7834745, + 'ie', + null, + '1,569.783', + ], + 'number in english (IE) format with fixed precision' => [ + 1569.7834745, + 'ie', + 0, + '1,570', + ], + 'number in norwegian format with default precision' => [ + 1500.51254567890, + 'no', + null, + "1{$nonBreakingSpace}500,513", + ], + 'number in norwegian format with fixed precision' => [ + 1500.51254567890, 'no', - '1 500,513', + 7, + "1{$nonBreakingSpace}500,5125457", ], ]; } @@ -37,16 +63,17 @@ public function providerTestToFormat() * * @param float $value * @param string $locale + * @param int|null $precision * @param string $expected */ - public function testToFormat(float $value, string $locale, string $expected) + public function testToFormat(float $value, string $locale, ?int $precision, string $expected) { $this->loadConfiguration()->loadServiceProvider(); - $currencyHelper = new NumberHelper($this->app->config); + $numberHelper = new NumberHelper($this->app->config); - $actual = $currencyHelper->toFormat($value, $locale); + $actual = $numberHelper->toFormat($value, $locale, $precision); - $this->assertEquals($expected, $actual); + $this->assertSame($expected, $actual); } }