Passed
Push — master ( f802e2...a1b288 )
by Sebastian
02:28
created

ConvertHelper::isStringASCII()   A

Complexity

Conditions 4
Paths 3

Size

Total Lines 11
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 4
eloc 5
c 0
b 0
f 0
nc 3
nop 1
dl 0
loc 11
rs 10
1
<?php
2
/**
3
 * File containing the {@see AppUtils\ConvertHelper} class.
4
 * 
5
 * @package Application Utils
6
 * @subpackage ConvertHelper
7
 * @see AppUtils\ConvertHelper
8
 */
9
10
namespace AppUtils;
11
12
/**
13
 * Static conversion helper class: offers a number of utility methods
14
 * to convert variable types, as well as specialized methods for working
15
 * with specific types like dates.
16
 * 
17
 * @package Application Utils
18
 * @subpackage ConvertHelper
19
 * @author Sebastian Mordziol <[email protected]>
20
 */
21
class ConvertHelper
22
{
23
    const ERROR_MONTHTOSTRING_NOT_VALID_MONTH_NUMBER = 23303;
24
    
25
    const ERROR_CANNOT_NORMALIZE_NON_SCALAR_VALUE = 23304;
26
    
27
    const ERROR_JSON_ENCODE_FAILED = 23305;
28
    
29
    /**
30
     * Normalizes tabs in the specified string by indenting everything
31
     * back to the minimum tab distance. With the second parameter,
32
     * tabs can optionally be converted to spaces as well (recommended
33
     * for HTML output).
34
     *
35
     * @param string $string
36
     * @param boolean $tabs2spaces
37
     * @return string
38
     */
39
    public static function normalizeTabs(string $string, bool $tabs2spaces = false)
40
    {
41
        $lines = explode("\n", $string);
42
        $max = 0;
43
        $min = 99999;
44
        foreach ($lines as $line) {
45
            $amount = substr_count($line, "\t");
46
            if ($amount > $max) {
47
                $max = $amount;
48
            }
49
50
            if ($amount > 0 && $amount < $min) {
51
                $min = $amount;
52
            }
53
        }
54
55
        $converted = array();
56
        foreach ($lines as $line) {
57
            $amount = substr_count($line, "\t") - $min;
58
            $line = trim($line);
59
            if ($amount >= 1) {
60
                $line = str_repeat("\t", $amount) . $line;
61
            }
62
63
            $converted[] = $line;
64
        }
65
66
        $string = implode("\n", $converted);
67
        if ($tabs2spaces) {
68
            $string = self::tabs2spaces($string);
69
        }
70
71
        return $string;
72
    }
73
74
    /**
75
     * Converts tabs to spaces in the specified string.
76
     * @param string $string
77
     * @return string
78
     */
79
    public static function tabs2spaces($string)
80
    {
81
        return str_replace("\t", '    ', $string);
82
    }
83
    
84
    /**
85
     * Converts the specified amount of seconds into
86
     * a human readable string split in months, weeks,
87
     * days, hours, minutes and seconds.
88
     *
89
     * @param float $seconds
90
     * @return string
91
     */
92
    public static function time2string($seconds)
93
    {
94
        static $units = null;
95
        if (is_null($units)) {
96
            $units = array(
97
                array(
98
                    'value' => 31 * 7 * 24 * 3600,
99
                    'singular' => t('month'),
100
                    'plural' => t('months')
101
                ),
102
                array(
103
                    'value' => 7 * 24 * 3600,
104
                    'singular' => t('week'),
105
                    'plural' => t('weeks')
106
                ),
107
                array(
108
                    'value' => 24 * 3600,
109
                    'singular' => t('day'),
110
                    'plural' => t('days')
111
                ),
112
                array(
113
                    'value' => 3600,
114
                    'singular' => t('hour'),
115
                    'plural' => t('hours')
116
                ),
117
                array(
118
                    'value' => 60,
119
                    'singular' => t('minute'),
120
                    'plural' => t('minutes')
121
                ),
122
                array(
123
                    'value' => 1,
124
                    'singular' => t('second'),
125
                    'plural' => t('seconds')
126
                )
127
            );
128
        }
129
130
        // specifically handle zero
131
        if ($seconds <= 0) {
132
            return '0 ' . t('seconds');
133
        }
134
        
135
        if($seconds < 1) {
136
            return t('less than a second');
137
        }
138
139
        $tokens = array();
140
        foreach ($units as $def) {
141
            $quot = intval($seconds / $def['value']);
142
            if ($quot) {
143
                $item = $quot . ' ';
144
                if (abs($quot) > 1) {
145
                    $item .= $def['plural'];
146
                } else {
147
                    $item .= $def['singular'];
148
                }
149
150
                $tokens[] = $item;
151
                $seconds -= $quot * $def['value'];
152
            }
153
        }
154
155
        $last = array_pop($tokens);
156
        if (empty($tokens)) {
157
            return $last;
158
        }
159
160
        return implode(', ', $tokens) . ' ' . t('and') . ' ' . $last;
161
    }
162
163
   /**
164
    * Converts a timestamp into an easily understandable
165
    * format, e.g. "2 hours", "1 day", "3 months"
166
    *
167
    * If you set the date to parameter, the difference
168
    * will be calculated between the two dates and not
169
    * the current time.
170
    *
171
    * @param integer|\DateTime $datefrom
172
    * @param integer|\DateTime $dateto
173
    * @return string
174
    */
175
    public static function duration2string($datefrom, $dateto = -1) : string
176
    {
177
         $converter = new ConvertHelper_DurationConverter();
178
         
179
         if($datefrom instanceof \DateTime)
180
         {
181
             $converter->setDateFrom($datefrom);
182
         }
183
         else
184
         {
185
             $converter->setDateFrom(self::timestamp2date($datefrom)); 
186
         }
187
188
         if($dateto instanceof \DateTime)
189
         {
190
             $converter->setDateTo($dateto);
191
         }
192
         else if($dateto > 0)
193
         {
194
             $converter->setDateTo(self::timestamp2date($dateto));
195
         }
196
197
         return $converter->convert();
198
    }
199
200
    /**
201
     * Adds syntax highlighting to the specified SQL string in HTML format
202
     * @param string $sql
203
     * @return string
204
     */
205
    public static function highlight_sql($sql)
206
    {
207
        $geshi = new  \GeSHi($sql, 'sql');
208
209
        return $geshi->parse_code();
210
    }
211
    
212
    public static function highlight_xml($xml, $formatSource=false)
213
    {
214
        if($formatSource) 
215
        {
216
            $dom = new \DOMDocument();
217
            $dom->loadXML($xml);
218
            $dom->preserveWhiteSpace = false;
219
            $dom->formatOutput = true;
220
            
221
            $xml = $dom->saveXML();
222
        }
223
        
224
        $geshi = new \GeSHi($xml, 'xml');
225
        
226
        return $geshi->parse_code();
227
    }
228
229
    public static function highlight_php($php)
230
    {
231
        $geshi = new \GeSHi($php, 'php');
232
    
233
        return $geshi->parse_code();
234
    }
235
    
236
    /**
237
     * Converts a number of bytes to a human readable form,
238
     * e.g. xx Kb / xx Mb / xx Gb
239
     *
240
     * @param $bytes
241
     * @param $precision
242
     * @return string
243
     */
244
    public static function bytes2readable($bytes, $precision = 1)
245
    {
246
        $kilobyte = 1024;
247
        $megabyte = $kilobyte * 1024;
248
        $gigabyte = $megabyte * 1024;
249
        $terabyte = $gigabyte * 1024;
250
251
        if (($bytes >= 0) && ($bytes < $kilobyte)) {
252
            return $bytes . ' ' . t('B');
253
254
        } elseif (($bytes >= $kilobyte) && ($bytes < $megabyte)) {
255
            return round($bytes / $kilobyte, $precision) . ' ' . t('Kb');
256
257
        } elseif (($bytes >= $megabyte) && ($bytes < $gigabyte)) {
258
            return round($bytes / $megabyte, $precision) . ' ' . t('Mb');
259
260
        } elseif (($bytes >= $gigabyte) && ($bytes < $terabyte)) {
261
            return round($bytes / $gigabyte, $precision) . ' ' . t('Gb');
262
263
        } elseif ($bytes >= $terabyte) {
264
            return round($bytes / $gigabyte, $precision) . ' ' . t('Tb');
265
        }
266
267
        return $bytes . ' ' . t('B');
268
    }
269
270
   /**
271
    * Cuts a text to the specified length if it is longer than the
272
    * target length. Appends a text to signify it has been cut at 
273
    * the end of the string.
274
    * 
275
    * @param string $text
276
    * @param int $targetLength
277
    * @param string $append
278
    * @return string
279
    */
280
    public static function text_cut(string $text, int $targetLength, string $append = '...') : string
281
    {
282
        $length = mb_strlen($text);
283
        if ($length <= $targetLength) {
284
            return $text;
285
        }
286
287
        $text = trim(mb_substr($text, 0, $targetLength)) . $append;
288
289
        return $text;
290
    }
291
292
    public static function var_dump($var, $html=true)
293
    {
294
        $info = parseVariable($var);
295
        
296
        if($html) {
297
            return $info->toHTML();
298
        }
299
        
300
        return $info->toString();
301
    }
302
    
303
    public static function print_r($var, $return=false, $html=true)
304
    {
305
        $result = self::var_dump($var, $html);
306
        
307
        if($html) {
308
            $result = 
309
            '<pre style="background:#fff;color:#333;padding:16px;border:solid 1px #bbb;border-radius:4px">'.
310
                $result.
311
            '</pre>';
312
        }
313
        
314
        if($return) {
315
            return $result;
316
        }
317
        
318
        echo $result;
319
    }
320
    
321
    protected static $booleanStrings = array(
322
        1 => true,
323
        0 => false,
324
        '1' => true,
325
        '0' => false,
326
        'true' => true,
327
        'false' => false,
328
        'yes' => true,
329
        'no' => false
330
    );
331
332
    public static function string2bool($string)
333
    {
334
        if($string === '' || $string === null) {
335
            return false;
336
        }
337
        
338
        if (is_bool($string)) {
339
            return $string;
340
        }
341
342
        if (!array_key_exists($string, self::$booleanStrings)) {
343
            throw new \InvalidArgumentException('Invalid string boolean representation');
344
        }
345
346
        return self::$booleanStrings[$string];
347
    }
348
    
349
   /**
350
    * Whether the specified string is a boolean string or boolean value.
351
    * Alias for {@link ConvertHelper::isBoolean()}.
352
    * 
353
    * @param mixed $string
354
    * @return bool
355
    * @deprecated
356
    * @see ConvertHelper::isBoolean()
357
    */
358
    public static function isBooleanString($string) : bool
359
    {
360
        return self::isBoolean($string);
361
    }
362
363
   /**
364
    * Alias for the {@\AppUtils\XMLHelper::string2xml()} method.
365
    * 
366
    * @param string $text
367
    * @return string
368
    * @deprecated
369
    */
370
    public static function text_makeXMLCompliant($text)
371
    {
372
        return XMLHelper::string2xml($text);
373
    }
374
375
    /**
376
     * Transforms a date into a generic human readable date, optionally with time.
377
     * If the year is the same as the current one, it is omitted.
378
     *
379
     * - 6 Jan 2012
380
     * - 12 Dec 2012 17:45
381
     * - 5 Aug
382
     *
383
     * @param \DateTime $date
384
     * @return string
385
     */
386
    public static function date2listLabel(\DateTime $date, $includeTime = false, $shortMonth = false)
387
    {
388
        $today = new \DateTime();
389
        if($date->format('d.m.Y') == $today->format('d.m.Y')) {
390
            $label = t('Today');
391
        } else {
392
            $label = $date->format('d') . '. ' . self::month2string((int)$date->format('m'), $shortMonth) . ' ';
393
            if ($date->format('Y') != date('Y')) {
394
                $label .= $date->format('Y');
395
            }
396
        }
397
        
398
        if ($includeTime) {
399
            $label .= $date->format(' H:i');
400
        }
401
402
        return trim($label);
403
    }
404
405
    protected static $months;
406
407
    /**
408
     * Returns a human readable month name given the month number. Can optionally
409
     * return the shorthand version of the month. Translated into the current
410
     * application locale.
411
     *
412
     * @param int|string $monthNr
413
     * @param boolean $short
414
     * @throws ConvertHelper_Exception
415
     * @return string
416
     */
417
    public static function month2string($monthNr, $short = false)
418
    {
419
        if (!isset(self::$months)) {
420
            self::$months = array(
421
                1 => array(t('January'), t('Jan')),
422
                2 => array(t('February'), t('Feb')),
423
                3 => array(t('March'), t('Mar')),
424
                4 => array(t('April'), t('Apr')),
425
                5 => array(t('May'), t('May')),
426
                6 => array(t('June'), t('Jun')),
427
                7 => array(t('July'), t('Jul')),
428
                8 => array(t('August'), t('Aug')),
429
                9 => array(t('September'), t('Sep')),
430
                10 => array(t('October'), t('Oct')),
431
                11 => array(t('November'), t('Nov')),
432
                12 => array(t('December'), t('Dec'))
433
            );
434
        }
435
436
        $monthNr = intval($monthNr);
437
        if (!isset(self::$months[$monthNr])) {
438
            throw new ConvertHelper_Exception(
439
                'Invalid month number',
440
                sprintf('%1$s is not a valid month number.', $monthNr),
441
                self::ERROR_MONTHTOSTRING_NOT_VALID_MONTH_NUMBER
442
            );
443
        }
444
445
        if ($short) {
446
            return self::$months[$monthNr][1];
447
        }
448
449
        return self::$months[$monthNr][0];
450
    }
451
452
    /**
453
     * Transliterates a string.
454
     *
455
     * @param string $string
456
     * @param string $spaceChar
457
     * @param string $lowercase
458
     * @return string
459
     */
460
    public static function transliterate($string, $spaceChar = '-', $lowercase = true)
461
    {
462
        $translit = new Transliteration();
463
        $translit->setSpaceReplacement($spaceChar);
464
        if ($lowercase) {
465
            $translit->setLowercase();
466
        }
467
468
        return $translit->convert($string);
469
    }
470
    
471
   /**
472
    * Retrieves the HEX character codes for all control
473
    * characters that the {@link stripControlCharacters()} 
474
    * method will remove.
475
    * 
476
    * @return string[]
477
    */
478
    public static function getControlCharactersAsHex()
479
    {
480
        $hexAlphabet = array('0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F');
481
        
482
        $stack = array();
483
        foreach(self::$controlChars as $char)
484
        {
485
            $tokens = explode('-', $char);
486
            $start = $tokens[0];
487
            $end = $tokens[1];
488
            $prefix = substr($start, 0, 3);
489
            $range = array();
490
            foreach($hexAlphabet as $number) {
491
                $range[] = $prefix.$number;
492
            }
493
            
494
            $use = false;
495
            foreach($range as $number) {
496
                if($number == $start) {
497
                    $use = true;
498
                }
499
                
500
                if($use) {
501
                    $stack[] = $number;
502
                }
503
                
504
                if($number == $end) {
505
                    break;
506
                }
507
            }
508
        }
509
        
510
        return $stack;
511
    }
512
    
513
   /**
514
    * Retrieves an array of all control characters that
515
    * the {@link stripControlCharacters()} method will 
516
    * remove, as the actual UTF-8 characters.
517
    * 
518
    * @return string[]
519
    */
520
    public static function getControlCharactersAsUTF8()
521
    {
522
        $chars = self::getControlCharactersAsHex();
523
        
524
        $result = array();
525
        foreach($chars as $char) {
526
            $result[] = hex2bin($char);
527
        }
528
        
529
        return $result;
530
    }
531
    
532
   /**
533
    * Retrieves all control characters as JSON encoded
534
    * characters, e.g. "\u200b".
535
    * 
536
    * @return string[]
537
    */
538
    public static function getControlCharactersAsJSON()
539
    {
540
        $chars = self::getControlCharactersAsHex();
541
        
542
        $result = array();
543
        foreach($chars as $char) {
544
            $result[] = '\u'.strtolower($char);
545
        }
546
        
547
        return $result;
548
    }
549
    
550
    protected static $controlChars =  array(
551
        '0000-0008', // control chars
552
        '000E-000F', // control chars
553
        '0010-001F', // control chars
554
        '2000-200F', // non-breaking space and co
555
    );
556
    
557
    protected static $controlCharsRegex;
558
559
    /**
560
     * Removes all control characters from the specified string
561
     * that can cause problems in some cases, like creating
562
     * valid XML documents. This includes invisible non-breaking
563
     * spaces.
564
     *
565
     * @param string $string
566
     * @return string
567
     * @see https://stackoverflow.com/a/8171868/2298192
568
     * @see https://unicode-table.com/en
569
     */
570
    public static function stripControlCharacters(string $string) : string
571
    {
572
        if(empty($string)) {
573
            return $string;
574
        }
575
        
576
        // create the regex from the unicode characters list
577
        if(!isset(self::$controlCharsRegex)) 
578
        {
579
            $chars = self::getControlCharactersAsHex();
580
581
            // we use the notation \x{0000} to specify the unicode character key
582
            // in the regular expression.
583
            $stack = array();
584
            foreach($chars as $char) {
585
                $stack[] = '\x{'.$char.'}';
586
            }
587
            
588
            self::$controlCharsRegex = '/['.implode('', $stack).']/u';
589
        }
590
        
591
        return preg_replace(self::$controlCharsRegex, '', $string);
592
    }
593
594
   /**
595
    * Converts a unicode character to the PHPO notation.
596
    * 
597
    * Example:
598
    * 
599
    * <pre>unicodeChar2php('"\u0000"')</pre>
600
    * 
601
    * Returns
602
    * 
603
    * <pre>\x0</pre>
604
    * 
605
    * @param string $unicodeChar
606
    * @return string
607
    */
608
    public static function unicodeChar2php($unicodeChar) 
609
    {
610
        $unicodeChar = json_decode($unicodeChar);
611
        
612
        /** @author Krinkle 2018 */
613
        $output = '';
614
        foreach (str_split($unicodeChar) as $octet) {
615
            $ordInt = ord($octet);
616
            // Convert from int (base 10) to hex (base 16), for PHP \x syntax
617
            $ordHex = base_convert($ordInt, 10, 16);
618
            $output .= '\x' . $ordHex;
619
        }
620
        return $output;
621
    }
622
    
623
    /**
624
     * Removes the extension from the specified filename
625
     * and returns the name without the extension.
626
     *
627
     * Example:
628
     * filename.html > filename
629
     * passed.test.jpg > passed.test
630
     * path/to/file/document.txt > document
631
     *
632
     * @param string $filename
633
     * @return string
634
     */
635
    public static function filenameRemoveExtension($filename)
636
    {
637
        return FileHelper::removeExtension($filename);
638
    }
639
    
640
    public static function areVariablesEqual($a, $b) : bool
641
    {
642
        $a = self::convertScalarForComparison($a);
643
        $b = self::convertScalarForComparison($b);
644
645
        return $a === $b;
646
    }
647
    
648
    protected static function convertScalarForComparison($scalar)
649
    {
650
        if($scalar === '' || is_null($scalar)) {
651
            return null;
652
        }
653
        
654
        if(is_bool($scalar)) {
655
            return self::bool2string($scalar);
656
        }
657
        
658
        if(is_array($scalar)) {
659
            $scalar = md5(serialize($scalar));
660
        }
661
        
662
        if($scalar !== null && !is_scalar($scalar)) {
663
            throw new ConvertHelper_Exception(
664
                'Not a scalar value in comparison',
665
                null,
666
                self::ERROR_CANNOT_NORMALIZE_NON_SCALAR_VALUE
667
            );
668
        }
669
        
670
        return strval($scalar);
671
    }
672
673
    /**
674
     * Compares two strings to check whether they are equal.
675
     * null and empty strings are considered equal.
676
     *
677
     * @param string $a
678
     * @param string $b
679
     * @return boolean
680
     */
681
    public static function areStringsEqual($a, $b) : bool
682
    {
683
        return self::areVariablesEqual($a, $b);
684
    }
685
686
    /**
687
     * Checks whether the two specified numbers are equal.
688
     * null and empty strings are considered as 0 values.
689
     *
690
     * @param number|string $a
691
     * @param number|string $b
692
     * @return boolean
693
     */
694
    public static function areNumbersEqual($a, $b) : bool
695
    {
696
        return self::areVariablesEqual($a, $b);
697
    }
698
699
    /**
700
     * Converts a boolean value to a string. Defaults to returning
701
     * 'true' or 'false', with the additional parameter it can also
702
     * return the 'yes' and 'no' variants.
703
     *
704
     * @param boolean|string $boolean
705
     * @param boolean $yesno
706
     * @return string
707
     */
708
    public static function bool2string($boolean, bool $yesno = false) : string
709
    {
710
        // allow 'yes', 'true', 'no', 'false' string notations as well
711
        if(!is_bool($boolean)) {
712
            $boolean = self::string2bool($boolean);
713
        }
714
        
715
        if ($boolean) {
716
            if ($yesno) {
717
                return 'yes';
718
            }
719
720
            return 'true';
721
        }
722
723
        if ($yesno) {
724
            return 'no';
725
        }
726
727
        return 'false';
728
    }
729
    
730
   /**
731
    * Converts an associative array with attribute name > value pairs
732
    * to an attribute string that can be used in an HTML tag. Empty 
733
    * attribute values are ignored.
734
    * 
735
    * Example:
736
    * 
737
    * array2attributeString(array(
738
    *     'id' => 45,
739
    *     'href' => 'http://www.mistralys.com'
740
    * ));
741
    * 
742
    * Result:
743
    * 
744
    * id="45" href="http://www.mistralys.com"
745
    * 
746
    * @param array $array
747
    * @return string
748
    */
749
    public static function array2attributeString($array)
750
    {
751
        $tokens = array();
752
        foreach($array as $attr => $value) {
753
            if($value == '' || $value == null) {
754
                continue;
755
            }
756
            
757
            $tokens[] = $attr.'="'.$value.'"';
758
        }
759
        
760
        if(empty($tokens)) {
761
            return '';
762
        }
763
        
764
        return ' '.implode(' ', $tokens);
765
    }
766
    
767
   /**
768
    * Converts a string so it can safely be used in a javascript
769
    * statement in an HTML tag: uses single quotes around the string
770
    * and encodes all special characters as needed.
771
    * 
772
    * @param string $string
773
    * @return string
774
    */
775
    public static function string2attributeJS($string, $quoted=true)
776
    {
777
        $converted = addslashes(htmlspecialchars(strip_tags($string), ENT_QUOTES, 'UTF-8'));
778
        if($quoted) {
779
            $converted = "'".$converted."'";
780
        } 
781
        
782
        return $converted;
783
    }
784
    
785
   /**
786
    * Checks if the specified string is a boolean value, which
787
    * includes string representations of boolean values, like 
788
    * <code>yes</code> or <code>no</code>, and <code>true</code>
789
    * or <code>false</code>.
790
    * 
791
    * @param mixed $value
792
    * @return boolean
793
    */
794
    public static function isBoolean($value) : bool
795
    {
796
        if(is_bool($value)) {
797
            return true;
798
        }
799
        
800
        if(!is_scalar($value)) {
801
            return false;
802
        }
803
        
804
        return array_key_exists($value, self::$booleanStrings);
805
    }
806
    
807
   /**
808
    * Converts an associative array to an HTML style attribute value string.
809
    * 
810
    * @param array $subject
811
    * @return string
812
    */
813
    public static function array2styleString(array $subject) : string
814
    {
815
        $tokens = array();
816
        foreach($subject as $name => $value) {
817
            $tokens[] = $name.':'.$value;
818
        }
819
        
820
        return implode(';', $tokens);
821
    }
822
    
823
   /**
824
    * Converts a DateTime object to a timestamp, which
825
    * is PHP 5.2 compatible.
826
    * 
827
    * @param \DateTime $date
828
    * @return integer
829
    */
830
    public static function date2timestamp(\DateTime $date) : int
831
    {
832
        return (int)$date->format('U');
833
    }
834
    
835
   /**
836
    * Converts a timestamp into a DateTime instance.
837
    * @param int $timestamp
838
    * @return \DateTime
839
    */
840
    public static function timestamp2date(int $timestamp) : \DateTime
841
    {
842
        $date = new \DateTime();
843
        $date->setTimestamp($timestamp);
844
        return $date;
845
    }
846
    
847
   /**
848
    * Strips an absolute path to a file within the application
849
    * to make the path relative to the application root path.
850
    * 
851
    * @param string $path
852
    * @return string
853
    * 
854
    * @see FileHelper::relativizePath()
855
    * @see FileHelper::relativizePathByDepth()
856
    */
857
    public static function fileRelativize(string $path) : string
858
    {
859
        return FileHelper::relativizePathByDepth($path);
860
    }
861
    
862
    /**
863
    * Converts a PHP regex to a javascript RegExp object statement.
864
    * 
865
    * NOTE: This is an alias for the JSHelper's `convertRegex` method. 
866
    * More details are available on its usage there.
867
    *
868
    * @param string $regex A PHP preg regex
869
    * @param string $statementType The type of statement to return: Defaults to a statement to create a RegExp object.
870
    * @return array|string Depending on the specified return type.
871
    * 
872
    * @see JSHelper::buildRegexStatement()
873
    */
874
    public static function regex2js(string $regex, string $statementType=JSHelper::JS_REGEX_OBJECT)
875
    {
876
        return JSHelper::buildRegexStatement($regex, $statementType);
877
    }
878
    
879
   /**
880
    * Converts the specified variable to JSON. Works just
881
    * like the native `json_encode` method, except that it
882
    * will trigger an exception on failure, which has the 
883
    * json error details included in its developer details.
884
    * 
885
    * @param mixed $variable
886
    * @param int|NULL $options JSON encode options.
887
    * @param int|NULL $depth 
888
    * @throws ConvertHelper_Exception
889
    * @return string
890
    */
891
    public static function var2json($variable, int $options=0, int $depth=512) : string
892
    {
893
        $result = json_encode($variable, $options, $depth);
894
        
895
        if($result !== false) {
896
            return $result;
897
        }
898
        
899
        throw new ConvertHelper_Exception(
900
            'Could not create json array'.json_last_error_msg(),
901
            sprintf(
902
                'The call to json_encode failed for the variable [%s]. JSON error details: #%s, %s',
903
                parseVariable($variable)->toString(),
904
                json_last_error(),
905
                json_last_error_msg()
906
            ),
907
            self::ERROR_JSON_ENCODE_FAILED
908
        );
909
    }
910
    
911
   /**
912
    * Strips all known UTF byte order marks from the specified string.
913
    * 
914
    * @param string $string
915
    * @return string
916
    */
917
    public static function stripUTFBom($string)
918
    {
919
        $boms = FileHelper::getUTFBOMs();
920
        foreach($boms as $bomChars) {
921
            $length = mb_strlen($bomChars);
922
            $text = mb_substr($string, 0, $length);
923
            if($text==$bomChars) {
924
                return mb_substr($string, $length);
925
            }
926
        }
927
        
928
        return $string;
929
    }
930
931
   /**
932
    * Converts a string to valid utf8, regardless
933
    * of the string's encoding(s).
934
    * 
935
    * @param string $string
936
    * @return string
937
    */
938
    public static function string2utf8($string)
939
    {
940
        if(!self::isStringASCII($string)) {
941
            return \ForceUTF8\Encoding::toUTF8($string);
942
        }
943
        
944
        return $string;
945
    }
946
    
947
   /**
948
    * Checks whether the specified string is an ASCII
949
    * string, without any special or UTF8 characters.
950
    * Note: empty strings and NULL are considered ASCII.
951
    * Any variable types other than strings are not.
952
    * 
953
    * @param mixed $string
954
    * @return boolean
955
    */
956
    public static function isStringASCII($string) : bool
957
    {
958
        if($string === '' || $string === NULL) {
959
            return true;
960
        }
961
        
962
        if(!is_string($string)) {
963
            return false;
964
        }
965
        
966
        return !preg_match('/[^\x00-\x7F]/', $string);
967
    }
968
    
969
    public static function highlight_url($url)
970
    {
971
        $url = htmlspecialchars($url);
972
        $url = str_replace(
973
            array('/', '='), 
974
            array('/<wbr>', '=<wbr>'), 
975
            $url
976
        );
977
        return $url;
978
    }
979
980
   /**
981
    * Calculates a percentage match of the source string with the target string.
982
    * 
983
    * Options are:
984
    * 
985
    * - maxLevenshtein, default: 10
986
    *   Any levenshtein results above this value are ignored.
987
    *   
988
    * - precision, default: 1
989
    *   The precision of the percentage float value
990
    * 
991
    * @param string $source
992
    * @param string $target
993
    * @param array $options
994
    * @return float
995
    */
996
    public static function matchString($source, $target, $options=array())
997
    {
998
        $defaults = array(
999
            'maxLevenshtein' => 10,
1000
            'precision' => 1
1001
        );
1002
        
1003
        $options = array_merge($defaults, $options);
1004
        
1005
        // avoid doing this via levenshtein
1006
        if($source == $target) {
1007
            return 100;
1008
        }
1009
        
1010
        $diff = levenshtein($source, $target);
1011
        if($diff > $options['maxLevenshtein']) {
1012
            return 0;
1013
        }
1014
        
1015
        $percent = $diff * 100 / ($options['maxLevenshtein'] + 1);
1016
        return round(100 - $percent, $options['precision']);
1017
    }
1018
    
1019
   /**
1020
    * Converts a date interval to a human readable string with
1021
    * all necessary time components, e.g. "1 year, 2 months and 4 days".
1022
    * 
1023
    * @param \DateInterval $interval
1024
    * @return string
1025
    * @see ConvertHelper_IntervalConverter
1026
    */
1027
    public static function interval2string(\DateInterval $interval) : string
1028
    {
1029
        $converter = new ConvertHelper_IntervalConverter();
1030
        return $converter->toString($interval);
1031
    }
1032
    
1033
    const INTERVAL_DAYS = 'days';
1034
    
1035
    const INTERVAL_HOURS = 'hours';
1036
    
1037
    const INTERVAL_MINUTES = 'minutes';
1038
    
1039
    const INTERVAL_SECONDS = 'seconds';
1040
    
1041
   /**
1042
    * Converts an interval to its total amount of days.
1043
    * @param \DateInterval $interval
1044
    * @return int
1045
    */
1046
    public static function interval2days(\DateInterval $interval) : int
1047
    {
1048
        return self::interval2total($interval, self::INTERVAL_DAYS);
1049
    }
1050
1051
   /**
1052
    * Converts an interval to its total amount of hours.
1053
    * @param \DateInterval $interval
1054
    * @return int
1055
    */
1056
    public static function interval2hours(\DateInterval $interval) : int
1057
    {
1058
        return self::interval2total($interval, self::INTERVAL_HOURS);
1059
    }
1060
    
1061
   /**
1062
    * Converts an interval to its total amount of minutes. 
1063
    * @param \DateInterval $interval
1064
    * @return int
1065
    */
1066
    public static function interval2minutes(\DateInterval $interval) : int
1067
    {
1068
        return self::interval2total($interval, self::INTERVAL_MINUTES);
1069
    }
1070
    
1071
   /**
1072
    * Converts an interval to its total amount of seconds.
1073
    * @param \DateInterval $interval
1074
    * @return int
1075
    */    
1076
    public static function interval2seconds(\DateInterval $interval) : int
1077
    {
1078
        return self::interval2total($interval, self::INTERVAL_SECONDS);
1079
    }
1080
    
1081
   /**
1082
    * Calculates the total amount of days / hours / minutes or seconds
1083
    * of a date interval object (depending in the specified units), and 
1084
    * returns the total amount.
1085
    * 
1086
    * @param \DateInterval $interval
1087
    * @param string $unit What total value to calculate.
1088
    * @return integer
1089
    * 
1090
    * @see ConvertHelper::INTERVAL_SECONDS
1091
    * @see ConvertHelper::INTERVAL_MINUTES
1092
    * @see ConvertHelper::INTERVAL_HOURS
1093
    * @see ConvertHelper::INTERVAL_DAYS
1094
    */
1095
    public static function interval2total(\DateInterval $interval, $unit=self::INTERVAL_SECONDS) : int
1096
    {
1097
        $total = (int)$interval->format('%a');
1098
        if ($unit == self::INTERVAL_DAYS) {
1099
            return $total;
1100
        }
1101
        
1102
        $total = ($total * 24) + ((int)$interval->h );
1103
        if ($unit == self::INTERVAL_HOURS) {
1104
            return $total;
1105
        }
1106
    
1107
        $total = ($total * 60) + ((int)$interval->i );
1108
        if ($unit == self::INTERVAL_MINUTES) {
1109
            return $total;
1110
        }
1111
1112
        $total = ($total * 60) + ((int)$interval->s );
1113
        if ($unit == self::INTERVAL_SECONDS) {
1114
            return $total;
1115
        }
1116
        
1117
        return 0;
1118
    }
1119
1120
    protected static $days;
1121
    
1122
    protected static $daysShort;
1123
1124
    protected static $daysInvariant = array(
1125
        'Monday',
1126
        'Tuesday',
1127
        'Wednesday',
1128
        'Thursday',
1129
        'Friday',
1130
        'Saturday',
1131
        'Sunday'
1132
    );
1133
    
1134
   /**
1135
    * Converts a date to the corresponding day name.
1136
    * 
1137
    * @param \DateTime $date
1138
    * @param bool $short
1139
    * @return string|NULL
1140
    */
1141
    public static function date2dayName(\DateTime $date, bool $short=false)
1142
    {
1143
        $day = $date->format('l');
1144
        $invariant = self::getDayNamesInvariant();
1145
        
1146
        $idx = array_search($day, $invariant);
1147
        if($idx !== false) {
1148
            $localized = self::getDayNames($short);
1149
            return $localized[$idx];
1150
        }
1151
        
1152
        return null;
1153
    }
1154
    
1155
   /**
1156
    * Retrieves a list of english day names.
1157
    * @return string[]
1158
    */
1159
    public static function getDayNamesInvariant()
1160
    {
1161
        return self::$daysInvariant;
1162
    }
1163
    
1164
   /**
1165
    * Retrieves the day names list for the current locale.
1166
    * 
1167
    * @param bool $short
1168
    * @return array
1169
    */
1170
    public static function getDayNames(bool $short=false) : array
1171
    {
1172
        if($short) {
1173
            if(!isset(self::$daysShort)) {
1174
                self::$daysShort = array(
1175
                    t('Mon'),
1176
                    t('Tue'),
1177
                    t('Wed'),
1178
                    t('Thu'),
1179
                    t('Fri'),
1180
                    t('Sat'),
1181
                    t('Sun')
1182
                );
1183
            }
1184
            
1185
            return self::$daysShort;
1186
        }
1187
        
1188
        if(!isset(self::$days)) {
1189
            self::$days = array(
1190
                t('Monday'),
1191
                t('Tuesday'),
1192
                t('Wednesday'),
1193
                t('Thursday'),
1194
                t('Friday'),
1195
                t('Saturday'),
1196
                t('Sunday')
1197
            );
1198
        }
1199
        
1200
        return self::$days;
1201
    }
1202
1203
    /**
1204
     * Implodes an array with a separator character, and the last item with "add".
1205
     * 
1206
     * @param array $list The indexed array with items to implode.
1207
     * @param string $sep The separator character to use.
1208
     * @param string $conjunction The word to use as conjunction with the last item in the list. NOTE: include spaces as needed.
1209
     * @return string
1210
     */
1211
    public static function implodeWithAnd(array $list, $sep = ', ', $conjunction = null)
1212
    {
1213
        if(empty($list)) {
1214
            return '';
1215
        }
1216
        
1217
        if(empty($conjunction)) {
1218
            $conjunction = t('and');
1219
        }
1220
        
1221
        $last = array_pop($list);
1222
        if($list) {
1223
            return implode($sep, $list) . $conjunction . ' ' . $last;
1224
        }
1225
        
1226
        return $last;
1227
    }
1228
    
1229
   /**
1230
    * Splits a string into an array of all characters it is composed of.
1231
    * Unicode character safe.
1232
    * 
1233
    * NOTE: Spaces and newlines (both \r and \n) are also considered single
1234
    * characters.
1235
    * 
1236
    * @param string $string
1237
    * @return array
1238
    */
1239
    public static function string2array(string $string) : array
1240
    {
1241
        $result = preg_split('//u', $string, null, PREG_SPLIT_NO_EMPTY);
1242
        if($result !== false) {
1243
            return $result;
1244
        }
1245
        
1246
        return array();
1247
    }
1248
    
1249
   /**
1250
    * Checks whether the specified string contains HTML code.
1251
    * 
1252
    * @param string $string
1253
    * @return boolean
1254
    */
1255
    public static function isStringHTML(string $string) : bool
1256
    {
1257
        if(preg_match('%<[a-z/][\s\S]*>%siU', $string)) {
1258
            return true;
1259
        }
1260
        
1261
        $decoded = html_entity_decode($string);
1262
        if($decoded !== $string) {
1263
            return true;
1264
        }
1265
        
1266
        return false;
1267
    }
1268
    
1269
   /**
1270
    * UTF8-safe wordwrap method: works like the regular wordwrap
1271
    * PHP function but compatible with UTF8. Otherwise the lengths
1272
    * are not calculated correctly.
1273
    * 
1274
    * @param string $str
1275
    * @param int $width
1276
    * @param string $break
1277
    * @param bool $cut
1278
    * @return string
1279
    */
1280
    public static function wordwrap(string $str, int $width = 75, string $break = "\n", bool $cut = false) : string 
1281
    {
1282
        $wrapper = new ConvertHelper_WordWrapper();
1283
        
1284
        return $wrapper
1285
        ->setLineWidth($width)
1286
        ->setBreakCharacter($break)
1287
        ->setCuttingEnabled($cut)
1288
        ->wrapText($str);
1289
    }
1290
    
1291
   /**
1292
    * Calculates the byte length of a string, taking into 
1293
    * account any unicode characters.
1294
    * 
1295
    * @param string $string
1296
    * @return int
1297
    * @see https://stackoverflow.com/a/9718273/2298192
1298
    */
1299
    public static function string2bytes($string)
1300
    {
1301
        return mb_strlen($string, '8bit');
1302
    }
1303
    
1304
   /**
1305
    * Creates a short, 8-character long hash for the specified string.
1306
    * 
1307
    * WARNING: Not cryptographically safe.
1308
    * 
1309
    * @param string $string
1310
    * @return string
1311
    */
1312
    public static function string2shortHash($string)
1313
    {
1314
        return hash('crc32', $string, false);
1315
    }
1316
    
1317
    public static function string2hash($string)
1318
    {
1319
        return md5($string);
1320
    }
1321
    
1322
    public static function callback2string($callback) : string
1323
    {
1324
        return parseVariable($callback)->toString();
1325
    }
1326
1327
    public static function exception2info(\Throwable $e) : ConvertHelper_ThrowableInfo
1328
    {
1329
        return self::throwable2info($e);
1330
    }
1331
    
1332
    public static function throwable2info(\Throwable $e) : ConvertHelper_ThrowableInfo
1333
    {
1334
        return ConvertHelper_ThrowableInfo::fromThrowable($e);
1335
    }
1336
    
1337
   /**
1338
    * Parses the specified query string like the native 
1339
    * function <code>parse_str</code>, without the key
1340
    * naming limitations.
1341
    * 
1342
    * Using parse_str, dots or spaces in key names are 
1343
    * replaced by underscores. This method keeps all names
1344
    * intact.
1345
    * 
1346
    * It still uses the parse_str implementation as it 
1347
    * is tested and tried, but fixes the parameter names
1348
    * after parsing, as needed.
1349
    * 
1350
    * @param string $queryString
1351
    * @return array
1352
    * @see ConvertHelper_QueryParser
1353
    */
1354
    public static function parseQueryString(string $queryString) : array
1355
    {
1356
        $parser = new ConvertHelper_QueryParser();
1357
        return $parser->parse($queryString);
1358
    }
1359
1360
   /**
1361
    * Searches for needle in the specified string, and returns a list
1362
    * of all occurrences, including the matched string. The matched 
1363
    * string is useful when doing a case insensitive search, as it 
1364
    * shows the exact matched case of needle.
1365
    *   
1366
    * @param string $needle
1367
    * @param string $haystack
1368
    * @param bool $caseInsensitive
1369
    * @return ConvertHelper_StringMatch[]
1370
    */
1371
    public static function findString(string $needle, string $haystack, bool $caseInsensitive=false)
1372
    {
1373
        if($needle === '') {
1374
            return array();
1375
        }
1376
        
1377
        $function = 'mb_strpos';
1378
        if($caseInsensitive) {
1379
            $function = 'mb_stripos';
1380
        }
1381
        
1382
        $pos = 0;
1383
        $positions = array();
1384
        $length = mb_strlen($needle);
1385
        
1386
        while( ($pos = $function($haystack, $needle, $pos)) !== false) 
1387
        {
1388
            $match = mb_substr($haystack, $pos, $length);
1389
            $positions[] = new ConvertHelper_StringMatch($pos, $match);
1390
            $pos += $length;
1391
        }
1392
        
1393
        return $positions;
1394
    }
1395
    
1396
   /**
1397
    * Like explode, but trims all entries, and removes 
1398
    * empty entries from the resulting array.
1399
    * 
1400
    * @param string $delimiter
1401
    * @param string $string
1402
    * @return string[]
1403
    */
1404
    public static function explodeTrim(string $delimiter, string $string) : array
1405
    {
1406
        if(empty($string) || empty($delimiter)) {
1407
            return array();
1408
        }
1409
        
1410
        $tokens = explode($delimiter, $string);
1411
        $tokens = array_map('trim', $tokens);
1412
        
1413
        $keep = array();
1414
        foreach($tokens as $token) {
1415
            if($token !== '') {
1416
                $keep[] = $token;
1417
            }
1418
        }
1419
        
1420
        return $keep;
1421
    }
1422
    
1423
    protected static $eolChars;
1424
1425
   /**
1426
    * Detects the most used end-of-line character in the subject string.
1427
    * 
1428
    * @param string $str The string to check.
1429
    * @return NULL|ConvertHelper_EOL The detected EOL instance, or NULL if none has been detected.
1430
    */
1431
    public static function detectEOLCharacter(string $subjectString) : ?ConvertHelper_EOL
1432
    {
1433
        if(empty($subjectString)) {
1434
            return null;
1435
        }
1436
        
1437
        if(!isset(self::$eolChars))
1438
        {
1439
            $cr = chr((int)hexdec('0d'));
1440
            $lf = chr((int)hexdec('0a'));
1441
            
1442
           self::$eolChars = array(
1443
               array(
1444
                   'char' => $cr.$lf,
1445
                   'type' => ConvertHelper_EOL::TYPE_CRLF,
1446
                   'description' => t('Carriage return followed by a line feed'),
1447
               ),
1448
               array(
1449
                   'char' => $lf.$cr,
1450
                   'type' => ConvertHelper_EOL::TYPE_LFCR,
1451
                   'description' => t('Line feed followed by a carriage return'),
1452
               ),
1453
               array(
1454
                  'char' => $lf,
1455
                  'type' => ConvertHelper_EOL::TYPE_LF,
1456
                  'description' => t('Line feed'),
1457
               ),
1458
               array(
1459
                  'char' => $cr,
1460
                  'type' => ConvertHelper_EOL::TYPE_CR,
1461
                  'description' => t('Carriage Return'),
1462
               ),
1463
            );
1464
        }
1465
        
1466
        $max = 0;
1467
        $results = array();
1468
        foreach(self::$eolChars as $def) 
1469
        {
1470
            $amount = substr_count($subjectString, $def['char']);
1471
            
1472
            if($amount > $max)
1473
            {
1474
                $max = $amount;
1475
                $results[] = $def;
1476
            }
1477
        }
1478
        
1479
        if(empty($results)) {
1480
            return null;
1481
        }
1482
        
1483
        return new ConvertHelper_EOL(
1484
            $results[0]['char'], 
1485
            $results[0]['type'],
1486
            $results[0]['description']
1487
        );
1488
    }
1489
1490
   /**
1491
    * Removes the specified keys from the target array,
1492
    * if they exist.
1493
    * 
1494
    * @param array $array
1495
    * @param array $keys
1496
    */
1497
    public static function arrayRemoveKeys(array &$array, array $keys) : void
1498
    {
1499
        foreach($keys as $key) 
1500
        {
1501
            if(array_key_exists($key, $array)) {
1502
                unset($array[$key]); 
1503
            }
1504
        }
1505
    }
1506
    
1507
   /**
1508
    * Checks if the specified variable is an integer or a string containing an integer.
1509
    * Accepts both positive and negative integers.
1510
    * 
1511
    * @param mixed $value
1512
    * @return bool
1513
    */
1514
    public static function isInteger($value) : bool
1515
    {
1516
        if(is_int($value)) {
1517
            return true;
1518
        }
1519
        
1520
        // booleans get converted to numbers, so they would
1521
        // actually match the regex.
1522
        if(is_bool($value)) {
1523
            return false;
1524
        }
1525
        
1526
        if(is_string($value) && $value !== '') {
1527
            return preg_match('/\A-?\d+\z/', $value) === 1;
1528
        }
1529
        
1530
        return false;    
1531
    }
1532
    
1533
   /**
1534
    * Converts an amount of seconds to a DateInterval object.
1535
    * 
1536
    * @param int $seconds
1537
    * @return \DateInterval
1538
    * @throws ConvertHelper_Exception If the date interval cannot be created.
1539
    * 
1540
    * @see ConvertHelper::ERROR_CANNOT_GET_DATE_DIFF
1541
    */
1542
    public static function seconds2interval(int $seconds) : \DateInterval
1543
    {
1544
        return ConvertHelper_DateInterval::fromSeconds($seconds)->getInterval();
1545
    }
1546
}
1547