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.
		
		
		
		
		
			
		
			
				
					
					
						
							993 lines
						
					
					
						
							24 KiB
						
					
					
				
			
		
		
	
	
							993 lines
						
					
					
						
							24 KiB
						
					
					
				<?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) {
 | 
						|
            /** @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($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\"");
 | 
						|
    }
 | 
						|
}
 | 
						|
 |