Files
lichterei-web/kirby/src/Toolkit/Str.php
2022-08-24 10:59:39 +02:00

1443 lines
37 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
namespace Kirby\Toolkit;
use DateTime;
use Exception;
use IntlDateFormatter;
use Kirby\Cms\Helpers;
use Kirby\Exception\InvalidArgumentException;
/**
* The String class provides a set
* of handy methods for string
* handling and manipulation.
*
* @package Kirby Toolkit
* @author Bastian Allgeier <bastian@getkirby.com>
* @link https://getkirby.com
* @copyright Bastian Allgeier
* @license https://opensource.org/licenses/MIT
*/
class Str
{
/**
* Language translation table
*
* @var array
*/
public static $language = [];
/**
* Ascii translation table
*
* @var array
*/
public static $ascii = [
'/°|₀/' => '0',
'/¹|₁/' => '1',
'/²|₂/' => '2',
'/³|₃/' => '3',
'/⁴|₄/' => '4',
'/⁵|₅/' => '5',
'/⁶|₆/' => '6',
'/⁷|₇/' => '7',
'/⁸|₈/' => '8',
'/⁹|₉/' => '9',
'/À|Á|Â|Ã|Å|Ǻ|Ā|Ă|Ą|Ǎ|Ä|A/' => 'A',
'/à|á|â|ã|å|ǻ|ā|ă|ą|ǎ|ª|æ|ǽ|ä|a|а/' => 'a',
'/Б/' => 'B',
'/б/' => 'b',
'/Ç|Ć|Ĉ|Ċ|Č|Ц/' => 'C',
'/ç|ć|ĉ|ċ|č|ц/' => 'c',
'/Ð|Ď|Đ/' => 'Dj',
'/ð|ď|đ/' => 'dj',
'/Д/' => 'D',
'/д/' => 'd',
'/È|É|Ê|Ë|Ē|Ĕ|Ė|Ę|Ě|Е|Ё|Э/' => 'E',
'/è|é|ê|ë|ē|ĕ|ė|ę|ě|е|ё|э/' => 'e',
'/Ф/' => 'F',
'/ƒ|ф/' => 'f',
'/Ĝ|Ğ|Ġ|Ģ|Г/' => 'G',
'/ĝ|ğ|ġ|ģ|г/' => 'g',
'/Ĥ|Ħ|Х/' => 'H',
'/ĥ|ħ|х/' => 'h',
'/Ì|Í|Î|Ï|Ĩ|Ī|Ĭ|Ǐ|Į|İ|И/' => 'I',
'/ì|í|î|ï|ĩ|ī|ĭ|ǐ|į|ı|и|i̇/' => 'i',
'/Ĵ|Й/' => 'J',
'/ĵ|й/' => 'j',
'/Ķ|К/' => 'K',
'/ķ|к/' => 'k',
'/Ĺ|Ļ|Ľ|Ŀ|Ł|Л/' => 'L',
'/ĺ|ļ|ľ|ŀ|ł|л/' => 'l',
'/М/' => 'M',
'/м/' => 'm',
'/Ñ|Ń|Ņ|Ň|Н/' => 'N',
'/ñ|ń|ņ|ň|ʼn|н/' => 'n',
'/Ò|Ó|Ô|Õ|Ō|Ŏ|Ǒ|Ő|Ơ|Ø|Ǿ|Ö|O/' => 'O',
'/ò|ó|ô|õ|ō|ŏ|ǒ|ő|ơ|ø|ǿ|º|ö|o|о/' => 'o',
'/П/' => 'P',
'/п/' => 'p',
'/Ŕ|Ŗ|Ř|Р/' => 'R',
'/ŕ|ŗ|ř|р/' => 'r',
'/Ś|Ŝ|Ş|Ș|Š|С/' => 'S',
'/ś|ŝ|ş|ș|š|ſ|с/' => 's',
'/Ţ|Ț|Ť|Ŧ|Т/' => 'T',
'/ţ|ț|ť|ŧ|т/' => 't',
'/Ù|Ú|Û|Ũ|Ū|Ŭ|Ů|Ű|Ų|Ư|Ǔ|Ǖ|Ǘ|Ǚ|Ǜ|У|Ü|U/' => 'U',
'/ù|ú|û|ũ|ū|ŭ|ů|ű|ų|ư|ǔ|ǖ|ǘ|ǚ|ǜ|у|ü|u/' => 'u',
'/В/' => 'V',
'/в/' => 'v',
'/Ý|Ÿ|Ŷ|Ы/' => 'Y',
'/ý|ÿ|ŷ|ы/' => 'y',
'/Ŵ/' => 'W',
'/ŵ/' => 'w',
'/Ź|Ż|Ž|З/' => 'Z',
'/ź|ż|ž|з/' => 'z',
'/Æ|Ǽ/' => 'AE',
'/ß/' => 'ss',
'/IJ/' => 'IJ',
'/ij/' => 'ij',
'/Œ/' => 'OE',
'/Ч/' => 'Ch',
'/ч/' => 'ch',
'/Ю/' => 'Ju',
'/ю/' => 'ju',
'/Я/' => 'Ja',
'/я/' => 'ja',
'/Ш/' => 'Sh',
'/ш/' => 'sh',
'/Щ/' => 'Shch',
'/щ/' => 'shch',
'/Ж/' => 'Zh',
'/ж/' => 'zh',
];
/**
* Default settings for class methods
*
* @var array
*/
public static $defaults = [
'slug' => [
'separator' => '-',
'allowed' => 'a-z0-9'
]
];
/**
* Parse accepted values and their quality from an
* accept string like an Accept or Accept-Language header
*
* @param string $input
* @return array
*/
public static function accepted(string $input): array
{
$items = [];
// check each type in the Accept header
foreach (static::split($input, ',') as $item) {
$parts = static::split($item, ';');
$value = A::first($parts); // $parts now only contains params
$quality = 1;
// check for the q param ("quality" of the type)
foreach ($parts as $param) {
$param = static::split($param, '=');
if (A::get($param, 0) === 'q' && !empty($param[1])) {
$quality = $param[1];
}
}
$items[$quality][] = $value;
}
// sort items by quality
krsort($items);
$result = [];
foreach ($items as $quality => $values) {
foreach ($values as $value) {
$result[] = [
'quality' => (float)$quality,
'value' => $value
];
}
}
return $result;
}
/**
* Returns the rest of the string after the given substring or character
*
* @param string $string
* @param string $needle
* @param bool $caseInsensitive
* @return string
*/
public static function after(string $string, string $needle, bool $caseInsensitive = false): string
{
$position = static::position($string, $needle, $caseInsensitive);
if ($position === false) {
return '';
}
return static::substr($string, $position + static::length($needle));
}
/**
* Removes the given substring or character only from the start of the string
* @since 3.7.0
*
* @param string $string
* @param string $needle
* @param bool $caseInsensitive
* @return string
*/
public static function afterStart(string $string, string $needle, bool $caseInsensitive = false): string
{
if ($needle === '') {
return $string;
}
if (static::startsWith($string, $needle, $caseInsensitive) === true) {
return static::substr($string, static::length($needle));
}
return $string;
}
/**
* Convert a string to 7-bit ASCII.
*
* @param string $string
* @return string
*/
public static function ascii(string $string): string
{
$string = str_replace(
array_keys(static::$language),
array_values(static::$language),
$string
);
$string = preg_replace(
array_keys(static::$ascii),
array_values(static::$ascii),
$string
);
return preg_replace('/[^\x09\x0A\x0D\x20-\x7E]/', '', $string);
}
/**
* Returns the beginning of a string before the given substring or character
*
* @param string $string
* @param string $needle
* @param bool $caseInsensitive
* @return string
*/
public static function before(string $string, string $needle, bool $caseInsensitive = false): string
{
$position = static::position($string, $needle, $caseInsensitive);
if ($position === false) {
return '';
}
return static::substr($string, 0, $position);
}
/**
* Removes the given substring or character only from the end of the string
* @since 3.7.0
*
* @param string $string
* @param string $needle
* @param bool $caseInsensitive
* @return string
*/
public static function beforeEnd(string $string, string $needle, bool $caseInsensitive = false): string
{
if ($needle === '') {
return $string;
}
if (static::endsWith($string, $needle, $caseInsensitive) === true) {
return static::substr($string, 0, -static::length($needle));
}
return $string;
}
/**
* Returns everything between two strings from the first occurrence of a given string
*
* @param string $string
* @param string $start
* @param string $end
* @return string
*/
public static function between(string $string = null, string $start, string $end): string
{
return static::before(static::after($string, $start), $end);
}
/**
* Converts a string to camel case
*
* @param string $value The string to convert
* @return string
*/
public static function camel(string $value = null): string
{
return lcfirst(static::studly($value));
}
/**
* Checks if a str contains another string
*
* @param string $string
* @param string $needle
* @param bool $caseInsensitive
* @return bool
*/
public static function contains(string $string = null, string $needle, bool $caseInsensitive = false): bool
{
if ($needle === '') {
return true;
}
$method = $caseInsensitive === true ? 'stripos' : 'strpos';
return call_user_func($method, $string ?? '', $needle) !== false;
}
/**
* Convert timestamp to date string
* according to locale settings
*
* @param int|null $time
* @param string|\IntlDateFormatter|null $format
* @param string $handler date, intl or strftime
* @return string|int
*/
public static function date(?int $time = null, $format = null, string $handler = 'date')
{
if (is_null($format) === true) {
return $time;
}
// $format is an IntlDateFormatter instance
if (is_a($format, 'IntlDateFormatter') === true) {
return $format->format($time ?? time());
}
// `intl` handler
if ($handler === 'intl') {
$datetime = new DateTime();
if ($time !== null) {
$datetime->setTimestamp($time);
}
return IntlDateFormatter::formatObject($datetime, $format);
}
// handle `strftime` to be able
// to suppress deprecation warning
// TODO: remove strftime support for PHP 9.0
if ($handler === 'strftime') {
// make sure timezone is set correctly
date_default_timezone_get();
return @strftime($format, $time);
}
return $handler($format, $time);
}
/**
* Converts a string to a different encoding
*
* @param string $string
* @param string $targetEncoding
* @param string $sourceEncoding (optional)
* @return string
*/
public static function convert($string, $targetEncoding, $sourceEncoding = null)
{
// detect the source encoding if not passed as third argument
if ($sourceEncoding === null) {
$sourceEncoding = static::encoding($string);
}
// no need to convert if the target encoding is the same
if (strtolower($sourceEncoding) === strtolower($targetEncoding)) {
return $string;
}
return iconv($sourceEncoding, $targetEncoding, $string);
}
/**
* Encode a string (used for email addresses)
*
* @param string $string
* @return string
*/
public static function encode(string $string): string
{
$encoded = '';
for ($i = 0; $i < static::length($string); $i++) {
$char = static::substr($string, $i, 1);
list(, $code) = unpack('N', mb_convert_encoding($char, 'UCS-4BE', 'UTF-8'));
$encoded .= rand(1, 2) === 1 ? '&#' . $code . ';' : '&#x' . dechex($code) . ';';
}
return $encoded;
}
/**
* Tries to detect the string encoding
*
* @param string $string
* @return string
*/
public static function encoding(string $string): string
{
return mb_detect_encoding($string, 'UTF-8, ISO-8859-1, windows-1251', true);
}
/**
* Checks if a string ends with the passed needle
*
* @param string $string
* @param string $needle
* @param bool $caseInsensitive
* @return bool
*/
public static function endsWith(string $string = null, string $needle, bool $caseInsensitive = false): bool
{
if ($needle === '') {
return true;
}
$probe = static::substr($string, -static::length($needle));
if ($caseInsensitive === true) {
$needle = static::lower($needle);
$probe = static::lower($probe);
}
return $needle === $probe;
}
/**
* Escape string for context specific output
* @since 3.7.0
*
* @param string $string Untrusted data
* @param string $context Location of output (`html`, `attr`, `js`, `css`, `url` or `xml`)
* @return string Escaped data
*/
public static function esc(string $string, string $context = 'html'): string
{
if (method_exists('Kirby\Toolkit\Escape', $context) === true) {
return Escape::$context($string);
}
return $string;
}
/**
* Creates an excerpt of a string
* It removes all html tags first and then cuts the string
* according to the specified number of chars.
*
* @param string $string The string to be shortened
* @param int $chars The final number of characters the string should have
* @param bool $strip True: remove the HTML tags from the string first
* @param string $rep The element, which should be added if the string is too long. Ellipsis is the default.
* @return string The shortened string
*/
public static function excerpt($string, $chars = 140, $strip = true, $rep = ' …')
{
if ($strip === true) {
$string = strip_tags(str_replace('<', ' <', $string));
}
// replace line breaks with spaces
$string = str_replace(PHP_EOL, ' ', trim($string));
// remove double spaces
$string = preg_replace('![ ]{2,}!', ' ', $string);
if ($chars === 0) {
return $string;
}
if (static::length($string) <= $chars) {
return $string;
}
return static::substr($string, 0, mb_strrpos(static::substr($string, 0, $chars), ' ')) . $rep;
}
/**
* Convert the value to a float with a decimal
* point, no matter what the locale setting is
*
* @param string|int|float $value
* @return string
*/
public static function float($value): string
{
// make sure $value is not null
$value ??= '';
// Convert exponential to decimal, 1e-8 as 0.00000001
if (strpos(strtolower($value), 'e') !== false) {
$value = rtrim(sprintf('%.16f', (float)$value), '0');
}
$value = str_replace(',', '.', $value);
$decimal = strlen(substr(strrchr($value, '.'), 1));
return number_format((float)$value, $decimal, '.', '');
}
/**
* Returns the rest of the string starting from the given character
*
* @param string $string
* @param string $needle
* @param bool $caseInsensitive
* @return string
*/
public static function from(string $string, string $needle, bool $caseInsensitive = false): string
{
$position = static::position($string, $needle, $caseInsensitive);
if ($position === false) {
return '';
}
return static::substr($string, $position);
}
/**
* Adds `-1` to a string or increments the ending number to allow `-2`, `-3`, etc.
* @since 3.7.0
*
* @param string $string The string to increment
* @param string $separator
* @param int $first Starting number
* @return string
*/
public static function increment(string $string, string $separator = '-', int $first = 1): string
{
preg_match('/(.+)' . preg_quote($separator, '/') . '([0-9]+)$/', $string, $matches);
if (isset($matches[2]) === true) {
// increment the existing ending number
return $matches[1] . $separator . ((int)$matches[2] + 1);
}
// append a new ending number
return $string . $separator . $first;
}
/**
* Checks if the given string is a URL
*
* @param string|null $string
* @return bool
* @deprecated 3.6.0 use `Kirby\Toolkit\V::url()` instead
* @todo Remove in 3.8.0
* @codeCoverageIgnore
*/
public static function isURL(?string $string = null): bool
{
Helpers::deprecated('Toolkit\Str::isUrl() has been deprecated and will be removed in Kirby 3.8.0. Use Toolkit\V::url() instead.');
return filter_var($string, FILTER_VALIDATE_URL) !== false;
}
/**
* Convert a string to kebab case.
*
* @param string $value
* @return string
*/
public static function kebab(string $value = null): string
{
return static::snake($value, '-');
}
/**
* A UTF-8 safe version of strlen()
*
* @param string $string
* @return int
*/
public static function length(string $string = null): int
{
return mb_strlen($string ?? '', 'UTF-8');
}
/**
* A UTF-8 safe version of strtolower()
*
* @param string $string
* @return string
*/
public static function lower(string $string = null): string
{
return mb_strtolower($string ?? '', 'UTF-8');
}
/**
* Safe ltrim alternative
*
* @param string $string
* @param string $trim
* @return string
*/
public static function ltrim(string $string, string $trim = ' '): string
{
return preg_replace('!^(' . preg_quote($trim) . ')+!', '', $string);
}
/**
* Get a character pool with various possible combinations
*
* @param string|array $type
* @param bool $array
* @return string|array
*/
public static function pool($type, bool $array = true)
{
$pool = [];
if (is_array($type) === true) {
foreach ($type as $t) {
$pool = array_merge($pool, static::pool($t));
}
} else {
switch (strtolower($type)) {
case 'alphalower':
$pool = range('a', 'z');
break;
case 'alphaupper':
$pool = range('A', 'Z');
break;
case 'alpha':
$pool = static::pool(['alphaLower', 'alphaUpper']);
break;
case 'num':
$pool = range(0, 9);
break;
case 'alphanum':
$pool = static::pool(['alpha', 'num']);
break;
}
}
return $array ? $pool : implode('', $pool);
}
/**
* Returns the position of a needle in a string
* if it can be found
*
* @param string $string
* @param string $needle
* @param bool $caseInsensitive
* @return int|bool
*/
public static function position(string $string = null, string $needle, bool $caseInsensitive = false)
{
if ($needle === '') {
throw new InvalidArgumentException('The needle must not be empty');
}
if ($caseInsensitive === true) {
$string = static::lower($string);
$needle = static::lower($needle);
}
return mb_strpos($string ?? '', $needle, 0, 'UTF-8');
}
/**
* Runs a string query.
* Check out the Query class for more information.
*
* @param string $query
* @param array $data
* @return string|null
*/
public static function query(string $query, array $data = [])
{
return (new Query($query, $data))->result();
}
/**
* Generates a random string that may be used for cryptographic purposes
*
* @param int $length The length of the random string
* @param string $type Pool type (type of allowed characters)
* @return string
*/
public static function random(int $length = null, string $type = 'alphaNum')
{
if ($length === null) {
$length = random_int(5, 10);
}
$pool = static::pool($type, false);
// catch invalid pools
if (!$pool) {
return false;
}
// regex that matches all characters *not* in the pool of allowed characters
$regex = '/[^' . $pool . ']/';
// collect characters until we have our required length
$result = '';
while (($currentLength = strlen($result)) < $length) {
$missing = $length - $currentLength;
$bytes = random_bytes($missing);
$result .= substr(preg_replace($regex, '', base64_encode($bytes)), 0, $missing);
}
return $result;
}
/**
* Replaces all or some occurrences of the search string with the replacement string
* Extension of the str_replace() function in PHP with an additional $limit parameter
*
* @param string|array $string String being replaced on (haystack);
* can be an array of multiple subject strings
* @param string|array $search Value being searched for (needle)
* @param string|array $replace Value to replace matches with
* @param int|array $limit Maximum possible replacements for each search value;
* multiple limits for each search value are supported;
* defaults to no limit
* @return string|array String with replaced values;
* if $string is an array, array of strings
*/
public static function replace($string, $search, $replace, $limit = -1)
{
// convert Kirby collections to arrays
if (is_a($string, 'Kirby\Toolkit\Collection') === true) {
$string = $string->toArray();
}
if (is_a($search, 'Kirby\Toolkit\Collection') === true) {
$search = $search->toArray();
}
if (is_a($replace, 'Kirby\Toolkit\Collection') === true) {
$replace = $replace->toArray();
}
// without a limit we might as well use the built-in function
if ($limit === -1) {
return str_replace($search, $replace, $string ?? '');
}
// if the limit is zero, the result will be no replacements at all
if ($limit === 0) {
return $string;
}
// multiple subjects are run separately through this method
if (is_array($string) === true) {
$result = [];
foreach ($string as $s) {
$result[] = static::replace($s, $search, $replace, $limit);
}
return $result;
}
// build an array of replacements
// we don't use an associative array because otherwise you couldn't
// replace the same string with different replacements
$replacements = static::replacements($search, $replace, $limit);
// run the string and the replacement array through the replacer
return static::replaceReplacements($string, $replacements);
}
/**
* Generates a replacement array out of dynamic input data
* Used for Str::replace()
*
* @param string|array $search Value being searched for (needle)
* @param string|array $replace Value to replace matches with
* @param int|array $limit Maximum possible replacements for each search value;
* multiple limits for each search value are supported;
* defaults to no limit
* @return array List of replacement arrays, each with a
* 'search', 'replace' and 'limit' attribute
*/
public static function replacements($search, $replace, $limit): array
{
$replacements = [];
if (is_array($search) === true && is_array($replace) === true) {
foreach ($search as $i => $s) {
// replace with an empty string if no replacement string was defined for this index;
// behavior is identical to the official PHP str_replace()
$r = $replace[$i] ?? '';
if (is_array($limit) === true) {
// don't apply a limit if no limit was defined for this index
$l = $limit[$i] ?? -1;
} else {
$l = $limit;
}
$replacements[] = ['search' => $s, 'replace' => $r, 'limit' => $l];
}
} elseif (is_array($search) === true && is_string($replace) === true) {
foreach ($search as $i => $s) {
if (is_array($limit) === true) {
// don't apply a limit if no limit was defined for this index
$l = $limit[$i] ?? -1;
} else {
$l = $limit;
}
$replacements[] = ['search' => $s, 'replace' => $replace, 'limit' => $l];
}
} elseif (is_string($search) === true && is_string($replace) === true && is_int($limit) === true) {
$replacements[] = compact('search', 'replace', 'limit');
} else {
throw new Exception('Invalid combination of $search, $replace and $limit params.');
}
return $replacements;
}
/**
* Takes a replacement array and processes the replacements
* Used for Str::replace()
*
* @param string $string String being replaced on (haystack)
* @param array $replacements Replacement array from Str::replacements()
* @return string String with replaced values
*/
public static function replaceReplacements(string $string, array $replacements): string
{
// replace in the order of the replacements
// behavior is identical to the official PHP str_replace()
foreach ($replacements as $replacement) {
if (is_int($replacement['limit']) === false) {
throw new Exception('Invalid limit "' . $replacement['limit'] . '".');
} elseif ($replacement['limit'] === -1) {
// no limit, we don't need our special replacement routine
$string = str_replace($replacement['search'], $replacement['replace'], $string);
} elseif ($replacement['limit'] > 0) {
// limit given, only replace for $replacement['limit'] times per replacement
$position = -1;
for ($i = 0; $i < $replacement['limit']; $i++) {
$position = strpos($string, $replacement['search'], $position + 1);
if (is_int($position) === true) {
$string = substr_replace($string, $replacement['replace'], $position, strlen($replacement['search']));
// adapt $pos to the now changed offset
$position = $position + strlen($replacement['replace']) - strlen($replacement['search']);
} else {
// no more match in the string
break;
}
}
}
}
return $string;
}
/**
* Safe rtrim alternative
*
* @param string $string
* @param string $trim
* @return string
*/
public static function rtrim(string $string, string $trim = ' '): string
{
return preg_replace('!(' . preg_quote($trim) . ')+$!', '', $string);
}
/**
* Replaces placeholders in string with values from the data array
* and escapes HTML in the results in `{{ }}` placeholders
* while leaving HTML special characters untouched in `{< >}` placeholders
*
* @since 3.6.0
*
* @param string|null $string The string with placeholders
* @param array $data Associative array with placeholders as
* keys and replacements as values.
* Supports query syntax.
* @param array $options An options array that contains:
* - fallback: if a token does not have any matches
* - callback: to be able to handle each matching result (escaping is applied after the callback)
*
* @return string The filled-in and partially escaped string
*/
public static function safeTemplate(string $string = null, array $data = [], array $options = []): string
{
$callback = is_a(($options['callback'] ?? null), 'Closure') === true ? $options['callback'] : null;
$fallback = $options['fallback'] ?? '';
// replace and escape
$string = static::template($string, $data, [
'start' => '{{',
'end' => '}}',
'callback' => function ($result, $query, $data) use ($callback) {
if ($callback !== null) {
$result = $callback($result, $query, $data);
}
return Escape::html($result);
},
'fallback' => $fallback
]);
// replace unescaped (specifically marked placeholders)
$string = static::template($string, $data, [
'start' => '{<',
'end' => '>}',
'callback' => $callback,
'fallback' => $fallback
]);
return $string;
}
/**
* Shortens a string and adds an ellipsis if the string is too long
*
* <code>
*
* echo Str::short('This is a very, very, very long string', 10);
* // output: This is a…
*
* echo Str::short('This is a very, very, very long string', 10, '####');
* // output: This i####
*
* </code>
*
* @param string $string The string to be shortened
* @param int $length The final number of characters the
* string should have
* @param string $appendix The element, which should be added if the
* string is too long. Ellipsis is the default.
* @return string The shortened string
*/
public static function short(string $string = null, int $length = 0, string $appendix = '…'): string
{
if ($string === null) {
return '';
}
if ($length === 0) {
return $string;
}
if (static::length($string) <= $length) {
return $string;
}
return static::substr($string, 0, $length) . $appendix;
}
/**
* Calculates the similarity between two strings with multibyte support
* @since 3.5.2
*
* @author Based on the work of Antal Áron
* @copyright Original Copyright (c) 2017, Antal Áron
* @license https://github.com/antalaron/mb-similar-text/blob/master/LICENSE MIT License
* @param string $first
* @param string $second
* @param bool $caseInsensitive If `true`, strings are compared case-insensitively
* @return array matches: Number of matching chars in both strings
* percent: Similarity in percent
*/
public static function similarity(string $first, string $second, bool $caseInsensitive = false): array
{
$matches = 0;
$percent = 0.0;
if ($caseInsensitive === true) {
$first = static::lower($first);
$second = static::lower($second);
}
if (static::length($first) + static::length($second) > 0) {
$pos1 = $pos2 = $max = 0;
$len1 = static::length($first);
$len2 = static::length($second);
for ($p = 0; $p < $len1; ++$p) {
for ($q = 0; $q < $len2; ++$q) {
for (
$l = 0;
($p + $l < $len1) && ($q + $l < $len2) &&
static::substr($first, $p + $l, 1) === static::substr($second, $q + $l, 1);
++$l
) {
// nothing to do
}
if ($l > $max) {
$max = $l;
$pos1 = $p;
$pos2 = $q;
}
}
}
$matches = $max;
if ($matches) {
if ($pos1 && $pos2) {
$similarity = static::similarity(
static::substr($first, 0, $pos1),
static::substr($second, 0, $pos2)
);
$matches += $similarity['matches'];
}
if (($pos1 + $max < $len1) && ($pos2 + $max < $len2)) {
$similarity = static::similarity(
static::substr($first, $pos1 + $max, $len1 - $pos1 - $max),
static::substr($second, $pos2 + $max, $len2 - $pos2 - $max)
);
$matches += $similarity['matches'];
}
}
$percent = ($matches * 200.0) / ($len1 + $len2);
}
return compact('matches', 'percent');
}
/**
* Convert a string to a safe version to be used in a URL
*
* @param string $string The unsafe string
* @param string $separator To be used instead of space and
* other non-word characters.
* @param string $allowed List of all allowed characters (regex)
* @param int $maxlength The maximum length of the slug
* @return string The safe string
*/
public static function slug(string $string = null, string $separator = null, string $allowed = null, int $maxlength = 128): string
{
$separator ??= static::$defaults['slug']['separator'];
$allowed ??= static::$defaults['slug']['allowed'];
$string = trim($string ?? '');
$string = static::lower($string);
$string = static::ascii($string);
// replace spaces with simple dashes
$string = preg_replace('![^' . $allowed . ']!i', $separator, $string);
if (strlen($separator) > 0) {
// remove double separators
$string = preg_replace('![' . preg_quote($separator) . ']{2,}!', $separator, $string);
}
// replace slashes with dashes
$string = str_replace('/', $separator, $string);
// trim leading and trailing non-word-chars
$string = preg_replace('!^[^a-z0-9]+!', '', $string);
$string = preg_replace('![^a-z0-9]+$!', '', $string);
// cut the string after the given maxlength
return static::short($string, $maxlength, false);
}
/**
* Convert a string to snake case.
*
* @param string $value
* @param string $delimiter
* @return string
*/
public static function snake(string $value = null, string $delimiter = '_'): string
{
if (!ctype_lower($value)) {
$value = preg_replace('/\s+/u', '', ucwords($value));
$value = static::lower(preg_replace('/(.)(?=[A-Z])/u', '$1' . $delimiter, $value));
}
return $value;
}
/**
* Better alternative for explode()
* It takes care of removing empty values
* and it has a built-in way to skip values
* which are too short.
*
* @param string $string The string to split
* @param string $separator The string to split by
* @param int $length The min length of values.
* @return array An array of found values
*/
public static function split($string, string $separator = ',', int $length = 1): array
{
if (is_array($string) === true) {
return $string;
}
// make sure $string is string
$string ??= '';
$parts = explode($separator, $string);
$out = [];
foreach ($parts as $p) {
$p = trim($p);
if (static::length($p) > 0 && static::length($p) >= $length) {
$out[] = $p;
}
}
return $out;
}
/**
* Checks if a string starts with the passed needle
*
* @param string $string
* @param string $needle
* @param bool $caseInsensitive
* @return bool
*/
public static function startsWith(string $string = null, string $needle, bool $caseInsensitive = false): bool
{
if ($needle === '') {
return true;
}
return static::position($string, $needle, $caseInsensitive) === 0;
}
/**
* Converts a string to studly caps case
* @since 3.7.0
*
* @param string $value The string to convert
* @return string
*/
public static function studly(string $value = null): string
{
return str_replace(' ', '', ucwords(str_replace(['-', '_'], ' ', $value)));
}
/**
* A UTF-8 safe version of substr()
*
* @param string $string
* @param int $start
* @param int $length
* @return string
*/
public static function substr(string $string = null, int $start = 0, int $length = null): string
{
return mb_substr($string ?? '', $start, $length, 'UTF-8');
}
/**
* Replaces placeholders in string with values from the data array
*
* <code>
*
* echo Str::template('From {{ b }} to {{ a }}', ['a' => 'there', 'b' => 'here']);
* // output: From here to there
*
* </code>
*
* @param string|null $string The string with placeholders
* @param array $data Associative array with placeholders as
* keys and replacements as values.
* Supports query syntax.
* @param array $options An options array that contains:
* - fallback: if a token does not have any matches
* - callback: to be able to handle each matching result
* - start: start placeholder
* - end: end placeholder
* @return string The filled-in string
*/
public static function template(string $string = null, array $data = [], array $options = []): string
{
$fallback = $options['fallback'] ?? null;
$callback = is_a(($options['callback'] ?? null), 'Closure') === true ? $options['callback'] : null;
$start = (string)($options['start'] ?? '{{');
$end = (string)($options['end'] ?? '}}');
// make sure $string is string
$string ??= '';
return preg_replace_callback('!' . $start . '(.*?)' . $end . '!', function ($match) use ($data, $fallback, $callback) {
$query = trim($match[1]);
// if the placeholder contains a dot, it is a query
if (strpos($query, '.') !== false) {
try {
$result = (new Query($match[1], $data))->result();
} catch (Exception $e) {
$result = null;
}
} else {
$result = $data[$query] ?? null;
}
// if we don't have a result, use the fallback if given
if ($result === null && $fallback !== null) {
$result = $fallback;
}
// callback on result if given
if ($callback !== null) {
$result = $callback((string)$result, $query, $data);
}
// if we still don't have a result, keep the original placeholder
return $result ?? $match[0];
}, $string);
}
/**
* Converts a filesize string with shortcuts
* like M, G or K to an integer value
*
* @param string $size
* @return int
*/
public static function toBytes(string $size): int
{
$size = trim($size);
$last = strtolower($size[strlen($size)-1] ?? '');
$size = (int)$size;
switch ($last) {
case 'g':
$size *= 1024;
// no break
case 'm':
$size *= 1024;
// no break
case 'k':
$size *= 1024;
}
return $size;
}
/**
* Convert the string to the given type
*
* @param string $string
* @param mixed $type
* @return mixed
*/
public static function toType($string, $type)
{
if (is_string($type) === false) {
$type = gettype($type);
}
switch ($type) {
case 'array':
return (array)$string;
case 'bool':
case 'boolean':
return filter_var($string, FILTER_VALIDATE_BOOLEAN);
case 'double':
case 'float':
return (float)$string;
case 'int':
case 'integer':
return (int)$string;
}
return (string)$string;
}
/**
* Safe trim alternative
*
* @param string $string
* @param string $trim
* @return string
*/
public static function trim(string $string, string $trim = ' '): string
{
return static::rtrim(static::ltrim($string, $trim), $trim);
}
/**
* A UTF-8 safe version of ucfirst()
*
* @param string $string
* @return string
*/
public static function ucfirst(string $string = null): string
{
return static::upper(static::substr($string, 0, 1)) . static::lower(static::substr($string, 1));
}
/**
* A UTF-8 safe version of ucwords()
*
* @param string $string
* @return string
*/
public static function ucwords(string $string = null): string
{
return mb_convert_case($string ?? '', MB_CASE_TITLE, 'UTF-8');
}
/**
* Removes all html tags and encoded chars from a string
*
* <code>
*
* echo str::unhtml('some <em>crazy</em> stuff');
* // output: some uber crazy stuff
*
* </code>
*
* @param string $string
* @return string The html string
*/
public static function unhtml(string $string = null): string
{
return Html::decode($string);
}
/**
* Returns the beginning of a string until the given character
*
* @param string $string
* @param string $needle
* @param bool $caseInsensitive
* @return string
*/
public static function until(string $string, string $needle, bool $caseInsensitive = false): string
{
$position = static::position($string, $needle, $caseInsensitive);
if ($position === false) {
return '';
}
return static::substr($string, 0, $position + static::length($needle));
}
/**
* A UTF-8 safe version of strotoupper()
*
* @param string $string
* @return string
*/
public static function upper(string $string = null): string
{
return mb_strtoupper($string ?? '', 'UTF-8');
}
/**
* Creates a compliant v4 UUID
* Taken from: https://github.com/symfony/polyfill
* @since 3.7.0
*
* @return string
*/
public static function uuid(): string
{
$uuid = bin2hex(random_bytes(16));
return sprintf(
'%08s-%04s-4%03s-%04x-%012s',
// 32 bits for "time_low"
substr($uuid, 0, 8),
// 16 bits for "time_mid"
substr($uuid, 8, 4),
// 16 bits for "time_hi_and_version",
// four most significant bits holds version number 4
substr($uuid, 13, 3),
// 16 bits:
// * 8 bits for "clk_seq_hi_res",
// * 8 bits for "clk_seq_low",
// two most significant bits holds zero and one for variant DCE1.1
hexdec(substr($uuid, 16, 4)) & 0x3fff | 0x8000,
// 48 bits for "node"
substr($uuid, 20, 12)
);
}
/**
* The widont function makes sure that there are no
* typographical widows at the end of a paragraph
* that's a single word in the last line
*
* @param string $string
* @return string
*/
public static function widont(string $string = null): string
{
// make sure $string is string
$string ??= '';
// Replace space between last word and punctuation
$string = preg_replace_callback('|(\S)\s(\S?)$|u', function ($matches) {
return $matches[1] . '&nbsp;' . $matches[2];
}, $string);
// Replace space between last two words
return preg_replace_callback('|(\s)(?=\S*$)(\S+)|u', function ($matches) {
if (static::contains($matches[2], '-')) {
$matches[2] = str_replace('-', '&#8209;', $matches[2]);
}
return '&nbsp;' . $matches[2];
}, $string);
}
/**
* Wraps the string with the given string(s)
* @since 3.7.0
*
* @param string $string String to wrap
* @param string $before String to prepend
* @param string|null $after String to append (if different from `$before`)
* @return string
*/
public static function wrap(string $string, string $before, string $after = null): string
{
return $before . $string . ($after ?? $before);
}
}