]> git.mxchange.org Git - core.git/blob - framework/main/classes/utils/strings/class_StringUtils.php
c97c9fe3d23c0c3247fa8fb7b3e691bbedeef494
[core.git] / framework / main / classes / utils / strings / class_StringUtils.php
1 <?php
2 // Own namespace
3 namespace Org\Mxchange\CoreFramework\Utils\Strings;
4
5 // Import framework stuff
6 use Org\Mxchange\CoreFramework\Bootstrap\FrameworkBootstrap;
7 use Org\Mxchange\CoreFramework\Configuration\FrameworkConfiguration;
8 use Org\Mxchange\CoreFramework\Generic\FrameworkInterface;
9 use Org\Mxchange\CoreFramework\Generic\NullPointerException;
10 use Org\Mxchange\CoreFramework\Object\BaseFrameworkSystem;
11
12 // Import SPL stuff
13 use \InvalidArgumentException;
14
15 /**
16  * A string utility class
17  *
18  * @author              Roland Haeder <webmaster@ship-simu.org>
19  * @version             0.0.0
20  * @copyright   Copyright (c) 2007, 2008 Roland Haeder, 2009 - 2023 Core Developer Team
21  * @license             GNU GPL 3.0 or any newer version
22  * @link                http://www.ship-simu.org
23  *
24  * This program is free software: you can redistribute it and/or modify
25  * it under the terms of the GNU General Public License as published by
26  * the Free Software Foundation, either version 3 of the License, or
27  * (at your option) any later version.
28  *
29  * This program is distributed in the hope that it will be useful,
30  * but WITHOUT ANY WARRANTY; without even the implied warranty of
31  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
32  * GNU General Public License for more details.
33  *
34  * You should have received a copy of the GNU General Public License
35  * along with this program. If not, see <http://www.gnu.org/licenses/>.
36  */
37 final class StringUtils extends BaseFrameworkSystem {
38         /**
39          * Thousands separator
40          */
41         private static $thousands = ''; // German
42
43         /**
44          * Decimal separator
45          */
46         private static $decimals  = ''; // German
47
48         /**
49          * In-method cache array
50          */
51         private static $cache = [];
52
53         /**
54          * Array with bitmasks and such for pack/unpack methods to support both
55          * 32-bit and 64-bit systems
56          */
57         private static $packingData = [
58                 32 => [
59                         'step'   => 3,
60                         'left'   => 0xffff0000,
61                         'right'  => 0x0000ffff,
62                         'factor' => 16,
63                         'format' => 'II',
64                 ],
65                 64 => [
66                         'step'   => 7,
67                         'left'   => 0xffffffff00000000,
68                         'right'  => 0x00000000ffffffff,
69                         'factor' => 32,
70                         'format' => 'NN'
71                 ]
72         ];
73
74         /**
75          * Hexadecimal->Decimal translation array
76          */
77         private static $hexdec = [
78                 '0' => 0,
79                 '1' => 1,
80                 '2' => 2,
81                 '3' => 3,
82                 '4' => 4,
83                 '5' => 5,
84                 '6' => 6,
85                 '7' => 7,
86                 '8' => 8,
87                 '9' => 9,
88                 'a' => 10,
89                 'b' => 11,
90                 'c' => 12,
91                 'd' => 13,
92                 'e' => 14,
93                 'f' => 15
94         ];
95
96         /**
97          * Decimal->hexadecimal translation array
98          */
99         private static $dechex = [
100                  0 => '0',
101                  1 => '1',
102                  2 => '2',
103                  3 => '3',
104                  4 => '4',
105                  5 => '5',
106                  6 => '6',
107                  7 => '7',
108                  8 => '8',
109                  9 => '9',
110                 10 => 'a',
111                 11 => 'b',
112                 12 => 'c',
113                 13 => 'd',
114                 14 => 'e',
115                 15 => 'f'
116         ];
117
118         /**
119          * Simple 64-bit check, thanks to "Salman A" from stackoverflow.com:
120          *
121          * The integer size is 4 bytes on 32-bit and 8 bytes on a 64-bit system.
122          */
123         private static $archArrayElement = 0;
124
125         /**
126          * Private constructor, no instance needed. If PHP would have a static initializer ...
127          *
128          * @return      void
129          */
130         private function __construct () {
131                 // Call parent constructor
132                 //* NOISY-DEBUG */ self::createDebugInstance(__CLASS__, __LINE__)->traceMessage('STRING-UTILS: CONSTRUCTED!');
133                 parent::__construct(__CLASS__);
134
135                 // Is one not set?
136                 //* NOISY-DEBUG */ self::createDebugInstance(__CLASS__, __LINE__)->debugMessage(sprintf('STRING-UTILS: self::archArrayElement=%d', self::$archArrayElement));
137                 if (empty(self::$archArrayElement)) {
138                         // Set array element
139                         self::$archArrayElement = (PHP_INT_SIZE === 8 ? 64 : 32);
140
141                         // Init from configuration
142                         self::$thousands = FrameworkBootstrap::getConfigurationInstance()->getConfigEntry('thousands_separator');
143                         self::$decimals = FrameworkBootstrap::getConfigurationInstance()->getConfigEntry('decimals_separator');
144                 }
145
146                 // Trace message
147                 //* NOISY-DEBUG */ self::createDebugInstance(__CLASS__, __LINE__)->traceMessage('STRING-UTILS: EXIT!');
148         }
149
150         /**
151          * Converts dashes to underscores, e.g. useable for configuration entries
152          *
153          * @param       $str    The string with maybe dashes inside
154          * @return      $str    The converted string with no dashed, but underscores
155          * @throws      NullPointerException    If $str is null
156          * @throws      InvalidArgumentException        If $str is empty
157          */
158         public static function convertDashesToUnderscores (string $str) {
159                 // Validate parameter
160                 //* NOISY-DEBUG */ self::createDebugInstance(__CLASS__, __LINE__)->traceMessage(sprintf('STRING-UTILS: str=%s - CALLED!', $str));
161                 if (empty($str)) {
162                         // Entry is empty
163                         throw new InvalidArgumentException('Parameter "str" is empty', FrameworkInterface::EXCEPTION_INVALID_ARGUMENT);
164                 }
165
166                 // Convert them all
167                 $str = str_replace('-', '_', $str);
168
169                 // Return converted string
170                 //* NOISY-DEBUG */ self::createDebugInstance(__CLASS__, __LINE__)->traceMessage(sprintf('STRING-UTILS: str=%s - EXIT!', $str));
171                 return $str;
172         }
173
174         /**
175          * Encodes raw data (almost any type) by "serializing" it and then pack it
176          * into a "binary format".
177          *
178          * @param       $rawData        Raw data (almost any type)
179          * @return      $encoded        Encoded data
180          * @throws      InvalidArgumentException        If $rawData has a non-serializable data type
181          */
182         public static function encodeData ($rawData) {
183                 // Make sure no objects or resources pass through
184                 //* N NOISY-DEBUG */ self::createDebugInstance(__CLASS__, __LINE__)->traceMessage(sprintf('STRING-UTILS: rawData[]=%s - CALLED!', gettype($rawData)));
185                 if (is_object($rawData) || is_resource($rawData)) {
186                         // Not all variable types should be serialized here
187                         throw new InvalidArgumentException(sprintf('rawData[]=%s cannot be serialized.', gettype($rawData)));
188                 }
189
190                 // Init instance
191                 $dummyInstance = new StringUtils();
192
193                 // First "serialize" it (json_encode() is faster than serialize())
194                 $encoded = self::packString(json_encode($rawData));
195
196                 // And return it
197                 //* N NOISY-DEBUG */ self::createDebugInstance(__CLASS__, __LINE__)->traceMessage(sprintf('STRING-UTILS: encoded()=%d - EXIT!', strlen($encoded)));
198                 return $encoded;
199         }
200
201         /**
202          * Converts e.g. a command from URL to a valid class by keeping out bad characters
203          *
204          * @param       $str            The string, what ever it is needs to be converted
205          * @return      $className      Generated class name
206          * @throws      InvalidArgumentException        If a paramter is invalid
207          */
208         public static final function convertToClassName (string $str) {
209                 // Is the parameter valid?
210                 //* N NOISY-DEBUG */ self::createDebugInstance(__CLASS__, __LINE__)->traceMessage(sprintf('STRING-UTILS: str=%s - CALLED!', $str));
211                 if (empty($str)) {
212                         // No empty strings, please
213                         throw new InvalidArgumentException('Parameter "str" is empty', FrameworkInterface::EXCEPTION_INVALID_ARGUMENT);
214                 } elseif (!isset(self::$cache[$str])) {
215                         // Init class name
216                         $className = '';
217
218                         // Convert all dashes in underscores
219                         $str = self::convertDashesToUnderscores($str);
220
221                         // Now use that underscores to get classname parts for hungarian style
222                         //* N NOISY-DEBUG */ self::createDebugInstance(__CLASS__, __LINE__)->debugMessage(sprintf('STRING-UTILS: str=%s - AFTER!', $str));
223                         foreach (explode('_', $str) as $strPart) {
224                                 // Make the class name part lower case and first upper case
225                                 $className .= ucfirst(strtolower($strPart));
226                         }
227
228                         // Set cache
229                         //* N NOISY-DEBUG */ self::createDebugInstance(__CLASS__, __LINE__)->debugMessage(sprintf('STRING-UTILS: self[%s]=%s - SET!', $str, $className));
230                         self::$cache[$str] = $className;
231                 }
232
233                 // Return class name
234                 //* N NOISY-DEBUG */ self::createDebugInstance(__CLASS__, __LINE__)->traceMessage(sprintf('STRING-UTILS: self[%s]=%s - EXIT!', $str, $className));
235                 return self::$cache[$str];
236         }
237
238         /**
239          * Formats computer generated price values into human-understandable formats
240          * with thousand and decimal separators.
241          *
242          * @param       $value          The in computer format value for a price
243          * @param       $currency       The currency symbol (use HTML-valid characters!)
244          * @param       $decNum         Number of decimals after commata
245          * @return      $price          The for the current language formated price string
246          * @throws      MissingDecimalsThousandsSeparatorException      If decimals or thousands separator is missing
247          */
248         public static function formatCurrency (float $value, string $currency = '&euro;', int $decNum = 2) {
249                 // Init instance
250                 //* NOISY-DEBUG */ self::createDebugInstance(__CLASS__, __LINE__)->traceMessage(sprintf('STRING-UTILS: value=%s,currency=%s,decNum=%d - CALLED!', $value, $currency, $decNum));
251                 $dummyInstance = new StringUtils();
252
253                 // Reformat the US number
254                 $price = number_format($value, $decNum, self::$decimals, self::$thousands) . $currency;
255
256                 // Return as string...
257                 //* NOISY-DEBUG */ self::createDebugInstance(__CLASS__, __LINE__)->traceMessage(sprintf('STRING-UTILS: price=%s - EXIT!', $price));
258                 return $price;
259         }
260
261         /**
262          * Converts a hexadecimal string, even with negative sign as first string to
263          * a decimal number using BC functions.
264          *
265          * This work is based on comment #86673 on php.net documentation page at:
266          * <http://de.php.net/manual/en/function.dechex.php#86673>
267          *
268          * @param       $hex    Hexadecimal string
269          * @return      $dec    Decimal number
270          * @throws      InvalidArgumentException        If a paramter is invalid
271          */
272         public static function hex2dec (string $hex) {
273                 // Check parameter
274                 //* NOISY-DEBUG */ self::createDebugInstance(__CLASS__, __LINE__)->traceMessage(sprintf('STRING-UTILS: hex=%s - CALLED!', $hex));
275                 if (empty($hex)) {
276                         // Throw IAE
277                         throw new InvalidArgumentException('Parameter "hex" is empty');
278                 }
279
280                 // Convert to all lower-case
281                 $hex = strtolower($hex);
282
283                 // Detect sign (negative/positive numbers)
284                 $sign = '';
285                 if (substr($hex, 0, 1) == '-') {
286                         $sign = '-';
287                         $hex = substr($hex, 1);
288                 }
289
290                 // Decode the hexadecimal string into a decimal number
291                 $dec = 0;
292                 for ($i = strlen($hex) - 1, $e = 1; $i >= 0; $i--, $e = bcmul($e, 16)) {
293                         $factor = self::$hexdec[substr($hex, $i, 1)];
294                         $dec = bcadd($dec, bcmul($factor, $e));
295                 }
296
297                 // Return the decimal number
298                 //* NOISY-DEBUG */ self::createDebugInstance(__CLASS__, __LINE__)->traceMessage(sprintf('STRING-UTILS: sign=%s,dec=%s - EXIT!', $sign, $dec));
299                 return $sign . $dec;
300         }
301
302         /**
303          * Converts even very large decimal numbers, also signed, to a hexadecimal
304          * string.
305          *
306          * This work is based on comment #97756 on php.net documentation page at:
307          * <http://de.php.net/manual/en/function.hexdec.php#97756>
308          *
309          * @param       $dec            Decimal number, even with negative sign
310          * @param       $maxLength      Optional maximum length of the string
311          * @return      $hex    Hexadecimal string
312          * @throws      InvalidArgumentException        If a paramter is invalid
313          */
314         public static function dec2hex (int $dec, int $maxLength = 0) {
315                 // Check parameter
316                 //* NOISY-DEBUG */ self::createDebugInstance(__CLASS__, __LINE__)->traceMessage(sprintf('STRING-UTILS: dec=%d,maxLength=%d - CALLED!', $dec, $maxLength));
317                 if ($dec < 0) {
318                         // Throw IAE
319                         throw new InvalidArgumentException(sprintf('dec=%d is below zero', $dec));
320                 } elseif ($maxLength != 0 && ($maxLength % 2) != 0) {
321                         // Throw it again
322                         throw new InvalidArgumentException(sprintf('maxLength=%d is not dividable by 2 or zero', $maxLength));
323                 }
324
325                 // Detect sign (negative/positive numbers)
326                 $sign = '';
327                 if ($dec < 0) {
328                         $sign = '-';
329                         $dec = abs($dec);
330                 }
331
332                 // Encode the decimal number into a hexadecimal string
333                 $hex = '';
334                 do {
335                         $hex = self::$dechex[($dec % (2 ^ 4))] . $hex;
336                         $dec /= (2 ^ 4);
337                 } while ($dec >= 1);
338
339                 /*
340                  * Leading zeros are required for hex-decimal "numbers". In some
341                  * situations more leading zeros are wanted, so check for both
342                  * conditions.
343                  */
344                 if ($maxLength > 0) {
345                         // Prepend more zeros
346                         $hex = str_pad($hex, $maxLength, '0', STR_PAD_LEFT);
347                 } elseif ((strlen($hex) % 2) != 0) {
348                         // Only make string's length dividable by 2
349                         $hex = '0' . $hex;
350                 }
351
352                 // Return the hexadecimal string
353                 //* NOISY-DEBUG */ self::createDebugInstance(__CLASS__, __LINE__)->traceMessage(sprintf('STRING-UTILS: sign=%s,hex=%s - EXIT!', $sign, $hex));
354                 return $sign . $hex;
355         }
356
357         /**
358          * Converts a ASCII string (0 to 255) into a decimal number.
359          *
360          * @param       $asc    The ASCII string to be converted
361          * @return      $dec    Decimal number
362          * @throws      InvalidArgumentException        If a paramter is invalid
363          */
364         public static function asc2dec (string $asc) {
365                 // Check parameter
366                 //* NOISY-DEBUG */ self::createDebugInstance(__CLASS__, __LINE__)->traceMessage(sprintf('STRING-UTILS: asc=%s - CALLED!', $asc));
367                 if (empty($asc)) {
368                         // Throw IAE
369                         throw new InvalidArgumentException('Parameter "asc" is empty');
370                 }
371
372                 // Convert it into a hexadecimal number
373                 $hex = bin2hex($asc);
374
375                 // And back into a decimal number
376                 //* NOISY-DEBUG */ self::createDebugInstance(__CLASS__, __LINE__)->debugMessage(sprintf('STRING-UTILS: hex=%s', $hex));
377                 $dec = self::hex2dec($hex);
378
379                 // Return it
380                 //* NOISY-DEBUG */ self::createDebugInstance(__CLASS__, __LINE__)->traceMessage(sprintf('STRING-UTILS: dec=%d - EXIT!', $dec));
381                 return $dec;
382         }
383
384         /**
385          * Converts a decimal number into an ASCII string.
386          *
387          * @param       $dec            Decimal number
388          * @return      $asc    An ASCII string
389          * @throws      InvalidArgumentException        If a paramter is invalid
390          */
391         public static function dec2asc (int $dec) {
392                 // Check parameter
393                 //* NOISY-DEBUG */ self::createDebugInstance(__CLASS__, __LINE__)->traceMessage(sprintf('STRING-UTILS: dec=%d - CALLED!', $dec));
394                 if ($dec < 0) {
395                         // Throw IAE
396                         throw new InvalidArgumentException(sprintf('dec=%d is below zero', $dec));
397                 }
398
399                 // First convert the number into a hexadecimal string
400                 $hex = self::dec2hex($dec);
401
402                 // Then convert it into the ASCII string
403                 //* NOISY-DEBUG */ self::createDebugInstance(__CLASS__, __LINE__)->debugMessage(sprintf('STRING-UTILS: hex=%s', $hex));
404                 $asc = self::hex2asc($hex);
405
406                 // Return it
407                 //* NOISY-DEBUG */ self::createDebugInstance(__CLASS__, __LINE__)->traceMessage(sprintf('STRING-UTILS: asc=%s - EXIT!', $asc));
408                 return $asc;
409         }
410
411         /**
412          * Converts a hexadecimal number into an ASCII string. Negative numbers
413          * are not allowed.
414          *
415          * @param       $hex    Hexadecimal string
416          * @return      $asc    An ASCII string
417          * @throws      InvalidArgumentException        If a paramter is invalid
418          */
419         public static function hex2asc (string $hex) {
420                 // Check for length, it must be devideable by 2
421                 /* DEBUG: */ self::createDebugInstance(__CLASS__, __LINE__)->traceMessage(sprintf('STRING-UTILS: hex=%s - CALLED!', $hex));
422                 if (empty($hex)) {
423                         // Throw IAE
424                         throw new InvalidArgumentException('Parameter "hex" is empty');
425                 } elseif ((strlen($hex) % 2) != 0) {
426                         // Throw it again
427                         throw new InvalidArgumentException(sprintf('hex=%s length not dividable by 2', $hex));
428                 }
429
430                 // Walk the string
431                 $asc = '';
432                 for ($idx = 0; $idx < strlen($hex); $idx+=2) {
433                         // Get the decimal number of the chunk
434                         $part = hexdec(substr($hex, $idx, 2));
435
436                         // Add it to the final string
437                         $asc .= chr($part);
438                 }
439
440                 // Return the final string
441                 //* NOISY-DEBUG */ self::createDebugInstance(__CLASS__, __LINE__)->traceMessage(sprintf('STRING-UTILS: asc=%s - EXIT!', $asc));
442                 return $asc;
443         }
444
445         /**
446          * Pack a string into a "binary format". Please execuse me that this is
447          * widely undocumented. :-(
448          *
449          * @param       $str            Unpacked string
450          * @return      $packed         Packed string
451          * @throws      InvalidArgumentException        If a paramter is invalid
452          * @todo        Improve documentation
453          */
454         private static function packString (string $str) {
455                 // Check parameter
456                 //* NOISY-DEBUG */ self::createDebugInstance(__CLASS__, __LINE__)->traceMessage(sprintf('STRING-UTILS: str=%s - CALLED!', $str));
457                 if (empty($str)) {
458                         // Throw IAE
459                         throw new InvalidArgumentException('Parameter "str" is empty');
460                 }
461
462                 // First compress the string (gzcompress is okay)
463                 $str = gzcompress($str);
464
465                 // Init variable
466                 $packed = '';
467
468                 // And start the "encoding" loop
469                 //* NOISY-DEBUG */ self::createDebugInstance(__CLASS__, __LINE__)->debugMessage(sprintf('STRING-UTILS: Looping through str()=%d ...', strlen($str)));
470                 for ($idx = 0; $idx < strlen($str); $idx += self::$packingData[self::$archArrayElement]['step']) {
471                         $big = 0;
472                         for ($i = 0; $i < self::$packingData[self::$archArrayElement]['step']; $i++) {
473                                 $factor = (self::$packingData[self::$archArrayElement]['step'] - 1 - $i);
474
475                                 if (($idx + $i) <= strlen($str)) {
476                                         $ord = ord(substr($str, ($idx + $i), 1));
477
478                                         $add = $ord * pow(256, $factor);
479
480                                         $big += $add;
481
482                                         //print 'idx=' . $idx . ',i=' . $i . ',ord=' . $ord . ',factor=' . $factor . ',add=' . $add . ',big=' . $big . PHP_EOL;
483                                 }
484                         }
485
486                         // Left/right parts (low/high?)
487                         $l = ($big & self::$packingData[self::$archArrayElement]['left']) >>self::$packingData[self::$archArrayElement]['factor'];
488                         $r = $big & self::$packingData[self::$archArrayElement]['right'];
489
490                         // Create chunk
491                         $chunk = str_pad(pack(self::$packingData[self::$archArrayElement]['format'], $l, $r), 8, '0', STR_PAD_LEFT);
492                         //* N NOISY-DEBUG */ self::createDebugInstance(__CLASS__, __LINE__)->debugMessage(sprintf('STRING-UTILS: big=%d,chunk(%d)=%s', $big, strlen($chunk), md5($chunk)));
493
494                         $packed .= $chunk;
495                 }
496
497                 // Return it
498                 //* NOISY-DEBUG */ self::createDebugInstance(__CLASS__, __LINE__)->traceMessage(sprintf('STRING-UTILS: packed=%s - EXIT!', $packed));
499                 return $packed;
500         }
501
502         /**
503          * Checks whether the given hexadecimal number is really a hex-number (only chars 0-9,a-f).
504          *
505          * @param       $num    A string consisting only chars between 0 and 9
506          * @param       $assertMismatch         Whether to assert mismatches
507          * @return      $ret    The (hopefully) secured hext-numbered value
508          * @throws      InvalidArgumentException        If a paramter is invalid
509          */
510         public static function hexval (string $num, bool $assertMismatch = false) {
511                 // Check parameter
512                 //* NOISY-DEBUG */ self::createDebugInstance(__CLASS__, __LINE__)->traceMessage(sprintf('STRING-UTILS: num=%s,assertMismatch=%d - CALLED!', $num, intval($assertMismatch)));
513                 if (empty($num)) {
514                         // Throw IAE
515                         throw new InvalidArgumentException('Parameter "num" is empty');
516                 }
517
518                 // Filter all numbers out
519                 $ret = preg_replace('/[^0123456789abcdefABCDEF]/', '', $num);
520
521                 // Assert only if requested
522                 //* NOISY-DEBUG */ self::createDebugInstance(__CLASS__, __LINE__)->debugMessage(sprintf('STRING-UTILS: ret=%s', $ret));
523                 if ($assertMismatch === true) {
524                         // Has the whole value changed?
525                         assert(('' . $ret . '' != '' . $num . '') && (!is_null($num)));
526                 }
527
528                 // Return result
529                 //* NOISY-DEBUG */ self::createDebugInstance(__CLASS__, __LINE__)->traceMessage(sprintf('STRING-UTILS: ret=%s - EXIT!', $ret));
530                 return $ret;
531         }
532
533 }