├── LICENSE ├── README.md ├── composer.json ├── phpunit.xml.dist └── src ├── Config ├── AbstractConfig.php ├── Area.php ├── Degree.php ├── Info.php ├── Length.php ├── Money.php ├── Temp.php ├── Time.php ├── Volume.php └── Weight.php ├── Exception.php ├── Formatter.php ├── Parser.php └── Type ├── AbstractType.php ├── Area.php ├── Degree.php ├── Info.php ├── Length.php ├── Money.php ├── Temp.php ├── Time.php ├── Volume.php └── Weight.php /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 JBZoo Content Construction Kit (CCK) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # JBZoo / SimpleTypes 2 | 3 | [![CI](https://github.com/JBZoo/SimpleTypes/actions/workflows/main.yml/badge.svg?branch=master)](https://github.com/JBZoo/SimpleTypes/actions/workflows/main.yml?query=branch%3Amaster) [![Coverage Status](https://coveralls.io/repos/github/JBZoo/SimpleTypes/badge.svg?branch=master)](https://coveralls.io/github/JBZoo/SimpleTypes?branch=master) [![Psalm Coverage](https://shepherd.dev/github/JBZoo/SimpleTypes/coverage.svg)](https://shepherd.dev/github/JBZoo/SimpleTypes) [![Psalm Level](https://shepherd.dev/github/JBZoo/SimpleTypes/level.svg)](https://shepherd.dev/github/JBZoo/SimpleTypes) [![CodeFactor](https://www.codefactor.io/repository/github/jbzoo/simpletypes/badge)](https://www.codefactor.io/repository/github/jbzoo/simpletypes/issues) 4 | [![Stable Version](https://poser.pugx.org/jbzoo/simpletypes/version)](https://packagist.org/packages/jbzoo/simpletypes/) [![Total Downloads](https://poser.pugx.org/jbzoo/simpletypes/downloads)](https://packagist.org/packages/jbzoo/simpletypes/stats) [![Dependents](https://poser.pugx.org/jbzoo/simpletypes/dependents)](https://packagist.org/packages/jbzoo/simpletypes/dependents?order_by=downloads) [![GitHub License](https://img.shields.io/github/license/jbzoo/simpletypes)](https://github.com/JBZoo/SimpleTypes/blob/master/LICENSE) 5 | 6 | 7 | The universal PHP library to convert any values and measures - money, weight, currency coverter, length and what ever you want ;) 8 | 9 | 10 | ## Installation 11 | ``` 12 | composer require jbzoo/simpletypes 13 | ``` 14 | 15 | ## Examples 16 | 17 | ```php 18 | use JBZoo\SimpleTypes\Config; 19 | use JBZoo\SimpleTypes\Money; 20 | use JBZoo\SimpleTypes\ConfigMoney; 21 | 22 | // Set config object for all Money objects as default 23 | Config::registerDefault('money', new ConfigMoney()); 24 | 25 | // Create any object, some different ways 26 | $money = new Money('10 eur'); 27 | $weight = new Weight('1000'); // Gram is default in the ConfigWeight class 28 | $length = new Length('500 km'); 29 | $money = new Money('100500 usd', new ConfigMoney()); // my custom params only for that object 30 | ``` 31 | 32 | ## A lot of types are ready to use 33 | 34 | SimpleTypes has such ready configurations like 35 | * [Area](https://github.com/JBZoo/SimpleTypes/blob/master/src/config/area.php) 36 | * [Degree](https://github.com/JBZoo/SimpleTypes/blob/master/src/config/degree.php) (geometry) 37 | * [Info](https://github.com/JBZoo/SimpleTypes/blob/master/src/config/info.php) (bytes, bits...) 38 | * [Length](https://github.com/JBZoo/SimpleTypes/blob/master/src/config/length.php) 39 | * [Money](https://github.com/JBZoo/SimpleTypes/blob/master/src/config/money.php) (Currency converter) 40 | * [Temperature](https://github.com/JBZoo/SimpleTypes/blob/master/src/config/temp.php) (Kelvin, Fahrenheit, Celsius and etc) 41 | * [Volume](https://github.com/JBZoo/SimpleTypes/blob/master/src/config/volume.php) 42 | * [Weight](https://github.com/JBZoo/SimpleTypes/blob/master/src/config/weight.php) 43 | 44 | You can add your own type. It's really easy. See this page below. 45 | 46 | ### Smart and useful parser 47 | 48 | SimpleTypes has really smart parser for all input values. 49 | It can find number, understand any decimal symbols, trim, letter cases, e.t.c... 50 | and it works really fast! 51 | 52 | ```php 53 | $money = new Money(' - 1 2 3 , 4 5 6 rub '); // Equals -123.456 rubles 54 | $money = new Money('1.0e+18 EUR '); // Really huge number. I'm rich! =) 55 | $money = new Money(' EuR 3,50 '); 56 | $money = new Money('usd'); // Just object with usd rule 57 | ``` 58 | 59 | ### Chaining method calls 60 | ```php 61 | $value = (new Money('4.95 usd')) 62 | ->add('10 usd') // $14.95 63 | ->subtract('2 eur') // $10.95 64 | ->negative() // -$10.95 65 | ->getClone() // copy of object is created 66 | ->division(5) // -$2.19 67 | ->multiply(10) // -$21.90 68 | ->convert('eur') // -10.95€ (For easy understanding we use 1 EUR = 2 USD) 69 | ->customFunc(function (Money $value) { // sometimes we would like something more than plus/minus ;) 70 | $value 71 | ->add(new Money('600 rub')) // 1.05€ (1 EUR = 50 RUB) 72 | ->add('-500%'); // -4.2€ 73 | }) 74 | ->abs(); // 4.2€ 75 | ``` 76 | 77 | ## Basic arithmetic 78 | Different ways to use basic arithmetic 79 | ```php 80 | // example #1 81 | $usd = new Money('10 usd'); 82 | $usd->add(new Money('10 eur')); 83 | 84 | // example #2 85 | $usd = (new Money('10 usd'))->add(new Money('10 eur')); 86 | 87 | // example #3 88 | $usd->add('10 eur'); 89 | 90 | // example #4 91 | $usd->add('10'); // eur is default in the ConfigMoney 92 | 93 | // example #5 94 | $usd->add(['10', 'eur']); 95 | ``` 96 | 97 | SimpleTypes can 98 | * add 99 | * subtract 100 | * division 101 | * multiply 102 | * use custom functions (Closure) 103 | * negative / positibe / invert sign / abs 104 | * use percent 105 | * setting empty value 106 | * setting another value and rule 107 | * create clone 108 | * converting to any rules (currencies, units) 109 | * rounding 110 | * comparing 111 | * ... and others 112 | 113 | ## Compare values 114 | 115 | ```php 116 | $kg = new Weight('1 kg'); // one kilogram 117 | $lb = new Weight('2 lb'); // two pounds 118 | 119 | var_dump($kg->compare($lb)); // false ("==" by default) 120 | var_dump($kg->compare($lb, '==')); // false 121 | var_dump($kg->compare($lb, '<')); // false 122 | var_dump($kg->compare($lb, '<=')); // false 123 | var_dump($kg->compare($lb, '>')); // true 124 | var_dump($kg->compare($lb, '>=')); // true 125 | ``` 126 | 127 | And same examples but we will use smart parser 128 | ```php 129 | $kg = new Weight('1 kg'); 130 | $lb = new Weight('2 lb'); 131 | 132 | var_dump($kg->compare('1000 g')); // true 133 | var_dump($kg->compare('2 lb', '==')); // false 134 | var_dump($kg->compare('2 lb', '<')); // false 135 | var_dump($kg->compare('2 lb', '<=')); // false 136 | var_dump($kg->compare('2 lb', '>')); // true 137 | var_dump($kg->compare('2 lb', '>=')); // true 138 | ``` 139 | 140 | ## Percent method 141 | Simple way for count difference between two values 142 | ```php 143 | $origPrice = new Money('100 usd'); 144 | $realPrice = new Money('40 eur'); 145 | 146 | $diff = $realPrice->percent($origPrice); 147 | echo $diff->text(); // 80% 148 | 149 | $discount = $realPrice->percent($origPrice, true); // revert flag added 150 | echo $discount->text(); // 20% 151 | ``` 152 | 153 | ## PHP magic methods 154 | 155 | Safe serialize/unserialize 156 | ```php 157 | $valBefore = $this->val('500 usd'); 158 | $valString = serialize($valBefore); 159 | $valAfter = unserialize($valString)->convert('eur'); 160 | $valBefore->compare($valAfter);// true 161 | ``` 162 | 163 | __toString() works like text() method 164 | ```php 165 | $val = $this->val('500 usd'); 166 | echo $val; // "$500.00" 167 | ``` 168 | 169 | __invoke() 170 | ```php 171 | $val = $this->val('10 eur'); 172 | // it's converting 173 | $val('usd'); // so object now contains "20 usd" (1 eur = 2 usd) 174 | // set new value and rule 175 | $val('100 rub'); 176 | $val('100', 'uah'); 177 | ``` 178 | 179 | ## Different ways for output and rendering 180 | ### Only text 181 | 182 | ```php 183 | $value = new Money('-50.666666 usd'); 184 | echo $value->text(); // "-$50.67" 185 | echo $value->text('rub'); // "-1 266,67 руб." (output without changing inner state) 186 | echo $value->noStyle('rub'); // "-1 266,67" (without symbol) 187 | ``` 188 | 189 | ### Simple HTML rendering 190 | ```php 191 | echo (new Money('-50.666666 usd'))->html('rub'); // render HTML, useful for JavaScript 192 | ``` 193 | Output (warping added just for clarity) 194 | ```php 195 | 202 | -1 266,67 203 | руб. 204 | 205 | ``` 206 | 207 | ### HTML Input type[text] 208 | ```php 209 | echo $value->htmlInput('rub', 'input-name-attr'); 210 | ``` 211 | Output (warping added just for clarity) 212 | ```html 213 | 224 | ``` 225 | 226 | **Notice:** Yes, we added a lot of data-attributes in the HTML code. It will be useful for JavaScript and converting without reload a page. 227 | 228 | 229 | ## Configuration of type 230 | 231 | All configuration classes should be extended from Config class 232 | For example, config for information 233 | ```php 234 | /** 235 | * Class ConfigInfo 236 | * @package JBZoo\SimpleTypes 237 | */ 238 | class ConfigInfo extends Config 239 | { 240 | /** 241 | * SimpleTypes uses it for converting and while parsing undefined values 242 | * @var string 243 | */ 244 | public $default = 'byte'; 245 | 246 | /** 247 | * To collect or not to collect logs for each object (need additional memory a little bit) 248 | * @var bool 249 | */ 250 | public $isDebug = true; 251 | 252 | /** 253 | * Array of converting rules and output format 254 | * return array 255 | */ 256 | public function getRules() 257 | { 258 | // key of array is alias for parser 259 | return array( 260 | 'byte' => array( 261 | 'rate' => 1 // Because 1 byte to byte is 1 =))) 262 | ), 263 | 264 | 'kb' => array( 265 | 'symbol' => 'KB', // symbol for output (->text(), ->html(), ...) 266 | 'round_type' => Formatter::ROUND_CLASSIC, // classic, float, ceil, none 267 | 'round_value' => Formatter::ROUND_DEFAULT, // Count of valuable number after decimal point for any arithmetic actions 268 | 'num_decimals' => '2', // Sets the number of decimal points 269 | 'decimal_sep' => '.', // Sets the separator for the decimal point. 270 | 'thousands_sep' => ' ', // Sets the thousands separator. 271 | 'format_positive' => '%v %s', // %v - replace to rounded and formated (number_format()) value 272 | 'format_negative' => '-%v %s', // %s - replace to symbol 273 | 'rate' => 1024, // How many bytes (default measure) in the 1 KB ? 274 | ), 275 | 276 | 'mb' => array( // Other params gets from $this->defaultParams variable 277 | 'symbol' => 'MB', 278 | 'rate' => 1024 * 1024, 279 | ), 280 | 281 | 'gb' => array( // Other params gets from $this->defaultParams variable 282 | 'symbol' => 'GB', 283 | 'rate' => 1024 * 1024 * 1024, 284 | ), 285 | 286 | 'bit' => array( 287 | 'symbol' => 'Bit', 288 | 'rate' => function ($value, $to) { // Custom callback function for difficult conversion 289 | if ($to == 'bit') { 290 | return $value * 8; 291 | } 292 | return $value / 8; 293 | }, 294 | ), 295 | ); 296 | } 297 | } 298 | ``` 299 | 300 | Usage example for our information type 301 | ```php 302 | // create config object 303 | $config = new ConfigInfo(); 304 | 305 | // you can register default config for all info-objects, 306 | Config::registerDefault('info', $config); 307 | $info1 = new Info('700 MB'); 308 | $info2 = new Info('1.4 GB'); 309 | 310 | // or add config object manually 311 | $info1 = new Info('700 MB', $config); 312 | $info2 = new Info('1.4 GB', $config); 313 | 314 | // Well... some calculations 315 | echo $info2->subtract($info1)->dump() . PHP_EOL; 316 | echo $info2->convert('mb')->dump() . PHP_EOL; 317 | print_r($info2->logs()); 318 | ``` 319 | 320 | Output 321 | ``` 322 | 0.71640625 gb; id=4 323 | 733.6 mb; id=4 324 | Array 325 | ( 326 | [0] => Id=4 has just created; dump="1.4 gb" 327 | [1] => Subtract "700 mb"; New value = "0.71640625 gb" 328 | [2] => Converted "gb"=>"mb"; New value = "733.6 mb"; 1 gb = 1024 mb 329 | ) 330 | ``` 331 | 332 | ### Debug information 333 | Show list of all actions with object. For example, this is history for chaining code 334 | ```php 335 | print_r($value->logs()); 336 | 337 | /** 338 | * Array 339 | * ( 340 | * [0] => Id=16 has just created; dump="4.95 usd" 341 | * [1] => Add "10 usd"; New value = "14.95 usd" 342 | * [2] => Subtract "2 eur"; New value = "10.95 usd" 343 | * [3] => Set negative; New value = "-10.95 usd" 344 | * [4] => Cloned from id=16 and created new with id=19; dump=-10.95 usd 345 | * [5] => Division with "5"; New value = "-2.19 usd" 346 | * [6] => Multiply with "10"; New value = "-21.9 usd" 347 | * [7] => Converted "usd"=>"eur"; New value = "-10.95 eur"; 1 usd = 0.5 eur 348 | * [8] => --> Function start 349 | * [9] => Add "600 rub"; New value = "1.05 eur" 350 | * [10] => Add "-500 %"; New value = "-4.2 eur" 351 | * [11] => <-- Function finished; New value = "-4.2 eur" 352 | * [12] => Set positive/abs; New value = "4.2 eur" 353 | * ) 354 | */ 355 | ``` 356 | 357 | Show real inner data without any formating and rounding. ID is unique number for SimpleType objects. 358 | ```php 359 | echo $value->dump(); // "4.2 eur; id=19" 360 | ``` 361 | 362 | Get object id 363 | ```php 364 | echo $value->getId(); // "19" 365 | ``` 366 | Show current value 367 | ```php 368 | echo $value->val(); // "4.2" 369 | ``` 370 | 371 | Show current rule 372 | ```php 373 | echo $value->rule(); // "eur" 374 | ``` 375 | 376 | ## License 377 | MIT 378 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name" : "jbzoo/simpletypes", 3 | "type" : "library", 4 | "description" : "The universal PHP library to convert any values and measures", 5 | "license" : "MIT", 6 | "keywords" : [ 7 | "converter", 8 | "acceleration", 9 | "area", 10 | "degree", 11 | "info", 12 | "length", 13 | "money", 14 | "number", 15 | "pressure", 16 | "speed", 17 | "temperature", 18 | "time", 19 | "volume", 20 | "weight" 21 | ], 22 | "authors" : [ 23 | { 24 | "name" : "Denis Smetannikov", 25 | "email" : "admin@jbzoo.com", 26 | "role" : "lead" 27 | } 28 | ], 29 | 30 | "minimum-stability" : "dev", 31 | "prefer-stable" : true, 32 | 33 | "require" : { 34 | "php" : "^8.1", 35 | "jbzoo/utils" : "^7.1" 36 | }, 37 | 38 | "require-dev" : { 39 | "jbzoo/toolbox-dev" : "^7.1" 40 | }, 41 | 42 | "autoload" : { 43 | "psr-4" : {"JBZoo\\SimpleTypes\\" : "src"} 44 | }, 45 | 46 | "autoload-dev" : { 47 | "psr-4" : {"JBZoo\\PHPUnit\\" : "tests"}, 48 | "files" : ["tests/phpunit-functions.php"] 49 | }, 50 | 51 | "config" : { 52 | "optimize-autoloader" : true, 53 | "allow-plugins" : {"composer/package-versions-deprecated" : true} 54 | }, 55 | 56 | "extra" : { 57 | "branch-alias" : { 58 | "dev-master" : "7.x-dev" 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 13 | 14 | 15 | 16 | tests 17 | 18 | 19 | 20 | 21 | 22 | src 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /src/Config/AbstractConfig.php: -------------------------------------------------------------------------------- 1 | '', 45 | 'rate' => 1, 46 | 47 | // number format 48 | 'num_decimals' => '2', 49 | 'decimal_sep' => '.', 50 | 'thousands_sep' => ' ', 51 | 52 | // templates 53 | 'format_positive' => '%v %s', 54 | 'format_negative' => '-%v %s', 55 | 56 | // round 57 | 'round_type' => Formatter::ROUND_CLASSIC, 58 | 'round_value' => Formatter::ROUND_DEFAULT, 59 | ]; 60 | 61 | /** 62 | * List of rules. 63 | * @return array 64 | */ 65 | abstract public function getRules(); 66 | 67 | public static function registerDefault(string $type, self $config): void 68 | { 69 | $type = \strtolower(\trim($type)); 70 | self::$configs[$type] = $config; 71 | } 72 | 73 | public static function getDefault(string $type): ?self 74 | { 75 | $type = \strtolower(\trim($type)); 76 | 77 | return self::$configs[$type] ?? null; 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/Config/Area.php: -------------------------------------------------------------------------------- 1 | default = 'm2'; 24 | } 25 | 26 | /** 27 | * {@inheritDoc} 28 | */ 29 | public function getRules(): array 30 | { 31 | return [ 32 | // SI 33 | 'mm2' => [ 34 | 'symbol' => 'mm2', 35 | 'rate' => 0.000001, 36 | ], 37 | 'cm2' => [ 38 | 'symbol' => 'cm2', 39 | 'rate' => 0.0001, 40 | ], 41 | 'm2' => [ 42 | 'symbol' => 'm2', 43 | 'rate' => 1, 44 | ], 45 | 'km2' => [ 46 | 'symbol' => 'km2', 47 | 'rate' => 1000000, 48 | ], 49 | 50 | // other 51 | 'ft2' => [ 52 | 'symbol' => 'sq ft', 53 | 'rate' => 0.09290341, 54 | ], 55 | 'ch2' => [ 56 | 'symbol' => 'sq ch', 57 | 'rate' => 404.6873, 58 | ], 59 | 'acr' => [ 60 | 'symbol' => 'Acre', 61 | 'rate' => 4046.873, 62 | ], 63 | 'ar' => [ 64 | 'symbol' => 'Ar', 65 | 'rate' => 100, 66 | ], 67 | 'ga' => [ 68 | 'symbol' => 'Ga', 69 | 'rate' => 10000, 70 | ], 71 | ]; 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/Config/Degree.php: -------------------------------------------------------------------------------- 1 | default = 'd'; 24 | } 25 | 26 | /** 27 | * {@inheritDoc} 28 | */ 29 | public function getRules(): array 30 | { 31 | return [ 32 | // degree 33 | 'd' => [ 34 | 'format_positive' => '%v%s', 35 | 'format_negative' => '-%v%s', 36 | 'symbol' => '°', 37 | ], 38 | 39 | // radian 40 | 'r' => [ 41 | 'symbol' => 'pi', 42 | 'rate' => static function (float $value, string $ruleTo): float { 43 | if ($ruleTo === 'd') { 44 | return $value * 180; 45 | } 46 | 47 | return $value / 180; 48 | }, 49 | ], 50 | 51 | // grads 52 | 'g' => [ 53 | 'symbol' => 'Grad', 54 | 'rate' => static function (float $value, string $ruleTo): float { 55 | if ($ruleTo === 'd') { 56 | return $value * 0.9; 57 | } 58 | 59 | return $value / 0.9; 60 | }, 61 | ], 62 | 63 | // turn (loop) 64 | 't' => [ 65 | 'symbol' => 'Turn', 66 | 'rate' => 360, 67 | ], 68 | ]; 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/Config/Info.php: -------------------------------------------------------------------------------- 1 | default = 'byte'; 26 | } 27 | 28 | /** 29 | * {@inheritDoc} 30 | */ 31 | public function getRules(): array 32 | { 33 | $base = 1024; 34 | 35 | $this->defaultParams['num_decimals'] = 0; 36 | $this->defaultParams['round_type'] = Formatter::ROUND_NONE; 37 | 38 | return [ 39 | 'byte' => [ 40 | 'symbol' => 'B', 41 | 'rate' => 1, 42 | ], 43 | 'kb' => [ 44 | 'symbol' => 'KB', 45 | 'rate' => $base ** 1, 46 | ], 47 | 'mb' => [ 48 | 'symbol' => 'MB', 49 | 'rate' => $base ** 2, 50 | ], 51 | 'gb' => [ 52 | 'symbol' => 'GB', 53 | 'rate' => $base ** 3, 54 | ], 55 | 'tb' => [ 56 | 'symbol' => 'TB', 57 | 'rate' => $base ** 4, 58 | ], 59 | 'pb' => [ 60 | 'symbol' => 'PB', 61 | 'rate' => $base ** 5, 62 | ], 63 | 'eb' => [ 64 | 'symbol' => 'EB', 65 | 'rate' => $base ** 6, 66 | ], 67 | 'zb' => [ 68 | 'symbol' => 'ZB', 69 | 'rate' => $base ** 7, 70 | ], 71 | 'yb' => [ 72 | 'symbol' => 'YB', 73 | 'rate' => $base ** 8, 74 | ], 75 | 76 | 'bit' => [ 77 | 'symbol' => 'Bit', 78 | 'rate' => static function (float $value, string $ruleTo) { 79 | if ($ruleTo === 'bit') { 80 | return $value * 8; 81 | } 82 | 83 | return $value / 8; 84 | }, 85 | ], 86 | ]; 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/Config/Length.php: -------------------------------------------------------------------------------- 1 | default = 'm'; 24 | } 25 | 26 | /** 27 | * {@inheritDoc} 28 | */ 29 | public function getRules(): array 30 | { 31 | return [ 32 | // SI 33 | 'mm' => [ 34 | 'symbol' => 'mm', 35 | 'rate' => 0.001, 36 | ], 37 | 'cm' => [ 38 | 'symbol' => 'cm', 39 | 'rate' => 0.01, 40 | ], 41 | 'dm' => [ 42 | 'symbol' => 'dm', 43 | 'rate' => 0.1, 44 | ], 45 | 'm' => [ 46 | 'symbol' => 'm', 47 | 'rate' => 1, 48 | ], 49 | 'km' => [ 50 | 'symbol' => 'km', 51 | 'rate' => 1000, 52 | ], 53 | 54 | // others 55 | 'p' => [ 56 | 'symbol' => 'Point', 57 | 'rate' => 0.000352777778, 58 | ], 59 | 'li' => [ 60 | 'symbol' => 'Link', 61 | 'rate' => 0.2012, 62 | ], 63 | 'in' => [ 64 | 'symbol' => 'Inches', 65 | 'rate' => 0.0254, 66 | ], 67 | 'ft' => [ 68 | 'symbol' => 'Foot', 69 | 'rate' => 0.3048, 70 | ], 71 | 'yd' => [ 72 | 'symbol' => 'Yard', 73 | 'rate' => 0.9144, 74 | ], 75 | 'mi' => [ 76 | 'symbol' => 'Mile', 77 | 'rate' => 1609.344, 78 | ], 79 | ]; 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/Config/Money.php: -------------------------------------------------------------------------------- 1 | default = 'eur'; 26 | } 27 | 28 | /** 29 | * {@inheritDoc} 30 | */ 31 | public function getRules(): array 32 | { 33 | $this->defaultParams['num_decimals'] = 2; 34 | $this->defaultParams['round_type'] = Formatter::ROUND_CLASSIC; 35 | $this->defaultParams['decimal_sep'] = '.'; 36 | $this->defaultParams['thousands_sep'] = ' '; 37 | $this->defaultParams['format_positive'] = '%v %s'; 38 | $this->defaultParams['format_negative'] = '-%v %s'; 39 | 40 | return [ 41 | 'eur' => [ 42 | 'symbol' => '€', 43 | 'rate' => 1, 44 | ], 45 | 46 | 'usd' => [ 47 | 'symbol' => '$', 48 | 'format_positive' => '%s%v', 49 | 'format_negative' => '-%s%v', 50 | 'rate' => 0.5, 51 | ], 52 | 53 | 'rub' => [ 54 | 'symbol' => 'руб.', 55 | 'decimal_sep' => ',', 56 | 'rate' => 0.02, 57 | ], 58 | 59 | 'uah' => [ 60 | 'symbol' => 'грн.', 61 | 'decimal_sep' => ',', 62 | 'rate' => 0.04, 63 | ], 64 | 65 | 'byr' => [ 66 | 'symbol' => 'Br', 67 | 'round_type' => Formatter::ROUND_CEIL, 68 | 'round_value' => '-2', 69 | 'num_decimals' => '0', 70 | 'rate' => 0.00005, 71 | ], 72 | 73 | '%' => [ 74 | 'symbol' => '%', 75 | 'format_positive' => '%v%s', 76 | 'format_negative' => '-%v%s', 77 | ], 78 | ]; 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/Config/Temp.php: -------------------------------------------------------------------------------- 1 | default = 'k'; 24 | } 25 | 26 | /** 27 | * {@inheritDoc} 28 | */ 29 | public function getRules(): array 30 | { 31 | $this->defaultParams['format_positive'] = '%v%s'; 32 | $this->defaultParams['format_negative'] = '-%v%s'; 33 | 34 | return [ 35 | // Celsius 36 | 'C' => [ 37 | 'symbol' => '°C', 38 | 'rate' => static function (float $value, string $ruleTo): float { 39 | if ($ruleTo === 'k') { 40 | $value += 273.15; 41 | } else { 42 | $value -= 273.15; 43 | } 44 | 45 | return $value; 46 | }, 47 | ], 48 | 49 | // Fahrenheit 50 | 'F' => [ 51 | 'symbol' => '°F', 52 | 'rate' => static function (float $value, string $ruleTo): float { 53 | if ($ruleTo === 'k') { 54 | $value = ($value + 459.67) * (5 / 9); 55 | } else { 56 | $value = $value * (9 / 5) - 459.67; 57 | } 58 | 59 | return $value; 60 | }, 61 | ], 62 | 63 | // Rankine 64 | 'R' => [ 65 | 'symbol' => '°R', 66 | 'rate' => static function (float $value, string $ruleTo): float { 67 | if ($ruleTo === 'k') { 68 | $value = $value * 5 / 9; 69 | } else { 70 | $value = $value * 9 / 5; 71 | } 72 | 73 | return $value; 74 | }, 75 | ], 76 | 77 | // Kelvin 78 | 'K' => [ 79 | 'symbol' => 'K', 80 | 'rate' => static fn (float $value): float => $value, 81 | ], 82 | ]; 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/Config/Time.php: -------------------------------------------------------------------------------- 1 | default = 's'; 24 | } 25 | 26 | /** 27 | * {@inheritDoc} 28 | */ 29 | public function getRules(): array 30 | { 31 | return [ 32 | 's' => [ 33 | 'symbol' => 'Sec', 34 | 'rate' => 1, 35 | ], 36 | 'm' => [ 37 | 'symbol' => 'Min', 38 | 'rate' => 60, 39 | ], 40 | 'h' => [ 41 | 'symbol' => 'H', 42 | 'rate' => 3600, 43 | ], 44 | 'd' => [ 45 | 'symbol' => 'Day', 46 | 'rate' => 86400, 47 | ], 48 | 'w' => [ 49 | 'symbol' => 'Week', 50 | 'rate' => 604800, 51 | ], 52 | 'mo' => [ 53 | 'symbol' => 'Month', // Only 30 days! 54 | 'rate' => 2592000, 55 | ], 56 | 'q' => [ 57 | 'symbol' => 'Quarter', // 3 months 58 | 'rate' => 7776000, 59 | ], 60 | 'y' => [ 61 | 'symbol' => 'Year', // 365.25 days 62 | 'rate' => 31557600, 63 | ], 64 | ]; 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/Config/Volume.php: -------------------------------------------------------------------------------- 1 | default = 'lit'; 24 | } 25 | 26 | /** 27 | * {@inheritDoc} 28 | * @see https://en.wikipedia.org/wiki/United_States_customary_units 29 | */ 30 | public function getRules(): array 31 | { 32 | return [ 33 | // SI 34 | 'ml' => [ 35 | 'symbol' => 'mL', 36 | 'rate' => 0.001, 37 | ], 38 | 'cm3' => [ 39 | 'symbol' => 'cm3', 40 | 'rate' => 0.1, 41 | ], 42 | 'm3' => [ 43 | 'symbol' => 'm3', 44 | 'rate' => 1000, 45 | ], 46 | 'lit' => [ 47 | 'symbol' => 'L', 48 | 'rate' => 1, 49 | ], 50 | // other 51 | 'qt' => [ 52 | 'symbol' => 'US quart', 53 | 'rate' => 0.946352946, 54 | ], 55 | 'pt' => [ 56 | 'symbol' => 'US pint', 57 | 'rate' => 0.56826125, 58 | ], 59 | 'gal' => [ 60 | 'symbol' => 'US gallon', 61 | 'rate' => 3.785411784, 62 | ], 63 | 'bbl' => [ 64 | 'symbol' => 'Barrel', 65 | 'rate' => 119.240471196, 66 | ], 67 | ]; 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/Config/Weight.php: -------------------------------------------------------------------------------- 1 | default = 'g'; 24 | } 25 | 26 | /** 27 | * {@inheritDoc} 28 | */ 29 | public function getRules(): array 30 | { 31 | return [ 32 | // SI 33 | 'g' => ['symbol' => 'g', 'rate' => 1], 34 | 'kg' => ['symbol' => 'Kg', 'rate' => 1000], 35 | 'ton' => ['symbol' => 'Tons', 'rate' => 1000000], 36 | 37 | // other 38 | 'gr' => ['symbol' => 'Grains', 'rate' => 0.06479891], 39 | 'dr' => ['symbol' => 'Drams', 'rate' => 1.7718451953125], 40 | 'oz' => ['symbol' => 'Ounces', 'rate' => 28.349523125], 41 | 'lb' => ['symbol' => 'Pounds', 'rate' => 453.59237], 42 | ]; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/Exception.php: -------------------------------------------------------------------------------- 1 | type = $type; 35 | $this->default = $default; 36 | 37 | // prepare rules 38 | $this->rules = \array_change_key_case($rules); 39 | 40 | foreach ($this->rules as $key => $item) { 41 | $this->rules[$key] = \array_merge($default, (array)$item); 42 | } 43 | } 44 | 45 | public function get(string $rule): array 46 | { 47 | if (\array_key_exists($rule, $this->rules)) { 48 | return (array)$this->rules[$rule]; 49 | } 50 | 51 | throw new Exception("Undefined rule: '{$rule}'"); 52 | } 53 | 54 | public function getList(bool $keysOnly = false): array 55 | { 56 | if ($keysOnly) { 57 | $keys = \array_keys($this->rules); 58 | $values = \array_keys($this->rules); 59 | 60 | return \array_combine($keys, $values); 61 | } 62 | 63 | return $this->rules; 64 | } 65 | 66 | public function text(float $value, string $rule, bool $showSymbol = true): string 67 | { 68 | $data = $this->format($value, $rule); 69 | $rData = $this->get($rule); 70 | $symbol = $showSymbol ? $rData['symbol'] : ''; 71 | 72 | $result = \str_replace( 73 | ['%v', '%s'], 74 | [$data['value'], $symbol], 75 | (string)$data['template'], 76 | ); 77 | 78 | return \trim($result); 79 | } 80 | 81 | public function html(array $current, array $orig, array $params): string 82 | { 83 | $data = $this->format($current['value'], $current['rule']); 84 | $rData = $this->get($current['rule']); 85 | 86 | $result = \str_replace( 87 | ['%v', '%s'], 88 | [ 89 | "{$data['value']}", 90 | "{$rData['symbol']}", 91 | ], 92 | (string)$data['template'], 93 | ); 94 | 95 | return ' [ 97 | 'simpleType', 98 | 'simpleType-block', 99 | "simpleType-{$this->type}", 100 | ], 101 | 'data-simpleType-id' => $params['id'], 102 | 'data-simpleType-value' => $current['value'], 103 | 'data-simpleType-rule' => $current['rule'], 104 | 'data-simpleType-orig-value' => $orig['value'], 105 | 'data-simpleType-orig-rule' => $orig['rule'], 106 | ]) . ">{$result}"; 107 | } 108 | 109 | public function htmlInput(array $current, array $orig, array $params): string 110 | { 111 | $inputValue = $params['formatted'] 112 | ? $this->text($current['value'], $current['rule']) 113 | : $this->text($current['value'], $current['rule'], false); 114 | 115 | return ' $inputValue, 117 | 'name' => $params['name'], 118 | 'type' => 'text', 119 | 'class' => [ 120 | 'simpleType', 121 | "simpleType-{$this->type}", 122 | 'simpleType-input', 123 | ], 124 | 'data-simpleType-id' => $params['id'], 125 | 'data-simpleType-value' => $current['value'], 126 | 'data-simpleType-rule' => $current['rule'], 127 | 'data-simpleType-orig-value' => $orig['value'], 128 | 'data-simpleType-orig-rule' => $orig['rule'], 129 | ]) . ' />'; 130 | } 131 | 132 | public function round(float $value, string $rule, array $params = []): float 133 | { 134 | $format = $this->get($rule); 135 | 136 | // prepare params 137 | $params = \array_merge(['roundType' => null, 'roundValue' => null], $params); 138 | 139 | // get vars 140 | $roundType = $params['roundType']; 141 | $roundValue = $params['roundValue']; 142 | 143 | if ($roundType === null) { 144 | $roundType = \array_key_exists('round_type', $format) ? $format['round_type'] : self::ROUND_NONE; 145 | } 146 | 147 | if ($roundValue === null) { 148 | $roundValue = \array_key_exists('round_value', $format) ? $format['round_value'] : self::ROUND_DEFAULT; 149 | } 150 | 151 | $roundValue = (int)$roundValue; 152 | 153 | if ($roundType === self::ROUND_CEIL) { 154 | $base = 10 ** $roundValue; 155 | $value = \ceil($value * $base) / $base; 156 | } elseif ($roundType === self::ROUND_CLASSIC) { 157 | $value = \round($value, $roundValue); 158 | } elseif ($roundType === self::ROUND_FLOOR) { 159 | $base = 10 ** $roundValue; 160 | $value = \floor($value * $base) / $base; 161 | } elseif ($roundType === self::ROUND_NONE) { 162 | $value = \round($value, self::ROUND_DEFAULT); // hack, because 123.400000001 !== 123.4 163 | } else { 164 | throw new Exception("Undefined round mode: '{$roundType}'"); 165 | } 166 | 167 | return $value; 168 | } 169 | 170 | public function changeRule(string $rule, array $newFormat): void 171 | { 172 | $oldFormat = $this->get($rule); 173 | 174 | $this->rules[$rule] = \array_merge($oldFormat, $newFormat); 175 | } 176 | 177 | public function addRule(string $rule, array $newFormat = []): void 178 | { 179 | if ($rule === '') { 180 | throw new Exception('Empty rule name'); 181 | } 182 | 183 | if (\array_key_exists($rule, $this->rules)) { 184 | throw new Exception("Format '{$rule}' already exists"); 185 | } 186 | 187 | $this->rules[$rule] = \array_merge($this->default, $newFormat); 188 | } 189 | 190 | public function removeRule(string $rule): bool 191 | { 192 | if (\array_key_exists($rule, $this->rules)) { 193 | unset($this->rules[$rule]); 194 | 195 | return true; 196 | } 197 | 198 | return false; 199 | } 200 | 201 | public static function htmlAttributes(array $attributes): string 202 | { 203 | $result = ''; 204 | 205 | foreach ($attributes as $key => $param) { 206 | $value = \implode(' ', (array)$param); 207 | $value = \htmlspecialchars($value, \ENT_QUOTES, 'UTF-8'); 208 | $value = \trim($value); 209 | $result .= " {$key}=\"{$value}\""; 210 | } 211 | 212 | return \trim($result); 213 | } 214 | 215 | /** 216 | * Convert value to money format from config. 217 | */ 218 | private function format(float $value, string $rule): array 219 | { 220 | $format = $this->get($rule); 221 | 222 | $roundedValue = $this->round($value, $rule); 223 | $isPositive = ($value >= 0); 224 | $valueStr = \number_format( 225 | \abs($roundedValue), 226 | (int)($format['num_decimals'] ?? 0), 227 | (string)($format['decimal_sep'] ?? '.'), 228 | (string)($format['thousands_sep'] ?? ''), 229 | ); 230 | 231 | $template = $isPositive ? $format['format_positive'] : $format['format_negative']; 232 | 233 | return [ 234 | 'value' => $valueStr, 235 | 'template' => $template, 236 | 'isPositive' => $isPositive, 237 | ]; 238 | } 239 | } 240 | -------------------------------------------------------------------------------- /src/Parser.php: -------------------------------------------------------------------------------- 1 | \strlen($item2) - \strlen($item1); 29 | 30 | \uksort($ruleList, $sortFunction); 31 | 32 | $this->rules = $ruleList; 33 | $this->default = $default; 34 | } 35 | 36 | public function parse(mixed $data = null, ?string $forceRule = null): array 37 | { 38 | $rule = null; 39 | 40 | if (\is_array($data)) { 41 | $value = $data[0] ?? null; 42 | $rule = $data[1] ?? null; 43 | 44 | return $this->parse($value, $rule); 45 | } 46 | 47 | $value = \strtolower(\trim((string)$data)); 48 | $aliases = $this->getCodeList(); 49 | 50 | foreach ($aliases as $alias) { 51 | if (\str_contains($value, $alias)) { 52 | $rule = $alias; 53 | $value = \str_ireplace($rule, '', $value); 54 | break; 55 | } 56 | } 57 | 58 | /** @phan-suppress-next-line PhanPartialTypeMismatchArgument */ 59 | $value = self::cleanValue($value); 60 | $rule = $this->checkRule($rule); 61 | 62 | if (!isStrEmpty($forceRule)) { 63 | $rule = $forceRule; 64 | } 65 | 66 | return [$value, $rule]; 67 | } 68 | 69 | public function getCodeList(): array 70 | { 71 | return \array_keys($this->rules); 72 | } 73 | 74 | public function checkRule(?string $rule): string 75 | { 76 | $cleanRule = self::cleanRule($rule); 77 | 78 | if (isStrEmpty($cleanRule)) { 79 | return $this->default; 80 | } 81 | 82 | if (\array_key_exists($cleanRule, $this->rules)) { 83 | return $cleanRule; 84 | } 85 | 86 | throw new Exception("Undefined rule: {$cleanRule}"); 87 | } 88 | 89 | public function addRule(string $newRule): void 90 | { 91 | $this->rules[$newRule] = $newRule; 92 | } 93 | 94 | public function removeRule(string $rule): bool 95 | { 96 | if (\array_key_exists($rule, $this->rules)) { 97 | unset($this->rules[$rule]); 98 | 99 | return true; 100 | } 101 | 102 | return false; 103 | } 104 | 105 | public static function cleanValue(null|float|int|string $value): float 106 | { 107 | $result = \trim((string)$value); 108 | 109 | $result = (string)\preg_replace('#[^0-9-+eE,.]#', '', $result); 110 | 111 | if (\preg_match('#\d[eE][-+]\d#', $result) === 0) { // TODO: Remove exponential format 112 | $result = \str_replace(['e', 'E'], '', $result); 113 | } 114 | 115 | $result = (float)\str_replace(',', '.', $result); 116 | 117 | return \round($result, Formatter::ROUND_DEFAULT); 118 | } 119 | 120 | public static function cleanRule(?string $rule): string 121 | { 122 | return \strtolower(\trim((string)$rule)); 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /src/Type/AbstractType.php: -------------------------------------------------------------------------------- 1 | prepareObject($value, $config); 53 | } 54 | 55 | public function __toString() 56 | { 57 | $this->log('__toString() called'); 58 | 59 | return $this->text(); 60 | } 61 | 62 | /** 63 | * Serialize. 64 | * @return array 65 | */ 66 | public function __sleep() 67 | { 68 | $result = []; 69 | $reflect = new \ReflectionClass($this); 70 | $propList = $reflect->getProperties(); 71 | 72 | foreach ($propList as $prop) { 73 | if ($prop->isStatic()) { 74 | continue; 75 | } 76 | $result[] = $prop->name; 77 | } 78 | 79 | $this->log('Serialized'); 80 | 81 | return $result; 82 | } 83 | 84 | /** 85 | * Wake up after serialize. 86 | */ 87 | public function __wakeup(): void 88 | { 89 | $this->log('--> wakeup start'); 90 | $this->prepareObject($this->dump(false)); 91 | $this->log('<-- Wakeup finish'); 92 | } 93 | 94 | /** 95 | * Clone object. 96 | */ 97 | public function __clone() 98 | { 99 | self::$counter++; 100 | $recentId = $this->uniqueId; 101 | $this->uniqueId = self::$counter; 102 | 103 | $this->log( 104 | "Cloned from id='{$recentId}' and created new with id='{$this->uniqueId}'; dump=" . $this->dump(false), 105 | ); 106 | } 107 | 108 | /** 109 | * @return float|string 110 | */ 111 | public function __get(string $name) 112 | { 113 | $name = \strtolower($name); 114 | 115 | if ($name === 'value') { 116 | return $this->val(); 117 | } 118 | 119 | if ($name === 'rule') { 120 | return $this->getRule(); 121 | } 122 | 123 | throw new Exception("{$this->type}: Undefined __get() called: '{$name}'"); 124 | } 125 | 126 | /** 127 | * @noinspection MagicMethodsValidityInspection 128 | */ 129 | public function __set(string $name, mixed $value): void 130 | { 131 | if ($name === 'value') { 132 | $this->set([$value]); 133 | } elseif ($name === 'rule') { 134 | $this->convert($value); 135 | } else { 136 | throw new Exception("{$this->type}: Undefined __set() called: '{$name}' = '{$value}'"); 137 | } 138 | } 139 | 140 | /** 141 | * Experimental! Methods aliases. 142 | * @deprecated 143 | */ 144 | public function __call(string $name, array $arguments): mixed 145 | { 146 | $name = \strtolower($name); 147 | if ($name === 'value') { 148 | return \call_user_func_array([$this, 'val'], $arguments); 149 | } 150 | 151 | if ($name === 'plus') { 152 | return \call_user_func_array([$this, 'add'], $arguments); 153 | } 154 | 155 | if ($name === 'minus') { 156 | return \call_user_func_array([$this, 'subtract'], $arguments); 157 | } 158 | 159 | throw new Exception("{$this->type}: Called undefined method: '{$name}'"); 160 | } 161 | 162 | public function __invoke(): self 163 | { 164 | $args = \func_get_args(); 165 | $argsCount = \count($args); 166 | $shortArgList = 1; 167 | $fullArgList = 2; 168 | 169 | if ($argsCount === 0) { 170 | $this->error('Undefined arguments'); 171 | } elseif ($argsCount === $shortArgList) { 172 | $rules = $this->formatter->getList(); 173 | 174 | if (\array_key_exists($args[0], $rules)) { 175 | return $this->convert($args[0]); 176 | } 177 | 178 | return $this->set($args[0]); 179 | } elseif ($argsCount === $fullArgList) { 180 | return $this->set([$args[0], $args[1]]); 181 | } 182 | 183 | throw new Exception("{$this->type}: Too many arguments"); 184 | } 185 | 186 | public function getId(): int 187 | { 188 | return $this->uniqueId; 189 | } 190 | 191 | public function val(?string $rule = null): float 192 | { 193 | $rule = Parser::cleanRule($rule); 194 | 195 | if ($rule !== $this->internalRule && !isStrEmpty($rule)) { 196 | return $this->customConvert($rule); 197 | } 198 | 199 | return $this->internalValue; 200 | } 201 | 202 | public function text(?string $rule = null): string 203 | { 204 | $rule = !isStrEmpty($rule) ? $this->parser->checkRule($rule) : $this->internalRule; 205 | $this->log("Formatted output in '{$rule}' as 'text'"); 206 | 207 | return $this->formatter->text($this->val($rule), $rule); 208 | } 209 | 210 | public function noStyle(?string $rule = null): string 211 | { 212 | $rule = !isStrEmpty($rule) ? $this->parser->checkRule($rule) : $this->internalRule; 213 | $this->log("Formatted output in '{$rule}' as 'noStyle'"); 214 | 215 | return $this->formatter->text($this->val($rule), $rule, false); 216 | } 217 | 218 | public function html(?string $rule = null): string 219 | { 220 | $rule = !isStrEmpty($rule) ? $this->parser->checkRule($rule) : $this->internalRule; 221 | $this->log("Formatted output in '{$rule}' as 'html'"); 222 | 223 | return $this->formatter->html( 224 | ['value' => $this->val($rule), 'rule' => $rule], 225 | ['value' => $this->internalValue, 'rule' => $this->internalRule], 226 | ['id' => $this->uniqueId], 227 | ); 228 | } 229 | 230 | public function htmlInput(?string $rule = null, ?string $name = null, bool $formatted = false): string 231 | { 232 | $rule = !isStrEmpty($rule) ? $this->parser->checkRule($rule) : $this->internalRule; 233 | $this->log("Formatted output in '{$rule}' as 'input'"); 234 | 235 | return $this->formatter->htmlInput( 236 | ['value' => $this->val($rule), 'rule' => $rule], 237 | ['value' => $this->internalValue, 'rule' => $this->internalRule], 238 | ['id' => $this->uniqueId, 'name' => $name, 'formatted' => $formatted], 239 | ); 240 | } 241 | 242 | public function isRule(string $rule): bool 243 | { 244 | $rule = $this->parser->checkRule($rule); 245 | 246 | return $rule === $this->internalRule; 247 | } 248 | 249 | public function getRule(): string 250 | { 251 | return $this->internalRule; 252 | } 253 | 254 | public function isEmpty(): bool 255 | { 256 | return (float)$this->internalValue === 0.0; 257 | } 258 | 259 | public function isPositive(): bool 260 | { 261 | return $this->internalValue > 0; 262 | } 263 | 264 | public function isNegative(): bool 265 | { 266 | return $this->internalValue < 0; 267 | } 268 | 269 | public function getRules(): array 270 | { 271 | return $this->formatter->getList(); 272 | } 273 | 274 | public function data(bool $toString = false): array|string 275 | { 276 | $data = [(string)$this->val(), $this->getRule()]; 277 | 278 | return $toString ? \implode(' ', $data) : $data; 279 | } 280 | 281 | public function getClone(): self 282 | { 283 | return clone $this; 284 | } 285 | 286 | /** 287 | * @SuppressWarnings(PHPMD.CyclomaticComplexity) 288 | */ 289 | public function compare( 290 | null|array|float|int|self|string $value, 291 | string $mode = '==', 292 | int $round = Formatter::ROUND_DEFAULT, 293 | ): bool { 294 | // prepare value 295 | $value = $this->getValidValue($value); 296 | 297 | $mode = \trim($mode); 298 | $mode = \in_array($mode, ['=', '==', '==='], true) ? '==' : $mode; 299 | 300 | $val1 = \round($this->val($this->internalRule), $round); 301 | $val2 = \round($value->val($this->internalRule), $round); 302 | 303 | $this->log( 304 | "Compared '{$this->dump(false)}' {$mode} '{$value->dump(false)}' // {$val1} {$mode} {$val2}, r={$round}", 305 | ); 306 | 307 | if ($mode === '==') { 308 | return $val1 === $val2; 309 | } 310 | 311 | if ($mode === '!=' || $mode === '!==') { 312 | return $val1 !== $val2; 313 | } 314 | 315 | if ($mode === '<') { 316 | return $val1 < $val2; 317 | } 318 | 319 | if ($mode === '>') { 320 | return $val1 > $val2; 321 | } 322 | 323 | if ($mode === '<=') { 324 | return $val1 <= $val2; 325 | } 326 | 327 | if ($mode === '>=') { 328 | return $val1 >= $val2; 329 | } 330 | 331 | throw new Exception("{$this->type}: Undefined compare mode: {$mode}"); 332 | } 333 | 334 | public function setEmpty(bool $getClone = false): self 335 | { 336 | return $this->modifier(0.0, 'Set empty', $getClone); 337 | } 338 | 339 | public function add(null|array|float|int|self|string $value, bool $getClone = false): self 340 | { 341 | return $this->customAdd($value, $getClone); 342 | } 343 | 344 | public function subtract(null|array|float|int|self|string $value, bool $getClone = false): self 345 | { 346 | return $this->customAdd($value, $getClone, true); 347 | } 348 | 349 | public function convert(string $newRule, bool $getClone = false): self 350 | { 351 | if ($newRule === '') { 352 | $newRule = $this->internalRule; 353 | } 354 | 355 | $newRule = $this->parser->checkRule($newRule); 356 | 357 | $obj = $getClone ? clone $this : $this; 358 | 359 | if ($newRule !== $obj->internalRule) { 360 | $obj->internalValue = $obj->customConvert($newRule, true); 361 | $obj->internalRule = $newRule; 362 | } 363 | 364 | return $obj; 365 | } 366 | 367 | public function invert(bool $getClone = false): self 368 | { 369 | $logMess = 'Invert sign'; 370 | if ($this->internalValue > 0) { 371 | $newValue = -1 * $this->internalValue; 372 | } elseif ($this->internalValue < 0) { 373 | $newValue = \abs((float)$this->internalValue); 374 | } else { 375 | $newValue = $this->internalValue; 376 | } 377 | 378 | return $this->modifier($newValue, $logMess, $getClone); 379 | } 380 | 381 | public function positive(bool $getClone = false): self 382 | { 383 | return $this->modifier(\abs((float)$this->internalValue), 'Set positive/abs', $getClone); 384 | } 385 | 386 | public function negative(bool $getClone = false): self 387 | { 388 | return $this->modifier(-1 * \abs((float)$this->internalValue), 'Set negative', $getClone); 389 | } 390 | 391 | public function abs(bool $getClone = false): self 392 | { 393 | return $this->positive($getClone); 394 | } 395 | 396 | public function multiply(float $number, bool $getClone = false): self 397 | { 398 | $multiplier = Parser::cleanValue($number); 399 | $newValue = $multiplier * $this->internalValue; 400 | 401 | return $this->modifier($newValue, "Multiply with '{$multiplier}'", $getClone); 402 | } 403 | 404 | public function division(float $number, bool $getClone = false): self 405 | { 406 | $divider = Parser::cleanValue($number); 407 | 408 | return $this->modifier($this->internalValue / $divider, "Division with '{$divider}'", $getClone); 409 | } 410 | 411 | public function percent(self|string $value, bool $revert = false): self 412 | { 413 | $value = $this->getValidValue($value); 414 | 415 | $percent = 0.0; 416 | if (!$this->isEmpty() && !$value->isEmpty()) { 417 | $percent = ($this->internalValue / $value->val($this->internalRule)) * 100; 418 | } 419 | 420 | if ($revert) { 421 | $percent = 100 - $percent; 422 | } 423 | 424 | $result = $this->getValidValue("{$percent}%"); 425 | $this->log("Calculate percent; '{$this->dump(false)}' / {$value->dump(false)} = {$result->dump(false)}"); 426 | 427 | return $result; 428 | } 429 | 430 | public function customFunc(\Closure $function, bool $getClone = false): self 431 | { 432 | $this->log('--> Function start'); 433 | $function($this); 434 | 435 | return $this->modifier($this->internalValue, '<-- Function finished', $getClone); 436 | } 437 | 438 | public function set(null|array|float|int|self|string $value, bool $getClone = false): self 439 | { 440 | $value = $this->getValidValue($value); 441 | 442 | $this->internalValue = $value->val(); 443 | $this->internalRule = $value->getRule(); 444 | 445 | return $this->modifier($this->internalValue, "Set new value = '{$this->dump(false)}'", $getClone); 446 | } 447 | 448 | public function round(int $roundValue, string $mode = Formatter::ROUND_CLASSIC): self 449 | { 450 | $oldValue = $this->internalValue; 451 | $newValue = $this->formatter->round($this->internalValue, $this->internalRule, [ 452 | 'roundValue' => $roundValue, 453 | 'roundType' => $mode, 454 | ]); 455 | 456 | $this->log("Rounded (size={$roundValue}; type={$mode}) '{$oldValue}' => {$newValue}"); 457 | 458 | $this->internalValue = $newValue; 459 | 460 | return $this; 461 | } 462 | 463 | public function getValidValue(null|array|float|int|self|string $value): self 464 | { 465 | if ($value instanceof self) { 466 | $thisClass = \strtolower(static::class); 467 | $valClass = \strtolower($value::class); 468 | if ($thisClass !== $valClass) { 469 | throw new Exception("{$this->type}: No valid object type given: {$valClass}"); 470 | } 471 | } else { 472 | /** 473 | * @psalm-suppress UnsafeInstantiation 474 | * @phpstan-ignore-next-line 475 | */ 476 | $value = new static($value, $this->getConfig()); 477 | } 478 | 479 | return $value; 480 | } 481 | 482 | public function error(string $message): void 483 | { 484 | $this->log($message); 485 | throw new Exception("{$this->type}: {$message}"); 486 | } 487 | 488 | public function dump(bool $showId = true): string 489 | { 490 | $uniqueId = $showId ? "; id={$this->uniqueId}" : ''; 491 | 492 | return "{$this->internalValue} {$this->internalRule}{$uniqueId}"; 493 | } 494 | 495 | public function log(string $message): void 496 | { 497 | if ($this->isDebug) { 498 | $this->logs[] = $message; 499 | } 500 | } 501 | 502 | public function logs(): array 503 | { 504 | return $this->logs; 505 | } 506 | 507 | public function changeRule(string $rule, array $newFormat): self 508 | { 509 | $rule = Parser::cleanRule($rule); 510 | $this->formatter->changeRule($rule, $newFormat); 511 | $this->log("The rule '{$rule}' changed"); 512 | 513 | return $this; 514 | } 515 | 516 | public function addRule(string $rule, array $newFormat = []): self 517 | { 518 | $form = $this->formatter; 519 | $rule = Parser::cleanRule($rule); 520 | $form->addRule($rule, $newFormat); 521 | $this->parser->addRule($rule); 522 | $this->log("The rule '{$rule}' added"); 523 | 524 | return $this; 525 | } 526 | 527 | public function removeRule(string $rule): self 528 | { 529 | $rule = Parser::cleanRule($rule); 530 | $this->formatter->removeRule($rule); 531 | $this->parser->removeRule($rule); 532 | $this->log("The rule '{$rule}' removed"); 533 | 534 | return $this; 535 | } 536 | 537 | public function getRuleData(string $rule): array 538 | { 539 | $rule = Parser::cleanRule($rule); 540 | 541 | return $this->formatter->get($rule); 542 | } 543 | 544 | protected function getConfig(?AbstractConfig $config = null): ?AbstractConfig 545 | { 546 | $defaultConfig = AbstractConfig::getDefault($this->type); 547 | 548 | $config ??= $defaultConfig; 549 | 550 | // Hack for getValidValue method 551 | if ($defaultConfig === null && $config !== null) { 552 | AbstractConfig::registerDefault($this->type, $config); 553 | } 554 | 555 | return $config; 556 | } 557 | 558 | /** 559 | * @SuppressWarnings(PHPMD.CyclomaticComplexity) 560 | */ 561 | protected function customConvert(string $rule, bool $addToLog = false): float 562 | { 563 | $from = $this->parser->checkRule($this->internalRule); 564 | $target = $this->parser->checkRule($rule); 565 | 566 | $ruleTo = $this->formatter->get($target); 567 | $ruleFrom = $this->formatter->get($from); 568 | $ruleDef = $this->formatter->get($this->default); 569 | 570 | $log = "'{$from}'=>'{$target}'"; 571 | 572 | $result = $this->internalValue; 573 | if ($from !== $target) { 574 | if (\is_callable($ruleTo['rate']) || \is_callable($ruleFrom['rate'])) { 575 | if (\is_callable($ruleFrom['rate'])) { 576 | $defNorm = $ruleFrom['rate']($this->internalValue, $this->default, $from); 577 | } else { 578 | $defNorm = $this->internalValue * $ruleFrom['rate'] * $ruleDef['rate']; 579 | } 580 | 581 | if (\is_callable($ruleTo['rate'])) { 582 | $result = $ruleTo['rate']($defNorm, $target, $this->default); 583 | } else { 584 | $result = $defNorm / $ruleTo['rate']; 585 | } 586 | } else { 587 | $defNorm = $this->internalValue * $ruleFrom['rate'] * $ruleDef['rate']; 588 | $result = $defNorm / $ruleTo['rate']; 589 | } 590 | 591 | if ($this->isDebug && $addToLog) { 592 | $message = [ 593 | "Converted {$log};", 594 | "New value = {$result} {$target};", 595 | \is_callable($ruleTo['rate']) ? "func({$from})" : "{$ruleTo['rate']} {$from}", 596 | '=', 597 | \is_callable($ruleFrom['rate']) ? "func({$target})" : "{$ruleFrom['rate']} {$target}", 598 | ]; 599 | 600 | $this->log(\implode(' ', $message)); 601 | } 602 | } 603 | 604 | return $result; 605 | } 606 | 607 | protected function customAdd( 608 | null|array|float|int|self|string $value, 609 | bool $getClone = false, 610 | bool $isSubtract = false, 611 | ): self { 612 | $value = $this->getValidValue($value); 613 | 614 | $addValue = 0; 615 | 616 | if ($this->internalRule === '%') { 617 | if ($value->getRule() === '%') { 618 | $addValue = $value->val(); 619 | } else { 620 | $this->error("Impossible add '{$value->dump(false)}' to '{$this->dump(false)}'"); 621 | } 622 | } elseif ($value->getRule() !== '%') { 623 | $addValue = $value->val($this->internalRule); 624 | } else { 625 | $addValue = $this->internalValue * $value->val() / 100; 626 | } 627 | 628 | if ($isSubtract) { 629 | $addValue *= -1; 630 | } 631 | 632 | $newValue = $this->internalValue + $addValue; 633 | $logMess = ($isSubtract ? 'Subtract' : 'Add') . " '{$value->dump(false)}'"; 634 | 635 | return $this->modifier($newValue, $logMess, $getClone); 636 | } 637 | 638 | protected function modifier(float $newValue, ?string $logMessage = null, bool $getClone = false): self 639 | { 640 | if ($getClone) { 641 | $clone = $this->getClone(); 642 | 643 | $clone->internalValue = $newValue; 644 | $clone->log("{$logMessage}; New value = '{$clone->dump(false)}'"); 645 | 646 | return $clone; 647 | } 648 | 649 | $this->internalValue = $newValue; 650 | $this->log("{$logMessage}; New value = '{$this->dump(false)}'"); 651 | 652 | return $this; 653 | } 654 | 655 | private function prepareObject(null|array|float|int|string $value = null, ?AbstractConfig $config = null): void 656 | { 657 | // $this->type = Str::class \strtolower(\str_replace(__NAMESPACE__ . '\\', '', static::class)); 658 | $this->type = Str::getClassName(static::class, true) ?? 'UndefinedType'; 659 | 660 | // get custom or global config 661 | $config = $this->getConfig($config); 662 | if ($config !== null) { 663 | // debug flag (for logging) 664 | $this->isDebug = $config->isDebug; 665 | 666 | // set default rule 667 | $this->default = \strtolower(\trim($config->default)); 668 | if (isStrEmpty($this->default)) { 669 | $this->error('Default rule cannot be empty!'); 670 | } 671 | 672 | // create formatter helper 673 | $this->formatter = new Formatter($config->getRules(), $config->defaultParams, $this->type); 674 | } else { 675 | $this->formatter = new Formatter(); 676 | } 677 | 678 | // check that default rule 679 | $rules = $this->formatter->getList(true); 680 | if (!\array_key_exists($this->default, $rules) || \count($rules) === 0) { 681 | throw new Exception("{$this->type}: Default rule not found!"); 682 | } 683 | 684 | // create parser helper 685 | $this->parser = new Parser($this->default, $rules); 686 | 687 | // parse data 688 | [$this->internalValue, $this->internalRule] = $this->parser->parse($value); 689 | 690 | // count unique id 691 | self::$counter++; 692 | $this->uniqueId = self::$counter; 693 | 694 | // success log 695 | $this->log("Id={$this->uniqueId} has just created; dump='{$this->dump(false)}'"); 696 | } 697 | } 698 | -------------------------------------------------------------------------------- /src/Type/Area.php: -------------------------------------------------------------------------------- 1 | isRule('d')) { 25 | $divider = 360; 26 | } elseif ($this->isRule('r')) { 27 | $divider = 2; 28 | } elseif ($this->isRule('g')) { 29 | $divider = 400; 30 | } elseif ($this->isRule('t')) { 31 | $divider = 1; 32 | } 33 | 34 | if ($divider > 0) { 35 | if ($this->internalValue <= (-1 * $divider)) { 36 | $this->internalValue = \fmod($this->internalValue, $divider); 37 | } elseif ($this->internalValue >= $divider) { 38 | $this->internalValue = \fmod($this->internalValue, $divider); 39 | } 40 | 41 | $this->log("Remove circles: {$this->dump(false)}"); 42 | } 43 | 44 | return $this; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/Type/Info.php: -------------------------------------------------------------------------------- 1 |