1
2/**
3 * This file is part of the Phalcon.
4 *
5 * (c) Phalcon Team <team@phalcon.com>
6 *
7 * For the full copyright and license information, please view the LICENSE
8 * file that was distributed with this source code.
9 */
10
11namespace Phalcon\Helper;
12
13use RuntimeException;
14
15/**
16 * This class offers quick string functions throughout the framework
17 */
18class Str
19{
20    const RANDOM_ALNUM    = 0; // Only alpha numeric characters [a-zA-Z0-9]
21    const RANDOM_ALPHA    = 1; // Only alphabetical characters [azAZ]
22    const RANDOM_DISTINCT = 5; // Only alpha numeric uppercase characters exclude similar sharacters [2345679ACDEFHJKLMNPRSTUVWXYZ]
23    const RANDOM_HEXDEC   = 2; // Only hexadecimal characters [0-9a-f]
24    const RANDOM_NOZERO   = 4; // Only numbers without 0 [1-9]
25    const RANDOM_NUMERIC  = 3; // Only numbers [0-9]
26
27    /**
28     * Converts strings to camelize style
29     *
30     * ```php
31     * use Phalcon\Helper\Str;
32     *
33     * echo Str::camelize("coco_bongo");            // CocoBongo
34     * echo Str::camelize("co_co-bon_go", "-");     // Co_coBon_go
35     * echo Str::camelize("co_co-bon_go", "_-");    // CoCoBonGo
36     * ```
37     *
38     * @param string $text
39     * @param mixed  $delimiter
40     *
41     * @return string
42     */
43    final public static function camelize(string! text, var delimiter = null) -> string
44    {
45        return text->camelize(delimiter);
46    }
47
48    /**
49     * Concatenates strings using the separator only once without duplication in
50     * places concatenation
51     *
52     * ```php
53     * $str = Phalcon\Helper\Str::concat(
54     *     "/",
55     *     "/tmp/",
56     *     "/folder_1/",
57     *     "/folder_2",
58     *     "folder_3/"
59     * );
60     *
61     * echo $str;   // /tmp/folder_1/folder_2/folder_3/
62     * ```
63     *
64     * @param string separator
65     * @param string a
66     * @param string b
67     * @param string ...N
68     *
69     * @return string
70     */
71    final public static function concat() -> string
72    {
73        var argument, arguments, data, first, last, prefix, delimiter, suffix;
74
75        let arguments = func_get_args();
76
77        if unlikely count(arguments) < 3 {
78            throw new Exception("concat needs at least three parameters");
79        }
80
81        let delimiter = Arr::first(arguments),
82            arguments = Arr::sliceRight(arguments),
83            first     = Arr::first(arguments),
84            last      = Arr::last(arguments),
85            prefix    = "",
86            suffix    = "",
87            data      = [];
88
89        if self::startsWith(first, delimiter) {
90            let prefix = delimiter;
91        }
92
93        if self::endsWith(last, delimiter) {
94            let suffix = delimiter;
95        }
96
97
98        for argument in arguments {
99            let data[] = trim(argument, delimiter);
100        }
101
102        return prefix . implode(delimiter, data) . suffix;
103    }
104
105    /**
106     * Returns number of vowels in provided string. Uses a regular expression
107     * to count the number of vowels (A, E, I, O, U) in a string.
108     *
109     * @param string $string
110     *
111     * @return int
112     */
113    final public static function countVowels(string! text) -> int
114    {
115        var matches;
116
117        preg_match_all("/[aeiou]/i", text, matches);
118
119        return count(matches[0]);
120    }
121
122    /**
123     * Decapitalizes the first letter of the string and then adds it with rest
124     * of the string. Omit the upperRest parameter to keep the rest of the
125     * string intact, or set it to true to convert to uppercase.
126     *
127     * @param string $string
128     * @param bool   $upperRest
129     * @param string $encoding
130     *
131     * @return string
132     */
133    final public static function decapitalize(
134        string! text,
135        bool upperRest = false,
136        string! encoding = "UTF-8"
137    ) -> string
138    {
139        var substr, suffix;
140
141        if function_exists("mb_substr") {
142            let substr = mb_substr(text, 1);
143        } else {
144            let substr = substr(text, 1);
145        }
146
147        if upperRest {
148            if function_exists("mb_strtoupper") {
149                let suffix = mb_strtoupper(substr, encoding);
150            } else {
151                let suffix = substr->upper();
152            }
153        } else {
154            let suffix = substr;
155        }
156
157        if function_exists("mb_strtolower") {
158            return mb_strtolower(mb_substr(text, 0, 1), encoding) . suffix;
159        } else {
160            return strtolower(substr(text, 0, 1)) . suffix;
161        }
162    }
163
164    /**
165     * Removes a number from a string or decrements that number if it already is defined.
166     * defined
167     *
168     * ```php
169     * use Phalcon\Helper\Str;
170     *
171     * echo Str::decrement("a_1");    // "a"
172     * echo Str::decrement("a_2");  // "a_1"
173     * ```
174     *
175     * @param string $text
176     * @param string $separator
177     *
178     * @return string
179     */
180    final public static function decrement(string text, string separator = "_") -> string
181    {
182        var parts, number;
183
184        let parts = explode(separator, text);
185
186        if fetch number, parts[1] {
187            let number--;
188            if (number <= 0) {
189                return parts[0];
190            }
191        }
192
193        return parts[0] . separator. number;
194    }
195
196    /**
197     * Accepts a file name (without extension) and returns a calculated
198     * directory structure with the filename in the end
199     *
200     * ```php
201     * use Phalcon\Helper\Str;
202     *
203     * echo Str::dirFromFile("file1234.jpg"); // fi/le/12/
204     * ```
205     *
206     * @param string $file
207     *
208     * @return string
209     */
210    final public static function dirFromFile(string! file) -> string
211    {
212        var name, start;
213
214        let name  = pathinfo(file, PATHINFO_FILENAME),
215            start = substr(name, 0, -2);
216
217         if !empty start {
218            let start = str_replace(".", "-", start);
219        }
220
221        if !start {
222            let start = substr(name, 0, 1);
223        }
224
225        return implode("/", str_split(start, 2)) . "/";
226    }
227
228    /**
229     * Accepts a directory name and ensures that it ends with
230     * DIRECTORY_SEPARATOR
231     *
232     * ```php
233     * use Phalcon\Helper\Str;
234     *
235     * echo Str::dirSeparator("/home/phalcon"); // /home/phalcon/
236     * ```
237     *
238     * @param string $directory
239     *
240     * @return string
241     */
242    final public static function dirSeparator(string! directory) -> string
243    {
244        return rtrim(directory, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR;
245    }
246
247    /**
248     * Generates random text in accordance with the template
249     *
250     * ```php
251     * use Phalcon\Helper\Str;
252     *
253     * // Hi my name is a Bob
254     * echo Str::dynamic("{Hi|Hello}, my name is a {Bob|Mark|Jon}!");
255     *
256     * // Hi my name is a Jon
257     * echo Str::dynamic("{Hi|Hello}, my name is a {Bob|Mark|Jon}!");
258     *
259     * // Hello my name is a Bob
260     * echo Str::dynamic("{Hi|Hello}, my name is a {Bob|Mark|Jon}!");
261     *
262     * // Hello my name is a Zyxep
263     * echo Str::dynamic(
264     *     "[Hi/Hello], my name is a [Zyxep/Mark]!",
265     *     "[", "]",
266     *     "/"
267     * );
268     * ```
269     *
270     * @param string $text
271     * @param string $leftDelimiter
272     * @param string $rightDelimiter
273     * @param string $separator
274     *
275     * @return string
276     */
277    final public static function dynamic(
278        string! text,
279        string! leftDelimiter = "{",
280        string! rightDelimiter = "}",
281        string! separator = "|"
282    ) -> string
283    {
284        var ldS, rdS, matches, match, words, word, sub;
285        string pattern;
286
287        if unlikely substr_count(text, leftDelimiter) !== substr_count(text, rightDelimiter) {
288            throw new RuntimeException(
289                "Syntax error in string \"" . text . "\""
290            );
291        }
292
293        let ldS = preg_quote(leftDelimiter),
294            rdS = preg_quote(rightDelimiter),
295            pattern = "/" . ldS . "([^" . ldS . rdS . "]+)" . rdS . "/",
296            matches = [];
297
298        if !preg_match_all(pattern, text, matches, 2) {
299            return text;
300        }
301
302        if typeof matches == "array" {
303            for match in matches {
304                if !isset match[0] || !isset match[1] {
305                    continue;
306                }
307
308                let words = explode(separator, match[1]),
309                    word  = words[array_rand(words)],
310                    sub   = preg_quote(match[0], separator),
311                    text  = preg_replace("/" . sub . "/", word, text, 1);
312            }
313        }
314
315        return text;
316    }
317
318    /**
319     * Check if a string ends with a given string
320     *
321     * ```php
322     * use Phalcon\Helper\Str;
323     *
324     * echo Str::endsWith("Hello", "llo");          // true
325     * echo Str::endsWith("Hello", "LLO", false);   // false
326     * echo Str::endsWith("Hello", "LLO");          // true
327     * ```
328     *
329     * @param string $text
330     * @param string $end
331     * @param bool   $ignoreCase
332     *
333     * @return bool
334     */
335    final public static function endsWith(string text, string end, bool ignoreCase = true) -> bool
336    {
337        return ends_with(text, end, ignoreCase);
338    }
339
340    /**
341     * Returns the first string there is between the strings from the
342     * parameter start and end.
343     *
344     * @param string $text
345     * @param string $start
346     * @param string $end
347     *
348     * @return string
349     */
350    final public static function firstBetween(
351        string! text,
352        string! start,
353        string! end
354    ) -> string
355    {
356        if function_exists("mb_strstr") {
357            let text = (string) mb_strstr(mb_strstr(text, start), end, true);
358        } else {
359            let text = (string) strstr(strstr(text, start), end, true);
360        }
361
362        return trim(
363            text,
364            start . end
365        );
366    }
367
368    /**
369     * Changes a text to a URL friendly one
370     *
371     * @param string     $text
372     * @param string     $separator
373     * @param bool       $lowercase
374     * @param mixed|null $replace
375     *
376     * @return string
377     * @throws Exception
378     */
379    final public static function friendly(
380        string! text,
381        string! separator = "-",
382        bool lowercase = true,
383        var replace = null
384    ) -> string {
385        var friendly, matrix, search;
386
387        let matrix = [
388                "Š"    : "S",     "š"    : "s", "Đ"    : "Dj", "Ð"    : "Dj",
389                "đ"    : "dj",    "Ž"    : "Z", "ž"    : "z",  "Č"    : "C",
390                "č"    : "c",     "Ć"    : "C", "ć"    : "c",  "À"    : "A",
391                "Á"    : "A",     "Â"    : "A", "Ã"    : "A",  "Ä"    : "A",
392                "Å"    : "A",     "Æ"    : "A", "Ç"    : "C",  "È"    : "E",
393                "É"    : "E",     "Ê"    : "E", "Ë"    : "E",  "Ì"    : "I",
394                "Í"    : "I",     "Î"    : "I", "Ï"    : "I",  "Ñ"    : "N",
395                "Ò"    : "O",     "Ó"    : "O", "Ô"    : "O",  "Õ"    : "O",
396                "Ö"    : "O",     "Ø"    : "O", "Ù"    : "U",  "Ú"    : "U",
397                "Û"    : "U",     "Ü"    : "U", "Ý"    : "Y",  "Þ"    : "B",
398                "ß"    : "Ss",    "à"    : "a", "á"    : "a",  "â"    : "a",
399                "ã"    : "a",     "ä"    : "a", "å"    : "a",  "æ"    : "a",
400                "ç"    : "c",     "è"    : "e", "é"    : "e",  "ê"    : "e",
401                "ë"    : "e",     "ì"    : "i", "í"    : "i",  "î"    : "i",
402                "ï"    : "i",     "ð"    : "o", "ñ"    : "n",  "ò"    : "o",
403                "ó"    : "o",     "ô"    : "o", "õ"    : "o",  "ö"    : "o",
404                "ø"    : "o",     "ù"    : "u", "ú"    : "u",  "û"    : "u",
405                "ý"    : "y",     "ý"    : "y", "þ"    : "b",  "ÿ"    : "y",
406                "Ŕ"    : "R",     "ŕ"    : "r", "ē"    : "e",  "'"    : "",
407                "&"    : " and ", "\r\n" : " ", "\n"   : " "
408        ];
409
410        if replace {
411            if unlikely (typeof replace != "array" && typeof replace != "string") {
412                throw new Exception(
413                    "Parameter replace must be an array or a string"
414                );
415            }
416
417            if typeof replace !== "array" {
418                let replace = [replace];
419            }
420
421            for search in replace {
422                let matrix[search] = " ";
423            }
424        }
425
426        let text     = str_replace(array_keys(matrix), array_values(matrix), text),
427            friendly = preg_replace(
428                "/[^a-zA-Z0-9\\/_|+ -]/",
429                "",
430                text
431            );
432
433        if lowercase {
434            let friendly = strtolower(friendly);
435        }
436
437        let friendly = preg_replace("/[\\/_|+ -]+/", separator, friendly),
438            friendly = trim(friendly, separator);
439
440        return friendly;
441    }
442
443    /**
444     * Makes an underscored or dashed phrase human-readable
445     *
446     * ```php
447     * use Phalcon\Helper\Str;
448     *
449     * echo Str::humanize("start-a-horse"); // "start a horse"
450     * echo Str::humanize("five_cats");     // "five cats"
451     * ```
452     *
453     * @param string $text
454     *
455     * @return string
456     */
457    final public static function humanize(string! text) -> string
458    {
459        return preg_replace("#[_-]+#", " ", trim(text));
460    }
461
462    /**
463     * Lets you determine whether or not a string includes another string.
464     *
465     * @param string $needle
466     * @param string $haystack
467     *
468     * @return bool
469     */
470    final public static function includes(string! needle, string! haystack) -> bool
471    {
472        if function_exists("mb_strpos") {
473            return false !== mb_strpos(haystack, needle);
474        } else {
475            return false !== strpos(haystack, needle);
476        }
477    }
478
479    /**
480     * Adds a number to a string or increment that number if it already is
481     * defined
482     *
483     * ```php
484     * use Phalcon\Helper\Str;
485     *
486     * echo Str::increment("a");    // "a_1"
487     * echo Str::increment("a_1");  // "a_2"
488     * ```
489     *
490     * @param string $text
491     * @param string $separator
492     *
493     * @return string
494     */
495    final public static function increment(string text, string separator = "_") -> string
496    {
497        var parts, number;
498
499        let parts = explode(separator, text);
500
501        if fetch number, parts[1] {
502            let number++;
503        } else {
504            let number = 1;
505        }
506
507        return parts[0] . separator. number;
508    }
509
510    /**
511     * Compare two strings and returns true if both strings are anagram,
512     * false otherwise.
513     *
514     * @param string $first
515     * @param string $second
516     *
517     * @return bool
518     */
519    final public static function isAnagram(string! first, string! second) -> bool
520    {
521        return count_chars(first, 1) === count_chars(second, 1);
522    }
523
524    /**
525     * Returns true if the given string is lower case, false otherwise.
526     *
527     * @param string $text
528     * @param string $encoding
529     *
530     * @return bool
531     */
532    final public static function isLower(string! text, string! encoding = "UTF-8") ->  bool
533    {
534        if function_exists("mb_strtolower") {
535            return text === mb_strtolower(text, encoding);
536        } else {
537            return text === text->lower();
538        }
539    }
540
541    /**
542     * Returns true if the given string is a palindrome, false otherwise.
543     *
544     * @param string $text
545     *
546     * @return bool
547     */
548    final public static function isPalindrome(string! text) -> bool
549    {
550        return strrev(text) === text;
551    }
552
553    /**
554     * Returns true if the given string is upper case, false otherwise.
555     *
556     * @param string text
557     * @param string encoding
558     *
559     * @return bool
560     */
561    final public static function isUpper(string! text, string! encoding = "UTF-8") -> bool
562    {
563        if function_exists("mb_strtoupper") {
564            return text === mb_strtoupper(text, encoding);
565        } else {
566            return text === text->upper();
567        }
568    }
569
570    /**
571     * Lowercases a string, this function makes use of the mbstring extension if
572     * available
573     *
574     * ```php
575     * echo Phalcon\Helper\Str::lower("HELLO"); // hello
576     * ```
577     *
578     * @param string $text
579     * @param string $encoding
580     *
581     * @return string
582     */
583    final public static function lower(string! text, string! encoding = "UTF-8") -> string
584    {
585        /**
586         * 'lower' checks for the mbstring extension to make a correct lowercase
587         * transformation
588         */
589        if function_exists("mb_strtolower") {
590            return mb_strtolower(text, encoding);
591        }
592
593        return text->lower();
594    }
595
596    /**
597     * Generates a random string based on the given type. Type is one of the
598     * RANDOM_* constants
599     *
600     * ```php
601     * use Phalcon\Helper\Str;
602     *
603     * echo Str::random(Str::RANDOM_ALNUM); // "aloiwkqz"
604     * ```
605     *
606     * @param int $type
607     * @param int $length
608     *
609     * @return string
610     */
611    final public static function random(int type = 0, long length = 8) -> string
612    {
613        var pool;
614        int end;
615        string text = "";
616
617        switch type {
618
619            case Str::RANDOM_ALPHA:
620                let pool = array_merge(range("a", "z"), range("A", "Z"));
621                break;
622
623            case Str::RANDOM_HEXDEC:
624                let pool = array_merge(range(0, 9), range("a", "f"));
625                break;
626
627            case Str::RANDOM_NUMERIC:
628                let pool = range(0, 9);
629                break;
630
631            case Str::RANDOM_NOZERO:
632                let pool = range(1, 9);
633                break;
634
635            case Str::RANDOM_DISTINCT:
636                let pool = str_split("2345679ACDEFHJKLMNPRSTUVWXYZ");
637                break;
638
639            default:
640                // RANDOM_ALNUM
641                let pool = array_merge(
642                    range(0, 9),
643                    range("a", "z"),
644                    range("A", "Z")
645                );
646
647                break;
648        }
649
650        let end = count(pool) - 1;
651
652        while strlen(text) < length {
653            let text .= pool[mt_rand(0, end)];
654        }
655
656        return text;
657    }
658
659    /**
660     * Reduces multiple slashes in a string to single slashes
661     *
662     * ```php
663     * // foo/bar/baz
664     * echo Phalcon\Helper\Str::reduceSlashes("foo//bar/baz");
665     *
666     * // http://foo.bar/baz/buz
667     * echo Phalcon\Helper\Str::reduceSlashes("http://foo.bar///baz/buz");
668     * ```
669     *
670     * @param string $text
671     *
672     * @return string
673     */
674    final public static function reduceSlashes(string! text) -> string
675    {
676        return preg_replace("#(?<!:)//+#", "/", text);
677    }
678
679    /**
680     * Check if a string starts with a given string
681     *
682     * ```php
683     * use Phalcon\Helper\Str;
684     *
685     * echo Str::startsWith("Hello", "He");         // true
686     * echo Str::startsWith("Hello", "he", false);  // false
687     * echo Str::startsWith("Hello", "he");         // true
688     * ```
689     *
690     * @param string $text
691     * @param string $start
692     * @param bool   $ignoreCase
693     *
694     * @return bool
695     */
696    final public static function startsWith(string! text, string! start, bool ignoreCase = true) -> bool
697    {
698        return starts_with(text, start, ignoreCase);
699    }
700
701    /**
702     * Uncamelize strings which are camelized
703     *
704     * ```php
705     * use Phalcon\Helper\Str;
706     *
707     * echo Str::uncamelize("CocoBongo");       // coco_bongo
708     * echo Str::uncamelize("CocoBongo", "-");  // coco-bongo
709     * ```
710     *
711     * @param string $text
712     * @param mixed  $delimiter
713     *
714     * @return string
715     */
716    final public static function uncamelize(string! text, var delimiter = null) -> string
717    {
718        return text->uncamelize(delimiter);
719    }
720
721    /**
722     * Makes a phrase underscored instead of spaced
723     *
724     * ```php
725     * use Phalcon\Helper\Str;
726     *
727     * echo Str::underscore("look behind");     // "look_behind"
728     * echo Str::underscore("Awesome Phalcon"); // "Awesome_Phalcon"
729     * ```
730     *
731     * @param string $text
732     *
733     * @return string
734     */
735    final public static function underscore(string! text) -> string
736    {
737        return preg_replace("#\s+#", "_", trim(text));
738    }
739
740    /**
741     * Uppercases a string, this function makes use of the mbstring extension if
742     * available
743     *
744     * ```php
745     * echo Phalcon\Helper\Str::upper("hello"); // HELLO
746     * ```
747     *
748     * @param string $text
749     * @param string $encoding
750     *
751     * @return string
752     */
753    final public static function upper(string! text, string! encoding = "UTF-8") -> string
754    {
755        /**
756         * 'upper' checks for the mbstring extension to make a correct lowercase
757         * transformation
758         */
759        if function_exists("mb_strtoupper") {
760            return mb_strtoupper(text, encoding);
761        }
762
763        return text->upper();
764    }
765}
766