» /rd/lib.framework/HB/HB.php

<?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 $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*/ $strint $limit) : array {
        if (
$str === null)
            return 
array_fill(0$limitnull);
        
$r explode($separator, (string) $str$limit);
        if (
count($r) === $limit)
            return 
$r;
        return 
array_pad($r$limitnull); // add extra null elements
    
}

    static function list(array 
$arrint $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($w0$letters), $letters"@");

        
$v 0;
        foreach (
range(0, ($letters 2)) as $r)
            
$v $v 27 + (ord($w[$r]) - 64);

        
$v $v 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 mktime1200$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(000$m$day2000+$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($hbdt04)) {
                if (!
$m = (int) substr($hbdt42)) {
                    
$dt_str = (string)$y;
                } else {
                    
$d = (int) substr($hbdt62);
                    if (
$d && $format = ($opts["format"] ?? "")) {
                        
$dt_str date($formatstrtotime("$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), 015));
    }

    
/**
    * 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($whatMB_CASE_LOWER"utf-8") : NULL;
        foreach ((array) 
$with as $v) {
            if (
$string !== NULL) {
                if (
$string == mb_convert_case($vMB_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 $sstring $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 $nnint $base 5int $max 9) {
        
$r $nn ? ( ceil(log($nn$base)) + ) : 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 $arraystring $last_separator " and "): string {
        
$result "";
        if (
$array) {
            switch (
count($array)) {
                case 
1:
                    
$result first($array);
                    break;
                default:
                    
$result join($separatorarray_slice($array0, -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 $objectstring $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($argsget_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 */ $callablebool $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 $callbackint $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 $cntstring $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];

        return $m(...$args);

    }

// end class