* @version 0.0.0
* @copyright Copyright (c) 2007, 2008 Roland Haeder, 2009 - 2023 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
//* NOISY-DEBUG */ self::createDebugInstance(__CLASS__, __LINE__)->traceMessage('STRING-UTILS: CONSTRUCTED!');
parent::__construct(__CLASS__);
// Is one not set?
//* NOISY-DEBUG */ self::createDebugInstance(__CLASS__, __LINE__)->debugMessage(sprintf('STRING-UTILS: self::archArrayElement=%d', self::$archArrayElement));
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');
}
// Trace message
//* NOISY-DEBUG */ self::createDebugInstance(__CLASS__, __LINE__)->traceMessage('STRING-UTILS: EXIT!');
}
/**
* 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
//* NOISY-DEBUG */ self::createDebugInstance(__CLASS__, __LINE__)->traceMessage(sprintf('STRING-UTILS: str=%s - CALLED!', $str));
if (empty($str)) {
// Entry is empty
throw new InvalidArgumentException('Parameter "str" is empty', FrameworkInterface::EXCEPTION_INVALID_ARGUMENT);
}
// Convert them all
$str = str_replace('-', '_', $str);
// Return converted string
//* NOISY-DEBUG */ self::createDebugInstance(__CLASS__, __LINE__)->traceMessage(sprintf('STRING-UTILS: str=%s - EXIT!', $str));
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
//* N NOISY-DEBUG */ self::createDebugInstance(__CLASS__, __LINE__)->traceMessage(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
//* N NOISY-DEBUG */ self::createDebugInstance(__CLASS__, __LINE__)->traceMessage(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?
//* N NOISY-DEBUG */ self::createDebugInstance(__CLASS__, __LINE__)->traceMessage(sprintf('STRING-UTILS: str=%s - CALLED!', $str));
if (empty($str)) {
// No empty strings, please
throw new InvalidArgumentException('Parameter "str" is empty', FrameworkInterface::EXCEPTION_INVALID_ARGUMENT);
} 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
//* N NOISY-DEBUG */ self::createDebugInstance(__CLASS__, __LINE__)->debugMessage(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
//* N NOISY-DEBUG */ self::createDebugInstance(__CLASS__, __LINE__)->debugMessage(sprintf('STRING-UTILS: self[%s]=%s - SET!', $str, $className));
self::$cache[$str] = $className;
}
// Return class name
//* N NOISY-DEBUG */ self::createDebugInstance(__CLASS__, __LINE__)->traceMessage(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
//* NOISY-DEBUG */ self::createDebugInstance(__CLASS__, __LINE__)->traceMessage(sprintf('STRING-UTILS: value=%s,currency=%s,decNum=%d - CALLED!', $value, $currency, $decNum));
$dummyInstance = new StringUtils();
// Reformat the US number
$price = number_format($value, $decNum, self::$decimals, self::$thousands) . $currency;
// Return as string...
//* NOISY-DEBUG */ self::createDebugInstance(__CLASS__, __LINE__)->traceMessage(sprintf('STRING-UTILS: price=%s - EXIT!', $price));
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
* @throws InvalidArgumentException If a paramter is invalid
*/
public static function hex2dec (string $hex) {
// Check parameter
//* NOISY-DEBUG */ self::createDebugInstance(__CLASS__, __LINE__)->traceMessage(sprintf('STRING-UTILS: hex=%s - CALLED!', $hex));
if (empty($hex)) {
// Throw IAE
throw new InvalidArgumentException('Parameter "hex" is empty');
}
// 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
//* NOISY-DEBUG */ self::createDebugInstance(__CLASS__, __LINE__)->traceMessage(sprintf('STRING-UTILS: sign=%s,dec=%s - EXIT!', $sign, $dec));
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
* @throws InvalidArgumentException If a paramter is invalid
*/
public static function dec2hex (int $dec, int $maxLength = 0) {
// Check parameter
//* NOISY-DEBUG */ self::createDebugInstance(__CLASS__, __LINE__)->traceMessage(sprintf('STRING-UTILS: dec=%d,maxLength=%d - CALLED!', $dec, $maxLength));
if ($dec < 0) {
// Throw IAE
throw new InvalidArgumentException(sprintf('dec=%d is below zero', $dec));
} elseif ($maxLength != 0 && ($maxLength % 2) != 0) {
// Throw it again
throw new InvalidArgumentException(sprintf('maxLength=%d is not dividable by 2 or zero', $maxLength));
}
// 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
//* NOISY-DEBUG */ self::createDebugInstance(__CLASS__, __LINE__)->traceMessage(sprintf('STRING-UTILS: sign=%s,hex=%s - EXIT!', $sign, $hex));
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
* @throws InvalidArgumentException If a paramter is invalid
*/
public static function asc2dec (string $asc) {
// Check parameter
//* NOISY-DEBUG */ self::createDebugInstance(__CLASS__, __LINE__)->traceMessage(sprintf('STRING-UTILS: asc=%s - CALLED!', $asc));
if (empty($asc)) {
// Throw IAE
throw new InvalidArgumentException('Parameter "asc" is empty');
}
// Convert it into a hexadecimal number
$hex = bin2hex($asc);
// And back into a decimal number
//* NOISY-DEBUG */ self::createDebugInstance(__CLASS__, __LINE__)->debugMessage(sprintf('STRING-UTILS: hex=%s', $hex));
$dec = self::hex2dec($hex);
// Return it
//* NOISY-DEBUG */ self::createDebugInstance(__CLASS__, __LINE__)->traceMessage(sprintf('STRING-UTILS: dec=%d - EXIT!', $dec));
return $dec;
}
/**
* Converts a decimal number into an ASCII string.
*
* @param $dec Decimal number
* @return $asc An ASCII string
* @throws InvalidArgumentException If a paramter is invalid
*/
public static function dec2asc (int $dec) {
// Check parameter
//* NOISY-DEBUG */ self::createDebugInstance(__CLASS__, __LINE__)->traceMessage(sprintf('STRING-UTILS: dec=%d - CALLED!', $dec));
if ($dec < 0) {
// Throw IAE
throw new InvalidArgumentException(sprintf('dec=%d is below zero', $dec));
}
// First convert the number into a hexadecimal string
$hex = self::dec2hex($dec);
// Then convert it into the ASCII string
//* NOISY-DEBUG */ self::createDebugInstance(__CLASS__, __LINE__)->debugMessage(sprintf('STRING-UTILS: hex=%s', $hex));
$asc = self::hex2asc($hex);
// Return it
//* NOISY-DEBUG */ self::createDebugInstance(__CLASS__, __LINE__)->traceMessage(sprintf('STRING-UTILS: asc=%s - EXIT!', $asc));
return $asc;
}
/**
* Converts a hexadecimal number into an ASCII string. Negative numbers
* are not allowed.
*
* @param $hex Hexadecimal string
* @return $asc An ASCII string
* @throws InvalidArgumentException If a paramter is invalid
*/
public static function hex2asc (string $hex) {
// Check for length, it must be devideable by 2
/* DEBUG: */ self::createDebugInstance(__CLASS__, __LINE__)->traceMessage(sprintf('STRING-UTILS: hex=%s - CALLED!', $hex));
if (empty($hex)) {
// Throw IAE
throw new InvalidArgumentException('Parameter "hex" is empty');
} elseif ((strlen($hex) % 2) != 0) {
// Throw it again
throw new InvalidArgumentException(sprintf('hex=%s length not dividable by 2', $hex));
}
// 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
//* NOISY-DEBUG */ self::createDebugInstance(__CLASS__, __LINE__)->traceMessage(sprintf('STRING-UTILS: asc=%s - EXIT!', $asc));
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
* @throws InvalidArgumentException If a paramter is invalid
* @todo Improve documentation
*/
private static function packString (string $str) {
// Check parameter
//* NOISY-DEBUG */ self::createDebugInstance(__CLASS__, __LINE__)->traceMessage(sprintf('STRING-UTILS: str=%s - CALLED!', $str));
if (empty($str)) {
// Throw IAE
throw new InvalidArgumentException('Parameter "str" is empty');
}
// First compress the string (gzcompress is okay)
$str = gzcompress($str);
// Init variable
$packed = '';
// And start the "encoding" loop
//* NOISY-DEBUG */ self::createDebugInstance(__CLASS__, __LINE__)->debugMessage(sprintf('STRING-UTILS: Looping through str()=%d ...', strlen($str)));
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);
//* N NOISY-DEBUG */ self::createDebugInstance(__CLASS__, __LINE__)->debugMessage(sprintf('STRING-UTILS: big=%d,chunk(%d)=%s', $big, strlen($chunk), md5($chunk)));
$packed .= $chunk;
}
// Return it
//* NOISY-DEBUG */ self::createDebugInstance(__CLASS__, __LINE__)->traceMessage(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
* @throws InvalidArgumentException If a paramter is invalid
*/
public static function hexval (string $num, bool $assertMismatch = false) {
// Check parameter
//* NOISY-DEBUG */ self::createDebugInstance(__CLASS__, __LINE__)->traceMessage(sprintf('STRING-UTILS: num=%s,assertMismatch=%d - CALLED!', $num, intval($assertMismatch)));
if (empty($num)) {
// Throw IAE
throw new InvalidArgumentException('Parameter "num" is empty');
}
// Filter all numbers out
$ret = preg_replace('/[^0123456789abcdefABCDEF]/', '', $num);
// Assert only if requested
//* NOISY-DEBUG */ self::createDebugInstance(__CLASS__, __LINE__)->debugMessage(sprintf('STRING-UTILS: ret=%s', $ret));
if ($assertMismatch === true) {
// Has the whole value changed?
assert(('' . $ret . '' != '' . $num . '') && (!is_null($num)));
}
// Return result
//* NOISY-DEBUG */ self::createDebugInstance(__CLASS__, __LINE__)->traceMessage(sprintf('STRING-UTILS: ret=%s - EXIT!', $ret));
return $ret;
}
}