* @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;
} // END - for
// 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;
}
}