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