<?php
/**
* SYNOPSIS: Common Homebase functions
* DESCRIPTION:
* COMMON! Functions not worthy for specific class
* Shortcuts
* ATTENTION:
* - Misplaced functions moved to HB_Legacy class
* - provides transparent PROXY to HB_HBX methods - you can call them as HB::$hbx_method(...)
* @see HB_HBX class
*
* @method static decodeBits(int $has, $getClassConstants, true $true)
*
*/
class HB
{
/**
* Convert To/From bigint form of fname-lname
* @param mixed $fname_id
* @param int $lname_id
* @return array|int|mixed|string
* @example HB::fl(HB::fl(HB::fl("Joe Black"))) works as: string=>bigint=>array=>string
* HB::fl($f, $l) => (bigint) $fl
* HB::fl("First Last") => (bigint) $fl
* HB::fl(['fname_id' => 23423, 'lname_id' => 234]) => (bigint) $fl
* HB::fl(['fname' => 23423, 'lname' => 234]) => (bigint) $fl
* HB::fl(['fl' => 100601018974442]) => (bigint) $fl
* HB::fl((bigint) $fl) => [$f, $l]
* HB::fl([$f, $l]) => (string) "First Last"
*/
static function fl($fname_id, $lname_id=-1) { # bigint | [$f, $l]
if (func_num_args()==1) {
$p = $fname_id;
if (is_numeric($p))
return [ $p >> 32, $p & 0xFFFFFFFF];
if (is_array($p)) {
if (isset($p[0]) && isset($p[1]))
return class_exists("Profile_Name") ? Profile_Name::full_name($p[0], "", $p[1]) : "";
if (isset($p['fname_id']) && isset($p['lname_id']))
return HB::fl($p['fname_id'], $p['lname_id']);
if (isset($p['fname']) && is_numeric($p['fname']) && is_numeric($p['lname']))
return HB::fl($p['fname'], $p['lname']);
if (isset($p['fl']))
return $p['fl'];
return 0;
}
list($fname_id, $m, $lname_id) = Profile_Name::ids(/* string-name */ $p);
}
return $fname_id << 32 | $lname_id;
}
/**
* return 0 if @ was used
* PHP8 changed @-error_reporting value from 0 to 4437
*/
static function error_reporting() { #
$v = \error_reporting();
return $v === 4437 ? 0 : $v;
}
/**
* fail-safe explode, always return $limit elements, empty elements are nulls
* @param string $separator
* @param ?string $str
* @param int $limit
* @return array
* @example: [$a, $b] = HB::explode(".", $path, 2); - php8 safe
*/
static function explode(string $separator, /*?string*/ $str, int $limit) : array {
if ($str === null)
return array_fill(0, $limit, null);
$r = explode($separator, (string) $str, $limit);
if (count($r) === $limit)
return $r;
return array_pad($r, $limit, null); // add extra null elements
}
static function list(array $arr, int $limit) {
return self::explode(',', implode(',', $arr), $limit);
}
// Check XML_XQuery: for examples
// how: false - html, 1 - array of html, "dom" - Dom
static function xq($html, $xpath, $how=false) { # html | array | Dom
return XML_XQuery::xquery($html, $xpath, $how);
}
// TEXT/HTML parsing helper
// "...($from)(..extracted..)($to)..."
static function between($string, $from, $to) { # "text", "", false (NO $from), null (NO $to)
$f = strpos($string, $from);
if ($f===false)
return false;
$f += strlen($from);
$t = strpos($string, $to, $f);
if ($t===false)
return null;
return trim(substr($string, $f, $t-$f));
}
// TEXT/HTML parsing helper
// fm - optional regexp for fm($fm) (first-match) query
static function splitBR($html, $fm="") { # array of matches
if ($fm)
$html = fm($fm, $html);
$r = array_map("trim", preg_split("!<br ?/?>!si", $html));
AH::cleanup($r); // no empty elements
return $r;
}
// Letters -> Unsigned Int
// 6.5 letters used, after that - unsorted
// if not latin - used word_index_unicode
static function word_index($w, $find_end=false) { # Int or Int-Int
$letters = 7;
$origin = $w;
$w = str_replace(["'", " ", "-"], '@', $w);
/* fuck unicode
if (strlen($w) > mb_strlen($w, 'UTF-8')) // if no latin symbols
return self::_word_index_unicode($w, $find_end);
*/
$w = strtoupper($w);
$w = preg_replace('![^A-Z@]!', '', $w);
$w = str_pad(substr($w, 0, $letters), $letters, "@");
$v = 0;
foreach (range(0, ($letters - 2)) as $r)
$v = $v * 27 + (ord($w[$r]) - 64);
$v = $v * 7 + intval((ord($w[$letters - 1]) - 64) / 4);
if (!$find_end)
return $v;
$v_end = self::word_index($origin . 'z');
return [$v, $v_end];
}
/*
* returns SHORT_INTEGER value represents given date of $time.
* NOTE! base is 01-01-2010, so value could be negative for dates prior to 2010.
*/
static function time2day($time=false){ # signed SHORT_integer - days from 2010-01-01
if (false === $time) {
$time = time();
}
$time_a = getdate( $time );
$a_new = mktime( 12, 0, 0, $time_a['mon'], $time_a['mday'], $time_a['year'] );
$b_new = 1262365200; // ==mktime( 12, 0, 0, 01, 01, 2010 );
return (int) round( ($a_new - $b_new) / 86400 );
}
// Returns TIME(INT) from DAY(SHORTINT) (see time2day)
static function day2time($intday){ # TIME(INT)
if ($intday < 0)
return strtotime("2010-01-01 $intday days");
return strtotime("2010-01-01 +$intday days");
}
// YMD as HB::hbdt
static function ymd2day(int $hbdt) : int { # day
return self::time2day(strtotime($hbdt));
}
// PHP IMPLEMENTATION OF MYSQL to_days(..)
// @param $date - "date" or (int) $time
// Ex: to_days(time()), to_days("2017-08-14), DB::one("select to_days(now())")
static function to_days(/* int | string */ $date) : int {
$d = is_int($date) ? $date : strtotime($date);
return (int) (719528 + $d / (60 * 60 * 24));
}
// PHP IMPLEMENTATION OF MYSQL to_days(..)
// @return time()
static function from_days(int $days) : int { # time
return (int) ( ($days - 719527.83333) * (60*60*24) );
}
// return ym pair for current/given ym
// return next/prev ym pair if delta
// ym can be "ym" string or [y, m], if ym is less than 100 it treated as delta to current ym
static function ym($ym="", $delta=0) { # [y,m]
if (! $ym)
$ym=date("ym");
if (is_array($ym)) {
list($y,$m)=$ym;
} else {
if ($ym<100) {
$delta=$ym;
$ym=date("ym");
}
$y=substr($ym,0,-2);
$m=substr($ym,-2);
$ym=[$y, $m];
}
if (! $delta)
return $ym;
$ym=date("ym", mktime (0,0,0,$m+$delta,1,$y));
return array(substr($ym,0,-2), substr($ym,-2));
}
// ym() as int
// if ym<100, it treated as delta
static function yms($ym="", $delta=0) { # ym (as int)
$ym=ym($ym, $delta);
return (int) ($ym[0].$ym[1]);
}
static function ym_time($ym, $day=1) { # uint
list($y,$m)=ym($ym);
return mktime(0, 0, 0, $m, $day, 2000+$y);
}
static function ym_format($ym, $format="F, Y") { # string
return date($format, ym_time($ym));
}
static $month = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
static $month_full = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'];
/**
* @param int $hbdt date representation with possible unknown fields
* (int) YYYYMMDD
* (int) YYYYMM00 year+month
* (int) YYYY0000 year only
* @param array $opts
* month_full - "Month DD, YYYY"
* format - date() function format
* @return false|string
*/
static function dt($hbdt, $opts=[]) { # string Date : "Year" | "Mon Year" | "Mon DD, YYYY" | "Month DD, YYYY" | ""
$dt_str = "";
if ($hbdt) {
if ($y = (int) substr($hbdt, 0, 4)) {
if (!$m = (int) substr($hbdt, 4, 2)) {
$dt_str = (string)$y;
} else {
$d = (int) substr($hbdt, 6, 2);
if ($d && $format = ($opts["format"] ?? "")) {
$dt_str = date($format, strtotime("$y/$m/$d"));
} else {
$mon = ($opts["month_full"]??0)?(self::$month_full[$m-1]??""):(self::$month[$m-1]??"");
$dt_str = $d ? "$mon $d, $y" : "$mon $y";
}
}
}
}
return $dt_str;
}
/**
* Universal date parser
* Understand *ALL* common date formats
* supports all formats from: http://en.wikipedia.org/wiki/Date_and_time_notation_in_the_United_States
* and more
* @param string $str
* @param array $options
* "dmy" - use "d/m/y" format instead of "m/d/y"
* @return array|false|int|string[]|void
*/
static function dtParse($str, array $options=[]) { # (int) HB::dt date | 0 (no date) | null = unrecognizable format
return HB_DateParser::dtParse($str, $options);
}
// convert address string to formalized location
static function parseLocation(/*string*/ $loc, $bark=true) { # [region, city_main, near] | [region] | [] | null
if (! $loc)
return;
$l = [];
if ($zip = fm("#(\d{5})$#", $loc)) {
list($l["region"], $l["city"], $cityname, $state, $l["city_main"], $geo) = Geo::_zip((int) $zip);
$l["geo"] = [$geo[1], $geo[0]];
} elseif (preg_match('#^(.+)[ ,] ?([a-zA-Z]{2})$#', $loc, $m)) {
$m[1] = trim($m[1], " ,");
list($x, $l["region"], $l["city"], $l["city_main"]) = Geo::id_path(1, $m[2], $m[1]);
#vd($loc, $m, $l["region"], $l["city"], $l["city_main"]);
} else {
return KRDB_PFL2_LinkedInParse::locality($loc, $bark);
}
if ($l["region"] && $l["city"])
return ["region" => $l["region"], "city_main" => $l["city_main"], "city" => $l["city"], "geo" => $l["geo"], "near" => true];
// no city case
if ($l["region"])
return ["region" => $l["region"]];
if ($bark)
Console::e("unknown location $loc", "warn");
}
// human-readable time representation, days, hours, minutes, seconds
static function timestr(/*int*/ $time) { # 12D23h12m | 05h10s
$s = "";
if ($time>=86400) {
$t = $time % 86400; // 24*3600
$days = number_format( ($time - $t) / 86400);
return $days."d ".gmdate("H:i", $t);
}
return gmdate("H:i:s", $time);
}
// 60bit HB hash
// if $ignore_case - data will be lowercased first
static function hash($data, $ignore_case=false) { # bigint 60bit_hash
return hexdec(substr(md5($ignore_case?mb_strtolower($data):$data), 0, 15));
}
/**
* mixed data comparison. Compare $what with $with
* - strings will be compared in case insensitive mode (MySQL style)
* - if $with is array or hash any matched subitem will return true (MongoDB style)
*
* Example: HB::compare("ABC", ["a", "b", "abc"]) == true, HB::compare("ABC", "abc")
*
* @param mixed $what
* @param mixed $with
* @param bool $strict
* @return bool
*
*/
static function compare($what, $with) { # true | false (equal or not)
$string = is_string($what) ? mb_convert_case($what, MB_CASE_LOWER, "utf-8") : NULL;
foreach ((array) $with as $v) {
if ($string !== NULL) {
if ($string == mb_convert_case($v, MB_CASE_LOWER, "utf-8"))
return true;
continue;
}
if ($what == $v)
return true;
}
return false;
}
static function stripBinary($s) { # printable characters
return preg_replace( '/[^[:print:]]/', '', $s);
}
static function timeEcho($data) { # echo "Y-m-d H:i:s x2s(data)\n"
echo date('Y-m-d H:i:s').' '.x2s($data)."\n";
}
// Alias of HB_Type::$type($value)
// Ex: HB::type($type, $value)
static function type($type, $value) {
return HB_Type::__callStatic($type, [$value]);
}
/**
* Validate & Sanitize type
* Exception-less version of HB::type
* Alias of HB_Type::t($value)
* [$email, $error] = HB::tp("email", $email)
*/
static function tp($type, $value) : array {
return HB_Type::tp($type, $value);
}
// Alias of HB_Type::test($type, $value)
static function typeTest($type, ...$args) : string { # Type Error (if any) or ""
return HB_Type::test($type, ...$args);
}
/**
* Make call according spec
*
* @param string $spec [object,method], Closure, "Class::method", "Class->method", "function"
* @param list $args Args.
* @return mixed
*/
static function call($spec, ...$args) {
if (! is_string($spec))
return $spec(...$args);
$m = [];
if (preg_match('/^([\w|\\\\]+)(::|->)(\w+)$/', $spec, $m)) {
if ($m[2] == '->' )
$m[1] = new $m[1];
$spec = [$m[1], $m[3]];
}
return $spec(...$args);
}
/**
* Universal-mixer: (runtime multiple-inheritance) for base: $object and set of objects: $mixins
* similar to runtime "Trait" addition
* - adding new methods to $object
* - replacing $object methods (you may replace number and types of method-arguments)
* - falling back from mixed method to original $object method (throw specific exception)
* - wrapping $object methods (call parent method)
* check hb\mixin\Mixer for details
*/
static function mix($obj, array $mixins, $Mixer = '\hb\mixin\Mixer') { # $Mixer instance
return new $Mixer($obj, ...$mixins);
}
// range() as a generator
// @test: iterator_to_array(HB::range(1, 10)) == range(1, 10)
static function range($start, $end, $step=1) { # generator
if (! is_int($start))
\Log::alert("only ints supported");
for ($i = $start; $i <= $end; $i+=$step)
yield $i;
}
/**
* have substring(s) in string(s) - case insensitive
* only full-word matches allowed.
*/
static function haveSubstring(/* string | array */ $str, /* string | array */ $substring) : bool {
if (is_array($str)) {
foreach ($str as $s) {
if (self::haveSubstring($s, $substring))
return true;
}
return false;
}
if (is_array($substring)) {
foreach ($substring as $ss) {
if (self::haveSubstring($str, $ss))
return true;
}
return false;
}
return (bool) preg_match("!\b\Q" . $substring . "\E\b!i", $str);
}
/**
* split string by NON-Escaped delimiter
* escape is "\"
* @return ["unescaped-results", ...]
*/
static function splitEscaped(string $s, string $delimiter = "," , /* CHAR */ string $escape = "\\") : array {
// return preg_split('/(?<!(?:\\\)),/', 'string1\,string2,abc,xyz-----,abc') << SLOW AND - return escaped data
if (strpos($s, $escape.$delimiter) === false)
return explode($delimiter, $s);
$s = str_replace($escape.$delimiter, "\u{0}\u{0}\u{7}", $s); // Agent#
$r = explode($delimiter, $s);
foreach ($r as &$t)
$t = str_replace("\u{0}\u{0}\u{7}", $delimiter, $t);
return $r;
}
/**
* Replacement of natural function count() to avoid PHP 7.2.1+ warnings about "argument passed in count() should be countable type"
* @param mixed $arr
* @return int
*/
/*
public static function count(&$arr) {
$count = 0;
if (@$arr) {
$count = count($arr);
}
return $count;
}
*/
/**
* logarithmic scale of your value in 0..9 range (see $max)
* with 0=0, 1=1, 2 = 2..log_base, ..., 8 = $base**6+1 .. $base ** 7 , 9 = $base ** 7 +
*
* suggested base based on your max value (use base ** 7 as estimate)
* base 4: 16K
* base 5: 80K
* base 6: 280K
* base 7: 800K
* base 8: 2M
* base 9: 5M
* base 10: 10M
* base 12: 35M
* base 17: 410M
* base 20: 13B
*
* base 4: 0 => 0, 1 => 1, 2 => 2..4, 3 => ..16, 4 => ..64, 5=> ..256, 6=> ..1024, 7 => ..4K, 8 => ..16K, 9 => 16K+
* base 5: 0 => 0, 1 => 1, 2 => 2..5, 3 => 6..25, 4 => 26..125, 5 => ..625, 6 => ..3K, 7 => ..15K, 8 => ..80K, 9 => 80K+
*/
static function scale(int $nn, int $base = 5, int $max = 9) {
$r = $nn ? ( ceil(log($nn, $base)) + 1 ) : 0;
return $r > $max ? $max : (int) $r;
}
/**
* Similar to standard join but use different glue before last element
* @param string $separator
* @param array|null $array
* @return string
* @example HB::join_and(", ", ["first", "second", "third"])
* HB::join_and(", ", ["first", "second", "third"], " and finally ")
*/
static function join_and(string $separator, ?array $array, string $last_separator = " and "): string {
$result = "";
if ($array) {
switch (count($array)) {
case 1:
$result = first($array);
break;
default:
$result = join($separator, array_slice($array, 0, -1)) . $last_separator . end($array);
break;
}
}
return $result;
}
// PHP7.4 version of $method(...$args)
// call method with named arguments
// Exception if cant call, required param missing or unknown param added
// HB::rcall(new Test(), "t1", ['a' => 'AA', 'b' => "22"])
static function rcall(object $object, string $method, array $args) { # mixed | \Error
$r = [];
$params = (new ReflectionMethod($object, $method))->getParameters();
foreach ($params as $p) {
$k = $p->name;
if (array_key_exists($k, $args)) {
$r[] = $args[$k];
unset($args[$k]);
} else {
if ($p->isDefaultValueAvailable()) {
$r[] = $p->getDefaultValue();
} else {
throw new \Error(get_class($object)."::$method parameter '$k' missing");
}
}
}
\error_if($args, get_class($object)."::$method unknown parameters ".x2s($args));
return [$object, $method](...$r);
}
/**
* Stringy composer package shortcut
* Doc: https://github.com/danielstjules/Stringy
*php-shell type: ? Stringy\Stringy
*/
static function s($string) { # Stringy\Stringy
return Stringy\Stringy::create($string);
}
/**
* Defer like in go
* Usage:
* $x = \HB::defer($callable, ...$args); // execute $callable when $x is unset
*
* use $x->cancel() to remove callback
* use $x->callback, $x->args to change callback / modify its arguments
*/
static function defer(callable $callable, ...$args) {
\HB_HBX::load();
return new _HB_Defer($callable, $args);
}
/**
* Exectute callable at the end of the script
* Usage:
* \HB::deferGlobal($callable);
* \HB::deferGlobal(['name' => $callable]);
* \HB::deferGlobal(['name' => $callable], first:true); // execute before 'previous' deferGlobal
* \HB::deferGlobal(['name' => null]); // cancel scheduled execution
* -- semi internal:
* \HB::deferGlobal(true); // is_something scheduled
* \HB::deferGlobal(false); // execute all NOW
*
* For php-fpm callback executed *AFTER* request is sent to visitor @see fastcgi_finish_request
* echo will NOT work
*
*/
static function deferGlobal(/* \Closure | [name=>\Closure] | true | false */ $callable, bool $first = true) {
static $execControl = null;
if ($callable === true) {
return $execControl;
}
if ($callable === false) { // execute all NOW !!
unset($execControl);
return;
}
\HB_HBX::load();
if (! $execControl)
$execControl = \HB::defer("_HB_Defer_Global::run"); // make sure defer is executed
return _HB_Defer_Global::add($callable, $first);
}
/**
* SmartCache = APC based smartcache
* @see \hb\cache\SmartCache
*/
static function scache(string $key, \Closure $callback, int $ttl=3600) {
static $smartCache = null;
if (! $smartCache)
$smartCache = \hb\cache\SmartCache::i(['namespace' => chr(\RdSite::siteID())]);
// $smartCache = new \hb\cache\SmartCache(['namespace' => chr(\RdSite::siteID())]);
return $smartCache($key, $callback, $ttl);
}
static function pluralize(string $single) : string { # plural
return HB_Pluralizer::pluralize($single);
}
static function unpluralize(string $plural) : string { # single
return HB_Pluralizer::singularize($plural);
}
static function cntPluralize(int $cnt, string $single, $_result="string") { # "$cnt banana(s)" or [$cnt, banana(s)]
return HB_Pluralizer::pluralize_if($cnt, $single, $_result);
}
// We proxy unknown calls to other classes
public static function __callStatic($method, $args) {
$m = ['HB_HBX', $method];
if (! is_callable($m))
$m = ['HB_Legacy', $method];
}
} // end class