|
|
|
<?php
|
|
|
|
|
|
|
|
namespace MightyPork\Utils;
|
|
|
|
|
|
|
|
use Collator;
|
|
|
|
use Illuminate\Database\Events\QueryExecuted;
|
|
|
|
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;
|
|
|
|
}
|
|
|
|
|
|
|
|
if ($value === null) return false;
|
|
|
|
|
|
|
|
// 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) {
|
|
|
|
$b = [];
|
|
|
|
/** @var QueryExecuted $query */
|
|
|
|
foreach ($query->bindings as $i=>$binding) $b[$i] = "'$binding'";
|
|
|
|
Log::debug('SQL: ' . preg_replace_array('/\\?/', $b, $query->sql));
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* 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(string $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(int $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(string $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\"");
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Convert number to arbitrary alphabet, using a system similar to EXCEL column numbering.
|
|
|
|
*
|
|
|
|
* @param int $n
|
|
|
|
* @param string[] $alphabet - array of characters to use
|
|
|
|
* @return string
|
|
|
|
*/
|
|
|
|
public static function alphabetEncode($n, $alphabet)
|
|
|
|
{
|
|
|
|
$key = '';
|
|
|
|
$abcsize = count($alphabet);
|
|
|
|
$i = $n;
|
|
|
|
|
|
|
|
do {
|
|
|
|
$mod = $i % $abcsize;
|
|
|
|
$key = $alphabet[$mod] . $key;
|
|
|
|
$i = (($i - $mod) / $abcsize) - 1;
|
|
|
|
} while($i >= 0);
|
|
|
|
|
|
|
|
return $key;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @param \SplFileObject|string $data
|
|
|
|
* @return array
|
|
|
|
*/
|
|
|
|
public static function csvToArray($data)
|
|
|
|
{
|
|
|
|
if ($data instanceof \SplFileObject) {
|
|
|
|
$data->setFlags(\SplFileObject::SKIP_EMPTY | \SplFileObject::DROP_NEW_LINE);
|
|
|
|
$lines = [];
|
|
|
|
while (! $data->eof()) {
|
|
|
|
$line = $data->fgetcsv();
|
|
|
|
if ($line !== null) {
|
|
|
|
$lines[] = $line;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return $lines;
|
|
|
|
}
|
|
|
|
else {
|
|
|
|
return array_map('str_getcsv', explode("\n", $data));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|