3 namespace Org\Mxchange\CoreFramework\Utils\Strings;
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;
13 use \InvalidArgumentException;
16 * A string utility class
18 * @author Roland Haeder <webmaster@ship-simu.org>
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
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.
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.
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/>.
37 final class StringUtils extends BaseFrameworkSystem {
41 private static $thousands = ''; // German
46 private static $decimals = ''; // German
49 * In-method cache array
51 private static $cache = [];
54 * Array with bitmasks and such for pack/unpack methods to support both
55 * 32-bit and 64-bit systems
57 private static $packingData = [
61 'right' => 0x0000ffff,
67 'left' => 0xffffffff00000000,
68 'right' => 0x00000000ffffffff,
75 * Hexadecimal->Decimal translation array
77 private static $hexdec = [
97 * Decimal->hexadecimal translation array
99 private static $dechex = [
119 * Simple 64-bit check, thanks to "Salman A" from stackoverflow.com:
121 * The integer size is 4 bytes on 32-bit and 8 bytes on a 64-bit system.
123 private static $archArrayElement = 0;
126 * Private constructor, no instance needed. If PHP would have a static initializer ...
130 private function __construct () {
131 // Call parent constructor
132 //* NOISY-DEBUG */ self::createDebugInstance(__CLASS__, __LINE__)->traceMessage('STRING-UTILS: CONSTRUCTED!');
133 parent::__construct(__CLASS__);
136 //* NOISY-DEBUG */ self::createDebugInstance(__CLASS__, __LINE__)->debugMessage(sprintf('STRING-UTILS: self::archArrayElement=%d', self::$archArrayElement));
137 if (empty(self::$archArrayElement)) {
139 self::$archArrayElement = (PHP_INT_SIZE === 8 ? 64 : 32);
141 // Init from configuration
142 self::$thousands = FrameworkBootstrap::getConfigurationInstance()->getConfigEntry('thousands_separator');
143 self::$decimals = FrameworkBootstrap::getConfigurationInstance()->getConfigEntry('decimals_separator');
147 //* NOISY-DEBUG */ self::createDebugInstance(__CLASS__, __LINE__)->traceMessage('STRING-UTILS: EXIT!');
151 * Converts dashes to underscores, e.g. useable for configuration entries
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
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));
163 throw new InvalidArgumentException('Parameter "str" is empty', FrameworkInterface::EXCEPTION_INVALID_ARGUMENT);
167 $str = str_replace('-', '_', $str);
169 // Return converted string
170 //* NOISY-DEBUG */ self::createDebugInstance(__CLASS__, __LINE__)->traceMessage(sprintf('STRING-UTILS: str=%s - EXIT!', $str));
175 * Encodes raw data (almost any type) by "serializing" it and then pack it
176 * into a "binary format".
178 * @param $rawData Raw data (almost any type)
179 * @return $encoded Encoded data
180 * @throws InvalidArgumentException If $rawData has a non-serializable data type
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)));
191 $dummyInstance = new StringUtils();
193 // First "serialize" it (json_encode() is faster than serialize())
194 $encoded = self::packString(json_encode($rawData));
197 //* N NOISY-DEBUG */ self::createDebugInstance(__CLASS__, __LINE__)->traceMessage(sprintf('STRING-UTILS: encoded()=%d - EXIT!', strlen($encoded)));
202 * Converts e.g. a command from URL to a valid class by keeping out bad characters
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
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));
212 // No empty strings, please
213 throw new InvalidArgumentException('Parameter "str" is empty', FrameworkInterface::EXCEPTION_INVALID_ARGUMENT);
214 } elseif (!isset(self::$cache[$str])) {
218 // Convert all dashes in underscores
219 $str = self::convertDashesToUnderscores($str);
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));
229 //* N NOISY-DEBUG */ self::createDebugInstance(__CLASS__, __LINE__)->debugMessage(sprintf('STRING-UTILS: self[%s]=%s - SET!', $str, $className));
230 self::$cache[$str] = $className;
234 //* N NOISY-DEBUG */ self::createDebugInstance(__CLASS__, __LINE__)->traceMessage(sprintf('STRING-UTILS: self[%s]=%s - EXIT!', $str, $className));
235 return self::$cache[$str];
239 * Formats computer generated price values into human-understandable formats
240 * with thousand and decimal separators.
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
248 public static function formatCurrency (float $value, string $currency = '€', int $decNum = 2) {
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();
253 // Reformat the US number
254 $price = number_format($value, $decNum, self::$decimals, self::$thousands) . $currency;
256 // Return as string...
257 //* NOISY-DEBUG */ self::createDebugInstance(__CLASS__, __LINE__)->traceMessage(sprintf('STRING-UTILS: price=%s - EXIT!', $price));
262 * Converts a hexadecimal string, even with negative sign as first string to
263 * a decimal number using BC functions.
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>
268 * @param $hex Hexadecimal string
269 * @return $dec Decimal number
270 * @throws InvalidArgumentException If a paramter is invalid
272 public static function hex2dec (string $hex) {
274 //* NOISY-DEBUG */ self::createDebugInstance(__CLASS__, __LINE__)->traceMessage(sprintf('STRING-UTILS: hex=%s - CALLED!', $hex));
277 throw new InvalidArgumentException('Parameter "hex" is empty');
280 // Convert to all lower-case
281 $hex = strtolower($hex);
283 // Detect sign (negative/positive numbers)
285 if (substr($hex, 0, 1) == '-') {
287 $hex = substr($hex, 1);
290 // Decode the hexadecimal string into a decimal number
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));
297 // Return the decimal number
298 //* NOISY-DEBUG */ self::createDebugInstance(__CLASS__, __LINE__)->traceMessage(sprintf('STRING-UTILS: sign=%s,dec=%s - EXIT!', $sign, $dec));
303 * Converts even very large decimal numbers, also signed, to a hexadecimal
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>
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
314 public static function dec2hex (int $dec, int $maxLength = 0) {
316 //* NOISY-DEBUG */ self::createDebugInstance(__CLASS__, __LINE__)->traceMessage(sprintf('STRING-UTILS: dec=%d,maxLength=%d - CALLED!', $dec, $maxLength));
319 throw new InvalidArgumentException('Parameter "dec" is empty');
320 } elseif ($maxLength != 0 && ($maxLength % 2) != 0) {
322 throw new InvalidArgumentException(sprintf('maxLength=%d is not dividable by 2 or zero', $maxLength));
325 // Detect sign (negative/positive numbers)
332 // Encode the decimal number into a hexadecimal string
335 $hex = self::$dechex[($dec % (2 ^ 4))] . $hex;
340 * Leading zeros are required for hex-decimal "numbers". In some
341 * situations more leading zeros are wanted, so check for both
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
352 // Return the hexadecimal string
353 //* NOISY-DEBUG */ self::createDebugInstance(__CLASS__, __LINE__)->traceMessage(sprintf('STRING-UTILS: sign=%s,hex=%s - EXIT!', $sign, $hex));
358 * Converts a ASCII string (0 to 255) into a decimal number.
360 * @param $asc The ASCII string to be converted
361 * @return $dec Decimal number
362 * @throws InvalidArgumentException If a paramter is invalid
364 public static function asc2dec (string $asc) {
366 //* NOISY-DEBUG */ self::createDebugInstance(__CLASS__, __LINE__)->traceMessage(sprintf('STRING-UTILS: asc=%s - CALLED!', $asc));
369 throw new InvalidArgumentException('Parameter "asc" is empty');
372 // Convert it into a hexadecimal number
373 $hex = bin2hex($asc);
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);
380 //* NOISY-DEBUG */ self::createDebugInstance(__CLASS__, __LINE__)->traceMessage(sprintf('STRING-UTILS: dec=%d - EXIT!', $dec));
385 * Converts a decimal number into an ASCII string.
387 * @param $dec Decimal number
388 * @return $asc An ASCII string
389 * @throws InvalidArgumentException If a paramter is invalid
391 public static function dec2asc (int $dec) {
393 //* NOISY-DEBUG */ self::createDebugInstance(__CLASS__, __LINE__)->traceMessage(sprintf('STRING-UTILS: dec=%d - CALLED!', $dec));
396 throw new InvalidArgumentException('Parameter "dec" is empty');
399 // First convert the number into a hexadecimal string
400 $hex = self::dec2hex($dec);
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);
407 //* NOISY-DEBUG */ self::createDebugInstance(__CLASS__, __LINE__)->traceMessage(sprintf('STRING-UTILS: asc=%s - EXIT!', $asc));
412 * Converts a hexadecimal number into an ASCII string. Negative numbers
415 * @param $hex Hexadecimal string
416 * @return $asc An ASCII string
417 * @throws InvalidArgumentException If a paramter is invalid
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));
424 throw new InvalidArgumentException('Parameter "hex" is empty');
425 } elseif ((strlen($hex) % 2) != 0) {
427 throw new InvalidArgumentException(sprintf('hex=%s length not dividable by 2', $hex));
432 for ($idx = 0; $idx < strlen($hex); $idx+=2) {
433 // Get the decimal number of the chunk
434 $part = hexdec(substr($hex, $idx, 2));
436 // Add it to the final string
440 // Return the final string
441 //* NOISY-DEBUG */ self::createDebugInstance(__CLASS__, __LINE__)->traceMessage(sprintf('STRING-UTILS: asc=%s - EXIT!', $asc));
446 * Pack a string into a "binary format". Please execuse me that this is
447 * widely undocumented. :-(
449 * @param $str Unpacked string
450 * @return $packed Packed string
451 * @throws InvalidArgumentException If a paramter is invalid
452 * @todo Improve documentation
454 private static function packString (string $str) {
456 //* NOISY-DEBUG */ self::createDebugInstance(__CLASS__, __LINE__)->traceMessage(sprintf('STRING-UTILS: str=%s - CALLED!', $str));
459 throw new InvalidArgumentException('Parameter "str" is empty');
462 // First compress the string (gzcompress is okay)
463 $str = gzcompress($str);
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']) {
472 for ($i = 0; $i < self::$packingData[self::$archArrayElement]['step']; $i++) {
473 $factor = (self::$packingData[self::$archArrayElement]['step'] - 1 - $i);
475 if (($idx + $i) <= strlen($str)) {
476 $ord = ord(substr($str, ($idx + $i), 1));
478 $add = $ord * pow(256, $factor);
482 //print 'idx=' . $idx . ',i=' . $i . ',ord=' . $ord . ',factor=' . $factor . ',add=' . $add . ',big=' . $big . PHP_EOL;
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'];
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)));
498 //* NOISY-DEBUG */ self::createDebugInstance(__CLASS__, __LINE__)->traceMessage(sprintf('STRING-UTILS: packed=%s - EXIT!', $packed));
503 * Checks whether the given hexadecimal number is really a hex-number (only chars 0-9,a-f).
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
510 public static function hexval (string $num, bool $assertMismatch = false) {
512 //* NOISY-DEBUG */ self::createDebugInstance(__CLASS__, __LINE__)->traceMessage(sprintf('STRING-UTILS: num=%s,assertMismatch=%d - CALLED!', $num, intval($assertMismatch)));
515 throw new InvalidArgumentException('Parameter "num" is empty');
518 // Filter all numbers out
519 $ret = preg_replace('/[^0123456789abcdefABCDEF]/', '', $num);
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)));
529 //* NOISY-DEBUG */ self::createDebugInstance(__CLASS__, __LINE__)->traceMessage(sprintf('STRING-UTILS: ret=%s - EXIT!', $ret));