diff --git a/.gitignore b/.gitignore index 841a7d6..462db9a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ composer.lock /vendor/ /**/output/ +/coverage.html diff --git a/README.md b/README.md index 676da05..13cdd66 100644 --- a/README.md +++ b/README.md @@ -9,12 +9,44 @@ Number Format Wrapper above number_format, api is very easy. # Changelog + +## v6.0 + +- global namespace `h4kuna\Number` renamed to `h4kuna\Format` +- add new namespace `h4kuna\Format\Date`, other files move to namespace `h4kuna\Format\Number` +- behavior is same like v5.0 but namespaces are different +- add support for php native [NumberFormatter](https://www.php.net/manual/en/class.numberformatter.php) +- I don't keep back compatibility, because v5.0 is not widespread, but here are aliases. You can add to your project and + will work. + +```php +use h4kuna; +class_alias(h4kuna\Format\Number\Formats::class, 'h4kuna\Number\Utils\Formats'); +class_alias(h4kuna\Format\Number\NumberFormat::class, 'h4kuna\Number\Format'); +class_alias(h4kuna\Format\Number\Percent::class, 'h4kuna\Number\Percent'); +class_alias(h4kuna\Format\Number\Round::class, 'h4kuna\Number\Utils\Round'); +class_alias(h4kuna\Format\Number\Tax::class, 'h4kuna\Number\Tax'); +class_alias(h4kuna\Format\Number\UnitValue::class, 'h4kuna\Number\Utils\UnitValue'); + +// parameters +class_alias(h4kuna\Format\Number\Parameters\ZeroClear::class, 'h4kuna\Number\Parameters\Format\ZeroClear'); + +// formatters +class_alias(h4kuna\Format\Number\Formatters\NumberFormatter::class, 'h4kuna\Number\NumberFormat'); + +// namespace Unit +class_alias(h4kuna\Format\Number\Units\Byte::class, 'h4kuna\Number\Units\Byte'); +class_alias(h4kuna\Format\Number\Units\Unit::class, 'h4kuna\Number\Units\Unit'); +class_alias(h4kuna\Format\Number\Units\UnitFormat::class, 'h4kuna\Number\Units\UnitFormat'); +``` + ## v5.0 + - support php 8.0+ -- add new static class [Format](src/Format.php), you can format numbers without instance of class +- add new static class [NumberFormat](src/Number/NumberFormat.php), you can format numbers without instance of class - class NumberFormat is immutable - **BC break** removed parameters like unit and decimals in method NumberFormat::format() - - let's use method modify() + - let's use method modify() - class Parameters removed, because php 8.0 has native support - **BC break** NumberFormat support for int numbers removed, like a parameter **intOnly** - **BC break** NumberFormat removed method enableExtendFormat() all options move to constructor @@ -22,49 +54,86 @@ Wrapper above number_format, api is very easy. - class NumberFormatFactory removed - parameter zeroClear is integer instead of bool -## v4.0 -- removed dependency on h4kuna/data-type -- support php 7.4+ -- removed interface NumberFormat -- renamed class NumberFormatState -> NumberFormat -- removed class UnitFormatState, replace by `NumberFormat` like `$nf = new NumberFormat(); $nf->enableExtendFormat();` -- removed class UnitPersistentFormatState, replace by `NumberFormat` like `$nf = new NumberFormat(); $nf->enableExtendFormat('1 MY_PERSISTENT_UNIT');` -- method format has second parameter like decimals and third is dynamic defined unit -- char for unit in mask changed to `⎵` -- added parameter nbsp to NumberFormat::__construct() - Install via composer ------------------- + ```sh composer require h4kuna/number-format ``` +## Number + +### Formats + +Keep formats in object and use if you need. + +```php +use h4kuna\Format\Number; + +$formats = new Number\Formats([ + 'EUR' => static fn (Number\Formats $formats) => new Number\Formatters\NumberFormatter(decimals: 0, nbsp: false, unit: '€'), // callback like factory if is needed + 'GBP' => new NumberFormatter(nbsp: false, unit: '£', mask: '⎵ 1'), +]); + +$formats->get('EUR')->format(5); // 5€ +$formats->get('GBP')->format(5); // £ 5,00 + +``` + +### Native NumberFormatter + +Create new class MyFormats and extends class [Utils\Formats](./src/Utils/Formats.php) for right suggestion. + +```php +use h4kuna\Format\Number;use h4kuna\Format\Utils; + +/** + * @extends Utils\Formats + */ +class MyFormats extends Utils\Formats +{ + +} + +setlocale(LC_TIME, 'cs_CZ.utf8'); + +$formats = new MyFormats([ + 'decimal' => static fn() => new Number\Formatters\IntlNumberFormatter(new \NumberFormatter(\Locale::getDefault(), \NumberFormatter::DECIMAL)), + 'currency' => static fn() => new Number\Formatters\IntlNumberFormatter(new \NumberFormatter(\Locale::getDefault(), \NumberFormatter::CURRENCY)), +]); + +$formats->get('decimal')->format(1000.1235); // 1 000,124 +$formats->get('currency')->format(1000.1235); // 1 000,12 Kč +``` + ### NumberFormat -Class has many parameters and all paremetes has default value. You can add parameters normaly by position or name of keys in array like first parameter. +Class has many parameters and all parameters has default value. ```php -use h4kuna\Number; +use h4kuna\Format\Number; // set decimals as 3 -$numberFormat = new Number\NumberFormat(3); +$numberFormat = new Number\Formatters\NumberFormatter(3); // or -$numberFormat = new Number\NumberFormat(decimals: 3); +$numberFormat = new Number\Formatters\NumberFormatter(decimals: 3); echo $numberFormat->format(1000); // 1 000,000 ``` #### Parameters + - decimals: [2] - decimalPoint: [','] - thousandsSeparator: [' '] - nbsp: [true] replace space by \  like utf-8 char -- zeroClear: - - [ZeroClear::NO] disabled - - [ZeroClear::DECIMALS_EMPTY] 1.0 -> `1`, 1.50 -> `1,50` - - [ZeroClear::DECIMALS] 1.0 -> `1`; 1.50 -> `1,5` +- zeroClear: + - [ZeroClear::NO] disabled + - [ZeroClear::DECIMALS_EMPTY] 1.0 -> `1`, 1.50 -> `1,50` + - [ZeroClear::DECIMALS] 1.0 -> `1`; 1.50 -> `1,5` - emptyValue: [\x00] disabled, if value is empty (by default `null` or empty string `''`) will display some wildcard -- zeroIsEmpty: [false] disabled, only `null` and empty string `''` are replaced by `emptyValue`, but by this option is zero empty value too +- zeroIsEmpty: [false] disabled, only `null` and empty string `''` are replaced by `emptyValue`, but by this option is + zero empty value too - unit: [''] disabled, define unit for formatted number, $, €, kg etc... - showUnitIfEmpty: [false] unit must be defined, show unit if is empty value - mask: [1 ⎵] if you want to define **1 €** or **$ 1** @@ -73,10 +142,11 @@ echo $numberFormat->format(1000); // 1 000,000 Here are tests for [more use cases](tests/src/NumberFormatTest.php). ### Format expect unit and use modify() + ```php -use h4kuna\Number; +use h4kuna\Format\Number; -$numberFormat = new Number\NumberFormat(mask: '⎵ 1', decimals: 3, unit: '€'); +$numberFormat = new Number\Formatters\NumberFormatter(mask: '⎵ 1', decimals: 3, unit: '€'); echo $numberFormat->format(1000, 3, '€'); // € 1 000,000 with nbsp $numberFormatDisableNbsp = $numberFormat->modify(nbsp: false); // keep previous setting and disable nbsp @@ -84,24 +154,24 @@ echo $numberFormatDisableNbsp->format(1000); // € 1 000,000 ``` ### Format persistent unit -This class is same like previous, but unit is persistent like currencies or temperature. + +This class is same like previous, but unit is persistent like currencies or temperature. ```php -use h4kuna\Number; +use h4kuna\Format\Number; -$numberFormat = new Number\NumberFormat(); +$numberFormat = new Number\Formatters\NumberFormatter(); $numberFormat->enableExtendFormat('€ 1'); echo $numberFormat->format(1000); // € 1 000,00 ``` -### NumberFormatFactory -For all previous cases is prepared [factory class](src/NumberFormatFactory.php). This class help you create new instance and support named parameters in constructor. [Visit test](tests/src/NumberFormatFactoryTest.php) - ### Tax ```php -$tax = new Tax(20); +use h4kuna\Format\Number; + +$tax = new Number\Tax(20); echo $tax->add(100); // 120 echo $tax->deduct(120); // 100.0 echo $tax->diff(120); // 20.0 @@ -110,42 +180,27 @@ echo $tax->diff(120); // 20.0 ### Percent ```php -$percent = new Percent(20); +use h4kuna\Format\Number; + +$percent = new Number\Percent(20); echo $percent->add(100); // 120.0 echo $percent->deduct(120); // 96.0 echo $percent->diff(120); // 24.0 ``` -## Integration to Nette framework - -In your neon file -```neon -services: - number: h4kuna\Number\NumberFormat(decimalPoint: '.', intOnly: 1, decimals: 1) #support named parameters by nette - - latte.latteFactory: - setup: - - addFilter('number', [@number, 'format']) -``` - -We added new filter number, in template use like: -```html -{=10000|number} // this render "1 000.0" with &nbps; like white space -``` - -# Units -Help us convert units in general [decimal system](//en.wikipedia.org/wiki/Metric_prefix#List_of_SI_prefixes). - ### Units\Unit + ```php -use h4kuna\Number\Units; +use h4kuna\Format\Number\Units; $unit = new Units\Unit(/* [string $from], [array $allowedUnits] */); ``` + * **$from** select default prefix for your units default is BASE = 0 * **$allowedUnits** if we need other units if is defined This example say: I have 50kilo (103) and convert to base 100 + ```php $unitValue = $unit->convertFrom(50, $unit::KILO, $unit::BASE); echo $unitValue->unit; // empty string mean BASE @@ -153,6 +208,7 @@ echo $unitValue->value; // 50000 ``` If second parameter is NULL then use from unit whose is defined in constructor. + ```php $unitValue = $unit->convertFrom(5000, NULL, $unit::KILO); // alias for this use case is @@ -161,7 +217,9 @@ $unitValue = $unit->convert(5000, $unit::KILO); echo $unitValue->unit; // k mean KILO echo $unitValue->value; // 5 ``` -If third parameter is NULL, class try find best unit. + +If third parameter is NULL, class try to find the best unit. + ```php $unitValue = $unit->convertFrom(5000000, $unit::MILI, NULL); echo $unitValue->unit; // k mean KILO @@ -169,6 +227,7 @@ echo $unitValue->value; // 5 ``` Last method, take string and convert how we need. This is good for Byte. + ```php $unitValue = $unit->fromString('100k', $unit::BASE); echo $unitValue->unit; // BASE @@ -176,7 +235,9 @@ echo $unitValue->value; // 100000 ``` ### Units\Byte + ```php +use h4kuna\Format\Number\Units; $unitValue = $byte = new Units\Byte(); $byte->fromString('128M'); echo $unitValue->unit; // BASE @@ -184,11 +245,95 @@ echo $unitValue->value; // 134217728 ``` ### Units\UnitFormat + If we need format our units. + ```php -$nff = new Number\NumberFormatFactory(); -$unitfFormat = new Units\UnitFormat('B', new Byte, $nff->createUnit()); +use h4kuna\Format\Number\Units; +$nff = new Number\Formats(); +$unitfFormat = new Units\UnitFormat('B', new Byte, $nff); $unitfFormat->convert(968884224); // '924,00 MB' $unitfFormat->convert(1024); // '1,00 kB' ``` + +## Date + +Define own formats for date and time. + +```php +use h4kuna\Format\Date; + +$formats = new Date\Formats([ + 'date' => new Date\Formatters\DateTimeFormatter('j. n. Y'), + 'time' => static fn () => new Date\Formatters\DateTimeFormatter('H:i'), // callback like factory if is needed + 'dateTime' => static fn () => new Date\Formatters\DateTimeFormatter('j. n. Y H:i'), +]); + +$dateObject = new \DateTime('2023-06-13 12:30:40'); +$formats->get('date')->format($dateObject); // 13. 6. 2023 +$formats->get('time')->format($dateObject); // 12:30 +$formats->get('dateTime')->format($dateObject); // 13. 6. 2023 12:30 +``` + +For better use, you can extends class Formats and add your own methods. + +```php +use h4kuna\Format\Date; + +class MyFormats extends Date\Formats +{ + public function date(?\DateTimeInterface $data): string + { + return $this->get('date')->format($data); + } +} + +$formats = new MyFormats([ + 'date' => new Date\Formatters\DateTimeFormatter('j. n. Y'), +]); + +$dateObject = new \DateTime('2023-06-13 12:30:40'); +$formats->date($dateObject); // 13. 6. 2023 +``` + +### Support IntlDateFormatter + +Format by locale. + +```php +use h4kuna\Format\Date; +use IntlDateFormatter; + +$intlFormatter = new IntlDateFormatter('cs_CZ', IntlDateFormatter::MEDIUM, IntlDateFormatter::MEDIUM,) + +$formats = new Date\Formats([ + 'date' => new Date\Formatters\IntlDateFormatter($intlFormatter), +]); + +$date = new \DateTime('2023-06-13 12:30:40'); +$formats->get('date')->format($date); // 12. 6. 2023 12:30:40 +``` + +## Integration to Nette framework + +In your neon file + +```neon +services: + number: h4kuna\Format\Number\NumberFormat(decimalPoint: '.', intOnly: 1, decimals: 1) #support named parameters by nette + + latte.latteFactory: + setup: + - addFilter('number', [@number, 'format']) +``` + +We added new filter number, in template use like: + +```html +{=10000|number} // this render "1 000.0" with &nbps; like white space +``` + +# Units + +Help us convert units in general [decimal system](//en.wikipedia.org/wiki/Metric_prefix#List_of_SI_prefixes). diff --git a/composer.json b/composer.json index e45c41e..fa6792d 100644 --- a/composer.json +++ b/composer.json @@ -15,6 +15,7 @@ "php": ">=8.0" }, "require-dev": { + "ext-intl": "*", "nette/utils": "^3.0 || ^4.0", "nette/tester": "^2.4", "phpstan/phpstan": "^1.8", @@ -24,19 +25,23 @@ }, "autoload": { "psr-4": { - "h4kuna\\Number\\": "src" + "h4kuna\\Format\\": "src" } }, "autoload-dev": { "psr-4": { - "h4kuna\\Number\\Tests\\": "tests/src" + "h4kuna\\Format\\Tests\\": "tests/src" } }, "config": { "sort-packages": true }, + "suggest": { + "ext-intl": "If you want to use IntlDateFormatter." + }, "scripts": { "phpstan": "vendor/bin/phpstan analyse", - "tests": "vendor/bin/tester -s -j 4 --colors 1 -s -C tests/src" + "tests": "vendor/bin/tester -s --colors 1 -s -C tests/src", + "coverage": "vendor/bin/tester -s --coverage coverage.html --coverage-src src/ --colors 1 -s -C tests/src" } } diff --git a/phpstan.neon b/phpstan.neon index 6c84dd3..412534e 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -3,8 +3,6 @@ parameters: paths: - src - tests/src - scanFiles: - - tests/bootstrap.php includes: - vendor/phpstan/phpstan-strict-rules/rules.neon diff --git a/src/Date/Formats.php b/src/Date/Formats.php new file mode 100644 index 0000000..96510a1 --- /dev/null +++ b/src/Date/Formats.php @@ -0,0 +1,19 @@ + + */ +class Formats extends Utils\Formats +{ + + protected function createDefaultCallback($object = null): callable + { + return static fn (): DateTimeFormatter => new DateTimeFormatter('Y-m-d H:i:s'); + } + +} diff --git a/src/Date/Formatter.php b/src/Date/Formatter.php new file mode 100644 index 0000000..c3856d0 --- /dev/null +++ b/src/Date/Formatter.php @@ -0,0 +1,10 @@ +initSpace(); + } + + + public function modify( + ?bool $nbsp = null, + ?string $emptyValue = null, + ): self + { + $that = clone $this; + $that->nbsp = $nbsp ?? $that->nbsp; + $that->emptyValue = $emptyValue ?? $that->emptyValue; + $that->initSpace(); + + return $that; + } + + + public function format(?DateTimeInterface $dateTime): string + { + return $dateTime === null ? $this->emptyValueSpace : $dateTime->format($this->formatSpace); + } + + + private function initSpace(): void + { + $this->emptyValueSpace = $this->nbsp ? Space::nbsp($this->emptyValue) : $this->emptyValue; + $this->formatSpace = $this->nbsp ? Space::nbsp($this->format) : $this->format; + } + +} diff --git a/src/Date/Formatters/IntlDateFormatter.php b/src/Date/Formatters/IntlDateFormatter.php new file mode 100644 index 0000000..bb21361 --- /dev/null +++ b/src/Date/Formatters/IntlDateFormatter.php @@ -0,0 +1,56 @@ +initSpace(); + } + + + public function modify( + ?bool $nbsp = null, + ?string $emptyValue = null, + ): self + { + $that = clone $this; + $that->nbsp = $nbsp ?? $that->nbsp; + $that->emptyValue = $emptyValue ?? $that->emptyValue; + $that->initSpace(); + + return $that; + } + + + public function format(?DateTimeInterface $dateTime): string + { + if ($dateTime === null) { + $result = $this->emptyValueSpace; + } else { + $result = $this->formatter->format($dateTime); + assert(is_string($result)); + } + + return $this->nbsp ? Space::nbsp($result) : $result; + } + + + private function initSpace(): void + { + $this->emptyValueSpace = $this->nbsp ? Space::nbsp($this->emptyValue) : $this->emptyValue; + } + +} diff --git a/src/Exceptions/InvalidArgumentException.php b/src/Exceptions/InvalidArgumentException.php index e245b07..22c7d10 100644 --- a/src/Exceptions/InvalidArgumentException.php +++ b/src/Exceptions/InvalidArgumentException.php @@ -1,6 +1,6 @@ + */ +class Formats extends Utils\Formats +{ + protected function createDefaultCallback($object = null): callable + { + if ($object === null) { + $object = new NumberFormatter(); + } + return static function (string|int $key) use ($object): NumberFormatter { + return $object->modify(unit: (string) $key); + }; + } +} diff --git a/src/Number/Formatter.php b/src/Number/Formatter.php new file mode 100644 index 0000000..bea9278 --- /dev/null +++ b/src/Number/Formatter.php @@ -0,0 +1,8 @@ +emptyValue = Space::nbsp($this->emptyValue); + } + + + public function modify( + ?string $emptyValue = null, + ?bool $zeroIsEmpty = null, + ): self + { + $that = clone $this; + $that->zeroIsEmpty = $zeroIsEmpty ?? $that->zeroIsEmpty; + $that->emptyValue = $emptyValue === null ? $this->emptyValue : Space::nbsp($emptyValue); + + return $that; + } + + + public function format(string|int|float|null $number): string + { + if (is_numeric($number) === false || ($this->zeroIsEmpty && (int) $number === 0)) { + return $this->emptyValue; + } + + $result = $this->formatter->format(is_string($number) ? (float) $number : $number); + assert(is_string($result)); + + return $result; + } +} diff --git a/src/NumberFormat.php b/src/Number/Formatters/NumberFormatter.php similarity index 84% rename from src/NumberFormat.php rename to src/Number/Formatters/NumberFormatter.php index 1278685..04418fe 100644 --- a/src/NumberFormat.php +++ b/src/Number/Formatters/NumberFormatter.php @@ -1,11 +1,14 @@ decimals, $this->decimalPoint, @@ -111,7 +114,7 @@ private function initMaskReplaced(): void if ($this->unit !== '' && $this->mask !== '') { $replace = ['⎵' => $this->unit]; if ($this->nbsp) { - $replace[' '] = Format::NBSP; + $replace[' '] = Space::NBSP; } $this->maskReplaced = strtr($this->mask, $replace); @@ -124,9 +127,9 @@ private function initMaskReplaced(): void private function initThousandsSeparator(): void { if ($this->nbsp && str_contains($this->thousandsSeparator, ' ')) { - $this->thousandsSeparator = strtr($this->thousandsSeparator, [' ' => Format::NBSP]); - } elseif ($this->nbsp === false && str_contains($this->thousandsSeparator, Format::NBSP)) { - $this->thousandsSeparator = strtr($this->thousandsSeparator, [Format::NBSP => ' ']); + $this->thousandsSeparator = Space::nbsp($this->thousandsSeparator); + } elseif ($this->nbsp === false && str_contains($this->thousandsSeparator, Space::NBSP)) { + $this->thousandsSeparator = Space::white($this->thousandsSeparator); } } diff --git a/src/Format.php b/src/Number/NumberFormat.php similarity index 80% rename from src/Format.php rename to src/Number/NumberFormat.php index a7faec0..4655f61 100644 --- a/src/Format.php +++ b/src/Number/NumberFormat.php @@ -1,19 +1,12 @@ 0 && ((int) $castNumber) == $castNumber) { $decimals = 0; @@ -93,12 +86,12 @@ private static function replace(bool $nbsp, string $formattedNumber, string $mas { if ($mask === '') { return $nbsp === true ? - str_replace(' ', self::NBSP, $formattedNumber) : + Space::nbsp($formattedNumber) : $formattedNumber; } return $nbsp === true ? - str_replace(['1', ' '], [$formattedNumber, self::NBSP], $mask) : + str_replace(['1', ' '], [$formattedNumber, Space::NBSP], $mask) : str_replace('1', $formattedNumber, $mask); } diff --git a/src/Parameters/Format/ZeroClear.php b/src/Number/Parameters/ZeroClear.php similarity index 77% rename from src/Parameters/Format/ZeroClear.php rename to src/Number/Parameters/ZeroClear.php index b541ec2..fdd766a 100644 --- a/src/Parameters/Format/ZeroClear.php +++ b/src/Number/Parameters/ZeroClear.php @@ -1,6 +1,6 @@ value . ' ' . $this->unit; + } + +} diff --git a/src/Units/Byte.php b/src/Number/Units/Byte.php similarity index 90% rename from src/Units/Byte.php rename to src/Number/Units/Byte.php index 28a2823..364c35d 100644 --- a/src/Units/Byte.php +++ b/src/Number/Units/Byte.php @@ -1,6 +1,6 @@ + * @var non-empty-array */ protected array $allowedUnits; - private string $from; - /** * @param array $allowedUnits */ - public function __construct(string $from = self::BASE, array $allowedUnits = null) + public function __construct(private string $from = self::BASE, array $allowedUnits = []) { - $this->from = $from; - if ($allowedUnits === null) { + if ($allowedUnits === []) { $this->allowedUnits = static::UNITS; } + $this->checkUnit($this->from); } @@ -71,7 +68,7 @@ public function getFrom(): string } - public function convert(float $number, ?string $unitTo = null): Utils\UnitValue + public function convert(float $number, ?string $unitTo = null): Format\Number\UnitValue { return $this->convertFrom($number, null, $unitTo); } @@ -81,7 +78,7 @@ public function convert(float $number, ?string $unitTo = null): Utils\UnitValue * @param string|null $unitFrom - NULL mean defined in constructor * @param string|null $unitTo - NULL mean automatic */ - public function convertFrom(float $number, ?string $unitFrom, ?string $unitTo = null): Utils\UnitValue + public function convertFrom(float $number, ?string $unitFrom, ?string $unitTo = null): Format\Number\UnitValue { if ($unitFrom === null) { $unitFrom = $this->from; @@ -106,11 +103,11 @@ public function convertFrom(float $number, ?string $unitFrom, ?string $unitTo = } - public function fromString(string $value, string $unitTo = self::BASE): Utils\UnitValue + public function fromString(string $value, string $unitTo = self::BASE): Format\Number\UnitValue { $result = preg_match('/^(?P(?:-)?\d*(?:(?:\.)(?:\d*)?)?)(?P[a-z]+)$/i', self::prepareNumber($value), $find); if ($result === false || isset($find['number']) === false || $find['number'] === '') { - throw new Number\Exceptions\InvalidArgumentException('Bad string, must be number and unit. Example "128M". Your: ' . $value); + throw new Format\Exceptions\InvalidArgumentException('Bad string, must be number and unit. Example "128M". Your: ' . $value); } return $this->convertFrom((float) $find['number'], $find['unit'], $unitTo); } @@ -122,12 +119,9 @@ protected function convertUnit(float $number, int $indexFrom, int $indexTo): flo } - private function autoConvert(float $number, string $unitFrom): Utils\UnitValue + private function autoConvert(float $number, string $unitFrom): Format\Number\UnitValue { $result = []; - if ($this->allowedUnits === []) { - throw new Number\Exceptions\InvalidArgumentException('Allowed units must exists.'); - } foreach ($this->allowedUnits as $unit => $index) { if ($this->allowedUnits[$unitFrom] === $index) { $temp = $number; @@ -150,16 +144,16 @@ private function autoConvert(float $number, string $unitFrom): Utils\UnitValue private function checkUnit(string $unit): void { if (!isset($this->allowedUnits[$unit])) { - throw new Number\Exceptions\InvalidArgumentException(sprintf('Unit: "%s let\'s set own.', $unit)); + throw new Format\Exceptions\InvalidArgumentException(sprintf('Unit: "%s let\'s set own.', $unit)); } } - public static function createUnitValue(float $value, string $unit): Utils\UnitValue + private static function createUnitValue(float $value, string $unit): Format\Number\UnitValue { - return new Utils\UnitValue( - $value, - $unit + return new Format\Number\UnitValue( + $value, + $unit ); } diff --git a/src/Units/UnitFormat.php b/src/Number/Units/UnitFormat.php similarity index 90% rename from src/Units/UnitFormat.php rename to src/Number/Units/UnitFormat.php index 6d9c226..cfa69bd 100644 --- a/src/Units/UnitFormat.php +++ b/src/Number/Units/UnitFormat.php @@ -1,15 +1,15 @@ + * @var array */ private array $formats = []; /** - * @param array $factories + * @param array $factories */ public function __construct( private array $factories = [], @@ -32,14 +31,14 @@ public function __construct( /** - * @param defaultCallback|NumberFormat $default + * @param defaultCallback|T $default */ - public function setDefault(callable|NumberFormat $default): void + public function setDefault($default): void { if ($this->default !== null) { throw new InvalidStateException('Default format could be setup only onetime.'); - } elseif ($default instanceof NumberFormat) { - $default = static fn (array $options): NumberFormat => $default->modify(...$options); + } elseif (is_callable($default) === false) { + $default = $this->createDefaultCallback($default); } $this->default = $default; @@ -47,32 +46,36 @@ public function setDefault(callable|NumberFormat $default): void /** - * @param formatCallback|NumberFormat $setup + * @param formatCallback|T $setup */ - public function add(string $key, callable|NumberFormat $setup): void + public function add(string|int $key, $setup): void { - if ($setup instanceof NumberFormat) { - $this->formats[$key] = $setup; - } else { + if (is_callable($setup)) { $this->factories[$key] = $setup; + unset($this->formats[$key]); + } else { + $this->formats[$key] = $setup; } } - public function has(string $key): bool + public function has(string|int $key): bool { return isset($this->formats[$key]) || isset($this->factories[$key]); } - public function get(string $key): NumberFormat + /** + * @return T + */ + public function get(string|int $key) { if (isset($this->formats[$key]) === false) { if (isset($this->factories[$key])) { $service = $this->factories[$key]; $format = is_callable($service) ? $service($this) : $service; } else { - $format = $this->getDefault()(['unit' => $key], $this, $key); + $format = $this->getDefault()($key, $this, null); } $this->formats[$key] = $format; unset($this->factories[$key]); @@ -88,10 +91,20 @@ public function get(string $key): NumberFormat public function getDefault(): callable { if ($this->default === null) { - $this->default = static fn (array $options): NumberFormat => new NumberFormat(...$options); + $this->default = $this->createDefaultCallback(); } return $this->default; } + + /** + * @param T|null $object + * @return defaultCallback + */ + protected function createDefaultCallback($object = null): callable + { + throw new InvalidStateException('Default format is not setup.'); + } + } diff --git a/src/Utils/Space.php b/src/Utils/Space.php new file mode 100644 index 0000000..02e2b6c --- /dev/null +++ b/src/Utils/Space.php @@ -0,0 +1,26 @@ + self::NBSP]); + } + + + public static function white(string $value): string + { + return strtr($value, [self::NBSP => ' ']); + } + +} diff --git a/src/Utils/UnitValue.php b/src/Utils/UnitValue.php deleted file mode 100644 index 09e536f..0000000 --- a/src/Utils/UnitValue.php +++ /dev/null @@ -1,24 +0,0 @@ -value = $value; - $this->unit = $unit; - } - - - public function __toString() - { - return $this->value . ' ' . $this->unit; - } - -} diff --git a/tests/bootstrap.php b/tests/bootstrap.php index afe3491..2c35a70 100644 --- a/tests/bootstrap.php +++ b/tests/bootstrap.php @@ -1,12 +1,5 @@ setDefault(fn () => new DateTimeFormatter('j. n. Y')); + + Assert::same(Space::nbsp('2. 1. 1986'), $formats->get('date')->format(new \DateTime('1986-01-02'))); + } + + + public function testUnknownFormat(): void + { + $formats = new Formats(); + + Assert::same(Space::nbsp('1986-01-02 01:02:03'), $formats->get('date')->format(new \DateTime('1986-01-02 01:02:03'))); + } + + + public function testIntlFormatter(): void + { + $formats = new Formats(); + $timezone = new \DateTimeZone('Europe/Prague'); + $locale = 'cs_CZ'; + $formats->add('date', new IntlDateFormatter(new \IntlDateFormatter($locale, \IntlDateFormatter::MEDIUM, \IntlDateFormatter::MEDIUM, $timezone, \IntlGregorianCalendar::createInstance($timezone, $locale)))); + + Assert::same(Space::nbsp('2. 1. 1986 0:00:00'), $formats->get('date')->format(new \DateTime('1986-01-02', $timezone))); + } + +} + +(new FormatsTest())->run(); diff --git a/tests/src/Date/Formatters/DateTimeFormatterTest.php b/tests/src/Date/Formatters/DateTimeFormatterTest.php new file mode 100644 index 0000000..b219168 --- /dev/null +++ b/tests/src/Date/Formatters/DateTimeFormatterTest.php @@ -0,0 +1,77 @@ + + */ + protected function provideFormat(): array + { + return [ + [ + ['nbsp' => false], + '2. 1. 1986 00:00:00', + new \DateTime('1986-01-02'), + ], + [ + [], + Space::nbsp('2. 1. 1986 00:00:00'), + new \DateTime('1986-01-02'), + ], + [ + ['emptyValue' => '-', 'nbsp' => false], + '2. 1. 1986 00:00:00', + new \DateTime('1986-01-02'), + ], + [ + ['emptyValue' => '-', 'nbsp' => false], + '-', + null, + ], + [ + ['emptyValue' => '- -'], + Space::nbsp('- -'), + null, + ], + ]; + } + + + /** + * @dataProvider provideFormat + * @param array{nbsp: bool, emptyValue: string} $parameters + */ + public function testFormat(array $parameters, string $expected, ?\DateTimeInterface $date): void + { + $intlDateFormatter = new DateTimeFormatter('j. n. Y H:i:s', ...$parameters); + Assert::same($expected, $intlDateFormatter->format($date)); + } + + + /** + * @dataProvider provideFormat + * @param array{nbsp: bool, emptyValue: string} $parameters + */ + public function testModify(array $parameters, string $expected, ?\DateTimeInterface $date): void + { + $dateFormatter = (new DateTimeFormatter('j. n. Y H:i:s'))->modify(...$parameters); + Assert::same($expected, $dateFormatter->format($date)); + } + +} + +(new DateTimeFormatterTest())->run(); diff --git a/tests/src/Date/Formatters/IntlDateFormatterTest.php b/tests/src/Date/Formatters/IntlDateFormatterTest.php new file mode 100644 index 0000000..342d591 --- /dev/null +++ b/tests/src/Date/Formatters/IntlDateFormatterTest.php @@ -0,0 +1,86 @@ + + */ + protected function provideFormat(): array + { + return [ + [ + ['nbsp' => false], + '2. 1. 1986 0:00:00', + new \DateTime('1986-01-02'), + ], + [ + [], + Space::nbsp('2. 1. 1986 0:00:00'), + new \DateTime('1986-01-02'), + ], + [ + ['emptyValue' => '-', 'nbsp' => false], + '2. 1. 1986 0:00:00', + new \DateTime('1986-01-02'), + ], + [ + ['emptyValue' => '-', 'nbsp' => false], + '-', + null, + ], + [ + ['emptyValue' => '- -'], + Space::nbsp('- -'), + null, + ], + ]; + } + + + /** + * @dataProvider provideFormat + * @param array{nbsp: bool, emptyValue: string} $parameters + */ + public function testFormat(array $parameters, string $expected, ?\DateTimeInterface $date): void + { + $formatter = new \IntlDateFormatter('cs_CZ', \IntlDateFormatter::MEDIUM, \IntlDateFormatter::MEDIUM); + + $intlDateFormatter = new IntlDateFormatter($formatter, ...$parameters); + Assert::same($expected, $intlDateFormatter->format($date)); + } + + + /** + * @dataProvider provideFormat + * @param array{nbsp: bool, emptyValue: string} $parameters + */ + public function testModify(array $parameters, string $expected, ?\DateTimeInterface $date): void + { + $formatter = new \IntlDateFormatter('cs_CZ', \IntlDateFormatter::MEDIUM, \IntlDateFormatter::MEDIUM); + + $intlDateFormatter = (new IntlDateFormatter($formatter))->modify(...$parameters); + Assert::same($expected, $intlDateFormatter->format($date)); + } + +} + +(new IntlDateFormatterTest())->run(); diff --git a/tests/src/Number/FormatsTest.php b/tests/src/Number/FormatsTest.php new file mode 100644 index 0000000..7c63dfc --- /dev/null +++ b/tests/src/Number/FormatsTest.php @@ -0,0 +1,39 @@ + fn (Formats $formats): NumberFormatter => new NumberFormatter(decimals: 0, nbsp: false, unit: '€'), + 'GBP' => new NumberFormatter(nbsp: false, unit: '£', mask: '⎵ 1'), +]); + +$formats->add('CZK', new NumberFormatter(decimals: 3, nbsp: false, unit: 'CZK')); +$formats->add('USD', static fn (Formats $formats +): NumberFormatter => $formats->getDefault()('USD', $formats, ['unit' => '$'])); + +$formats->setDefault(static function (string|int $key, Formats $self, mixed $options) { + $options ??= []; + assert(is_array($options)); + $options['unit'] = $options['unit'] ?? $key; + $options['nbsp'] = $options['nbsp'] ?? false; + $options['decimals'] = $options['decimals'] ?? 0; + return new NumberFormatter(...$options); +}); + +Assert::exception(function () use ($formats) { + $formats->setDefault(fn () => new NumberFormatter(nbsp: false)); +}, InvalidStateException::class); + +Assert::same('100 UNKNOWN', $formats->get('UNKNOWN')->format(100)); +Assert::same('100 $', $formats->get('USD')->format(100)); +Assert::same('100,000 CZK', $formats->get('CZK')->format(100)); +Assert::same('100 CZK', $formats->get('CZK')->modify(decimals: 0)->format(100)); +Assert::same('5 €', $formats->get('EUR')->format(5)); +Assert::same('£ 5,00', $formats->get('GBP')->format(5)); diff --git a/tests/src/Number/Formatters/IntlNumberFormatterTest.php b/tests/src/Number/Formatters/IntlNumberFormatterTest.php new file mode 100644 index 0000000..9d11168 --- /dev/null +++ b/tests/src/Number/Formatters/IntlNumberFormatterTest.php @@ -0,0 +1,100 @@ + + */ + protected function provideFormat(): array + { + return [ + [ + [], + '1 000,123', + 1000.1234, + ], + [ + [], + '1 000,124', + 1000.1235, + ], + [ + ['zeroIsEmpty' => true], + '', + 0, + ], + [ + ['zeroIsEmpty' => true, 'emptyValue' => '?'], + '?', + 0, + ], + [ + ['zeroIsEmpty' => false, 'emptyValue' => '?'], + '0', + 0, + ], + [ + ['zeroIsEmpty' => false, 'emptyValue' => '?'], + '?', + null, + ], + [ + ['zeroIsEmpty' => false, 'emptyValue' => '?'], + '?', + '', + ], + [ + ['zeroIsEmpty' => false, 'emptyValue' => '? ?'], + Space::nbsp('? ?'), + null, + ], + [ + [], + '11,1', + 11.1, + ], + ]; + } + + + /** + * @dataProvider provideFormat + * @param array{emptyValue: string, zeroIsEmpty: bool} $parameters + * @param string|int|float|null $number + */ + public function testFormat(array $parameters, string $expected, $number): void + { + $numberFormatter = new \NumberFormatter('cs_CZ', \NumberFormatter::DECIMAL); + $numberFormat = new IntlNumberFormatter($numberFormatter, ...$parameters); + Assert::same(Space::nbsp($expected), $numberFormat->format($number)); + } + + + /** + * @dataProvider provideFormat + * @param array{emptyValue: string, zeroIsEmpty: bool} $parameters + * @param string|int|float|null $number + */ + public function testModify(array $parameters, string $expected, $number): void + { + $numberFormatter = new \NumberFormatter('cs_CZ', \NumberFormatter::DECIMAL); + $numberFormat = (new IntlNumberFormatter($numberFormatter))->modify(...$parameters); + Assert::same(Space::nbsp($expected), $numberFormat->format($number)); + } + +} + +(new IntlNumberFormatterTest())->run(); diff --git a/tests/src/NumberFormatTest.php b/tests/src/Number/Formatters/NumberFormatterTest.php similarity index 64% rename from tests/src/NumberFormatTest.php rename to tests/src/Number/Formatters/NumberFormatterTest.php index 88378bf..9b5614f 100644 --- a/tests/src/NumberFormatTest.php +++ b/tests/src/Number/Formatters/NumberFormatterTest.php @@ -1,19 +1,20 @@ -1], + '120', + 123, + ], [ ['decimalPoint' => '.'], - nbsp('1 000.00'), + Space::nbsp('1 000.00'), 1000, ], [ @@ -54,22 +60,22 @@ protected function provideFormat(): array ], [ ['unit' => 'g'], - nbsp('1 000,00 g'), + Space::nbsp('1 000,00 g'), 1000, ], [ ['unit' => 'g', 'showUnitIfEmpty' => false], - nbsp('0,00'), + Space::nbsp('0,00'), 0, ], [ ['unit' => '$', 'mask' => '⎵1'], - nbsp('$1 000,00'), + Space::nbsp('$1 000,00'), 1000, ], [ ['round' => Round::BY_FLOOR], - nbsp('1,00'), + Space::nbsp('1,00'), 1.005, ], [ @@ -102,7 +108,7 @@ protected function provideFormat(): array */ public function testFormat(array $parameters, string $expected, float|int|null|string $number): void { - $numberFormat = new NumberFormat(...$parameters); + $numberFormat = new NumberFormatter(...$parameters); Assert::same($expected, $numberFormat->format($number)); } @@ -113,11 +119,18 @@ public function testFormat(array $parameters, string $expected, float|int|null|s */ public function testModify(array $parameters, string $expected, float|int|null|string $number): void { - $nf = new NumberFormat(); + $nf = new NumberFormatter(); $numberFormat = $nf->modify(...$parameters); Assert::same($expected, $numberFormat->format($number)); } + + public function testRoundCallback(): void + { + $numberFormat = new NumberFormatter(round: fn (float $number, int $precision) => round($number, $precision)); + Assert::same('1,01', $numberFormat->format('1.005')); + } + } -(new NumberFormatTest())->run(); +(new NumberFormatterTest())->run(); diff --git a/tests/src/FormatTest.php b/tests/src/Number/NumberFormatTest.php similarity index 90% rename from tests/src/FormatTest.php rename to tests/src/Number/NumberFormatTest.php index 1d99ef4..502316e 100644 --- a/tests/src/FormatTest.php +++ b/tests/src/Number/NumberFormatTest.php @@ -1,19 +1,20 @@ > @@ -87,7 +88,7 @@ protected function provideBase(): array */ public function testNumber(string $expected, array $input): void { - Assert::same($expected, Format::base(...$input)); + Assert::same($expected, NumberFormat::base(...$input)); } @@ -126,7 +127,7 @@ protected static function provideUnit(): array ], ], [ - nbsp('1 655 kg'), + Space::nbsp('1 655 kg'), [ 'number' => 1655, 'mask' => '1 kg', @@ -143,7 +144,7 @@ protected static function provideUnit(): array ], ], [ - nbsp('1 655'), + Space::nbsp('1 655'), [ 'number' => 1655, 'decimals' => 0, @@ -305,9 +306,9 @@ protected static function provideUnit(): array */ public function testUnit(string $expected, array $input): void { - Assert::same($expected, Format::unit(...$input)); + Assert::same($expected, NumberFormat::unit(...$input)); } } -(new FormatTest())->run(); +(new NumberFormatTest())->run(); diff --git a/tests/src/PercentTest.php b/tests/src/Number/PercentTest.php similarity index 78% rename from tests/src/PercentTest.php rename to tests/src/Number/PercentTest.php index 1e6dc40..faece25 100644 --- a/tests/src/PercentTest.php +++ b/tests/src/Number/PercentTest.php @@ -1,12 +1,12 @@ convert(1024)->value); diff --git a/tests/src/Number/Units/UnitFormatTest.php b/tests/src/Number/Units/UnitFormatTest.php new file mode 100644 index 0000000..f182728 --- /dev/null +++ b/tests/src/Number/Units/UnitFormatTest.php @@ -0,0 +1,16 @@ +convert(968884224)); +Assert::same(Space::nbsp('1,00 kB'), $uf->convert(1024)); + +Assert::same(Space::nbsp('31 457 280,00'), $uf->fromString('30M')); diff --git a/tests/src/Units/UnitTest.php b/tests/src/Number/Units/UnitTest.php similarity index 69% rename from tests/src/Units/UnitTest.php rename to tests/src/Number/Units/UnitTest.php index 5178316..2f6283c 100644 --- a/tests/src/Units/UnitTest.php +++ b/tests/src/Number/Units/UnitTest.php @@ -1,12 +1,12 @@ convert(1.0, $unit::BASE); Assert::same(1.0, $unitValue->value); Assert::same($unit::BASE, $unitValue->unit); @@ -36,9 +36,34 @@ public function testConvert(): void } + public function testGetter(): void + { + $unit = new Format\Number\Units\Unit(); + $data = $unit->getUnits(); + Assert::same($unit::UNITS, $unit->getUnits()); + Assert::same($unit::BASE, $unit->getFrom()); + } + + + public function testNotAllowedUnit(): void + { + $unit = new Format\Number\Units\Unit(); + Assert::exception(fn () => $unit->convert(0, 'foo'), Format\Exceptions\InvalidArgumentException::class); + + Assert::exception(fn ( + ) => new Format\Number\Units\Unit('foo'), Format\Exceptions\InvalidArgumentException::class); + $unit = new Format\Number\Units\Unit(); + + Assert::exception(fn ( + ) => $unit->convertFrom(0, null, 'foo'), Format\Exceptions\InvalidArgumentException::class); + + Assert::exception(fn () => $unit->convertFrom(0, 'foo'), Format\Exceptions\InvalidArgumentException::class); + } + + public function testDiffBase(): void { - $unit = new Number\Units\Unit(Number\Units\Unit::KILO); + $unit = new Format\Number\Units\Unit(Format\Number\Units\Unit::KILO); $unitValue = $unit->convert(1, $unit::KILO); Assert::same(1.0, $unitValue->value); Assert::same($unit::KILO, $unitValue->unit); @@ -50,12 +75,13 @@ public function testDiffBase(): void $unitValue = $unit->convert(1, $unit::MEGA); Assert::same(0.001, $unitValue->value); Assert::same($unit::MEGA, $unitValue->unit); + Assert::same('0.001 M', (string) $unitValue); } public function testConvertAuto(): void { - $unit = new Number\Units\Unit(); + $unit = new Format\Number\Units\Unit(); $unitValue = $unit->convert(10.0); Assert::same(10.0, $unitValue->value); Assert::same($unit::BASE, $unitValue->unit); @@ -76,7 +102,7 @@ public function testConvertAuto(): void public function testFromString(): void { - $unit = new Number\Units\Unit(); + $unit = new Format\Number\Units\Unit(); $unitValue = $unit->fromString('128M'); Assert::same(128000000.0, $unitValue->value); Assert::same($unit::BASE, $unitValue->unit); @@ -110,11 +136,11 @@ public function testFromString(): void Assert::exception(function () use ($unit) { $unit->fromString('M'); - }, Number\Exceptions\InvalidArgumentException::class); + }, Format\Exceptions\InvalidArgumentException::class); Assert::exception(function () use ($unit) { $unit->fromString('128'); - }, Number\Exceptions\InvalidArgumentException::class); + }, Format\Exceptions\InvalidArgumentException::class); } } diff --git a/tests/src/TestCase.php b/tests/src/TestCase.php index c8fc9b4..f8cc76b 100644 --- a/tests/src/TestCase.php +++ b/tests/src/TestCase.php @@ -1,6 +1,6 @@ convert(968884224)); -Assert::same(nbsp('1,00 kB'), $uf->convert(1024)); - -Assert::same(nbsp('31 457 280,00'), $uf->fromString('30M')); diff --git a/tests/src/Utils/FormatsTest.php b/tests/src/Utils/FormatsTest.php index 83f5a5b..4fc8070 100644 --- a/tests/src/Utils/FormatsTest.php +++ b/tests/src/Utils/FormatsTest.php @@ -1,35 +1,47 @@ fn (Formats $formats): NumberFormat => new NumberFormat(decimals: 0, nbsp: false, unit: '€'), - 'GBP' => new NumberFormat(nbsp: false, unit: '£', mask: '⎵ 1'), -]); - -$formats->add('CZK', new NumberFormat(decimals: 3, nbsp: false, unit: 'CZK')); -$formats->add('USD', static fn (Formats $formats -): NumberFormat => $formats->getDefault()(['unit' => '$'], $formats, 'USD')); -$formats->setDefault(static function (array $options) { - $options['nbsp'] = $options['nbsp'] ?? false; - $options['decimals'] = $options['decimals'] ?? 0; - return new NumberFormat(...$options); -}); - -Assert::exception(function () use ($formats) { - $formats->setDefault(fn () => new NumberFormat(nbsp: false)); -}, InvalidStateException::class); - -Assert::same('100 UNKNOWN', $formats->get('UNKNOWN')->format(100)); -Assert::same('100 $', $formats->get('USD')->format(100)); -Assert::same('100,000 CZK', $formats->get('CZK')->format(100)); -Assert::same('100 CZK', $formats->get('CZK')->modify(decimals: 0)->format(100)); -Assert::same('5 €', $formats->get('EUR')->format(5)); -Assert::same('£ 5,00', $formats->get('GBP')->format(5)); +/** + * @testCase + */ +final class FormatsTest extends TestCase +{ + public function testDefaultIsNotDefinedFailed(): void + { + Assert::throws(fn () => (new Formats())->get('any'), InvalidStateException::class);; + Assert::throws(fn () => (new Formats())->setDefault('any'), InvalidStateException::class);; + } + + + public function testFormats(): void + { + $formats = new Formats([ + 'CZK' => 'Kč', + 'EUR' => static fn (): string => '€', + ]); + $formats->add('GBP', '£'); + $formats->setDefault(static fn (string|int $key): string => "-$key-"); + + Assert::true($formats->has('EUR')); // live + Assert::true($formats->has('CZK')); // factories + Assert::false($formats->has('usd')); + + Assert::same('Kč', $formats->get('CZK')); + Assert::same('€', $formats->get('EUR')); + Assert::same('£', $formats->get('GBP')); + Assert::same('-unknown-', $formats->get('unknown')); + + $formats->add('CZK', static fn (): string => 'Kčs'); + Assert::same('Kčs', $formats->get('CZK')); + } +} + +(new FormatsTest())->run();