datatable.directory codebase https://datatable.directory/
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
datatable.directory/porklib/Utils/Str.php

694 lines
16 KiB

7 years ago
<?php
namespace MightyPork\Utils;
use MightyPork\Exceptions\FormatException;
use Illuminate\Support\Collection;
use Log;
class Str extends \Illuminate\Support\Str
{
protected static $cty_snake_cache = [];
/**
* Split and trim
*
* TODO unit test
*
* @param string $haystack string to split
* @param string|string[] $delimiters delimiter
* @return array pieces, trimmed.
*/
public static function splitTrim($haystack, $delimiters=[',', ';', '|'])
{
$haystack = trim($haystack);
if (strlen($haystack) == 0) return [];
return array_map('trim', self::split($haystack, $delimiters));
}
/**
* Customized snake case for cty aliases
*
* @param $camel
* @return string
*/
public static function snakeAlias($camel)
{
if (isset(static::$cty_snake_cache[$camel]))
return static::$cty_snake_cache[$camel];
$c = $camel;
$c = preg_replace_callback('/([A-Z0-9]+)(?![a-z])/', function ($m) {
return ucfirst(strtolower($m[1]));
}, $c);
$c = self::snake($c, '_');
// fix for underscores in cty class name
$c = preg_replace('/_+/', '_', $c);
return static::$cty_snake_cache[$camel] = $c;
}
/**
* Split a string using one or more delimiters
*
* TODO unit test
*
* @param string $haystack
* @param string|array $delimiters
* @return array
*/
public static function split($haystack, $delimiters)
{
if (is_string($delimiters)) {
return explode($delimiters, $haystack);
}
// make sure it's array
if (!is_array($delimiters)) {
$delimiters = [$delimiters];
}
// helper
$regex_escape = function ($x) {
return preg_quote($x, '/');
};
// compose splitting regex
$reg = "/" . implode('|', array_map($regex_escape, $delimiters)) . "/";
return preg_split($reg, $haystack);
}
/**
* Split "CSV" string to items, trim each item.
* Empty values at start and end are discarded.
*
* TODO unit test
*
* @param string $str
* @return array items
*/
public static function splitCsv($str)
{
return array_map('trim', explode(',', trim($str, ',')));
}
/**
* Pad an integer to 2 digit
*
* TODO unit test
*
* @param int $int the number
* @return string padded with zero
*/
public static function pad2($int)
{
return sprintf("%02d", $int);
}
/**
* Remove diacritics from a string
*
* TODO unit test
*
* @param string $str
* @return string ascii
*/
public static function asciify($str)
{
return iconv('UTF-8', 'US-ASCII//TRANSLIT', $str);
}
/**
* Public for unit tests
*
* Convert mask to regex
*
* @param $mask
* @return mixed|string
*/
public static function _pregMaskPrepare($mask)
{
$mask = preg_quote($mask);
// number = repeat
$mask = preg_replace('#(?<![.*+,{\d\\\\]|^)(\d+)#', '{$1}', $mask); // can repeat ?
$mask = strtr($mask, [
'\?' => '?',
'\*' => '*',
'\+' => '+',
'\{' => '{',
'\}' => '}',
'\(' => '(',
'\)' => ')',
'd' => '\d',
'F' => '-?\d+(\.\d+)?',
'D' => '-?\d+',
'a' => '[[:alpha:]]',
'\\\\' => '\\',
]);
$mask = strtr($mask, [
'\\\\' => '\\',
]);
return $mask;
}
/**
* Match a string against a mask.
*
* Special symbols:
* - `*` repeat previous any number of times
* - `?` previous is optional
* - `()` grouping
* - `a` alpha
* - `d` digit
* - `F` float value d, d.ddd with optional leading -
* - `+` repeat previous symbol any number of times
* - Number - repeat N times
* - {N}, {M,N} - repeat n times, like in regex
*
* Any other characters are matched literally.
*
* @param string $mask mask to match against
* @param string $string tested string
* @return bool matches
*/
public static function maskMatch($mask, $string)
{
$mask = self::_pregMaskPrepare($mask);
return 1 === preg_match('|^' . $mask . '$|u', $string);
}
/**
* Format a string with {0} {foo} or {}
*
* TODO unit test
*
* @param string $format
* @param array ...$args substitutions. Can also be an explicit array.
* @return string
*/
public static function format($format, ...$args)
{
$args = func_get_args();
$format = array_shift($args);
// explicit array given
if (is_array($args[0])) {
$args = $args[0];
}
$format = preg_replace_callback('#\{\}#', function () {
static $i = 0;
return '{' . ($i++) . '}';
}, $format);
return str_replace(
array_map(function ($k) {
return '{' . $k . '}';
}, array_keys($args)),
array_values($args),
$format
);
}
/**
* Remove whitespace on the left side of a block.
* This function is quite expensive, so it's recommended
* to cache the result, if possible.
*
* <b>NOTE:</b> The current implementation converts all leading tabs to spaces.
*
* @param string $txt text to trim
* @param int $tab_size number of spaces per tab, default 4
* @param bool $ltrim_nl remove a leading newline
* @param bool $ign_noindent ignore lines with zero indentation
* @return string left-trimmed block.
*/
public static function unindentBlock($txt, $tab_size = 4, $ltrim_nl = true, $ign_noindent = true)
{
$pad = 1024; // max indent size
$tabsp = str_repeat(' ', $tab_size);
if ($ltrim_nl) {
// also make sure first line is not blank
$txt = ltrim($txt, "\n");
}
$txt = preg_replace_callback('/^([ \t]*)(?=[^ \t\n]|$)/m',
function ($m) use (&$pad, $tabsp, $ign_noindent) {
static $i = 0;
$indent = $m[1];
if ($indent == '' && $ign_noindent) {
// no indentation, perhaps stripped by editor?
return '';
}
// normalize tab to 4 spaces
$normalized = strtr($indent, ["\t" => $tabsp]);
$len = strlen($normalized);
$pad = min($pad, $len);
$i++;
return $normalized;
},
$txt
);
return preg_replace("/^ {{$pad}}/m", '', $txt);
}
/**
* Get a simple string representation of an array, similar to json_encode(),
* except without the obnoxious quotes and escapes.
*
* @param array $array
* @return string [a, b, c]
*/
public static function arr($array)
{
if ($array instanceof Collection || Utils::isAssoc($array)) {
$x = '[';
foreach ($array as $k => $v) {
$x .= "$k:" . json_encode($v) . ', ';
}
$x = rtrim($x, ', ') . ']';
return $x;
} else {
return '[' . implode(', ', collect($array)->map(function ($x) {
return is_array($x) ? json_encode($x, JSON_UNESCAPED_UNICODE) : (string) $x;
})->toArray()) . ']';
}
}
/**
* Get substring from last occurrence of token
*
* TODO unit test
*
* @param string $token token delimiting the last chunk from left. Not included.
* @param string $haystack
* @return string part of haystack after token.
*/
public static function fromLast($token, $haystack)
{
$rpos = strrpos($haystack, $token);
if ($rpos === false) return $haystack;
return substr($haystack, $rpos + strlen($token));
}
public static function rpad($str, $len, $fill = ' ')
{
$filln = max(0, $len - mb_strlen($str));
return $str . str_repeat(mb_substr($fill, 0, 1), $filln);
}
public static function lpad($str, $len, $fill = ' ')
{
$filln = max(0, $len - mb_strlen($str));
return str_repeat(mb_substr($fill, 0, 1), $filln) . $str;
}
/**
* Remove wrapping quotes from a string.
* C-slashes will also be removed.
*
* @param $str
* @return string
*/
public static function unquote($str)
{
if (!$str || !is_string($str)) {
return $str;
}
$a = $str[0];
$b = $str[strlen($str) - 1];
if ($a == $b && $a == '"' || $b == "'") {
$str = substr($str, 1, strlen($str) - 2);
}
$str = stripcslashes($str);
return $str;
}
/**
* Apply a rewrite.
*
* - Simple foo|bar rewrites 0|1 (or false|true)
* - Key-value rewrite is possible with 7=foo|9=bar
* - '*' (asterisk) matches everything (9=foo|*=other)
* - '\*' - match literal asterisk
* - starts with % - format using sprintf
* - Compare funcs can also be used: lt, gt, le, ge, range, in
* example: lt(100)=Foo|range(100,200)=Bar|gt(200)=Baz
*
* @param mixed $value value from expression
* @param string $rewrite rewrite patterns, | separated
* @return mixed result to show
*/
public static function rewrite($value, $rewrite)
{
// TODO předělat na jednodušší zápis
if ($rewrite[0] == '%') return sprintf($rewrite, $value);
$rewrite_map = [];
foreach (explode('|', trim($rewrite)) as $i => $rw) {
$ar = preg_split('/(?<![\\\\])=/', $rw);
if (count($ar) == 2) {
// key value pair
$key = trim(str_replace('\=', '=', $ar[0]));
$rewrite_map[$key] = trim(str_replace('\=', '=', $ar[1]));
} elseif (count($ar) == 1) {
// literal rewrite
$rewrite_map[$i] = $rw;
} else {
Log::warning("Invalid rewrite format: $rw");
return $value; // don't rewrite it
}
}
// apply the rewrite if any
foreach ($rewrite_map as $k => $replacement) {
if (is_numeric($k) && (((int)$k == (int)$value) || abs((float)$k - (float)$value)<0.00001)) {
// exact match
return $replacement;
}
else if (($k==='true'||$k==='false') && (Utils::parseBool($k) == Utils::parseBool($value))) {
// bool match
return $replacement;
}
else if (preg_match('/([a-z]+)\(([^)]+)\)/i', $k, $mm)) {
// we have a comparing function
if (static::testCompareFunc($value, $mm[1], $mm[2])) {
return $replacement;
}
}
else if ($k === '*') {
return $replacement; // catch-all
}
else if ($k === '\\*') {
if ($value === '*') {
return $replacement; // literal asterisk
}
}
}
return $value;
}
/**
* Check if compare function matches value, copied from FB2
*
* @param mixed $value value to format
* @param string $func function name
* @param mixed $argument extra argument for the func
* @return bool
*/
private static function testCompareFunc($value, $func, $argument)
{
$value_f = floatval($value);
$arg_f = floatval($argument);
$fun = trim(strtolower($func));
switch ($fun) {
case 'lt':
return $value_f < $arg_f;
case 'gt':
return $value_f > $arg_f;
case 'le':
return $value_f <= $arg_f;
case 'ge':
return $value_f >= $arg_f;
case 'eq':
return $value_f == $arg_f; // this is kinda useless, but to make the set complete
case 'range': // range(-10,0) = zima
$bounds = array_map(function ($x) {
return floatval(trim($x));
}, explode(',', $argument));
if (count($bounds) != 2) {
Log::error("Invalid range bounds: $argument");
return false;
}
return $value_f >= $bounds[0] && $value_f < $bounds[1];
case 'in': // in(10,20,30) = 10, 20 or 30 *funguje i pro string in(ZAP,VYP) = ZAP nebo VYP
$bounds = array_map('trim', explode(',', $argument));
return in_array(trim($value), $bounds);
default:
Log::error("Invalid rewrite function: $func");
return false;
}
}
/**
* Apply multiple sets of substitutions to a format and get all the results.
*
* @param string $format Format same as for Str::format(). Fields are marked {}, {0} or {key}
* @param array $subs_arrays array of arrays of substitutions - eg. [[a1, b1], [a2, b2], ...]
* @return array
*/
public static function mapFormat($format, $subs_arrays)
{
$gather = [];
foreach ($subs_arrays as $subs) {
if (!is_array($subs)) $subs = [$subs];
$gather[] = Str::format($format, $subs);
}
return $gather;
}
/**
* Find all needle positions within a haystack
*
* @param string $haystack
* @param string $needle
* @return array
*/
public static function findPositions($haystack, $needle)
{
$lastPos = 0;
$positions = [];
while (($lastPos = strpos($haystack, $needle, $lastPos)) !== false) {
$positions[] = $lastPos;
$lastPos = $lastPos + strlen($needle);
}
return $positions;
}
/**
* Discard positions in a string that are preceded by an unescaped backslash.
*
* @param string $str
* @param int[] $positions
* @return int[]
*/
public static function discardEscapedPositions($str, array $positions)
{
$actualPos = [];
foreach ($positions as $pos) {
if ($pos >= 1) {
if ($str[$pos - 1] == '\\') {
if ($pos >= 2 && $str[$pos - 2] == '\\') {
// escaped backslash before - it's a valid quote
} else {
// this quote is escaped
continue;
}
}
}
$actualPos[] = $pos;
}
return $actualPos;
}
/**
* Split a string at given positions
*
* @param string $string
* @param int[] $positions
* @return string[]
*/
public static function splitAt($string, $positions)
{
$chunks = [];
array_push($positions, strlen($string));
array_unshift($positions, -1);
foreach ($positions as $i => $position) {
if ($position >= strlen($string)) break;
$chunks[] = substr($string, $position + 1, $positions[$i + 1] - $position - 1);
}
return $chunks;
}
/**
* Split a string to pieces by commas, ignoring commas within strings delimited by double quotes.
* A double quote can be escaped using a backslash.
*
* @param string $str
* @param string $delimiter
* @return mixed
*/
public static function splitCommandArgs($str, $delimiter = ',')
{
if ($str === '') return [];
// Find unescaped quotes
$quotes = Str::findPositions($str, '"');
$quotes = Str::discardEscapedPositions($str, $quotes);
if (count($quotes) % 2 != 0) {
throw new FormatException("Unmatched quote in command arguments: $str");
}
$commas = Str::findPositions($str, $delimiter);
$commas = Utils::discardPositionsWithinPairs($commas, $quotes);
$chunks = Str::splitAt($str, $commas);
$arr = collect($chunks)->trim()->toArray();
return $arr;
}
/**
* Get substring to first match of a token
*
* @param string $token token delimiting the parts
* @param string $haystack full string
* @return string portion of the string until the first token; or the whole string if token is not present.
*/
public static function toFirst($token, $haystack)
{
$lpos = strpos($haystack, $token);
if ($lpos === false) return $haystack;
return substr($haystack, 0, $lpos);
}
/**
* Get substring from first match of a token
*
* @param string $token token delimiting the parts
* @param string $haystack full string
* @return string portion of the string until the first token; or empty string if token is not present.
*/
public static function fromFirst($token, $haystack, $exclusive = false)
{
$lpos = strpos($haystack, $token);
if ($lpos === false) return '';
return substr($haystack, $lpos+($exclusive?1:0));
}
public static function toLast($token, $haystack)
{
$rpos = strrpos($haystack, $token);
if ($rpos === false) return $haystack;
return self::substr($haystack, 0, $rpos);
}
/**
* Check if translation exists
*
* @param string $descrKey tested transl key
* @return bool exists
*/
public static function translationExists($descrKey)
{
$tr = trans($descrKey);
return !preg_match('/^(\w+\.)+(\w+)$/', $tr);
}
/**
* Expand array of strings using bash-style repeat patterns {a,b,c}, {from..to}
*
* ie. to produce ad1 to ad4, use ad{1..4}. Supports multiple patterns, producing all permutations.
*
* @param string|string[] $sourceStrings
* @return string[]
*/
public static function expandBashRepeat($sourceStrings)
{
if (is_string($sourceStrings)) $sourceStrings = [$sourceStrings];
$outputs = [];
foreach($sourceStrings as $str) {
$i=0;
$arrays = [];
$str2 = preg_replace_callback('/\{([^{}]+)\}/', function($m) use(&$i, &$arrays) {
$seq = explode(',',$m[1]);
if(count($seq)>=2) {
$arrays[$i] = $seq;
} else {
$ab = explode('..',$m[1]);
if (!$ab) { return $m[0]; }
$a = intval($ab[0]);
$b = intval($ab[1]);
$arrays[$i] = range($a, $b);
}
return '{{'.($i++).'}}';
}, $str);
$strs = [$str2];
for($n=count($arrays)-1;$n>=0;$n--) {
$arr = $arrays[$n];
$tmpstrs = [];
foreach($arr as $subs) {
foreach($strs as $ss) {
$tmpstrs[] = str_replace('{{'.$n.'}}', $subs, $ss);
}
}
$strs = $tmpstrs;
}
$outputs = array_merge($outputs, $strs);
}
return $outputs;
}
/**
* Remove trailing commas from JSON string
*
* @param string $str
* @return string
*/
public static function cleanJson($str)
{
return preg_replace('/,\s*([}\]])/s','\1', $str);
}
public static function ellipsis($str, $maxlen)
{
$len = mb_strlen($str);
if ($len > $maxlen) {
return mb_substr($str, 0, $maxlen) . '…';
}
return $str;
}
}