Completed
Pull Request — master (#76)
by Luke
02:26
created

functions.php ➔ get_range_start_end()   B

Complexity

Conditions 5
Paths 9

Size

Total Lines 21
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 15
CRAP Score 5.0061

Importance

Changes 0
Metric Value
cc 5
eloc 14
nc 9
nop 2
dl 0
loc 21
ccs 15
cts 16
cp 0.9375
crap 5.0061
rs 8.7624
c 0
b 0
f 0
1
<?php
2
/*
3
 * Nozavroni/Collections
4
 * Just another collections library for PHP5.6+.
5
 *
6
 * @copyright Copyright (c) 2016 Luke Visinoni <[email protected]>
7
 * @author    Luke Visinoni <[email protected]>
8
 * @license   https://github.com/nozavroni/collections/blob/master/LICENSE The MIT License (MIT)
9
 */
10
namespace Noz;
11
12
use Closure;
13
use Illuminate\Support\Str;
14
use InvalidArgumentException;
15
use Iterator;
16
use Noz\Immutable\Collection;
0 ignored issues
show
Bug introduced by
This use statement conflicts with another class in this namespace, Noz\Collection.

Let’s assume that you have a directory layout like this:

.
|-- OtherDir
|   |-- Bar.php
|   `-- Foo.php
`-- SomeDir
    `-- Foo.php

and let’s assume the following content of Bar.php:

// Bar.php
namespace OtherDir;

use SomeDir\Foo; // This now conflicts the class OtherDir\Foo

If both files OtherDir/Foo.php and SomeDir/Foo.php are loaded in the same runtime, you will see a PHP error such as the following:

PHP Fatal error:  Cannot use SomeDir\Foo as Foo because the name is already in use in OtherDir/Foo.php

However, as OtherDir/Foo.php does not necessarily have to be loaded and the error is only triggered if it is loaded before OtherDir/Bar.php, this problem might go unnoticed for a while. In order to prevent this error from surfacing, you must import the namespace with a different alias:

// Bar.php
namespace OtherDir;

use SomeDir\Foo as SomeDirFoo; // There is no conflict anymore.
Loading history...
17
use Noz\Immutable\Sequence;
18
use Noz\Contracts\CollectionInterface;
19
use RuntimeException;
20
use Serializable;
21
use Traversable;
22
23
use Symfony\Component\Serializer\Serializer;
24
use Symfony\Component\Serializer\Encoder\XmlEncoder;
25
use Symfony\Component\Serializer\Encoder\JsonEncoder;
26
use Symfony\Component\Serializer\Encoder\YamlEncoder;
27
use Symfony\Component\Serializer\Encoder\CsvEncoder;
28
use Symfony\Component\Serializer\Normalizer\ObjectNormalizer;
29
30
/**
31
 * Collection factory.
32
 *
33
 * Simply an alias to (new Collection($in)). Allows for a little more concise and
34
 * simpler instantiation of a collection. Also I plan to eventually support
35
 * additional input types that will make this function more flexible and forgiving
36
 * than simply instantiating a Collection object, but for now the two are identical.
37
 *
38
 * @param array|Iterator $data Either an array or an iterator of data
39
 *
40
 * @return CollectionInterface
0 ignored issues
show
Documentation introduced by
Should the return type not be Illuminate\Support\Collection?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
41
 */
42
function collect($data = null)
43
{
44 9
    return Collection::factory($data);
45
}
46
47
/**
48
 * Invoke a callable and return result.
49
 *
50
 * Pass in a callable followed by whatever arguments you want passed to
51
 * it and this function will invoke it with your arguments and return
52
 * the result.
53
 *
54
 * @param callable $callback The callback function to invoke
55
 * @param array ...$args     The args to pass to your callable
56
 *
57
 * @return mixed The result of your invoked callable
58
 */
59
function invoke(callable $callback, ...$args)
60
{
61 7
    return call_user_func($callback, ...$args);
62
}
63
64
/**
65
 * Underscore function.
66
67
 * This function is meant to work sort of like jQuery's "$()". It is a contextual catch-all type function. It works
68
 * as a short-hand alias for invoke, collect, and with.
69
70
 * @param callable|mixed    $in
71
 * @param mixed ...         $_
72
 *
73
 * @return mixed|CollectionInterface
74
 */
75
function _($in, ...$args)
76
{
77 5
    if (is_callable($in)) {
78 3
        return invoke($in, ...$args);
79
    }
80 2
    if (is_traversable($in)) {
81 1
        return collect($in);
82
    }
83 1
    return $in;
84
}
85
86
/**
87
 * Determine if data is traversable.
88
 *
89
 * Pass in any variable and this function will tell you whether or not it
90
 * is traversable. Basically this just means that it is either an array or an iterator.
91
 * This function was written simply because I was tired of if statements that checked
92
 * whether a variable was an array or a descendant of \Iterator. So I wrote this guy.
93
 *
94
 * @param mixed $data The variable to determine traversability
95
 *
96
 * @return bool True if $input is an array or an Iterator
97
 */
98
function is_traversable($data)
99
{
100 12
    return is_array($data) || $data instanceof Traversable;
101
}
102
103
/**
104
 * Can data be converted to an array?
105
 *
106
 * @param mixed $data The data to check
107
 *
108
 * @return bool
109
 */
110
function is_arrayable($data)
111
{
112 5
    if (!is_array($data)) {
113 5
        if (is_object($data)) {
114
            return (
115 3
                method_exists($data, 'toArray') ||
116
                $data instanceof Traversable
117 3
            );
118
        }
119 4
        return false;
120
    }
121 2
    return true;
122
}
123
124
/**
125
 * Convert any traversable to an array.
126
 *
127
 * This is useful because it will take any traversable data structure (including an array) and return an array.
128
 * It can optionally do this recursively.
129
 *
130
 * @param array|Traversable $data      Traversable data
131
 * @param bool              $recursive Apply recursively?
132
 *
133
 * @return array
134
 */
135
function traversable_to_array($data, $recursive = true)
136
{
137 2
    $arr = [];
138 2
    foreach ($data as $key => $val) {
139 2
        if ($recursive) {
140 2
            if (is_traversable($val)) {
141
                $val = traversable_to_array($val);
142
            }
143 2
        }
144 2
        $arr[$key] = $val;
145 2
    }
146 2
    return $arr;
147
}
148
149
/**
150
 * Convert data to an array.
151
 *
152
 * Accepts any kind of data and converts it to an array. If strict mode is on, only data that returns true from
153
 * is_arrayable() will be converted to an array. Anything else will cause an InvalidArgumentException to be thrown.
154
155
 * @param mixed $data   Data to convert to array
156
 * @param bool  $strict Whether to use strict mode
157
158
 * @return array
159
 *
160
 * @throws InvalidArgumentException
161
 */
162
function to_array($data, $strict = true)
163
{
164 2
    if (is_arrayable($data)) {
165 1
        if (!is_array($data)) {
166
            // this is what makes toArray() work recursively
167
            // it must stay right where it is do not move it
168 1
            if (method_exists($data, 'toArray')) {
169 1
                $data = $data->toArray();
170 1
            }
171 1
            if ($data instanceof Traversable) {
172 1
                $data = traversable_to_array($data);
173 1
            }
174 1
        }
175 1
        return $data;
176
    }
177 2
    if ($strict) {
178 1
        throw new InvalidArgumentException(sprintf(
179 1
            'Invalid argument for "%s". Cannot convert "%s" to an array.',
180 1
            __FUNCTION__,
181 1
            typeof($data)
182 1
        ));
183
    }
184 1
    if (is_object($data)) {
185 1
        $values = [];
186 1
        foreach ($data as $key => $val) {
187 1
            $values[$key] = $val;
188 1
        }
189 1
        return $values;
190
    }
191 1
    if (is_null($data)) {
192 1
        return [];
193
    }
194 1
    return [$data];
195
}
196
197
/**
198
 * Get object count.
199
 *
200
 * This function will accept any data and attempt to return its count.
201
 *
202
 * @param mixed $data The data to count
203
 *
204
 * @return int
205
 */
206
function get_count($data)
207
{
208 8
    if (is_null($data)) {
209 5
        return $data;
210
    }
211
212 5
    if (is_array($data)) {
213 1
        return count($data);
214
    }
215
216 5
    if (is_numeric($data)) {
217 3
        return (int) $data;
218
    }
219
220 5
    if ($data === '') {
221 1
        return 0;
222
    }
223
224 5
    if (is_object($data)) {
225 4
        if (method_exists($data, 'count')) {
226 2
            $count = $data->count();
227 4
        } elseif (method_exists($data, '__toString')) {
228 3
            $count = (int) $data->__toString();
229 3
        } elseif (is_traversable($data)) {
230 1
            $count = 0;
231 1
            foreach ($data as $item) {
232 1
                $count++;
233 1
            }
234 1
        }
235 4
        if (isset($count)) {
236 4
            return (int) $count;
237
        }
238
    }
239
240 1
    throw new RuntimeException('Cannot convert to int.');
241
}
242
243
/**
244
 * Normalize offset to positive integer.
245
 *
246
 * Provided with the requested offset, whether it be a string, an integer (positive or negative), or some type of
247
 * object, this function will normalize it to a positive integer offset or, failing that, it will throw an exception.
248
 * A negative offset will require either the traversable that is being indexed or its total count in order to normalize
249
250
 * @param int|mixed $offset The offset to normalize
251
 * @param int|array|traversable $count  Either the traversable count, or the traversable itself.
252
253
 * @return int
254
255
 * @throws RuntimeException If offset cannot be normalized
256
 * @throws InvalidArgumentException If offset is negative and count is not provided
257
 */
258
function normalize_offset($offset, $count = null)
259
{
260 6
    if (is_object($offset) && method_exists($offset, '__toString')) {
261 1
        $offset = (string) $offset;
262 1
    }
263
264 6
    if (!is_numeric($offset)) {
265 3
        throw new RuntimeException('Invalid offset.');
266
    }
267
268 3
    $count = get_count($count);
269
270 3
    if ($offset < 0) {
271 3
        if (is_null($count)) {
272 1
            throw new InvalidArgumentException('Cannot normalize negative offset without a total count.');
273
        }
274 2
        $offset += $count;
275 2
    }
276 2
    return (int) $offset;
277
}
278
279
/**
280
 * Get range start and end from string.
281
 *
282
 * Provided a string in the format of "start:end" and the total items in a collection, this function will return an
283
 * array in the form of [start, length].
284
 *
285
 * @param string $range The range to get (in the format of "start:end"
286
 * @param int|array|traversable $count  Either the traversable count, or the traversable itself.
287
 *
288
 * @return array [start, length]
289
 */
290
function get_range_start_end($range, $count = null)
291
{
292 3
    $count = get_count($count);
293 3
    if (Str::contains($range, Sequence::SLICE_DELIM)) {
294
        // return slice as a new sequence
295 2
        list($start, $end) = explode(Sequence::SLICE_DELIM, $range, 2);
296 2
        if ($start == '') {
297 1
            $start = 0;
298 1
        }
299 2
        if ($end == '') {
300 1
            $end = $count - 1;
301 1
        }
302 2
        $start = normalize_offset($start, $count);
303 1
        $end = normalize_offset($end, $count) + 1;
304 1
        if ($end > $start) {
305 1
            $length = $end - $start;
306 1
            return [$start, $length];
307
        }
308
    }
309 1
    throw new RuntimeException('Invalid index range/offset.');
310
}
311
312
/**
313
 * Get data type.
314
 *
315
 * Inspects data to determine its type.
316
 *
317
 * @param mixed  $data       The data to check
318
 * @param bool   $meta       Whether to include meta data such as length/size
319
// * @param string $returnType What type of value to return (array or string)
0 ignored issues
show
Bug introduced by
There is no parameter named $returnType. Was it maybe removed?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function.

Consider the following example. The parameter $italy is not defined by the method finale(...).

/**
 * @param array $germany
 * @param array $island
 * @param array $italy
 */
function finale($germany, $island) {
    return "2:1";
}

The most likely cause is that the parameter was removed, but the annotation was not.

Loading history...
320
 *
321
 * @return string
0 ignored issues
show
Documentation introduced by
Should the return type not be Hamcrest\Core\IsTypeOf?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
322
 */
323
function typeof($data, $meta = true/*, $returnType = 'string'*/)
0 ignored issues
show
Unused Code Comprehensibility introduced by
43% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
324
{
325 3
    $type = gettype($data);
326 3
    if ($meta) {
327
        switch($type) {
328 3
            case 'object':
329 2
                $class = get_class($data);
330 2
                return "{$type} <{$class}>";
331 2
            case 'resource':
332 1
                $restype = get_resource_type($data);
333 1
                return "{$type} <{$restype}>";
334
        }
335 2
    } else {
336
        switch($type) {
337 1
            case 'object':
338 1
                return get_class($data);
339 1
            case 'resource':
340 1
                return get_resource_type($data);
341
        }
342
    }
343 2
    return $type;
344
}
345
346
// BEGIN debug/testing functions
347
348
/**
349
 * Dump and die.
350
 *
351
 * @param mixed $input Data to dump
352
 * @param bool  $exit  Should we exit after dump?
353
 * @param bool  $label Should we print a label?
354
 * @codeCoverageIgnore
355
 */
356
function dd($input, $exit = true, $label = null)
357
{
358
    if (is_null($label)) {
359
        $trace = debug_backtrace(DEBUG_BACKTRACE_PROVIDE_OBJECT, 1);
360
        $label = 'File: ';
361
        $label .= pathinfo($trace[0]['file'], PATHINFO_FILENAME);
362
        $label .= ':' . $trace[0]['line'];
363
        echo $label . "\n";
364
    } else {
365
        echo $label . "\n" . implode(
366
                array_map(
367
                    function () {
368
                        return '-';
369
                    },
370
                    str_split($label)
371
                )
372
            ) . "\n";
373
    }
374
    var_dump($input);
375
    echo "\n";
376
    if ($exit) {
377
        exit;
378
    }
379
}
380
381
/**
382
 * Exactly the same as var_dump, except that it returns its output rather than dumping it.
383
 */
384
function sdump($var)
385
{
386 2
    ob_start();
387 2
    var_dump($var);
0 ignored issues
show
Security Debugging Code introduced by
var_dump($var); looks like debug code. Are you sure you do not want to remove it? This might expose sensitive data.
Loading history...
388 2
    return ob_get_clean();
389
}
390
391
/**
392
 * Get object hash/checksum.
393
 * Using a var_dump of an object, this will return a hash that tells you if anything in your object has changed. Just
394
 * create a hash of an object, do some stuff with it, then create another hash of it and compare the two. If they are
395
 * different, teh object has changed in some way.
396
 *
397
 * @param object $obj The object to hash
398
 * @param string $alg The hash algorithm (supports md5 or sha1)
399
 *
400
 * @return string
401
 *
402
 * @throws InvalidArgumentException
403
 */
404
function object_hash($obj, $alg = 'md5')
405
{
406 2
    $algorithms = ['md5', 'sha1'];
407 2
    if (!in_array($alg, $algorithms)) {
408 1
        throw new InvalidArgumentException(sprintf(
409 1
            '"%s" is not a valid hash algorithm (%s).',
410 1
            $alg,
411 1
            implode(', ', $algorithms)
412 1
        ));
413
    }
414 1
    return call_user_func($alg, sdump($obj));
415
}
416