* @version 0.0.0 * @copyright Copyright (c) 2007, 2008 Roland Haeder, 2009 - 2021 Core Developer Team * @license GNU GPL 3.0 or any newer version * @link http://www.ship-simu.org * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ final class StringUtils extends BaseFrameworkSystem { /** * Thousands separator */ private static $thousands = ''; // German /** * Decimal separator */ private static $decimals = ''; // German /** * In-method cache array */ private static $cache = []; /** * Array with bitmasks and such for pack/unpack methods to support both * 32-bit and 64-bit systems */ private static $packingData = [ 32 => [ 'step' => 3, 'left' => 0xffff0000, 'right' => 0x0000ffff, 'factor' => 16, 'format' => 'II', ], 64 => [ 'step' => 7, 'left' => 0xffffffff00000000, 'right' => 0x00000000ffffffff, 'factor' => 32, 'format' => 'NN' ] ]; /** * Hexadecimal->Decimal translation array */ private static $hexdec = [ '0' => 0, '1' => 1, '2' => 2, '3' => 3, '4' => 4, '5' => 5, '6' => 6, '7' => 7, '8' => 8, '9' => 9, 'a' => 10, 'b' => 11, 'c' => 12, 'd' => 13, 'e' => 14, 'f' => 15 ]; /** * Decimal->hexadecimal translation array */ private static $dechex = [ 0 => '0', 1 => '1', 2 => '2', 3 => '3', 4 => '4', 5 => '5', 6 => '6', 7 => '7', 8 => '8', 9 => '9', 10 => 'a', 11 => 'b', 12 => 'c', 13 => 'd', 14 => 'e', 15 => 'f' ]; /** * Simple 64-bit check, thanks to "Salman A" from stackoverflow.com: * * The integer size is 4 bytes on 32-bit and 8 bytes on a 64-bit system. */ private static $archArrayElement = 0; /** * Private constructor, no instance needed. If PHP would have a static initializer ... * * @return void */ private function __construct () { // Call parent constructor parent::__construct(__CLASS__); // Is one not set? if (empty(self::$archArrayElement)) { // Set array element self::$archArrayElement = (PHP_INT_SIZE === 8 ? 64 : 32); // Init from configuration self::$thousands = FrameworkBootstrap::getConfigurationInstance()->getConfigEntry('thousands_separator'); self::$decimals = FrameworkBootstrap::getConfigurationInstance()->getConfigEntry('decimals_separator'); } } /** * Converts dashes to underscores, e.g. useable for configuration entries * * @param $str The string with maybe dashes inside * @return $str The converted string with no dashed, but underscores * @throws NullPointerException If $str is null * @throws InvalidArgumentException If $str is empty */ public static function convertDashesToUnderscores (string $str) { // Validate parameter if (empty($str)) { // Entry is empty throw new InvalidArgumentException('Parameter "str" is empty'); } // Convert them all $str = str_replace('-', '_', $str); // Return converted string return $str; } /** * Encodes raw data (almost any type) by "serializing" it and then pack it * into a "binary format". * * @param $rawData Raw data (almost any type) * @return $encoded Encoded data * @throws InvalidArgumentException If $rawData has a non-serializable data type */ public static function encodeData ($rawData) { // Make sure no objects or resources pass through //* NOISY-DEBUG */ self::createDebugInstance(__CLASS__, __LINE__)->debugOutput(sprintf('STRING-UTILS: rawData[]=%s - CALLED!', gettype($rawData))); if (is_object($rawData) || is_resource($rawData)) { // Not all variable types should be serialized here throw new InvalidArgumentException(sprintf('rawData[]=%s cannot be serialized.', gettype($rawData))); } // Init instance $dummyInstance = new StringUtils(); // First "serialize" it (json_encode() is faster than serialize()) $encoded = self::packString(json_encode($rawData)); // And return it //* NOISY-DEBUG */ self::createDebugInstance(__CLASS__, __LINE__)->debugOutput(sprintf('STRING-UTILS: encoded()=%d - EXIT!', strlen($encoded))); return $encoded; } /** * Converts e.g. a command from URL to a valid class by keeping out bad characters * * @param $str The string, what ever it is needs to be converted * @return $className Generated class name * @throws InvalidArgumentException If a paramter is invalid */ public static final function convertToClassName (string $str) { // Is the parameter valid? //* NOISY-DEBUG */ self::createDebugInstance(__CLASS__, __LINE__)->debugOutput(sprintf('STRING-UTILS: str=%s - CALLED!', $str)); if (empty($str)) { // No empty strings, please throw new InvalidArgumentException('Parameter "str" is empty', self::EXCEPTION_CONFIG_KEY_IS_EMPTY); } elseif (!isset(self::$cache[$str])) { // Init class name $className = ''; // Convert all dashes in underscores $str = self::convertDashesToUnderscores($str); // Now use that underscores to get classname parts for hungarian style //* NOISY-DEBUG */ self::createDebugInstance(__CLASS__, __LINE__)->debugOutput(sprintf('STRING-UTILS: str=%s - AFTER!', $str)); foreach (explode('_', $str) as $strPart) { // Make the class name part lower case and first upper case $className .= ucfirst(strtolower($strPart)); } // Set cache //* NOISY-DEBUG */ self::createDebugInstance(__CLASS__, __LINE__)->debugOutput(sprintf('STRING-UTILS: self[%s]=%s - SET!', $str, $className)); self::$cache[$str] = $className; } // Return class name //* NOISY-DEBUG */ self::createDebugInstance(__CLASS__, __LINE__)->debugOutput(sprintf('STRING-UTILS: self[%s]=%s - EXIT!', $str, $className)); return self::$cache[$str]; } /** * Formats computer generated price values into human-understandable formats * with thousand and decimal separators. * * @param $value The in computer format value for a price * @param $currency The currency symbol (use HTML-valid characters!) * @param $decNum Number of decimals after commata * @return $price The for the current language formated price string * @throws MissingDecimalsThousandsSeparatorException If decimals or thousands separator is missing */ public static function formatCurrency (float $value, string $currency = '€', int $decNum = 2) { // Init instance $dummyInstance = new StringUtils(); // Reformat the US number $price = number_format($value, $decNum, self::$decimals, self::$thousands) . $currency; // Return as string... return $price; } /** * Converts a hexadecimal string, even with negative sign as first string to * a decimal number using BC functions. * * This work is based on comment #86673 on php.net documentation page at: * * * @param $hex Hexadecimal string * @return $dec Decimal number */ public static function hex2dec (string $hex) { // Convert to all lower-case $hex = strtolower($hex); // Detect sign (negative/positive numbers) $sign = ''; if (substr($hex, 0, 1) == '-') { $sign = '-'; $hex = substr($hex, 1); } // Decode the hexadecimal string into a decimal number $dec = 0; for ($i = strlen($hex) - 1, $e = 1; $i >= 0; $i--, $e = bcmul($e, 16)) { $factor = self::$hexdec[substr($hex, $i, 1)]; $dec = bcadd($dec, bcmul($factor, $e)); } // Return the decimal number return $sign . $dec; } /** * Converts even very large decimal numbers, also signed, to a hexadecimal * string. * * This work is based on comment #97756 on php.net documentation page at: * * * @param $dec Decimal number, even with negative sign * @param $maxLength Optional maximum length of the string * @return $hex Hexadecimal string */ public static function dec2hex (string $dec, int $maxLength = 0) { // maxLength can be zero or devideable by 2 assert(($maxLength == 0) || (($maxLength % 2) == 0)); // Detect sign (negative/positive numbers) $sign = ''; if ($dec < 0) { $sign = '-'; $dec = abs($dec); } // Encode the decimal number into a hexadecimal string $hex = ''; do { $hex = self::$dechex[($dec % (2 ^ 4))] . $hex; $dec /= (2 ^ 4); } while ($dec >= 1); /* * Leading zeros are required for hex-decimal "numbers". In some * situations more leading zeros are wanted, so check for both * conditions. */ if ($maxLength > 0) { // Prepend more zeros $hex = str_pad($hex, $maxLength, '0', STR_PAD_LEFT); } elseif ((strlen($hex) % 2) != 0) { // Only make string's length dividable by 2 $hex = '0' . $hex; } // Return the hexadecimal string return $sign . $hex; } /** * Converts a ASCII string (0 to 255) into a decimal number. * * @param $asc The ASCII string to be converted * @return $dec Decimal number */ public static function asc2dec (string $asc) { // Convert it into a hexadecimal number $hex = bin2hex($asc); // And back into a decimal number $dec = self::hex2dec($hex); // Return it return $dec; } /** * Converts a decimal number into an ASCII string. * * @param $dec Decimal number * @return $asc An ASCII string */ public static function dec2asc (string $dec) { // First convert the number into a hexadecimal string $hex = self::dec2hex($dec); // Then convert it into the ASCII string $asc = self::hex2asc($hex); // Return it return $asc; } /** * Converts a hexadecimal number into an ASCII string. Negative numbers * are not allowed. * * @param $hex Hexadecimal string * @return $asc An ASCII string */ public static function hex2asc ($hex) { // Check for length, it must be devideable by 2 //* DEBUG: */ self::createDebugInstance(__CLASS__, __LINE__)->debugOutput('hex='.$hex); assert((strlen($hex) % 2) == 0); // Walk the string $asc = ''; for ($idx = 0; $idx < strlen($hex); $idx+=2) { // Get the decimal number of the chunk $part = hexdec(substr($hex, $idx, 2)); // Add it to the final string $asc .= chr($part); } // Return the final string return $asc; } /** * Pack a string into a "binary format". Please execuse me that this is * widely undocumented. :-( * * @param $str Unpacked string * @return $packed Packed string * @todo Improve documentation */ private static function packString (string $str) { // First compress the string (gzcompress is okay) //* NOISY-DEBUG */ self::createDebugInstance(__CLASS__, __LINE__)->debugOutput(sprintf('STRING-UTILS: str=%s - CALLED!', $str)); $str = gzcompress($str); // Init variable $packed = ''; // And start the "encoding" loop for ($idx = 0; $idx < strlen($str); $idx += self::$packingData[self::$archArrayElement]['step']) { $big = 0; for ($i = 0; $i < self::$packingData[self::$archArrayElement]['step']; $i++) { $factor = (self::$packingData[self::$archArrayElement]['step'] - 1 - $i); if (($idx + $i) <= strlen($str)) { $ord = ord(substr($str, ($idx + $i), 1)); $add = $ord * pow(256, $factor); $big += $add; //print 'idx=' . $idx . ',i=' . $i . ',ord=' . $ord . ',factor=' . $factor . ',add=' . $add . ',big=' . $big . PHP_EOL; } } // Left/right parts (low/high?) $l = ($big & self::$packingData[self::$archArrayElement]['left']) >>self::$packingData[self::$archArrayElement]['factor']; $r = $big & self::$packingData[self::$archArrayElement]['right']; // Create chunk $chunk = str_pad(pack(self::$packingData[self::$archArrayElement]['format'], $l, $r), 8, '0', STR_PAD_LEFT); //* NOISY-DEBUG */ self::createDebugInstance(__CLASS__, __LINE__)->debugOutput(sprintf('STRING-UTILS: big=%d,chunk(%d)=%s', $big, strlen($chunk), md5($chunk))); $packed .= $chunk; } // Return it //* NOISY-DEBUG */ self::createDebugInstance(__CLASS__, __LINE__)->debugOutput(sprintf('STRING-UTILS: packed=%s - EXIT!', $packed)); return $packed; } /** * Checks whether the given hexadecimal number is really a hex-number (only chars 0-9,a-f). * * @param $num A string consisting only chars between 0 and 9 * @param $assertMismatch Whether to assert mismatches * @return $ret The (hopefully) secured hext-numbered value */ public static function hexval (string $num, bool $assertMismatch = false) { // Filter all numbers out $ret = preg_replace('/[^0123456789abcdefABCDEF]/', '', $num); // Assert only if requested if ($assertMismatch === true) { // Has the whole value changed? assert(('' . $ret . '' != '' . $num . '') && (!is_null($num))); } // Return result return $ret; } }