initial
This commit is contained in:
@@ -0,0 +1,85 @@
|
||||
<?php
|
||||
|
||||
namespace MightyPork\Utils;
|
||||
|
||||
use MightyPork\Exceptions\NotExistException;
|
||||
use ReflectionClass;
|
||||
|
||||
/** Trait for checking if const is defined in class */
|
||||
trait Enum
|
||||
{
|
||||
private static $cache_enum_constants = null;
|
||||
|
||||
/**
|
||||
* Get all constants
|
||||
*
|
||||
* @param string $prefix required name prefix (filter)
|
||||
* @return array name -> value array
|
||||
*/
|
||||
public static function getConstants($prefix = '')
|
||||
{
|
||||
if (static::$cache_enum_constants != null) return static::$cache_enum_constants;
|
||||
|
||||
$map = (new ReflectionClass(static::class))->getConstants();
|
||||
$selected = [];
|
||||
foreach ($map as $k => $v) {
|
||||
if ($prefix === '' || strpos($k, $prefix) === 0) {
|
||||
$selected[$k] = $v;
|
||||
}
|
||||
}
|
||||
|
||||
static::$cache_enum_constants = $selected;
|
||||
return $selected;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a constant with a given value is defined in this class.
|
||||
*
|
||||
* @param mixed $value checked value
|
||||
* @param string $prefix required name prefix
|
||||
* @return bool
|
||||
*/
|
||||
public static function isDefined($value, $prefix = '')
|
||||
{
|
||||
return in_array($value, static::getConstants($prefix));
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a constant with a given name is defined in this class.
|
||||
*
|
||||
* @param string $name checked name
|
||||
* @param string $prefix required name prefix
|
||||
* @return bool
|
||||
*/
|
||||
public static function hasConstant($name, $prefix = '')
|
||||
{
|
||||
return array_key_exists($name, static::getConstants($prefix));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get constant value dynamically by name
|
||||
*
|
||||
* @param string $name
|
||||
* @return mixed
|
||||
*/
|
||||
public static function forName($name)
|
||||
{
|
||||
return constant(static::class . '::' . $name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get constant name by it's value
|
||||
*
|
||||
* @param mixed $value
|
||||
* @return string
|
||||
*/
|
||||
public static function nameOf($value)
|
||||
{
|
||||
$k = array_search($value, static::getConstants());
|
||||
|
||||
if ($k == false)
|
||||
throw new NotExistException("Invalid value '$value' for enum " . static::class);
|
||||
|
||||
return $k;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
namespace MightyPork\Utils;
|
||||
|
||||
use MightyPork\Exceptions\NotExistException;
|
||||
use Illuminate\Support\Collection;
|
||||
|
||||
/**
|
||||
* Improved collection with object access to fields
|
||||
*/
|
||||
class ObjCollection extends Collection
|
||||
{
|
||||
public function __get($name)
|
||||
{
|
||||
if (isset($this[$name])) return $this[$name];
|
||||
|
||||
throw new NotExistException("No '$name' in collection.");
|
||||
}
|
||||
|
||||
public function __isset($name)
|
||||
{
|
||||
return isset($this[$name]);
|
||||
}
|
||||
|
||||
public function __set($name, $value)
|
||||
{
|
||||
$this[$name] = $value;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
<?php
|
||||
|
||||
namespace MightyPork\Utils;
|
||||
|
||||
|
||||
/**
|
||||
* Profiling funcs
|
||||
*/
|
||||
class Profiler
|
||||
{
|
||||
/**
|
||||
* Get current time
|
||||
*
|
||||
* @return float time (s)
|
||||
*/
|
||||
public static function now()
|
||||
{
|
||||
list($usec, $sec) = explode(" ", microtime());
|
||||
return ((float) $usec + (float) $sec);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Start profiling
|
||||
*
|
||||
* @return float current time (profiling token)
|
||||
*/
|
||||
public static function start()
|
||||
{
|
||||
return self::now();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Measure time elapsed since start, formatted to 3 decimal places for printing.
|
||||
*
|
||||
* @param float $start time obtained with "start()"
|
||||
* @return float time elapsed (s)
|
||||
*/
|
||||
public static function end($start)
|
||||
{
|
||||
return sprintf('%0.3f', self::now() - $start);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,413 @@
|
||||
<?php
|
||||
|
||||
namespace MightyPork\Utils;
|
||||
use Lang;
|
||||
use PhpOffice\PhpSpreadsheet\Cell\Coordinate;
|
||||
use PhpOffice\PhpSpreadsheet\Spreadsheet;
|
||||
use PhpOffice\PhpSpreadsheet\Style\Fill;
|
||||
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
|
||||
use PhpOffice\PhpSpreadsheet\Writer\IWriter;
|
||||
|
||||
/**
|
||||
* Abstraction above PhpOffice's spreadsheets with stable API.
|
||||
* Fixing incompatibilities in one place will fix them in the entire application.
|
||||
*/
|
||||
class SpreadsheetBuilder
|
||||
{
|
||||
private $activeSheetNum = -1;
|
||||
private $sheetCount = 0;
|
||||
|
||||
/** @var Spreadsheet Spreadsheet */
|
||||
private $book = null;
|
||||
|
||||
/** @var Worksheet */
|
||||
private $activeSheet = null;
|
||||
|
||||
/** @var string - document title */
|
||||
private $title;
|
||||
|
||||
public function __construct($title, $subject = '', $creator='PhpOffice Spreadsheet')
|
||||
{
|
||||
$this->title = $title;
|
||||
$this->book = new Spreadsheet();
|
||||
|
||||
$this->book->getProperties()
|
||||
->setCreator($creator)
|
||||
->setTitle($title)
|
||||
->setSubject($subject)
|
||||
->setDescription('Created ' . date('r'));
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getTitle()
|
||||
{
|
||||
return $this->title;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a sheet and select it as active.
|
||||
* Returns sheet index.
|
||||
*
|
||||
* @param $label - sheet label
|
||||
* @return int - new sheet number, 0-based
|
||||
*/
|
||||
public function addSheet($label)
|
||||
{
|
||||
if ($this->activeSheetNum == -1) {
|
||||
// PhpOffice workbook has one sheet by default,
|
||||
// we require user to 'add' it before using it
|
||||
$this->sheetCount = 1;
|
||||
$this->activeSheet = $this->book->setActiveSheetIndex(0);
|
||||
$this->activeSheetNum = 0;
|
||||
}
|
||||
else {
|
||||
$this->sheetCount++;
|
||||
$this->activeSheet = $this->book->addSheet(new Worksheet());
|
||||
$this->activeSheetNum = $this->sheetCount-1;
|
||||
}
|
||||
|
||||
$this->activeSheet->setTitle($label);
|
||||
return $this->activeSheetNum;
|
||||
}
|
||||
|
||||
/**
|
||||
* Select a sheet for cell modifications
|
||||
*
|
||||
* @param int $number - sheet index, 0-based
|
||||
*/
|
||||
public function selectSheet($number)
|
||||
{
|
||||
$this->activeSheet = $this->book->setActiveSheetIndex($number);
|
||||
$this->activeSheetNum = $number;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a cell value in the current sheet
|
||||
*
|
||||
* @param int $row - 0-based row
|
||||
* @param int $column - 0-based column
|
||||
* @param mixed $value - value to set
|
||||
*/
|
||||
public function setValue($row, $column, $value)
|
||||
{
|
||||
$this->activeSheet->setCellValueByColumnAndRow($column+1, $row+1, $value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a row of values
|
||||
*
|
||||
* @param int $row - 0-based row
|
||||
* @param int $startcol - first column to fill
|
||||
* @param array $values - array of values to write into the row
|
||||
*/
|
||||
public function setRow($row, $startcol, $values)
|
||||
{
|
||||
$i = 0;
|
||||
foreach ($values as $value) {
|
||||
$this->setValue($row, $startcol + $i, $value);
|
||||
$i++;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a row of values
|
||||
*
|
||||
* @param int $startrow - 0-based start row
|
||||
* @param int $startcol - 0-based start column
|
||||
* @param array $values - array of row vectors ([ [first,row], [second,row], ...]
|
||||
*/
|
||||
public function setGrid($startrow, $startcol, $values)
|
||||
{
|
||||
foreach ($values as $row => $row_values) {
|
||||
$this->setRow($startrow + $row, $startcol, $row_values);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Make a cell a given style
|
||||
*
|
||||
* @param int $row - 0-based row
|
||||
* @param int $column - 0-based column
|
||||
*/
|
||||
private function setStyle($row, $column, $array)
|
||||
{
|
||||
$coord = Coordinate::stringFromColumnIndex($column+1) . ($row+1);
|
||||
|
||||
$this->activeSheet->getStyle($coord)
|
||||
->applyFromArray($array);
|
||||
}
|
||||
|
||||
/**
|
||||
* Make an area of cells a given style
|
||||
*
|
||||
* @param $startrow - 0-based first row
|
||||
* @param $startcol - 0-based first column
|
||||
* @param $rows - number of rows
|
||||
* @param $cols - number of columns
|
||||
*/
|
||||
private function setStyleArea($startrow, $startcol, $rows, $cols, $array)
|
||||
{
|
||||
$coord = Coordinate::stringFromColumnIndex($startcol+1) . ($startrow+1) .
|
||||
':' . Coordinate::stringFromColumnIndex($startcol+$cols) . ($startrow+$rows);
|
||||
|
||||
$this->activeSheet->getStyle($coord)
|
||||
->applyFromArray($array);
|
||||
}
|
||||
|
||||
/**
|
||||
* Make a cell bold
|
||||
*
|
||||
* @param int $row - 0-based row
|
||||
* @param int $column - 0-based column
|
||||
*/
|
||||
public function setBold($row, $column)
|
||||
{
|
||||
$this->setStyle($row, $column, ['font' => ['bold' => 'true']]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Make an area of cells bold
|
||||
*
|
||||
* @param $startrow - 0-based first row
|
||||
* @param $startcol - 0-based first column
|
||||
* @param $rows - number of rows
|
||||
* @param $cols - number of columns
|
||||
*/
|
||||
public function setBoldArea($startrow, $startcol, $rows, $cols)
|
||||
{
|
||||
$this->setStyleArea($startrow, $startcol, $rows, $cols, ['font' => ['bold' => 'true']]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Make a cell italic
|
||||
*
|
||||
* @param int $row - 0-based row
|
||||
* @param int $column - 0-based column
|
||||
*/
|
||||
public function setItalic($row, $column)
|
||||
{
|
||||
$this->setStyle($row, $column, ['font' => ['italic' => 'true']]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Make an area of cells italic
|
||||
*
|
||||
* @param $startrow - 0-based first row
|
||||
* @param $startcol - 0-based first column
|
||||
* @param $rows - number of rows
|
||||
* @param $cols - number of columns
|
||||
*/
|
||||
public function setItalicArea($startrow, $startcol, $rows, $cols)
|
||||
{
|
||||
$this->setStyleArea($startrow, $startcol, $rows, $cols, ['font' => ['italic' => 'true']]);
|
||||
}
|
||||
|
||||
private function convertColor($color)
|
||||
{
|
||||
if ($color[0] == '#') $rgb = substr($color, 1);
|
||||
else {
|
||||
$map = [
|
||||
'black' => '000000',
|
||||
'gray' => '808080',
|
||||
'silver' => 'C0C0C0',
|
||||
'white' => 'FFFFFF',
|
||||
'maroon' => '800000',
|
||||
'red' => 'FF0000',
|
||||
'olive' => '808000',
|
||||
'yellow' => 'FFFF00',
|
||||
'green' => '008000',
|
||||
'lime' => '00FF00',
|
||||
'teal' => '008080',
|
||||
'aqua' => '00FFFF',
|
||||
'navy' => '000080',
|
||||
'blue' => '0000FF',
|
||||
'purple' => '800080',
|
||||
'fuchsia' => 'FF00FF',
|
||||
];
|
||||
|
||||
if (isset($map[$color])) {
|
||||
$rgb = $map[$color];
|
||||
} else {
|
||||
throw new \LogicException("Bad color format $color");
|
||||
}
|
||||
}
|
||||
|
||||
return ['rgb' => $rgb];
|
||||
}
|
||||
|
||||
/**
|
||||
* Set cell colors
|
||||
*
|
||||
* @param int $row - 0-based row
|
||||
* @param int $column - 0-based column
|
||||
* @param string $text - foreground
|
||||
* @param string $background - background
|
||||
*/
|
||||
public function setColors($row, $column, $text = null, $background = null)
|
||||
{
|
||||
$this->setColorsArea($row, $column, 1, 1, $text, $background);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set colors of an area of cells
|
||||
*
|
||||
* @param $startrow - 0-based first row
|
||||
* @param $startcol - 0-based first column
|
||||
* @param $rows - number of rows
|
||||
* @param $cols - number of columns
|
||||
* @param string $text - foreground
|
||||
* @param string $background - background
|
||||
*/
|
||||
public function setColorsArea($startrow, $startcol, $rows, $cols, $text = null, $background = null)
|
||||
{
|
||||
$ar = [];
|
||||
|
||||
if ($text !== null) {
|
||||
$ar['font'] = [
|
||||
'color' => $this->convertColor($text),
|
||||
];
|
||||
}
|
||||
|
||||
if ($background !== null) {
|
||||
$ar['fill'] = [
|
||||
'fillType' => Fill::FILL_SOLID,
|
||||
'color' => $this->convertColor($background),
|
||||
];
|
||||
}
|
||||
|
||||
$this->setStyleArea($startrow, $startcol, $rows, $cols, $ar);
|
||||
}
|
||||
|
||||
/**
|
||||
* Merge cells
|
||||
*
|
||||
* @param $startrow - 0-based first row
|
||||
* @param $startcol - 0-based first column
|
||||
* @param $rows - number of rows
|
||||
* @param $cols - number of columns
|
||||
*/
|
||||
public function mergeCells($startrow, $startcol, $rows, $cols)
|
||||
{
|
||||
$coord = Coordinate::stringFromColumnIndex($startcol+1) . ($startrow+1) .
|
||||
':' . Coordinate::stringFromColumnIndex($startcol+$cols) . ($startrow+$rows);
|
||||
|
||||
$this->activeSheet->mergeCells($coord);
|
||||
}
|
||||
|
||||
/**
|
||||
* Merge row cells
|
||||
*
|
||||
* @param $startrow - 0-based first row
|
||||
* @param $startcol - 0-based first column
|
||||
* @param $rows - number of rows
|
||||
* @param $cols - number of columns
|
||||
*/
|
||||
public function mergeRowCells($startrow, $startcol, $cols)
|
||||
{
|
||||
$this->mergeCells($startrow, $startcol, 1, $cols);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a column's width
|
||||
*
|
||||
* @param int $column - 0-based column number
|
||||
* @param int $width - width in pixels
|
||||
*/
|
||||
public function setColumnWidth($column, $width)
|
||||
{
|
||||
$this->activeSheet->getColumnDimensionByColumn($column+1)->setWidth($width);
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare writer and resolve mime type for export
|
||||
*
|
||||
* @param string $format - one of : xls, xlsx, csv, ods
|
||||
* @return array (mime, writer)
|
||||
*/
|
||||
private function prepareExport($format = 'xlsx')
|
||||
{
|
||||
switch ($format) {
|
||||
case 'xls':
|
||||
$writerName = 'Xls';
|
||||
$mimeType = 'application/vnd.ms-excel';
|
||||
break;
|
||||
|
||||
case 'xlsx':
|
||||
$writerName = 'Xlsx';
|
||||
$mimeType = 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet';
|
||||
break;
|
||||
|
||||
case 'ods':
|
||||
$writerName = 'Ods';
|
||||
$mimeType = 'application/vnd.oasis.opendocument.spreadsheet';
|
||||
break;
|
||||
|
||||
default:
|
||||
case 'csv':
|
||||
$writerName = 'Csv';
|
||||
$mimeType = 'text/csv';
|
||||
break;
|
||||
}
|
||||
|
||||
// Set active sheet index to the first sheet, so Excel opens this as the first sheet
|
||||
$this->book->setActiveSheetIndex(0);
|
||||
$writer = \PhpOffice\PhpSpreadsheet\IOFactory::createWriter($this->book, $writerName);
|
||||
|
||||
return [$mimeType, $writer];
|
||||
}
|
||||
|
||||
/**
|
||||
* Export and send to browser as a response for user to download
|
||||
*
|
||||
* @param string $base_filename - filename without suffix
|
||||
* @param string $format - one of : xls, xlsx, csv, ods
|
||||
*/
|
||||
public function exportToBrowser($base_filename, $format = 'xlsx')
|
||||
{
|
||||
list($mimeType, $writer) = $this->prepareExport($format);
|
||||
|
||||
/** @var IWriter $writer */
|
||||
$base_filename .= '.' . $format;
|
||||
|
||||
ob_end_clean();
|
||||
|
||||
// Redirect output to a client’s web browser (Xlsx)
|
||||
header("Content-Type: $mimeType; charset=utf-8");
|
||||
header("Content-Disposition: attachment;filename=\"$base_filename\"");
|
||||
header('Content-Language: ' . Lang::locale());
|
||||
|
||||
// Cache headers
|
||||
|
||||
header('Cache-Control: max-age=0');
|
||||
// IE9
|
||||
header('Cache-Control: max-age=1');
|
||||
// Other IE headers
|
||||
header('Expires: Mon, 26 Jul 1997 05:00:00 GMT'); // Date in the past
|
||||
header('Last-Modified: ' . gmdate('D, d M Y H:i:s') . ' GMT'); // always modified
|
||||
header('Cache-Control: cache, must-revalidate'); // HTTP/1.1
|
||||
header('Pragma: no-cache'); // HTTP/1.0
|
||||
|
||||
$writer->save('php://output');
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Export and send to browser as a response for user to download
|
||||
*
|
||||
* @param string $base_filename - filename without suffix
|
||||
* @param string $format - one of : xls, xlsx, csv, ods
|
||||
* @return string - the mime type
|
||||
*/
|
||||
public function exportToFile($base_filename, $format = 'xlsx', $path='/tmp')
|
||||
{
|
||||
list($mimeType, $writer) = $this->prepareExport($format);
|
||||
|
||||
/** @var IWriter $writer */
|
||||
$writer->save($path . DIRECTORY_SEPARATOR . "$base_filename.$format");
|
||||
|
||||
return $mimeType;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,693 @@
|
||||
<?php
|
||||
|
||||
namespace MightyPork\Utils;
|
||||
|
||||
use MightyPork\Exceptions\FormatException;
|
||||
use Illuminate\Support\Collection;
|
||||
use Log;
|
||||
|
||||
class Str extends \Illuminate\Support\Str
|
||||
{
|
||||
protected static $cty_snake_cache = [];
|
||||
|
||||
/**
|
||||
* Split and trim
|
||||
*
|
||||
* TODO unit test
|
||||
*
|
||||
* @param string $haystack string to split
|
||||
* @param string|string[] $delimiters delimiter
|
||||
* @return array pieces, trimmed.
|
||||
*/
|
||||
public static function splitTrim($haystack, $delimiters=[',', ';', '|'])
|
||||
{
|
||||
$haystack = trim($haystack);
|
||||
if (strlen($haystack) == 0) return [];
|
||||
return array_map('trim', self::split($haystack, $delimiters));
|
||||
}
|
||||
|
||||
/**
|
||||
* Customized snake case for cty aliases
|
||||
*
|
||||
* @param $camel
|
||||
* @return string
|
||||
*/
|
||||
public static function snakeAlias($camel)
|
||||
{
|
||||
if (isset(static::$cty_snake_cache[$camel]))
|
||||
return static::$cty_snake_cache[$camel];
|
||||
|
||||
$c = $camel;
|
||||
|
||||
$c = preg_replace_callback('/([A-Z0-9]+)(?![a-z])/', function ($m) {
|
||||
return ucfirst(strtolower($m[1]));
|
||||
}, $c);
|
||||
|
||||
$c = self::snake($c, '_');
|
||||
|
||||
// fix for underscores in cty class name
|
||||
$c = preg_replace('/_+/', '_', $c);
|
||||
|
||||
return static::$cty_snake_cache[$camel] = $c;
|
||||
}
|
||||
|
||||
/**
|
||||
* Split a string using one or more delimiters
|
||||
*
|
||||
* TODO unit test
|
||||
*
|
||||
* @param string $haystack
|
||||
* @param string|array $delimiters
|
||||
* @return array
|
||||
*/
|
||||
public static function split($haystack, $delimiters)
|
||||
{
|
||||
if (is_string($delimiters)) {
|
||||
return explode($delimiters, $haystack);
|
||||
}
|
||||
|
||||
// make sure it's array
|
||||
if (!is_array($delimiters)) {
|
||||
$delimiters = [$delimiters];
|
||||
}
|
||||
|
||||
// helper
|
||||
$regex_escape = function ($x) {
|
||||
return preg_quote($x, '/');
|
||||
};
|
||||
|
||||
// compose splitting regex
|
||||
$reg = "/" . implode('|', array_map($regex_escape, $delimiters)) . "/";
|
||||
|
||||
return preg_split($reg, $haystack);
|
||||
}
|
||||
|
||||
/**
|
||||
* Split "CSV" string to items, trim each item.
|
||||
* Empty values at start and end are discarded.
|
||||
*
|
||||
* TODO unit test
|
||||
*
|
||||
* @param string $str
|
||||
* @return array items
|
||||
*/
|
||||
public static function splitCsv($str)
|
||||
{
|
||||
return array_map('trim', explode(',', trim($str, ',')));
|
||||
}
|
||||
|
||||
/**
|
||||
* Pad an integer to 2 digit
|
||||
*
|
||||
* TODO unit test
|
||||
*
|
||||
* @param int $int the number
|
||||
* @return string padded with zero
|
||||
*/
|
||||
public static function pad2($int)
|
||||
{
|
||||
return sprintf("%02d", $int);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove diacritics from a string
|
||||
*
|
||||
* TODO unit test
|
||||
*
|
||||
* @param string $str
|
||||
* @return string ascii
|
||||
*/
|
||||
public static function asciify($str)
|
||||
{
|
||||
return iconv('UTF-8', 'US-ASCII//TRANSLIT', $str);
|
||||
}
|
||||
|
||||
/**
|
||||
* Public for unit tests
|
||||
*
|
||||
* Convert mask to regex
|
||||
*
|
||||
* @param $mask
|
||||
* @return mixed|string
|
||||
*/
|
||||
public static function _pregMaskPrepare($mask)
|
||||
{
|
||||
$mask = preg_quote($mask);
|
||||
|
||||
// number = repeat
|
||||
$mask = preg_replace('#(?<![.*+,{\d\\\\]|^)(\d+)#', '{$1}', $mask); // can repeat ?
|
||||
|
||||
$mask = strtr($mask, [
|
||||
'\?' => '?',
|
||||
'\*' => '*',
|
||||
'\+' => '+',
|
||||
'\{' => '{',
|
||||
'\}' => '}',
|
||||
'\(' => '(',
|
||||
'\)' => ')',
|
||||
'd' => '\d',
|
||||
'F' => '-?\d+(\.\d+)?',
|
||||
'D' => '-?\d+',
|
||||
'a' => '[[:alpha:]]',
|
||||
'\\\\' => '\\',
|
||||
]);
|
||||
|
||||
$mask = strtr($mask, [
|
||||
'\\\\' => '\\',
|
||||
]);
|
||||
|
||||
return $mask;
|
||||
}
|
||||
|
||||
/**
|
||||
* Match a string against a mask.
|
||||
*
|
||||
* Special symbols:
|
||||
* - `*` repeat previous any number of times
|
||||
* - `?` previous is optional
|
||||
* - `()` grouping
|
||||
* - `a` alpha
|
||||
* - `d` digit
|
||||
* - `F` float value d, d.ddd with optional leading -
|
||||
* - `+` repeat previous symbol any number of times
|
||||
* - Number - repeat N times
|
||||
* - {N}, {M,N} - repeat n times, like in regex
|
||||
*
|
||||
* Any other characters are matched literally.
|
||||
*
|
||||
* @param string $mask mask to match against
|
||||
* @param string $string tested string
|
||||
* @return bool matches
|
||||
*/
|
||||
public static function maskMatch($mask, $string)
|
||||
{
|
||||
$mask = self::_pregMaskPrepare($mask);
|
||||
return 1 === preg_match('|^' . $mask . '$|u', $string);
|
||||
}
|
||||
|
||||
/**
|
||||
* Format a string with {0} {foo} or {}
|
||||
*
|
||||
* TODO unit test
|
||||
*
|
||||
* @param string $format
|
||||
* @param array ...$args substitutions. Can also be an explicit array.
|
||||
* @return string
|
||||
*/
|
||||
public static function format($format, ...$args)
|
||||
{
|
||||
$args = func_get_args();
|
||||
$format = array_shift($args);
|
||||
|
||||
// explicit array given
|
||||
if (is_array($args[0])) {
|
||||
$args = $args[0];
|
||||
}
|
||||
|
||||
$format = preg_replace_callback('#\{\}#', function () {
|
||||
static $i = 0;
|
||||
return '{' . ($i++) . '}';
|
||||
}, $format);
|
||||
|
||||
return str_replace(
|
||||
array_map(function ($k) {
|
||||
return '{' . $k . '}';
|
||||
}, array_keys($args)),
|
||||
|
||||
array_values($args),
|
||||
|
||||
$format
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove whitespace on the left side of a block.
|
||||
* This function is quite expensive, so it's recommended
|
||||
* to cache the result, if possible.
|
||||
*
|
||||
* <b>NOTE:</b> The current implementation converts all leading tabs to spaces.
|
||||
*
|
||||
* @param string $txt text to trim
|
||||
* @param int $tab_size number of spaces per tab, default 4
|
||||
* @param bool $ltrim_nl remove a leading newline
|
||||
* @param bool $ign_noindent ignore lines with zero indentation
|
||||
* @return string left-trimmed block.
|
||||
*/
|
||||
public static function unindentBlock($txt, $tab_size = 4, $ltrim_nl = true, $ign_noindent = true)
|
||||
{
|
||||
$pad = 1024; // max indent size
|
||||
$tabsp = str_repeat(' ', $tab_size);
|
||||
|
||||
if ($ltrim_nl) {
|
||||
// also make sure first line is not blank
|
||||
$txt = ltrim($txt, "\n");
|
||||
}
|
||||
|
||||
$txt = preg_replace_callback('/^([ \t]*)(?=[^ \t\n]|$)/m',
|
||||
function ($m) use (&$pad, $tabsp, $ign_noindent) {
|
||||
static $i = 0;
|
||||
|
||||
$indent = $m[1];
|
||||
|
||||
if ($indent == '' && $ign_noindent) {
|
||||
// no indentation, perhaps stripped by editor?
|
||||
return '';
|
||||
}
|
||||
|
||||
// normalize tab to 4 spaces
|
||||
$normalized = strtr($indent, ["\t" => $tabsp]);
|
||||
|
||||
$len = strlen($normalized);
|
||||
$pad = min($pad, $len);
|
||||
|
||||
$i++;
|
||||
|
||||
return $normalized;
|
||||
},
|
||||
$txt
|
||||
);
|
||||
|
||||
return preg_replace("/^ {{$pad}}/m", '', $txt);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a simple string representation of an array, similar to json_encode(),
|
||||
* except without the obnoxious quotes and escapes.
|
||||
*
|
||||
* @param array $array
|
||||
* @return string [a, b, c]
|
||||
*/
|
||||
public static function arr($array)
|
||||
{
|
||||
if ($array instanceof Collection || Utils::isAssoc($array)) {
|
||||
$x = '[';
|
||||
foreach ($array as $k => $v) {
|
||||
$x .= "$k:" . json_encode($v) . ', ';
|
||||
}
|
||||
$x = rtrim($x, ', ') . ']';
|
||||
return $x;
|
||||
} else {
|
||||
return '[' . implode(', ', collect($array)->map(function ($x) {
|
||||
return is_array($x) ? json_encode($x, JSON_UNESCAPED_UNICODE) : (string) $x;
|
||||
})->toArray()) . ']';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get substring from last occurrence of token
|
||||
*
|
||||
* TODO unit test
|
||||
*
|
||||
* @param string $token token delimiting the last chunk from left. Not included.
|
||||
* @param string $haystack
|
||||
* @return string part of haystack after token.
|
||||
*/
|
||||
public static function fromLast($token, $haystack)
|
||||
{
|
||||
$rpos = strrpos($haystack, $token);
|
||||
if ($rpos === false) return $haystack;
|
||||
return substr($haystack, $rpos + strlen($token));
|
||||
}
|
||||
|
||||
public static function rpad($str, $len, $fill = ' ')
|
||||
{
|
||||
$filln = max(0, $len - mb_strlen($str));
|
||||
return $str . str_repeat(mb_substr($fill, 0, 1), $filln);
|
||||
}
|
||||
|
||||
public static function lpad($str, $len, $fill = ' ')
|
||||
{
|
||||
$filln = max(0, $len - mb_strlen($str));
|
||||
return str_repeat(mb_substr($fill, 0, 1), $filln) . $str;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove wrapping quotes from a string.
|
||||
* C-slashes will also be removed.
|
||||
*
|
||||
* @param $str
|
||||
* @return string
|
||||
*/
|
||||
public static function unquote($str)
|
||||
{
|
||||
if (!$str || !is_string($str)) {
|
||||
return $str;
|
||||
}
|
||||
|
||||
$a = $str[0];
|
||||
$b = $str[strlen($str) - 1];
|
||||
if ($a == $b && $a == '"' || $b == "'") {
|
||||
$str = substr($str, 1, strlen($str) - 2);
|
||||
}
|
||||
|
||||
$str = stripcslashes($str);
|
||||
return $str;
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply a rewrite.
|
||||
*
|
||||
* - Simple foo|bar rewrites 0|1 (or false|true)
|
||||
* - Key-value rewrite is possible with 7=foo|9=bar
|
||||
* - '*' (asterisk) matches everything (9=foo|*=other)
|
||||
* - '\*' - match literal asterisk
|
||||
* - starts with % - format using sprintf
|
||||
* - Compare funcs can also be used: lt, gt, le, ge, range, in
|
||||
* example: lt(100)=Foo|range(100,200)=Bar|gt(200)=Baz
|
||||
*
|
||||
* @param mixed $value value from expression
|
||||
* @param string $rewrite rewrite patterns, | separated
|
||||
* @return mixed result to show
|
||||
*/
|
||||
public static function rewrite($value, $rewrite)
|
||||
{
|
||||
// TODO předělat na jednodušší zápis
|
||||
if ($rewrite[0] == '%') return sprintf($rewrite, $value);
|
||||
|
||||
$rewrite_map = [];
|
||||
foreach (explode('|', trim($rewrite)) as $i => $rw) {
|
||||
$ar = preg_split('/(?<![\\\\])=/', $rw);
|
||||
|
||||
if (count($ar) == 2) {
|
||||
// key value pair
|
||||
$key = trim(str_replace('\=', '=', $ar[0]));
|
||||
$rewrite_map[$key] = trim(str_replace('\=', '=', $ar[1]));
|
||||
} elseif (count($ar) == 1) {
|
||||
// literal rewrite
|
||||
$rewrite_map[$i] = $rw;
|
||||
} else {
|
||||
Log::warning("Invalid rewrite format: $rw");
|
||||
return $value; // don't rewrite it
|
||||
}
|
||||
}
|
||||
|
||||
// apply the rewrite if any
|
||||
foreach ($rewrite_map as $k => $replacement) {
|
||||
if (is_numeric($k) && (((int)$k == (int)$value) || abs((float)$k - (float)$value)<0.00001)) {
|
||||
// exact match
|
||||
return $replacement;
|
||||
}
|
||||
else if (($k==='true'||$k==='false') && (Utils::parseBool($k) == Utils::parseBool($value))) {
|
||||
// bool match
|
||||
return $replacement;
|
||||
}
|
||||
else if (preg_match('/([a-z]+)\(([^)]+)\)/i', $k, $mm)) {
|
||||
// we have a comparing function
|
||||
if (static::testCompareFunc($value, $mm[1], $mm[2])) {
|
||||
return $replacement;
|
||||
}
|
||||
}
|
||||
else if ($k === '*') {
|
||||
return $replacement; // catch-all
|
||||
}
|
||||
else if ($k === '\\*') {
|
||||
if ($value === '*') {
|
||||
return $replacement; // literal asterisk
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if compare function matches value, copied from FB2
|
||||
*
|
||||
* @param mixed $value value to format
|
||||
* @param string $func function name
|
||||
* @param mixed $argument extra argument for the func
|
||||
* @return bool
|
||||
*/
|
||||
private static function testCompareFunc($value, $func, $argument)
|
||||
{
|
||||
$value_f = floatval($value);
|
||||
$arg_f = floatval($argument);
|
||||
|
||||
$fun = trim(strtolower($func));
|
||||
switch ($fun) {
|
||||
case 'lt':
|
||||
return $value_f < $arg_f;
|
||||
case 'gt':
|
||||
return $value_f > $arg_f;
|
||||
case 'le':
|
||||
return $value_f <= $arg_f;
|
||||
case 'ge':
|
||||
return $value_f >= $arg_f;
|
||||
case 'eq':
|
||||
return $value_f == $arg_f; // this is kinda useless, but to make the set complete
|
||||
|
||||
case 'range': // range(-10,0) = zima
|
||||
$bounds = array_map(function ($x) {
|
||||
return floatval(trim($x));
|
||||
}, explode(',', $argument));
|
||||
|
||||
if (count($bounds) != 2) {
|
||||
Log::error("Invalid range bounds: $argument");
|
||||
return false;
|
||||
}
|
||||
|
||||
return $value_f >= $bounds[0] && $value_f < $bounds[1];
|
||||
|
||||
case 'in': // in(10,20,30) = 10, 20 or 30 *funguje i pro string in(ZAP,VYP) = ZAP nebo VYP
|
||||
$bounds = array_map('trim', explode(',', $argument));
|
||||
return in_array(trim($value), $bounds);
|
||||
|
||||
default:
|
||||
Log::error("Invalid rewrite function: $func");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply multiple sets of substitutions to a format and get all the results.
|
||||
*
|
||||
* @param string $format Format same as for Str::format(). Fields are marked {}, {0} or {key}
|
||||
* @param array $subs_arrays array of arrays of substitutions - eg. [[a1, b1], [a2, b2], ...]
|
||||
* @return array
|
||||
*/
|
||||
public static function mapFormat($format, $subs_arrays)
|
||||
{
|
||||
$gather = [];
|
||||
foreach ($subs_arrays as $subs) {
|
||||
if (!is_array($subs)) $subs = [$subs];
|
||||
$gather[] = Str::format($format, $subs);
|
||||
}
|
||||
return $gather;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find all needle positions within a haystack
|
||||
*
|
||||
* @param string $haystack
|
||||
* @param string $needle
|
||||
* @return array
|
||||
*/
|
||||
public static function findPositions($haystack, $needle)
|
||||
{
|
||||
$lastPos = 0;
|
||||
$positions = [];
|
||||
|
||||
while (($lastPos = strpos($haystack, $needle, $lastPos)) !== false) {
|
||||
$positions[] = $lastPos;
|
||||
$lastPos = $lastPos + strlen($needle);
|
||||
}
|
||||
|
||||
return $positions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Discard positions in a string that are preceded by an unescaped backslash.
|
||||
*
|
||||
* @param string $str
|
||||
* @param int[] $positions
|
||||
* @return int[]
|
||||
*/
|
||||
public static function discardEscapedPositions($str, array $positions)
|
||||
{
|
||||
$actualPos = [];
|
||||
foreach ($positions as $pos) {
|
||||
if ($pos >= 1) {
|
||||
if ($str[$pos - 1] == '\\') {
|
||||
if ($pos >= 2 && $str[$pos - 2] == '\\') {
|
||||
// escaped backslash before - it's a valid quote
|
||||
} else {
|
||||
// this quote is escaped
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$actualPos[] = $pos;
|
||||
}
|
||||
|
||||
return $actualPos;
|
||||
}
|
||||
|
||||
/**
|
||||
* Split a string at given positions
|
||||
*
|
||||
* @param string $string
|
||||
* @param int[] $positions
|
||||
* @return string[]
|
||||
*/
|
||||
public static function splitAt($string, $positions)
|
||||
{
|
||||
$chunks = [];
|
||||
array_push($positions, strlen($string));
|
||||
array_unshift($positions, -1);
|
||||
|
||||
foreach ($positions as $i => $position) {
|
||||
if ($position >= strlen($string)) break;
|
||||
$chunks[] = substr($string, $position + 1, $positions[$i + 1] - $position - 1);
|
||||
}
|
||||
|
||||
return $chunks;
|
||||
}
|
||||
|
||||
/**
|
||||
* Split a string to pieces by commas, ignoring commas within strings delimited by double quotes.
|
||||
* A double quote can be escaped using a backslash.
|
||||
*
|
||||
* @param string $str
|
||||
* @param string $delimiter
|
||||
* @return mixed
|
||||
*/
|
||||
public static function splitCommandArgs($str, $delimiter = ',')
|
||||
{
|
||||
if ($str === '') return [];
|
||||
|
||||
// Find unescaped quotes
|
||||
$quotes = Str::findPositions($str, '"');
|
||||
$quotes = Str::discardEscapedPositions($str, $quotes);
|
||||
|
||||
if (count($quotes) % 2 != 0) {
|
||||
throw new FormatException("Unmatched quote in command arguments: $str");
|
||||
}
|
||||
|
||||
$commas = Str::findPositions($str, $delimiter);
|
||||
$commas = Utils::discardPositionsWithinPairs($commas, $quotes);
|
||||
|
||||
$chunks = Str::splitAt($str, $commas);
|
||||
|
||||
$arr = collect($chunks)->trim()->toArray();
|
||||
|
||||
return $arr;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get substring to first match of a token
|
||||
*
|
||||
* @param string $token token delimiting the parts
|
||||
* @param string $haystack full string
|
||||
* @return string portion of the string until the first token; or the whole string if token is not present.
|
||||
*/
|
||||
public static function toFirst($token, $haystack)
|
||||
{
|
||||
$lpos = strpos($haystack, $token);
|
||||
if ($lpos === false) return $haystack;
|
||||
return substr($haystack, 0, $lpos);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get substring from first match of a token
|
||||
*
|
||||
* @param string $token token delimiting the parts
|
||||
* @param string $haystack full string
|
||||
* @return string portion of the string until the first token; or empty string if token is not present.
|
||||
*/
|
||||
public static function fromFirst($token, $haystack, $exclusive = false)
|
||||
{
|
||||
$lpos = strpos($haystack, $token);
|
||||
if ($lpos === false) return '';
|
||||
return substr($haystack, $lpos+($exclusive?1:0));
|
||||
}
|
||||
|
||||
public static function toLast($token, $haystack)
|
||||
{
|
||||
$rpos = strrpos($haystack, $token);
|
||||
if ($rpos === false) return $haystack;
|
||||
return self::substr($haystack, 0, $rpos);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if translation exists
|
||||
*
|
||||
* @param string $descrKey tested transl key
|
||||
* @return bool exists
|
||||
*/
|
||||
public static function translationExists($descrKey)
|
||||
{
|
||||
$tr = trans($descrKey);
|
||||
return !preg_match('/^(\w+\.)+(\w+)$/', $tr);
|
||||
}
|
||||
|
||||
/**
|
||||
* Expand array of strings using bash-style repeat patterns {a,b,c}, {from..to}
|
||||
*
|
||||
* ie. to produce ad1 to ad4, use ad{1..4}. Supports multiple patterns, producing all permutations.
|
||||
*
|
||||
* @param string|string[] $sourceStrings
|
||||
* @return string[]
|
||||
*/
|
||||
public static function expandBashRepeat($sourceStrings)
|
||||
{
|
||||
if (is_string($sourceStrings)) $sourceStrings = [$sourceStrings];
|
||||
$outputs = [];
|
||||
foreach($sourceStrings as $str) {
|
||||
$i=0;
|
||||
$arrays = [];
|
||||
$str2 = preg_replace_callback('/\{([^{}]+)\}/', function($m) use(&$i, &$arrays) {
|
||||
$seq = explode(',',$m[1]);
|
||||
if(count($seq)>=2) {
|
||||
$arrays[$i] = $seq;
|
||||
} else {
|
||||
$ab = explode('..',$m[1]);
|
||||
if (!$ab) { return $m[0]; }
|
||||
$a = intval($ab[0]);
|
||||
$b = intval($ab[1]);
|
||||
$arrays[$i] = range($a, $b);
|
||||
}
|
||||
|
||||
return '{{'.($i++).'}}';
|
||||
}, $str);
|
||||
|
||||
$strs = [$str2];
|
||||
for($n=count($arrays)-1;$n>=0;$n--) {
|
||||
$arr = $arrays[$n];
|
||||
$tmpstrs = [];
|
||||
|
||||
foreach($arr as $subs) {
|
||||
foreach($strs as $ss) {
|
||||
$tmpstrs[] = str_replace('{{'.$n.'}}', $subs, $ss);
|
||||
}
|
||||
}
|
||||
|
||||
$strs = $tmpstrs;
|
||||
}
|
||||
|
||||
$outputs = array_merge($outputs, $strs);
|
||||
}
|
||||
return $outputs;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove trailing commas from JSON string
|
||||
*
|
||||
* @param string $str
|
||||
* @return string
|
||||
*/
|
||||
public static function cleanJson($str)
|
||||
{
|
||||
return preg_replace('/,\s*([}\]])/s','\1', $str);
|
||||
}
|
||||
|
||||
public static function ellipsis($str, $maxlen)
|
||||
{
|
||||
$len = mb_strlen($str);
|
||||
if ($len > $maxlen) {
|
||||
return mb_substr($str, 0, $maxlen) . '…';
|
||||
}
|
||||
|
||||
return $str;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,991 @@
|
||||
<?php
|
||||
|
||||
namespace MightyPork\Utils;
|
||||
|
||||
use Collator;
|
||||
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;
|
||||
}
|
||||
|
||||
// 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) {
|
||||
Log::debug("--- Query ---");
|
||||
Log::debug('SQL: ' . $query->sql);
|
||||
Log::debug('Bindings: ' . json_encode($query->bindings, 128));
|
||||
Log::debug("-------------");
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 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\"");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user