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/Utils.php

994 lines
24 KiB

7 years ago
<?php
namespace MightyPork\Utils;
use Collator;
use Illuminate\Database\Events\QueryExecuted;
7 years ago
use MightyPork\Exceptions\ArgumentException;
use Illuminate\Log\Events\MessageLogged;
use Log;
use MightyPork\Exceptions\FormatException;
use MongoDB\BSON\ObjectID;
use Monolog\Formatter\LineFormatter;
use ReflectionClass;
use ReflectionMethod;
/**
* Miscellaneous utilities, mostly from FB2
*/
class Utils
{
/** @var Collator */
private static $collator = null;
/**
* Clamp to an array with lower and upper bounds
*
* @param float $value
* @param float[] $arr
* @return float
*/
public static function arrClamp($value, $arr)
{
if (!$arr) return $value;
if ($arr[0] !== null) $value = max($arr[0], $value);
if ($arr[1] !== null) $value = min($arr[1], $value);
return $value;
}
/**
* Convert possible "boolean" values to 1/0
*
* TODO unit test
*
* @param mixed $value boolean, integer or string
* @return int 1 or 0
*/
public static function parseBool01($value)
{
// already boolean - to 0 / 1
if (is_bool($value)) {
return $value ? 1 : 0;
}
7 years ago
if ($value === null) return false;
7 years ago
// convert to string if needed (ie. SimpleXMLElement)
// small overhead for numbers (negligible)
$value = (string) $value;
// Number or numeric string
if (is_numeric($value)) {
return ((int) $value) != 0 ? 1 : 0;
}
// match string constants
switch (strtolower("$value")) {
case 'on':
case 'true':
case 'yes':
return 1;
case 'off':
case 'false':
case 'no':
case 'null':
case '': // empty string
return 0;
default:
Log::warning("Could not convert \"$value\" to boolean.");
return 0;
}
}
/**
* Convert possible "boolean" values to true/false
*
* @param mixed $value boolean, integer or string
* @return bool
*/
public static function parseBool($value)
{
return (bool) self::parseBool01($value);
}
/**
* Get string representation of object type.
*
* Aliased as global helper "typeof"
*
* TODO unit test
*
* @param mixed $var inspected variable
* @return string
*/
public static function getType($var)
{
if (is_object($var)) return get_class($var);
if (is_null($var)) return 'null';
if (is_string($var)) return 'string';
if (is_array($var)) return 'array';
if (is_int($var)) return 'integer';
if (is_bool($var)) return 'boolean';
if (is_float($var)) return 'float';
if (is_resource($var)) return 'resource';
return 'unknown';
}
/**
* provide a Java style exception trace
*
* @param \Throwable $e
* @return array stack trace as array of lines
* from comment at: http://php.net/manual/en/class.exception.php
*/
public static function getStackTrace($e)
{
return explode("\n", self::getStackTraceAsString($e));
}
/**
* provide a Java style exception trace
*
* @param \Throwable $e
* @param array|null $seen internal for recursion
* @return string stack trace as string with multiple lines
*/
public static function getStackTraceAsString($e, $seen = null)
{
if (!config('fb.pretty_stack_trace')) {
return $e->getTraceAsString();
}
$starter = $seen ? 'Caused by: ' : '';
$result = [];
if (!$seen) $seen = [];
$trace = $e->getTrace();
$prev = $e->getPrevious();
$result[] = sprintf('%s%s: %s', $starter, get_class($e), $e->getMessage());
$file = $e->getFile();
$line = $e->getLine();
while (true) {
$current = "$file:$line";
if (is_array($seen) && in_array($current, $seen)) {
$result[] = sprintf(" ... %d more", count($trace) + 1);
break;
}
$resline = sprintf(" at %s%s%s(%s%s%s)",
count($trace) && array_key_exists('class', $trace[0]) ? $trace[0]['class'] : '',
count($trace) && array_key_exists('class', $trace[0]) && array_key_exists('function', $trace[0]) ? '->' : '',
count($trace) && array_key_exists('function', $trace[0]) ? $trace[0]['function'] : '(main)',
$line === null ? $file : basename($file),
$line === null ? '' : ':',
$line === null ? '' : $line);
$result[] = $resline;
if (
false !== strpos($resline, "Illuminate\\Console\\Command->execute") ||
false !== strpos($resline, "Illuminate\\Routing\\ControllerDispatcher->dispatch") ||
false !== strpos($resline, "Unknown Source") ||
false !== strpos($resline, "eval(SandboxController.php")
) {
$result[] = sprintf(" ... %d more", count($trace) + 1);
break;
}
if (is_array($seen)) {
$seen[] = "$file:$line";
}
if (!count($trace)) break;
$file = array_key_exists('file', $trace[0]) ? $trace[0]['file'] : 'Unknown Source';
$line = array_key_exists('file', $trace[0]) && array_key_exists('line', $trace[0]) && $trace[0]['line'] ? $trace[0]['line'] : null;
array_shift($trace);
}
$result = join("\n", $result);
if ($prev) {
$result .= "\n" . self::getStackTraceAsString($prev, $seen);
}
return $result;
}
/**
* Diff arrays of objects by one of their properties.
*
* TODO unit test
*
* @param array $array1
* @param array $array2
* @param string $prop property name (accessed using the arrow operator)
* @return array of (first - second) array.
*/
public static function arrayDiffProp($array1, $array2, $prop)
{
return array_udiff($array1, $array2, function ($a, $b) use ($prop) {
return $a->$prop - $b->$prop;
});
}
/**
* Sort array by field of each item
*
* TODO unit test
*
* @param array $array the array to sort
* @param string|string[] $prop property or properties
*/
public static function arraySortProp(&$array, $prop)
{
if (!is_array($prop)) $prop = [$prop];
@usort($array, function ($a, $b) use ($prop) {
// compare by each property
foreach ($prop as $pr) {
$cmp = strcasecmp($a->$pr, $b->$pr);
if ($cmp != 0) return $cmp;
}
return 0;
});
}
/**
* Group an array using a callback or each element's property/index
*
* TODO unit test
*
* @param array $arr array to group
* @param callable|string $grouper hashing function, or property/index of each item to group by
* @return array grouped
*/
public static function groupBy(array $arr, $grouper)
{
$first_el = reset($arr);
$is_array = is_array($first_el) || ($first_el instanceof \ArrayAccess);
// get real grouper func
$func = is_callable($grouper) ? $grouper : function ($x) use ($is_array, $grouper) {
if ($is_array) {
return $x[$grouper];
} else {
return $x->$grouper;
}
};
$grouped = [];
foreach ($arr as $item) {
$hash = $func($item);
if (!isset($grouped[$hash])) {
$grouped[$hash] = [];
}
$grouped[$hash][] = $item;
}
return $grouped;
}
/**
* Get union of two arrays, preserving order if possible.
*
* @param array $ar1
* @param array $ar2
* @return array merged
*/
public static function arrayUnion(array $ar1, array $ar2)
{
return array_unique(array_merge($ar1, $ar2));
}
/**
* Create an array with keys from given array and all the same values.
*
* It's array_combine() combined with array_fill()
*
* @param array $keys
* @param mixed $fill the value for all keys
* @return array
*/
public static function arrayKeysFill(array $keys, $fill)
{
return array_combine($keys, array_fill(0, count($keys), $fill));
}
/**
* Extract n-th bit from an int array.
*
* Bits are numbered from LSB to MSB, and from array index 0 upwards
*
* TODO unit test
*
* @param array $ints array of ints
* @param int $bit index of a bit to retrieve
* @param int $itemSize number of bits per array item, default 16
* @return int the bit, 0/1
*/
public static function bitFromIntArray(array $ints, $bit, $itemSize = 16)
{
$nth = (int) floor($bit / $itemSize);
$bit_n = $bit - ($itemSize * $nth);
if ($nth > count($ints)) return 0;
return (int) (0 != ($ints[$nth] & (1 << $bit_n)));
}
/**
* Set socket timeout in MS
*
* @param resource $socket socket
* @param int $SO_xxTIMEO timeout type constant
* @param int $timeout_ms timeout (millis)
*/
public static function setSocketTimeout(&$socket, $SO_xxTIMEO, $timeout_ms)
{
$to_sec = floor($timeout_ms / 1000);
$to_usec = ($timeout_ms - $to_sec * 1000) * 1000;
$timeout = ['sec' => $to_sec, 'usec' => $to_usec];
socket_set_option($socket, SOL_SOCKET, $SO_xxTIMEO, $timeout);
}
/**
* Check if value is in given range
*
* @param int|float $value value
* @param int|float $low lower bound - included
* @param int|float $high upper bound - included
* @return bool is in range
*/
public static function inRange($value, $low, $high)
{
// swap bounds if reverse
if ($low > $high) {
$sw = $low;
$low = $high;
$high = $sw;
}
return $value >= $low && $value <= $high;
}
/**
* Reverse the effect of array_chunk.
*
* @param array $array
* @return array
*/
public static function array_unchunk($array)
{
return call_user_func_array('array_merge', $array);
}
/**
* Clamp value to given bounds
*
* @param float $value input
* @param float $min lower bound - included
* @param float $max upper bound - included
* @return float result
*/
public static function clamp($value, $min, $max)
{
// swap bounds if reverse
if ($min > $max) {
$sw = $min;
$min = $max;
$max = $sw;
}
if ($value < $min) {
$value = $min;
} else if ($value > $max) {
$value = $max;
}
return $value;
}
/**
* Get today as the nr of days since epoch
*
* @return int nr of days
*/
public static function unixDay()
{
$dt = new \DateTime('@' . ntime());
$epoch = new \DateTime('1970-01-01');
$diff = $dt->diff($epoch);
return (int) $diff->format("%a");
}
/**
* Check if the value looks like a Mongo key (ObjectID).
* Mongo key is a 24 chars long hex string, so it's unlikely to occur as an alias.
*
* TODO unit test
*
* @param mixed $key object to inspect (string or ObjectID)
* @return bool is _id value
*/
public static function isMongoKey($key)
{
if ($key instanceof ObjectID) return true;
return (is_string($key) and strlen($key) === 24 and ctype_xdigit($key));
}
/**
* Check if array is associative
*
* TODO unit test
*
* @param array $array
* @return bool is associative (keys are not a sequence of numbers starting 0)
*/
public static function isAssoc(array $array)
{
return array_keys($array) !== range(0, count($array) - 1);
}
/**
* Ceil to closest multiple of 0.5
*
* @param float $n
* @return float rounded up to 0.5
*/
public static function halfCeil($n)
{
return ceil($n * 2) / 2;
}
/**
* Round to closest multiple of 0.5
*
* @param float $n
* @return float rounded to 0.5
*/
public static function halfRound($n)
{
return round($n * 2) / 2;
}
/**
* Floor to closest multiple of 0.5
*
* @param float $n
* @return float rounded down to 0.5
*/
public static function halfFloor($n)
{
return ceil($n * 2) / 2;
}
/**
* A dirty way to read private or protected fields.
* Use only for debugging or vendor hacking when really needed.
*
* @param mixed $obj
* @param string $prop
* @return mixed
*/
public static function readProtected($obj, $prop)
{
$reflection = new ReflectionClass($obj);
$property = $reflection->getProperty($prop);
$property->setAccessible(true);
return $property->getValue($obj);
}
/**
* A dirty way to write private or protected fields.
* Use only for debugging or vendor hacking when really needed.
*
* @param mixed $obj
* @param string $prop
* @param mixed $value
*/
public static function writeProtected($obj, $prop, $value)
{
$reflection = new ReflectionClass($obj);
$property = $reflection->getProperty($prop);
$property->setAccessible(true);
$property->setValue($obj, $value);
}
/**
* A dirty way to call private or protected methods.
* Use only for debugging or vendor hacking when really needed.
*
* @param mixed $obj
* @param string $methodName
* @param array ...$arguments
* @return mixed
*/
public static function callProtected($obj, $methodName, ...$arguments)
{
$method = new ReflectionMethod(get_class($obj), $methodName);
$method->setAccessible(true);
return $method->invoke($obj, ...$arguments);
}
/**
* Get class constants
*
* @param string $class f. q. class name
* @return array constants
*/
public static function getClassConstants($class)
{
$reflect = new ReflectionClass($class);
return $reflect->getConstants();
}
/**
* Start printing log messages to stdout (for debug)
*/
public static function logToStdout($htmlEscape = true)
{
$lf = new LineFormatter();
Log::listen(function ($obj) use ($htmlEscape, $lf) {
/** @var MessageLogged $obj */
$message = $obj->message;
$context = $obj->context;
$level = $obj->level;
if ($message instanceof \Exception) {
$message = self::getStackTraceAsString($message);
}
if ($htmlEscape) $message = e($message);
$time = microtime(true) - APP_START_TIME;
echo sprintf("%7.3f", $time) . " [$level] $message";
if (!empty($context)) echo Utils::callProtected($lf, 'stringify', $context);
echo "\n";
});
}
public static function logQueries()
{
\DB::listen(function ($query) {
/** @var QueryExecuted $query */
foreach ($query->bindings as $i=>$binding) $b[$i] = "'$binding'";
Log::debug('SQL: ' . preg_replace_array('/\\?/', $b, $query->sql));
7 years ago
});
}
/**
* Given an array of comma and quote positions, returns all commas that aren't within a pair of quotes.
*
* @param int[] $commas
* @param int[] $quotes
* @return int[]
*/
public static function discardPositionsWithinPairs($commas, $quotes)
{
$pairs = array_chunk($quotes, 2);
$goodCommas = [];
foreach ($commas as $comma) {
$found = false;
foreach ($pairs as $pair) {
if ($comma > $pair[0] && $comma < $pair[1]) {
$found = true;
break;
}
}
if (!$found) {
$goodCommas[] = $comma;
}
}
return $goodCommas;
}
/**
* Check if any item in an array is true.
*
* @param bool[] $arr
* @return bool
*/
public static function anyTrue($arr)
{
foreach ($arr as $item) {
if ($item) return true;
}
return false;
}
/**
* Get all keys from an array whose values match a given set.
*
* @param array $arr
* @param array|mixed $allowed_values
* @return array
*/
public static function keysOfValues($arr, $allowed_values)
{
if (!is_array($allowed_values)) {
$allowed_values = [$allowed_values];
}
$matched = [];
foreach ($arr as $k => $v) {
if (in_array($v, $allowed_values)) {
$matched[] = $k;
}
}
return $matched;
}
/**
* Convert value within range to a ratio of this range.
* eg. 5 on 0..10 = 0.5
*
* @param float $value
* @param float $minValue
* @param float $maxValue
* @return float
*/
public static function valToRatio($value, $minValue, $maxValue)
{
$value = Utils::clamp($value, $minValue, $maxValue);
return ($value - $minValue) / ($maxValue - $minValue);
}
/**
* Apply ratio to a range & get proportional value.
* eg. 0.5 on 0..10 = 5
*
* @param $ratio
* @param $minValue
* @param $maxValue
* @return mixed
*/
public static function ratioToVal($ratio, $minValue, $maxValue)
{
$ratio = Utils::clamp($ratio, 0, 1);
return $minValue + ($maxValue - $minValue) * $ratio;
}
/**
* Range transformation
*
* @param float $value
* @param float $minInput
* @param float $maxInput
* @param float $minOutput
* @param float $maxOutput
* @return float
*/
public static function transform($value, $minInput, $maxInput, $minOutput, $maxOutput)
{
$ratio = self::valToRatio($value, $minInput, $maxInput);
return self::ratioToVal($ratio, $minOutput, $maxOutput);
}
private static function initSortCollator()
{
if (self::$collator === null) {
if (extension_loaded('intl')) {
if (is_object($collator = collator_create('cs_CZ.UTF-8'))) {
$collator->setAttribute(Collator::NUMERIC_COLLATION, Collator::ON);
self::$collator = $collator;
}
} else {
Log::warning('!! Intl PHP extension not installed / enabled.');
self::$collator = false;
}
}
}
public static function mbNatCaseCompare($a, $b)
{
self::initSortCollator();
$a = "$a";
$b = "$b";
if (strlen($a) > 0 && $a[0] == '_') if (strlen($b) == 0 || $b[0] != '_') return 1;
if (strlen($b) > 0 && $b[0] == '_') if (strlen($a) == 0 || $a[0] != '_') return -1;
if ($a === "" && $b !== "") return 1;
if ($a !== "" && $b === "") return -1;
if ($a === "" && $b === "") return 0;
if (self::$collator)
return self::$collator->compare($a, $b);
else
return strnatcasecmp($a, $b);
}
public static function mbNatCaseSort(&$arr)
{
uasort($arr, function ($a, $b) {
return self::mbNatCaseCompare($a, $b);
});
}
/**
* Check if exception should be reported in eventlog.
*
* @param \Exception $e
* @return bool
*/
public static function shouldReportException(\Exception $e)
{
$eh = app(\Illuminate\Contracts\Debug\ExceptionHandler::class);
return Utils::callProtected($eh, 'shouldReport', $e);
}
const GEO_DEG = 'GEO_DEGREES';
const GEO_DEG_MIN = 'GEO_DEG_MIN';
const GEO_DEG_MIN_SEC = 'GEO_DEG_MIN_SEC';
/**
* @param string|int $coord
* @return float
*/
public static function geoToDegrees($coord)
{
$coord = "$coord";
// remove any whitespace
$coord = preg_replace('/\s/', '', $coord);
if (strlen($coord) == 0) throw new ArgumentException("\"$coord\" is not in a known GPS coordinate format");
// remove leading letter, invert if needed
$invert = false;
if (in_array($coord[0], ['N', 'S', 'E', 'W'])) {
$invert = in_array($coord[0], ['S', 'W']);
$coord = substr($coord, 1);
}
if (in_array($coord[strlen($coord) - 1], ['N', 'S', 'E', 'W'])) {
$invert = in_array($coord[strlen($coord) - 1], ['S', 'W']);
$coord = substr($coord, 0, -1);
}
$invert = $invert ? -1 : 1;
// Degrees
if (Str::maskMatch("F°?", $coord)) {
return $invert * floatval(rtrim($coord, '°'));
}
if (Str::maskMatch("D°F'?", $coord)) {
list($d, $m) = explode('°', $coord);
$d = floatval($d);
if ($d < 0) {
$d = -$d;
$invert *= -1;
}
$m = floatval(rtrim($m, "'"));
return $invert * ($d + $m / 60);
}
if (Str::maskMatch("D°D'F\"?", $coord)) {
list($d, $ms) = explode('°', $coord);
list($m, $s) = explode("'", $ms);
$d = floatval($d);
if ($d < 0) {
$d = -$d;
$invert *= -1;
}
$m = floatval($m);
$s = floatval(rtrim($s, '"'));
return $invert * ($d + ($m + $s / 60) / 60);
}
throw new ArgumentException("\"$coord\" is not in a known GPS coordinate format");
}
public static function convertGeoNS($coord, $target = self::GEO_DEG)
{
$result = self::convertGeo($coord, $target);
if ($result[0] == '-') {
$result = 'S ' . substr($result, 1);
} else {
$result = 'N ' . $result;
}
return $result;
}
public static function convertGeoEW($coord, $target = self::GEO_DEG)
{
$result = self::convertGeo($coord, $target);
if ($result[0] == '-') {
$result = 'W ' . substr($result, 1);
} else {
$result = 'E ' . $result;
}
return $result;
}
public static function convertGeo($coord, $target = self::GEO_DEG)
{
$degrees = self::geoToDegrees($coord);
$invert = $degrees < 0;
$degrees = abs($degrees);
$invert = $invert ? -1 : 1;
switch ($target) {
case self::GEO_DEG:
return "" . $invert * round($degrees, 5);
case self::GEO_DEG_MIN:
$frac = $degrees - floor($degrees);
$mins = $frac * 60;
return ($invert * floor($degrees)) . "° " . round($mins, 3) . "'";
case self::GEO_DEG_MIN_SEC:
$frac = $degrees - floor($degrees);
$mins = $frac * 60;
$frac = $mins - floor($mins);
$secs = $frac * 60;
return ($invert * floor($degrees)) . "° " . floor($mins) . "' " . round($secs, 1) . "\"";
default:
throw new ArgumentException("$target is not a valid geo coord format");
}
}
public static function formatFileSize($bytes)
{
if ($bytes >= 1073741824)
{
$bytes = number_format($bytes / 1073741824, 2) . ' GB';
}
elseif ($bytes >= 1048576)
{
$bytes = number_format($bytes / 1048576, 2) . ' MB';
}
elseif ($bytes >= 1024)
{
$bytes = number_format($bytes / 1024, 2) . ' kB';
}
else
{
$bytes = $bytes . ' B';
}
return $bytes;
}
/**
* Format date safely for y2038
*
* @param string $format date format string
* @param int|null $timestamp formatted timestamp, or null for current time
* @return string result
*/
public static function fdate($format, $timestamp = null)
{
if ($timestamp === null) $timestamp = time();
$dt = new \DateTime('@' . $timestamp); // UTC
$dt->setTimezone(new \DateTimeZone(date('e'))); // set local timezone
return $dt->format($format);
}
/**
* Format time as XXd XXh XXm XXs (parse-able by HumanTime)
*
* @param int $secs seconds
* @param bool $rough get only approximate time (for estimate)
* @return string result
*/
public static function ftime($secs, $rough = false)
{
$d = (int) ($secs / 86400);
$secs -= $d * 86400;
$h = (int) ($secs / 3600);
$secs -= $h * 3600;
$m = (int) ($secs / 60);
$secs -= $m * 60;
$s = $secs;
$chunks = [];
if ($rough) {
if ($d > 5) {
if ($h > 15) $d++; // round up
$chunks[] = "{$d}d";
} elseif ($d >= 1) {
$chunks[] = "{$d}d";
if ($h) $chunks[] = "{$h}h";
} else {
// under 1 d
if ($h >= 4) {
if ($m > 40) $h++; // round up
$chunks[] = "{$h}h";
} elseif ($h >= 1) {
$chunks[] = "{$h}h";
if ($m) $chunks[] = "{$m}m";
} else {
if ($m) $chunks[] = "{$m}m";
if ($s || empty($chunks)) {
$chunks[] = "{$s}s";
}
}
}
} else {
// precise
if ($d) $chunks[] = "{$d}d";
if ($h) $chunks[] = "{$h}h";
if ($m) $chunks[] = "{$m}m";
if ($s || empty($chunks)) {
$chunks[] = "{$s}s";
}
}
return trim(implode(' ', $chunks));
}
/**
* Get time in seconds from user entered interval
*
* @param $time
* @return int seconds
*/
public static function strToSeconds($time)
{
// seconds pass through
if (preg_match('/^\d+$/', trim("$time"))) {
return intval($time);
}
$time = preg_replace('/(\s*|,|and)/i', '', $time);
$pieces = preg_split('/(?<=[a-z])(?=\d)/i', $time, 0, PREG_SPLIT_NO_EMPTY);
return array_sum(array_map('self::strToSeconds_do', $pieces));
}
/** @noinspection PhpUnusedPrivateMethodInspection */
private static function strToSeconds_do($time)
{
if (preg_match('/^(\d+)\s*(s|secs?|seconds?)$/', $time, $m)) {
return intval($m[1]);
}
if (preg_match('/^(\d+)\s*(m|mins?|minutes?)$/', $time, $m)) {
return intval($m[1]) * 60;
}
if (preg_match('/^(\d+)\s*(h|hours?)$/', $time, $m)) {
return intval($m[1]) * 60*60;
}
if (preg_match('/^(\d+)\s*(d|days?)$/', $time, $m)) {
return intval($m[1]) * 86400;
}
if (preg_match('/^(\d+)\s*(w|weeks?)$/', $time, $m)) {
return intval($m[1]) * 86400*7;
}
if (preg_match('/^(\d+)\s*(M|months?)$/', $time, $m)) {
return intval($m[1]) * 86400*30;
}
if (preg_match('/^(\d+)\s*(y|years?)$/', $time, $m)) {
return intval($m[1]) * 86400*365;
}
throw new FormatException("Bad time interval: \"$time\"");
}
}