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.
693 lines
16 KiB
693 lines
16 KiB
<?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;
|
|
}
|
|
}
|
|
|