1
|
|
|
<?php |
2
|
|
|
/** |
3
|
|
|
* CSVelte: Slender, elegant CSV for PHP |
4
|
|
|
* Inspired by Python's CSV module and Frictionless Data and the W3C's CSV |
5
|
|
|
* standardization efforts, CSVelte was written in an effort to take all the |
6
|
|
|
* suck out of working with CSV. |
7
|
|
|
* |
8
|
|
|
* @version v0.2.1 |
9
|
|
|
* @copyright Copyright (c) 2016 Luke Visinoni <[email protected]> |
10
|
|
|
* @author Luke Visinoni <[email protected]> |
11
|
|
|
* @license https://github.com/deni-zen/csvelte/blob/master/LICENSE The MIT License (MIT) |
12
|
|
|
*/ |
13
|
|
|
namespace CSVelte; |
14
|
|
|
|
15
|
|
|
use \Iterator; |
16
|
|
|
use \Countable; |
17
|
|
|
use \ArrayAccess; |
18
|
|
|
use \OutOfBoundsException; |
19
|
|
|
use \InvalidArgumentException; |
20
|
|
|
use \RuntimeException; |
21
|
|
|
use CSVelte\Collection; |
22
|
|
|
|
23
|
|
|
use function CSVelte\collect; |
24
|
|
|
|
25
|
|
|
/** |
26
|
|
|
* Collection class. |
27
|
|
|
* |
28
|
|
|
* Represents a collection of data. This is a one-dimensional structure that is |
29
|
|
|
* represented internally with a simple array. It provides several very |
30
|
|
|
* convenient operations to be performed on its data. |
31
|
|
|
* |
32
|
|
|
* @package CSVelte |
33
|
|
|
* @copyright (c) 2016, Luke Visinoni <[email protected]> |
34
|
|
|
* @author Luke Visinoni <[email protected]> |
35
|
|
|
* @since v0.2.1 |
36
|
|
|
* @todo Most of this class's methods will return a new Collection class |
37
|
|
|
* rather than modify the existing class. There needs to be a clear distinction |
38
|
|
|
* as to which ones don't and why. Also, some methods return a single value. |
39
|
|
|
* These also need to be clear. |
40
|
|
|
* @todo Need to make sure method naming, args, return values, concepts, etc. |
41
|
|
|
* are consistent. This is a very large class with a LOT of methods. It will |
42
|
|
|
* be very difficult to not let it blow up and get extremely messy. Go through |
43
|
|
|
* and refactor each method. Make sure there is nothing superfluous and that |
44
|
|
|
* everything makes sense and is intuitive to use. Also, because this class |
45
|
|
|
* is so enourmous it is going to be a bitch to test. Good test coverage is |
46
|
|
|
* going to require a LOT of tests. So put that on the list as well... |
47
|
|
|
* @todo Implement whichever SPL classes/interfaces you can (that make sense). |
48
|
|
|
* Probably a good idea to implement/extend some of these: |
49
|
|
|
* Interfaces - RecursiveIterator, SeekableIterator, OuterIterator, IteratorAggregate |
50
|
|
|
* Classes - FilterIterator, CallbackFilterIterator, CachingIterator, IteratorIterator, etc. |
51
|
|
|
* @replaces \CSVelte\Utils |
52
|
|
|
*/ |
53
|
|
|
class Collection implements Countable, ArrayAccess |
54
|
|
|
{ |
55
|
|
|
/** |
56
|
|
|
* Constants used as comparison operators in where() method |
57
|
|
|
*/ |
58
|
|
|
|
59
|
|
|
/** @var const Use this operator constant to test for identity (exact same) **/ |
60
|
|
|
const WHERE_ID = '==='; |
61
|
|
|
|
62
|
|
|
/** @var const Use this operator constant to test for non-identity **/ |
63
|
|
|
const WHERE_NID = '!=='; |
64
|
|
|
|
65
|
|
|
/** @var const Use this operator constant to test for equality **/ |
66
|
|
|
const WHERE_EQ = '=='; |
67
|
|
|
|
68
|
|
|
/** @var const Use this operator constant to test for non-equality **/ |
69
|
|
|
const WHERE_NEQ = '!='; |
70
|
|
|
|
71
|
|
|
/** @var const Use this operator constant to test for less-than **/ |
72
|
|
|
const WHERE_LT = '<'; |
73
|
|
|
|
74
|
|
|
/** @var const Use this operator constant to test for greater-than or equal-to **/ |
75
|
|
|
const WHERE_LTE = '<='; |
76
|
|
|
|
77
|
|
|
/** @var const Use this operator constant to test for greater-than **/ |
78
|
|
|
const WHERE_GT = '>'; |
79
|
|
|
|
80
|
|
|
/** @var const Use this operator constant to test for greater-than or equal-to **/ |
81
|
|
|
const WHERE_GTE = '>='; |
82
|
|
|
|
83
|
|
|
/** @var const Use this operator constant to test for case insensitive equality **/ |
84
|
|
|
const WHERE_LIKE = 'like'; |
85
|
|
|
|
86
|
|
|
/** @var const Use this operator constant to test for case instensitiv inequality **/ |
87
|
|
|
const WHERE_NLIKE = '!like'; |
88
|
|
|
|
89
|
|
|
/** @var const Use this operator constant to test for descendants or instances of a class **/ |
90
|
|
|
const WHERE_ISA = 'instanceof'; |
91
|
|
|
|
92
|
|
|
/** @var const Use this operator constant to test for values that aren't descendants or instances of a class **/ |
93
|
|
|
const WHERE_NISA = '!instanceof'; |
94
|
|
|
|
95
|
|
|
/** @var const Use this operator constant to test for internal PHP types **/ |
96
|
|
|
const WHERE_TOF = 'typeof'; |
97
|
|
|
|
98
|
|
|
/** @var const Use this operator constant to test for internal PHP type (negated) **/ |
99
|
|
|
const WHERE_NTOF = '!typeof'; |
100
|
|
|
|
101
|
|
|
/** @var const Use this operator constant to test against a regex pattern **/ |
102
|
|
|
const WHERE_MATCH = 'match'; |
103
|
|
|
|
104
|
|
|
/** @var const Use this operator constant to test against a regex pattern (negated) **/ |
105
|
|
|
const WHERE_NMATCH = '!match'; |
106
|
|
|
|
107
|
|
|
/** |
108
|
|
|
* Underlying array |
109
|
|
|
* @var array The array of data for this collection |
110
|
|
|
*/ |
111
|
|
|
protected $data = []; |
112
|
|
|
|
113
|
|
|
/** |
114
|
|
|
* Collection constructor. |
115
|
|
|
* |
116
|
|
|
* Set the data for this collection using $data |
117
|
|
|
* |
118
|
|
|
* @param array|ArrayAccess|null $data Either an array or an object that can be accessed |
119
|
|
|
* as if it were an array. |
120
|
|
|
*/ |
121
|
127 |
|
public function __construct($data = null) |
122
|
|
|
{ |
123
|
127 |
|
$this->assertArrayOrIterator($data); |
124
|
126 |
|
if (!is_null($data)) { |
125
|
125 |
|
$this->setData($data); |
|
|
|
|
126
|
125 |
|
} |
127
|
126 |
|
} |
128
|
|
|
|
129
|
|
|
/** |
130
|
|
|
* Invokes the object as a function. |
131
|
|
|
* |
132
|
|
|
* If called with no arguments, it will return underlying data array |
133
|
|
|
* If called with array as first argument, array will be merged into data array |
134
|
|
|
* If called with second param, it will call $this->set($key, $val) |
135
|
|
|
* If called with null as first param and key as second param, it will call $this->offsetUnset($key) |
136
|
|
|
* |
137
|
|
|
* @param null|array $val If an array, it will be merged into the collection |
138
|
|
|
* If both this arg and second arg are null, underlying data array will be returned |
139
|
|
|
* @param null|any $key If null and first arg is callable, this method will call map with callable |
140
|
|
|
* If this value is not null but first arg is, it will call $this->offsetUnset($key) |
141
|
|
|
* If this value is not null and first arg is anything other than callable, it will return $this->set($key, $val) |
142
|
|
|
* @see the description for various possible method signatures |
143
|
|
|
* @return mixed The return value depends entirely upon the arguments passed |
144
|
|
|
* to it. See description for various possible arguments/return value combinations |
145
|
|
|
*/ |
146
|
1 |
|
public function __invoke($val = null, $key = null) |
147
|
|
|
{ |
148
|
1 |
|
if (is_null($val)) { |
149
|
|
|
if (is_null($key)) { |
150
|
|
|
return $this->data; |
151
|
|
|
} else { |
152
|
|
|
return $this->offsetUnset($key); |
153
|
|
|
} |
154
|
|
|
} else { |
155
|
1 |
|
if (is_null($key)) { |
156
|
1 |
|
if (is_array($val)) return $this->merge($val); |
157
|
|
|
else { |
158
|
1 |
|
if (is_callable($val)) { |
159
|
1 |
|
return $this->map($val); |
160
|
|
|
} /*else { |
161
|
|
|
return $this->set($key, $val); |
162
|
|
|
}*/ |
163
|
|
|
} |
164
|
|
|
} else { |
165
|
|
|
$this->offsetSet($key, $val); |
166
|
|
|
} |
167
|
|
|
} |
168
|
|
|
return $this; |
169
|
|
|
} |
170
|
|
|
|
171
|
|
|
/** |
172
|
|
|
* Set internal collection data. |
173
|
|
|
* |
174
|
|
|
* Use an array or iterator to set this collection's data. |
175
|
|
|
* |
176
|
|
|
* @param array|Iterator $data The data to set for this collection |
177
|
|
|
* @return $this |
178
|
|
|
* @throws InvalidArgumentException If invalid data type |
179
|
|
|
*/ |
180
|
125 |
|
protected function setData($data) |
181
|
|
|
{ |
182
|
125 |
|
$this->assertArrayOrIterator($data); |
183
|
125 |
|
foreach ($data as $key => $val) { |
184
|
124 |
|
$this->data[$key] = $val; |
185
|
125 |
|
} |
186
|
125 |
|
return $this; |
187
|
|
|
} |
188
|
|
|
|
189
|
|
|
/** |
190
|
|
|
* Get data as an array. |
191
|
|
|
* |
192
|
|
|
* @return array Collection data as an array |
193
|
|
|
*/ |
194
|
63 |
|
public function toArray() |
195
|
|
|
{ |
196
|
63 |
|
$data = []; |
197
|
63 |
|
foreach($this->data as $key => $val) { |
198
|
63 |
|
$data[$key] = (is_object($val) && method_exists($val, 'toArray')) ? $val->toArray() : $val; |
199
|
63 |
|
} |
200
|
63 |
|
return $data; |
201
|
|
|
} |
202
|
|
|
|
203
|
|
|
/** |
204
|
|
|
* Get array keys |
205
|
|
|
* |
206
|
|
|
* @return \CSVelte\Collection The collection's keys (as a collection) |
207
|
|
|
*/ |
208
|
3 |
|
public function keys() |
209
|
|
|
{ |
210
|
3 |
|
return new self(array_keys($this->data)); |
211
|
|
|
} |
212
|
|
|
|
213
|
|
|
/** |
214
|
|
|
* Merge data (array or iterator) |
215
|
|
|
* |
216
|
|
|
* Pass an array to this method to have it merged into the collection. A new |
217
|
|
|
* collection will be created with the merged data and returned. |
218
|
|
|
* |
219
|
|
|
* @param array|iterator $data Data to merge into the collection |
220
|
|
|
* @param boolean $overwrite Whether existing values should be overwritten |
221
|
|
|
* @return \CSVelte\Collection A new collection with $data merged into it |
222
|
|
|
*/ |
223
|
1 |
|
public function merge($data = null, $overwrite = true) |
224
|
|
|
{ |
225
|
1 |
|
$this->assertArrayOrIterator($data); |
226
|
1 |
|
$coll = new self($this->data); |
227
|
1 |
|
foreach ($data as $key => $val) { |
|
|
|
|
228
|
1 |
|
$coll->set($key, $val, $overwrite); |
|
|
|
|
229
|
1 |
|
} |
230
|
1 |
|
return $coll; |
231
|
|
|
} |
232
|
|
|
|
233
|
|
|
/** |
234
|
|
|
* Test whether this collection contains the given value, optionally at a |
235
|
|
|
* specific key. |
236
|
|
|
* |
237
|
|
|
* This will return true if the collection contains a value equivalent to $val. |
238
|
|
|
* If $val is a callable (function/method), than the callable will be called |
239
|
|
|
* with $val, $key as its arguments (in that order). If the callable returns |
240
|
|
|
* any truthy value, than this method will return true. |
241
|
|
|
* |
242
|
|
|
* @param any|callable $val Either the value to check for or a callable that |
243
|
|
|
* accepts $key,$val and returns true if collection contains $val |
244
|
|
|
* @param any $key If not null, the only the value for this key will be checked |
245
|
|
|
* @return boolean True if this collection contains $val, $key |
246
|
|
|
*/ |
247
|
48 |
|
public function contains($val, $key = null) |
248
|
|
|
{ |
249
|
48 |
|
if (is_callable($callback = $val)) { |
250
|
47 |
|
foreach ($this->data as $key => $val) { |
251
|
47 |
|
if ($callback($val, $key)) return true; |
252
|
40 |
|
} |
253
|
42 |
|
} elseif (in_array($val, $this->data)) { |
254
|
8 |
|
return (is_null($key) || (isset($this->data[$key]) && $this->data[$key] == $val)); |
255
|
|
|
} |
256
|
42 |
|
return false; |
257
|
|
|
} |
258
|
|
|
|
259
|
|
|
/** |
260
|
|
|
* Tabular Where Search. |
261
|
|
|
* |
262
|
|
|
* Search for values of a certain key that meet a particular search criteria |
263
|
|
|
* using either one of the "Collection::WHERE_" class constants, or its string |
264
|
|
|
* counterpart. |
265
|
|
|
* |
266
|
|
|
* Warning: Only works for tabular collections (2-dimensional data array) |
267
|
|
|
* |
268
|
|
|
* @param string $key The key to compare to $val |
269
|
|
|
* @param mixed|Callable $val Either a value to test against or a callable to |
270
|
|
|
* run your own custom "where comparison logic" |
271
|
|
|
* @param string $comp The type of comparison operation ot use (such as "==" |
272
|
|
|
* or "instanceof"). Must be one of the self::WHERE_* constants' values |
273
|
|
|
* listed at the top of this class. |
274
|
|
|
* @return \CSVelte\Collection A collection of rows that meet the criteria |
275
|
|
|
* specified by $key, $val, and $comp |
276
|
|
|
*/ |
277
|
4 |
|
public function where($key, $val, $comp = null) |
278
|
|
|
{ |
279
|
4 |
|
$this->assertIsTabular(); |
280
|
3 |
|
$data = []; |
281
|
3 |
|
if ($this->has($key, true)) { |
|
|
|
|
282
|
3 |
|
if (is_callable($val)) { |
283
|
1 |
|
foreach ($this->data as $ln => $row) { |
284
|
1 |
|
if ($val($row[$key], $key)) { |
285
|
1 |
|
$data[$ln] = $row; |
286
|
1 |
|
} |
287
|
1 |
|
} |
288
|
1 |
|
} else { |
289
|
3 |
|
foreach ($this->data as $ln => $row) { |
290
|
3 |
|
$fieldval = $row[$key]; |
291
|
3 |
|
switch (strtolower($comp)) { |
292
|
3 |
|
case self::WHERE_ID: |
293
|
1 |
|
$comparison = $fieldval === $val; |
294
|
1 |
|
break; |
295
|
3 |
|
case self::WHERE_NID: |
296
|
1 |
|
$comparison = $fieldval !== $val; |
297
|
1 |
|
break; |
298
|
3 |
|
case self::WHERE_LT: |
299
|
|
|
$comparison = $fieldval < $val; |
300
|
|
|
break; |
301
|
3 |
|
case self::WHERE_LTE: |
302
|
1 |
|
$comparison = $fieldval <= $val; |
303
|
1 |
|
break; |
304
|
3 |
|
case self::WHERE_GT: |
305
|
1 |
|
$comparison = $fieldval > $val; |
306
|
1 |
|
break; |
307
|
3 |
|
case self::WHERE_GTE: |
308
|
|
|
$comparison = $fieldval >= $val; |
309
|
|
|
break; |
310
|
3 |
|
case self::WHERE_LIKE: |
311
|
1 |
|
$comparison = strtolower($fieldval) == strtolower($val); |
312
|
1 |
|
break; |
313
|
3 |
|
case self::WHERE_NLIKE: |
314
|
1 |
|
$comparison = strtolower($fieldval) != strtolower($val); |
315
|
1 |
|
break; |
316
|
3 |
|
case self::WHERE_ISA: |
317
|
1 |
|
$comparison = (is_object($fieldval) && $fieldval instanceof $val); |
318
|
1 |
|
break; |
319
|
3 |
|
case self::WHERE_NISA: |
320
|
1 |
|
$comparison = (!is_object($fieldval) || !($fieldval instanceof $val)); |
321
|
1 |
|
break; |
322
|
3 |
|
case self::WHERE_TOF: |
323
|
1 |
|
$comparison = (strtolower(gettype($fieldval)) == strtolower($val)); |
324
|
1 |
|
break; |
325
|
3 |
|
case self::WHERE_NTOF: |
326
|
1 |
|
$comparison = (strtolower(gettype($fieldval)) != strtolower($val)); |
327
|
1 |
|
break; |
328
|
3 |
|
case self::WHERE_NEQ: |
329
|
1 |
|
$comparison = $fieldval != $val; |
330
|
1 |
|
break; |
331
|
2 |
|
case self::WHERE_MATCH: |
332
|
1 |
|
$match = preg_match($val, $fieldval); |
333
|
1 |
|
$comparison = $match === 1; |
334
|
1 |
|
break; |
335
|
2 |
|
case self::WHERE_NMATCH: |
336
|
1 |
|
$match = preg_match($val, $fieldval); |
337
|
1 |
|
$comparison = $match === 0; |
338
|
1 |
|
break; |
339
|
1 |
|
case self::WHERE_EQ: |
340
|
1 |
|
default: |
341
|
1 |
|
$comparison = $fieldval == $val; |
342
|
1 |
|
break; |
343
|
3 |
|
} |
344
|
3 |
|
if ($comparison) { |
345
|
3 |
|
$data[$ln] = $row; |
346
|
3 |
|
} |
347
|
3 |
|
} |
348
|
|
|
} |
349
|
3 |
|
} |
350
|
3 |
|
return new self($data); |
351
|
|
|
} |
352
|
|
|
|
353
|
|
|
/** |
354
|
|
|
* Get the key at a given numerical position. |
355
|
|
|
* |
356
|
|
|
* This method will give you the key at the specified numerical offset, |
357
|
|
|
* regardless of how it's indexed (associatively, unordered numerical, etc.). |
358
|
|
|
* This allows you to find out what the first key is. Or the second. etc. |
359
|
|
|
* |
360
|
|
|
* @param int $pos Numerical position |
361
|
|
|
* @return mixed The key at numerical position |
362
|
|
|
* @throws \OutOfBoundsException If you request a position that doesn't exist |
363
|
|
|
* @todo Allow negative $pos to start counting from end |
364
|
|
|
*/ |
365
|
71 |
|
public function getKeyAtPosition($pos) |
366
|
|
|
{ |
367
|
71 |
|
$i = 0; |
368
|
71 |
|
foreach ($this->data as $key => $val) { |
369
|
70 |
|
if ($i === $pos) return $key; |
370
|
15 |
|
$i++; |
371
|
20 |
|
} |
372
|
17 |
|
throw new OutOfBoundsException("Collection data does not contain a key at given position: " . $pos); |
373
|
|
|
} |
374
|
|
|
|
375
|
|
|
/** |
376
|
|
|
* Get the value at a given numerical position. |
377
|
|
|
* |
378
|
|
|
* This method will give you the value at the specified numerical offset, |
379
|
|
|
* regardless of how it's indexed (associatively, unordered numerical, etc.). |
380
|
|
|
* This allows you to find out what the first value is. Or the second. etc. |
381
|
|
|
* |
382
|
|
|
* @param int $pos Numerical position |
383
|
|
|
* @return mixed The value at numerical position |
384
|
|
|
* @throws \OutOfBoundsException If you request a position that doesn't exist |
385
|
|
|
* @todo Allow negative $pos to start counting from end |
386
|
|
|
*/ |
387
|
69 |
|
public function getValueAtPosition($pos) |
388
|
|
|
{ |
389
|
69 |
|
return $this->data[$this->getKeyAtPosition($pos)]; |
390
|
|
|
} |
391
|
|
|
|
392
|
|
|
/** |
393
|
|
|
* Determine if this collection has a value at the specified numerical position. |
394
|
|
|
* |
395
|
|
|
* @param int $pos Numerical position |
396
|
|
|
* @return boolean Whether there exists a value at specified position |
397
|
|
|
*/ |
398
|
63 |
|
public function hasPosition($pos) |
399
|
|
|
{ |
400
|
|
|
try { |
401
|
63 |
|
$this->getKeyAtPosition($pos); |
402
|
63 |
|
} catch (OutOfBoundsException $e) { |
403
|
10 |
|
return false; |
404
|
|
|
} |
405
|
62 |
|
return true; |
406
|
|
|
} |
407
|
|
|
|
408
|
|
|
/** |
409
|
|
|
* "Pop" an item from the end of a collection. |
410
|
|
|
* |
411
|
|
|
* Removes an item from the bottom of the collection's underlying array and |
412
|
|
|
* returns it. This will actually remove the item from the collection. |
413
|
|
|
* |
414
|
|
|
* @param boolean $discard Whether to discard the popped item and return |
415
|
|
|
* $this instead of the default behavior |
416
|
|
|
* @return mixed Whatever the last item in the collection is |
417
|
|
|
*/ |
418
|
1 |
|
public function pop($discard = false) |
419
|
|
|
{ |
420
|
1 |
|
$popped = array_pop($this->data); |
421
|
1 |
|
return ($discard) ? $this : $popped; |
422
|
|
|
} |
423
|
|
|
|
424
|
|
|
/** |
425
|
|
|
* "Shift" an item from the top of a collection. |
426
|
|
|
* |
427
|
|
|
* Removes an item from the top of the collection's underlying array and |
428
|
|
|
* returns it. This will actually remove the item from the collection. |
429
|
|
|
* |
430
|
|
|
* @param boolean $discard Whether to discard the shifted item and return |
431
|
|
|
* $this instead of the default behavior |
432
|
|
|
* @return mixed Whatever the first item in the collection is |
433
|
|
|
*/ |
434
|
22 |
|
public function shift($discard = false) |
435
|
|
|
{ |
436
|
22 |
|
$shifted = array_shift($this->data); |
437
|
22 |
|
return ($discard) ? $this : $shifted; |
438
|
|
|
} |
439
|
|
|
|
440
|
|
|
/** |
441
|
|
|
* "Push" an item onto the end of the collection. |
442
|
|
|
* |
443
|
|
|
* Adds item(s) to the end of the collection's underlying array. |
444
|
|
|
* |
445
|
|
|
* @param mixed ... The item(s) to push onto the end of the collection. You may |
446
|
|
|
* also add additional arguments to push multiple items onto the end |
447
|
|
|
* @return $this |
448
|
|
|
*/ |
449
|
22 |
|
public function push() |
450
|
|
|
{ |
451
|
22 |
|
foreach (func_get_args() as $arg) { |
452
|
22 |
|
array_push($this->data, $arg); |
453
|
22 |
|
} |
454
|
22 |
|
return $this; |
455
|
|
|
} |
456
|
|
|
|
457
|
|
|
/** |
458
|
|
|
* "Unshift" an item onto the beginning of the collection. |
459
|
|
|
* |
460
|
|
|
* Adds item(s) to the beginning of the collection's underlying array. |
461
|
|
|
* |
462
|
|
|
* @param mixed ... The item(s) to push onto the top of the collection. You may |
463
|
|
|
* also add additional arguments to add multiple items |
464
|
|
|
* @return $this |
465
|
|
|
*/ |
466
|
1 |
|
public function unshift() |
467
|
|
|
{ |
468
|
1 |
|
foreach (array_reverse(func_get_args()) as $arg) { |
469
|
1 |
|
array_unshift($this->data, $arg); |
470
|
1 |
|
} |
471
|
1 |
|
return $this; |
472
|
|
|
} |
473
|
|
|
|
474
|
|
|
/** |
475
|
|
|
* "Insert" an item at a given numerical position. |
476
|
|
|
* |
477
|
|
|
* Regardless of how the collection is keyed (numerically or otherwise), this |
478
|
|
|
* method will insert an item at a given numerical position. If the given |
479
|
|
|
* position is more than there are items in the collection, the given item |
480
|
|
|
* will simply be added to the end. Nothing is overwritten with this method. |
481
|
|
|
* All elements that come after $offset will simply be shifted a space. |
482
|
|
|
* |
483
|
|
|
* Note: This method is one of the few that will modify the collection in |
484
|
|
|
* place rather than returning a new one. |
485
|
|
|
* |
486
|
|
|
* @param mixed ... The item(s) to push onto the top of the collection. You may |
487
|
|
|
* also add additional arguments to add multiple items |
488
|
|
|
* @return $this |
489
|
|
|
*/ |
490
|
1 |
|
public function insert($offset, $item) |
491
|
|
|
{ |
492
|
1 |
|
$top = array_slice($this->data, 0, $offset); |
493
|
1 |
|
$bottom = array_slice($this->data, $offset); |
494
|
1 |
|
$this->data = array_merge($top, [$item], $bottom); |
495
|
1 |
|
return $this; |
496
|
|
|
} |
497
|
|
|
|
498
|
|
|
/** |
499
|
|
|
* Pad collection to specified length. |
500
|
|
|
* |
501
|
|
|
* Pad the collection to a specific length, filling it with a given value. A |
502
|
|
|
* new collection with padded values is returned. |
503
|
|
|
* |
504
|
|
|
* @param int $size The number of values you want this collection to have |
505
|
|
|
* @param any $with The value you want to pad the collection with |
506
|
|
|
* @return \CSVelte\Collection A new collection, padded to specified size |
507
|
|
|
*/ |
508
|
1 |
|
public function pad($size, $with = null) |
509
|
|
|
{ |
510
|
1 |
|
return new self(array_pad($this->data, (int) $size, $with)); |
511
|
|
|
} |
512
|
|
|
|
513
|
|
|
/** |
514
|
|
|
* Check if this collection has a value at the given key. |
515
|
|
|
* |
516
|
|
|
* If this is a tabular data collection, this will check if the table has the |
517
|
|
|
* given key by default. You can change this behavior by passing false as the |
518
|
|
|
* second argument (this will change the behavior to check for a given key |
519
|
|
|
* at the row-level so it will likely only ever be numerical). |
520
|
|
|
* |
521
|
|
|
* @param any $key The key you want to check |
522
|
|
|
* @return boolean Whether there's a value at $key |
523
|
|
|
*/ |
524
|
26 |
|
public function has($key, $column = true) |
525
|
|
|
{ |
526
|
|
|
// we only need to check one row for the existance of $key because the |
527
|
|
|
// isTabular() method ensures every row has the same keys |
528
|
26 |
|
if ($column && $this->isTabular() && $first = reset($this->data)) { |
529
|
3 |
|
return array_key_exists($key, $first); |
530
|
|
|
} |
531
|
|
|
// if we don't have tabular data or we don't want to check for a column... |
532
|
23 |
|
return array_key_exists($key, $this->data); |
533
|
|
|
} |
534
|
|
|
|
535
|
|
|
/** |
536
|
|
|
* Get the value at the given key. |
537
|
|
|
* |
538
|
|
|
* If there is a value at the given key, it will be returned. If there isn't, |
539
|
|
|
* a default may be specified. If you would like for this method to throw an |
540
|
|
|
* exception when there is no value at $key, pass true as the third argument |
541
|
|
|
* |
542
|
|
|
* @param any $key The key you want to test for |
543
|
|
|
* @param any $default The default to return if there is no value at $key |
544
|
|
|
* @param boolean $throwExc Whether to throw an exception on failure to find |
545
|
|
|
* a value at the given key. |
546
|
|
|
* @return mixed Either the value at $key or the specified default |
547
|
|
|
* value |
548
|
|
|
* @throws \OutOfBoundsException If value can't be found at $key and $throwExc |
549
|
|
|
* is set to true |
550
|
|
|
*/ |
551
|
34 |
|
public function get($key, $default = null, $throwExc = false) |
552
|
|
|
{ |
553
|
34 |
|
if (array_key_exists($key, $this->data)) { |
554
|
32 |
|
return $this->data[$key]; |
555
|
|
|
} else { |
556
|
7 |
|
if ($throwExc) { |
557
|
6 |
|
throw new OutOfBoundsException("Collection data does not contain value for given key: " . $key); |
558
|
|
|
} |
559
|
|
|
} |
560
|
2 |
|
return $default; |
561
|
|
|
} |
562
|
|
|
|
563
|
|
|
/** |
564
|
|
|
* Set value for the given key. |
565
|
|
|
* |
566
|
|
|
* Given $key, this will set $this->data[$key] to the value of $val. If that |
567
|
|
|
* index already has a value, it will be overwritten unless $overwrite is set |
568
|
|
|
* to false. In that case nothing happens. |
569
|
|
|
* |
570
|
|
|
* @param any $key The key you want to set a value for |
571
|
|
|
* @param any $value The value you want to set key to |
572
|
|
|
* @param boolean $overwrite Whether to overwrite existing value |
573
|
|
|
* @return $this |
574
|
|
|
*/ |
575
|
24 |
|
public function set($key, $value = null, $overwrite = true) |
576
|
|
|
{ |
577
|
24 |
|
if (!array_key_exists($key, $this->data) || $overwrite) { |
578
|
24 |
|
$this->data[$key] = $value; |
579
|
24 |
|
} |
580
|
24 |
|
return $this; |
581
|
|
|
} |
582
|
|
|
|
583
|
|
|
/** |
584
|
|
|
* Unset value at the given offset. |
585
|
|
|
* |
586
|
|
|
* This method is used when the end-user uses a colleciton as an array and |
587
|
|
|
* calls unset($collection[5]). |
588
|
|
|
* |
589
|
|
|
* @param mixed $offset The offset at which to unset |
590
|
|
|
* @return $this |
591
|
|
|
* @todo create an alias for this... maybe delete() or remove() |
592
|
|
|
*/ |
593
|
1 |
|
public function offsetUnset($offset) |
594
|
|
|
{ |
595
|
1 |
|
if ($this->has($offset)) { |
596
|
1 |
|
unset($this->data[$offset]); |
597
|
1 |
|
} |
598
|
1 |
|
return $this; |
599
|
|
|
} |
600
|
|
|
|
601
|
|
|
/** |
602
|
|
|
* Alias of self::has |
603
|
|
|
* |
604
|
|
|
* @param int|mixed The offset to test for |
605
|
|
|
* @return boolean Whether a value exists at $offset |
606
|
|
|
*/ |
607
|
|
|
public function offsetExists($offset) |
608
|
|
|
{ |
609
|
|
|
return $this->has($offset); |
610
|
|
|
} |
611
|
|
|
|
612
|
|
|
/** |
613
|
|
|
* Alias of self::set |
614
|
|
|
* |
615
|
|
|
* @param int|mixed The offset to set |
616
|
|
|
* @param any The value to set it to |
617
|
|
|
* @return boolean |
618
|
|
|
*/ |
619
|
|
|
public function offsetSet($offset, $value) |
620
|
|
|
{ |
621
|
|
|
$this->set($offset, $value); |
622
|
|
|
return $this; |
623
|
|
|
} |
624
|
|
|
|
625
|
|
|
/** |
626
|
|
|
* Alias of self::get |
627
|
|
|
* |
628
|
|
|
* @param int|mixed The offset to get |
629
|
|
|
* @return mixed The value at $offset |
630
|
|
|
*/ |
631
|
|
|
public function offsetGet($offset) |
632
|
|
|
{ |
633
|
|
|
return $this->get($offset); |
634
|
|
|
} |
635
|
|
|
|
636
|
|
|
/** |
637
|
|
|
* Increment an item. |
638
|
|
|
* |
639
|
|
|
* Increment the item specified by $key by one value. Intended for integers |
640
|
|
|
* but also works (using this term loosely) for letters. Any other data type |
641
|
|
|
* it may modify is unintended behavior at best. |
642
|
|
|
* |
643
|
|
|
* This method modifies its internal data array rather than returning a new |
644
|
|
|
* collection. |
645
|
|
|
* |
646
|
|
|
* @param mixed $key The key of the item you want to increment. |
647
|
|
|
* @return $this |
648
|
|
|
*/ |
649
|
3 |
|
public function increment($key) |
650
|
|
|
{ |
651
|
3 |
|
$val = $this->get($key, null, true); |
652
|
3 |
|
$this->set($key, ++$val); |
|
|
|
|
653
|
3 |
|
return $this; |
654
|
|
|
} |
655
|
|
|
|
656
|
|
|
/** |
657
|
|
|
* Decrement an item. |
658
|
|
|
* |
659
|
|
|
* Frcrement the item specified by $key by one value. Intended for integers. |
660
|
|
|
* Does not work for letters and if it does anything to anything else, it's |
661
|
|
|
* unintended at best. |
662
|
|
|
* |
663
|
|
|
* This method modifies its internal data array rather than returning a new |
664
|
|
|
* collection. |
665
|
|
|
* |
666
|
|
|
* @param mixed $key The key of the item you want to decrement. |
667
|
|
|
* @return $this |
668
|
|
|
*/ |
669
|
1 |
|
public function decrement($key) |
670
|
|
|
{ |
671
|
1 |
|
$val = $this->get($key, null, true); |
672
|
1 |
|
$this->set($key, --$val); |
|
|
|
|
673
|
1 |
|
return $this; |
674
|
|
|
} |
675
|
|
|
|
676
|
|
|
/** |
677
|
|
|
* Count the items in this collection. |
678
|
|
|
* |
679
|
|
|
* Returns either the number of items in the collection or, if this is a |
680
|
|
|
* collection of tabular data, and you pass true as the first argument, you |
681
|
|
|
* will get back a collection containing the count of each row (which will |
682
|
|
|
* always be the same so maybe I should still just return an integer). |
683
|
|
|
* |
684
|
|
|
* @param boolean $multi Whether to count just the items in the collection or |
685
|
|
|
* to count the items in each tabular data row. |
686
|
|
|
* @return int|\CSVelte\Collection Either an integer count or a collection of counts |
687
|
|
|
*/ |
688
|
44 |
|
public function count($multi = false) |
689
|
|
|
{ |
690
|
44 |
|
if ($multi) { |
691
|
|
|
// if every value is an array... |
692
|
1 |
|
if (false !== ($condRet = $this->if2DMapInternalMethod(__METHOD__))) { |
693
|
1 |
|
return $condRet; |
|
|
|
|
694
|
|
|
} |
695
|
|
|
} |
696
|
|
|
// just count main array |
697
|
44 |
|
return count($this->data); |
698
|
|
|
} |
699
|
|
|
|
700
|
|
|
/** |
701
|
|
|
* Collection map. |
702
|
|
|
* |
703
|
|
|
* Apply a callback to each element in the collection and return the |
704
|
|
|
* resulting collection. The resulting collection will contain the return |
705
|
|
|
* values of each call to $callback. |
706
|
|
|
* |
707
|
|
|
* @param Callable $callback A callback to apply to each item in the collection |
708
|
|
|
* @return \CSVelte\Collection A collection of callback return values |
709
|
|
|
*/ |
710
|
33 |
|
public function map(Callable $callback) |
711
|
|
|
{ |
712
|
33 |
|
return new self(array_map($callback, $this->data)); |
713
|
|
|
} |
714
|
|
|
|
715
|
|
|
/** |
716
|
|
|
* Walk the collection. |
717
|
|
|
* |
718
|
|
|
* Walk through each item in the collection, calling a function for each |
719
|
|
|
* item in the collection. This is one of the few methods that doesn't return |
720
|
|
|
* a new collection. All changes will be to the existing collection object. |
721
|
|
|
* |
722
|
|
|
* Note: return false from the collback to stop walking. |
723
|
|
|
* |
724
|
|
|
* @param Callable $callback A callback function to call for each item in the collection |
725
|
|
|
* @param any $userdata Any extra data you'd like passed to your callback |
726
|
|
|
* @return $this |
727
|
|
|
*/ |
728
|
34 |
|
public function walk(Callable $callback, $userdata = null) |
729
|
|
|
{ |
730
|
34 |
|
array_walk($this->data, $callback, $userdata); |
731
|
34 |
|
return $this; |
732
|
|
|
} |
733
|
|
|
|
734
|
|
|
/** |
735
|
|
|
* Call a user function for each item in the collection. If function returns |
736
|
|
|
* false, loop is terminated. |
737
|
|
|
* |
738
|
|
|
* @return $this |
739
|
|
|
* @todo I'm not entirely sure what this method should do... return new |
740
|
|
|
* collection? modify this one? |
741
|
|
|
* @todo This method appears to be a duplicate of walk(). Is it even necessary? |
742
|
|
|
*/ |
743
|
2 |
|
public function each(Callable $callback) |
744
|
|
|
{ |
745
|
2 |
|
foreach ($this->data as $key => $val) { |
746
|
2 |
|
if (!$ret = $callback($val, $key)) { |
747
|
2 |
|
if ($ret === false) break; |
748
|
2 |
|
} |
749
|
2 |
|
} |
750
|
2 |
|
return $this; |
751
|
|
|
} |
752
|
|
|
|
753
|
|
|
/** |
754
|
|
|
* Reduce collection to single value. |
755
|
|
|
* |
756
|
|
|
* Reduces the collection to a single value by calling a callback for each |
757
|
|
|
* item in the collection, carrying along an accumulative value as it does so. |
758
|
|
|
* The final value is then returned. |
759
|
|
|
* |
760
|
|
|
* @param Callable $callback The function to reduce the collection |
761
|
|
|
* @param any $initial The initial value to set the accumulative value to |
762
|
|
|
* @return mixed Whatever the final value from the callback is |
763
|
|
|
*/ |
764
|
|
|
public function reduce(Callable $callback, $initial = null) |
765
|
|
|
{ |
766
|
|
|
return array_reduce($this->data, $callback, $initial); |
767
|
|
|
} |
768
|
|
|
|
769
|
|
|
/** |
770
|
|
|
* Filter out unwanted items using a callback function. |
771
|
|
|
* |
772
|
|
|
* @param Callable $callback |
773
|
|
|
* @return CSVelte\Collection A new collection with filtered items removed |
774
|
|
|
*/ |
775
|
23 |
|
public function filter(Callable $callback) |
776
|
|
|
{ |
777
|
23 |
|
$keys = []; |
778
|
23 |
|
foreach ($this->data as $key => $val) { |
779
|
23 |
|
if (false === $callback($val, $key)) $keys[$key] = true; |
780
|
23 |
|
} |
781
|
23 |
|
return new self(array_diff_key($this->data, $keys)); |
782
|
|
|
} |
783
|
|
|
|
784
|
|
|
/** |
785
|
|
|
* Get first match. |
786
|
|
|
* |
787
|
|
|
* Get first value that meets criteria specified with $callback function. |
788
|
|
|
* |
789
|
|
|
* @param Callable $callback A callback with arguments ($val, $key). If it |
790
|
|
|
* returns true, that $val will be returned. |
791
|
|
|
* @return mixed The first $val that meets criteria specified with $callback |
792
|
|
|
*/ |
793
|
1 |
|
public function first(Callable $callback) |
794
|
|
|
{ |
795
|
1 |
|
foreach ($this->data as $key => $val) { |
796
|
1 |
|
if ($callback($val, $key)) return $val; |
797
|
1 |
|
} |
798
|
|
|
return null; |
799
|
|
|
} |
800
|
|
|
|
801
|
|
|
/** |
802
|
|
|
* Get last match. |
803
|
|
|
* |
804
|
|
|
* Get last value that meets criteria specified with $callback function. |
805
|
|
|
* |
806
|
|
|
* @param Callable $callback A callback with arguments ($val, $key). If it |
807
|
|
|
* returns true, that $val will be returned. |
808
|
|
|
* @return mixed The last $val that meets criteria specified with $callback |
809
|
|
|
*/ |
810
|
1 |
|
public function last(Callable $callback) |
811
|
|
|
{ |
812
|
1 |
|
$elem = null; |
813
|
1 |
|
foreach ($this->data as $key => $val) { |
814
|
1 |
|
if ($callback($val, $key)) $elem = $val; |
815
|
1 |
|
} |
816
|
1 |
|
return $elem; |
817
|
|
|
} |
818
|
|
|
|
819
|
|
|
/** |
820
|
|
|
* Collection value frequency. |
821
|
|
|
* |
822
|
|
|
* Returns an array where the key is a value in the collection and the value |
823
|
|
|
* is the number of times that value appears in the collection. |
824
|
|
|
* |
825
|
|
|
* @return CSVelte\Collection A collection of value frequencies (see description) |
826
|
|
|
*/ |
827
|
23 |
|
public function frequency() |
828
|
|
|
{ |
829
|
23 |
|
if (false !== ($condRet = $this->if2DMapInternalMethod(__METHOD__))) { |
830
|
22 |
|
return $condRet; |
831
|
|
|
} |
832
|
23 |
|
$freq = []; |
833
|
23 |
|
foreach ($this->data as $val) { |
834
|
23 |
|
$key = is_numeric($val) ? $val : (string) $val; |
835
|
23 |
|
if (!isset($freq[$key])) { |
836
|
23 |
|
$freq[$key] = 0; |
837
|
23 |
|
} |
838
|
23 |
|
$freq[$key]++; |
839
|
23 |
|
} |
840
|
23 |
|
return new self($freq); |
841
|
|
|
} |
842
|
|
|
|
843
|
|
|
/** |
844
|
|
|
* Unique collection. |
845
|
|
|
* |
846
|
|
|
* Returns a collection with duplicate values removed. If two-dimensional, |
847
|
|
|
* then each array within the collection will have its duplicates removed. |
848
|
|
|
* |
849
|
|
|
* @return CSVelte\Collection A new collection with duplicate values removed. |
850
|
|
|
*/ |
851
|
23 |
|
public function unique() |
852
|
|
|
{ |
853
|
23 |
|
if (false !== ($condRet = $this->if2DMapInternalMethod(__METHOD__))) { |
854
|
5 |
|
return $condRet; |
855
|
|
|
} |
856
|
21 |
|
return new self(array_unique($this->data)); |
857
|
|
|
} |
858
|
|
|
|
859
|
|
|
/** |
860
|
|
|
* Get duplicate values. |
861
|
|
|
* |
862
|
|
|
* Returns a collection of arrays where the key is the duplicate value |
863
|
|
|
* and the value is an array of keys from the original collection. |
864
|
|
|
* |
865
|
|
|
* @return CSVelte\Collection A new collection with duplicate values. |
866
|
|
|
*/ |
867
|
6 |
|
public function duplicates() |
868
|
|
|
{ |
869
|
6 |
|
$dups = []; |
870
|
|
|
$this->walk(function($val, $key) use (&$dups) { |
871
|
6 |
|
$dups[$val][] = $key; |
872
|
6 |
|
}); |
873
|
|
|
return (new self($dups))->filter(function($val, $key) { |
|
|
|
|
874
|
6 |
|
return (count($val) > 1); |
875
|
6 |
|
}); |
876
|
|
|
} |
877
|
|
|
|
878
|
|
|
/** |
879
|
|
|
* Reverse keys/values. |
880
|
|
|
* |
881
|
|
|
* Get a new collection where the keys and values have been swapped. |
882
|
|
|
* |
883
|
|
|
* @return CSVelte\Collection A new collection where keys/values have been swapped |
884
|
|
|
*/ |
885
|
1 |
|
public function flip() |
886
|
|
|
{ |
887
|
1 |
|
return new self(array_flip($this->data)); |
888
|
|
|
} |
889
|
|
|
|
890
|
|
|
/** |
891
|
|
|
* Return an array of key/value pairs. |
892
|
|
|
* |
893
|
|
|
* Return array can either be in [key,value] or [key => value] format. The |
894
|
|
|
* first is the default. |
895
|
|
|
* |
896
|
|
|
* @param boolean Whether you want pairs in [k => v] rather than [k, v] format |
897
|
|
|
* @return CSVelte\Collection A collection of key/value pairs |
898
|
|
|
*/ |
899
|
1 |
|
public function pairs($alt = false) |
900
|
|
|
{ |
901
|
1 |
|
return new self(array_map( |
902
|
|
|
function ($key, $val) use ($alt) { |
903
|
1 |
|
if ($alt) { |
904
|
1 |
|
return [$key => $val]; |
905
|
|
|
} else { |
906
|
1 |
|
return [$key, $val]; |
907
|
|
|
} |
908
|
1 |
|
}, |
909
|
1 |
|
array_keys($this->data), |
910
|
1 |
|
array_values($this->data) |
911
|
1 |
|
)); |
912
|
|
|
} |
913
|
|
|
|
914
|
|
|
/** |
915
|
|
|
* Get average of data items. |
916
|
|
|
* |
917
|
|
|
* @return mixed The average of all items in collection |
918
|
|
|
*/ |
919
|
2 |
|
public function sum() |
920
|
|
|
{ |
921
|
2 |
|
if (false !== ($condRet = $this->if2DMapInternalMethod(__METHOD__))) { |
922
|
1 |
|
return $condRet; |
923
|
|
|
} |
924
|
2 |
|
$this->assertNumericValues(); |
925
|
2 |
|
return array_sum($this->data); |
926
|
|
|
} |
927
|
|
|
|
928
|
|
|
/** |
929
|
|
|
* Get average of data items. |
930
|
|
|
* |
931
|
|
|
* If two-dimensional it will return a collection of averages. |
932
|
|
|
* |
933
|
|
|
* @return mixed|CSVelte\Collection The average of all items in collection |
934
|
|
|
*/ |
935
|
8 |
|
public function average() |
936
|
|
|
{ |
937
|
8 |
|
if (false !== ($condRet = $this->if2DMapInternalMethod(__METHOD__))) { |
938
|
6 |
|
return $condRet; |
939
|
|
|
} |
940
|
8 |
|
$this->assertNumericValues(); |
941
|
8 |
|
$total = array_sum($this->data); |
942
|
8 |
|
$count = count($this->data); |
943
|
8 |
|
return $total / $count; |
944
|
|
|
} |
945
|
|
|
|
946
|
|
|
/** |
947
|
|
|
* Get largest item in the collection |
948
|
|
|
* |
949
|
|
|
* @return mixed The largest item in the collection |
950
|
|
|
*/ |
951
|
8 |
|
public function max() |
952
|
|
|
{ |
953
|
8 |
|
if (false !== ($condRet = $this->if2DMapInternalMethod(__METHOD__))) { |
954
|
1 |
|
return $condRet; |
955
|
|
|
} |
956
|
8 |
|
$this->assertNumericValues(); |
957
|
8 |
|
return max($this->data); |
958
|
|
|
} |
959
|
|
|
|
960
|
|
|
/** |
961
|
|
|
* Get smallest item in the collection |
962
|
|
|
* |
963
|
|
|
* @return mixed The smallest item in the collection |
964
|
|
|
*/ |
965
|
5 |
|
public function min() |
966
|
|
|
{ |
967
|
5 |
|
if (false !== ($condRet = $this->if2DMapInternalMethod(__METHOD__))) { |
968
|
1 |
|
return $condRet; |
969
|
|
|
} |
970
|
5 |
|
$this->assertNumericValues(); |
971
|
5 |
|
return min($this->data); |
972
|
|
|
} |
973
|
|
|
|
974
|
|
|
/** |
975
|
|
|
* Get mode of data items. |
976
|
|
|
* |
977
|
|
|
* @return mixed The mode of all items in collection |
978
|
|
|
*/ |
979
|
8 |
|
public function mode() |
980
|
|
|
{ |
981
|
8 |
|
if (false !== ($condRet = $this->if2DMapInternalMethod(__METHOD__))) { |
982
|
6 |
|
return $condRet; |
983
|
|
|
} |
984
|
|
|
$strvals = $this->map(function($val){ |
985
|
8 |
|
return (string) $val; |
986
|
8 |
|
}); |
987
|
8 |
|
$this->assertNumericValues(); |
988
|
7 |
|
$counts = array_count_values($strvals->toArray()); |
989
|
7 |
|
arsort($counts); |
990
|
7 |
|
$mode = key($counts); |
991
|
7 |
|
return (strpos($mode, '.')) ? floatval($mode) : intval($mode); |
992
|
|
|
} |
993
|
|
|
|
994
|
|
|
/** |
995
|
|
|
* Get median of data items. |
996
|
|
|
* |
997
|
|
|
* @return mixed The median of all items in collection |
998
|
|
|
*/ |
999
|
2 |
|
public function median() |
1000
|
|
|
{ |
1001
|
2 |
|
if (false !== ($condRet = $this->if2DMapInternalMethod(__METHOD__))) { |
1002
|
1 |
|
return $condRet; |
1003
|
|
|
} |
1004
|
2 |
|
$this->assertNumericValues(); |
1005
|
2 |
|
$count = count($this->data); |
1006
|
2 |
|
natcasesort($this->data); |
1007
|
2 |
|
$middle = ($count / 2); |
1008
|
2 |
|
$values = array_values($this->data); |
1009
|
2 |
|
if ($count % 2 == 0) { |
1010
|
|
|
// even number, use middle |
1011
|
2 |
|
$low = $values[$middle - 1]; |
1012
|
2 |
|
$high = $values[$middle]; |
1013
|
2 |
|
return ($low + $high) / 2; |
1014
|
|
|
} |
1015
|
|
|
// odd number return median |
1016
|
2 |
|
return $values[$middle]; |
1017
|
|
|
} |
1018
|
|
|
|
1019
|
|
|
/** |
1020
|
|
|
* Join items together into a string |
1021
|
|
|
* |
1022
|
|
|
* @param string $glue The string to join items together with |
1023
|
|
|
* @return string A string with all items in the collection strung together |
1024
|
|
|
* @todo Make this work with 2D collection |
1025
|
|
|
*/ |
1026
|
18 |
|
public function join($glue) |
1027
|
|
|
{ |
1028
|
18 |
|
return implode($glue, $this->data); |
1029
|
|
|
} |
1030
|
|
|
|
1031
|
|
|
/** |
1032
|
|
|
* Is the collection empty? |
1033
|
|
|
* |
1034
|
|
|
* @return boolean Whether the collection is empty |
1035
|
|
|
*/ |
1036
|
1 |
|
public function isEmpty() |
1037
|
|
|
{ |
1038
|
1 |
|
return empty($this->data); |
1039
|
|
|
} |
1040
|
|
|
|
1041
|
|
|
/** |
1042
|
|
|
* Immediately invoke a callback. |
1043
|
|
|
* |
1044
|
|
|
* @param Callable $callback A callback to invoke with ($this) |
1045
|
|
|
* @return mixed Whatever the callback returns |
1046
|
|
|
*/ |
1047
|
1 |
|
public function value(Callable $callback) |
1048
|
|
|
{ |
1049
|
1 |
|
return $callback($this); |
1050
|
|
|
} |
1051
|
|
|
|
1052
|
|
|
/** |
1053
|
|
|
* Sort the collection. |
1054
|
|
|
* |
1055
|
|
|
* This method can sort your collection in any which way you please. By |
1056
|
|
|
* default it uses a case-insensitive natural order algorithm, but you can |
1057
|
|
|
* pass it any sorting algorithm you like. |
1058
|
|
|
* |
1059
|
|
|
* @param Callable $sort_func The sorting function you want to use |
|
|
|
|
1060
|
|
|
* @param boolean $preserve_keys Whether you want to preserve keys |
1061
|
|
|
* @return CSVelte\Collection A new collection sorted by $callback |
1062
|
|
|
*/ |
1063
|
23 |
|
public function sort(Callable $callback = null, $preserve_keys = true) |
1064
|
|
|
{ |
1065
|
23 |
|
if (is_null($callback)) $callback = 'strcasecmp'; |
1066
|
23 |
|
if (!is_callable($callback)) { |
1067
|
|
|
throw new InvalidArgumentException(sprintf( |
1068
|
|
|
'Invalid argument supplied for %s. Expected %s, got: "%s".', |
1069
|
|
|
__METHOD__, |
1070
|
|
|
'Callable', |
1071
|
|
|
gettype($callback) |
1072
|
|
|
)); |
1073
|
|
|
} |
1074
|
23 |
|
$data = $this->data; |
1075
|
23 |
|
if ($preserve_keys) { |
1076
|
23 |
|
uasort($data, $callback); |
1077
|
23 |
|
} else { |
1078
|
2 |
|
usort($data, $callback); |
1079
|
|
|
} |
1080
|
23 |
|
return new self($data); |
1081
|
|
|
} |
1082
|
|
|
|
1083
|
|
|
/** |
1084
|
|
|
* Order tabular data. |
1085
|
|
|
* |
1086
|
|
|
* Order a tabular dataset by a given key/comparison algorithm |
1087
|
|
|
* |
1088
|
|
|
* @param string $key The key you want to order by |
1089
|
|
|
* @param Callable $cmp The sorting comparison algorithm to use |
1090
|
|
|
* @param boolean $preserve_keys Whether keys should be preserved |
1091
|
|
|
* @return CSVelte\Collection A new collection sorted by $cmp and $key |
1092
|
|
|
*/ |
1093
|
1 |
|
public function orderBy($key, Callable $cmp = null, $preserve_keys = true) |
1094
|
|
|
{ |
1095
|
1 |
|
$this->assertIsTabular(); |
1096
|
|
|
return $this->sort(function($a, $b) use ($key, $cmp) { |
1097
|
1 |
|
if (!isset($a[$key]) || !isset($b[$key])) { |
1098
|
|
|
throw new RuntimeException('Cannot order collection by non-existant key: ' . $key); |
1099
|
|
|
} |
1100
|
1 |
|
if (is_null($cmp)) { |
1101
|
1 |
|
return strcasecmp($a[$key], $b[$key]); |
1102
|
|
|
} else { |
1103
|
1 |
|
return $cmp($a[$key], $b[$key]); |
1104
|
|
|
} |
1105
|
1 |
|
}, $preserve_keys); |
1106
|
|
|
} |
1107
|
|
|
|
1108
|
|
|
/** |
1109
|
|
|
* Reverse collection order. |
1110
|
|
|
* |
1111
|
|
|
* Reverse the order of items in a collection. Sometimes it's easier than |
1112
|
|
|
* trying to write a particular sorting algurithm that sorts forwards and back. |
1113
|
|
|
* |
1114
|
|
|
* @param boolean $preserve_keys Whether keys should be preserved |
1115
|
|
|
* @return CSVelte\Collection A new collection in reverse order |
1116
|
|
|
*/ |
1117
|
22 |
|
public function reverse($preserve_keys = true) |
1118
|
|
|
{ |
1119
|
22 |
|
return new self(array_reverse($this->data, $preserve_keys)); |
1120
|
|
|
} |
1121
|
|
|
|
1122
|
39 |
|
protected function if2DMapInternalMethod($method) |
1123
|
|
|
{ |
1124
|
39 |
|
if ($this->is2D()) { |
1125
|
29 |
|
$method = explode('::', $method, 2); |
1126
|
29 |
|
if (count($method) == 2) { |
1127
|
29 |
|
$method = $method[1]; |
1128
|
|
|
return $this->map(function($val) use ($method) { |
1129
|
29 |
|
return (new self($val))->$method(); |
1130
|
29 |
|
}); |
1131
|
|
|
} |
1132
|
|
|
} |
1133
|
38 |
|
return false; |
1134
|
|
|
} |
1135
|
|
|
|
1136
|
|
|
/** |
1137
|
|
|
* Is this collection two-dimensional |
1138
|
|
|
* |
1139
|
|
|
* If all items of the collection are arrays this will return true. |
1140
|
|
|
* |
1141
|
|
|
* @return boolean whether this is two-dimensional |
1142
|
|
|
*/ |
1143
|
46 |
|
public function is2D() |
1144
|
|
|
{ |
1145
|
|
|
return !$this->contains(function($val){ |
1146
|
46 |
|
return !is_array($val); |
1147
|
46 |
|
}); |
1148
|
|
|
return false; |
|
|
|
|
1149
|
|
|
} |
1150
|
|
|
|
1151
|
|
|
/** |
1152
|
|
|
* Is this a tabular collection? |
1153
|
|
|
* |
1154
|
|
|
* If this is a two-dimensional collection with the same keys in every array, |
1155
|
|
|
* this method will return true. |
1156
|
|
|
* |
1157
|
|
|
* @return boolean Whether this is a tabular collection |
1158
|
|
|
*/ |
1159
|
35 |
|
public function isTabular() |
1160
|
|
|
{ |
1161
|
35 |
|
if ($this->is2D()) { |
1162
|
|
|
// look through each item in the collection and if an array, grab its keys |
1163
|
|
|
// and throw them in an array to be analyzed later... |
1164
|
32 |
|
$test = []; |
1165
|
|
|
$this->walk(function($val, $key) use (&$test) { |
1166
|
11 |
|
if (is_array($val)) { |
1167
|
11 |
|
$test[$key] = array_keys($val); |
1168
|
11 |
|
return true; |
1169
|
|
|
} |
1170
|
|
|
return false; |
1171
|
32 |
|
}); |
1172
|
|
|
|
1173
|
|
|
// if the list of array keys is shorter than the total amount of items in |
1174
|
|
|
// the collection, than this is not tabular data |
1175
|
32 |
|
if (count($test) != count($this)) return false; |
1176
|
|
|
|
1177
|
|
|
// loop through the array of each item's array keys that we just created |
1178
|
|
|
// and compare it to the FIRST item. If any array contains different keys |
1179
|
|
|
// than this is not tabular data. |
1180
|
32 |
|
$first = array_shift($test); |
1181
|
32 |
|
foreach ($test as $key => $keys) { |
1182
|
11 |
|
$diff = array_diff($first, $keys); |
1183
|
11 |
|
if (!empty($diff)) return false; |
1184
|
28 |
|
} |
1185
|
28 |
|
return true; |
1186
|
|
|
} |
1187
|
24 |
|
return false; |
1188
|
|
|
} |
1189
|
|
|
|
1190
|
|
|
/** |
1191
|
|
|
* Assert this collection is two-dimensional. |
1192
|
|
|
* |
1193
|
|
|
* Although a collection must be two-dimensional to be tabular, the opposite |
1194
|
|
|
* is not necessarily true. This will throw an exception if this collection |
1195
|
|
|
* contains anything but arrays. |
1196
|
|
|
* |
1197
|
|
|
* @throws |
1198
|
|
|
*/ |
1199
|
|
|
protected function assertIs2D() |
1200
|
|
|
{ |
1201
|
|
|
if (!$this->is2D()) { |
1202
|
|
|
throw new RuntimeException('Invalid data type, requires two-dimensional array.'); |
1203
|
|
|
} |
1204
|
|
|
} |
1205
|
|
|
|
1206
|
5 |
|
protected function assertIsTabular() |
1207
|
|
|
{ |
1208
|
5 |
|
if (!$this->isTabular()) { |
1209
|
1 |
|
throw new RuntimeException('Invalid data type, requires tabular data (two-dimensional array where each sub-array has the same keys).'); |
1210
|
|
|
} |
1211
|
4 |
|
} |
1212
|
|
|
|
1213
|
|
|
protected function assertNumericValues() |
1214
|
|
|
{ |
1215
|
18 |
|
if ($this->contains(function($val){ |
1216
|
18 |
|
return !is_numeric($val); |
1217
|
18 |
|
})) { |
1218
|
|
|
// can't average non-numeric data |
1219
|
1 |
|
throw new InvalidArgumentException(sprintf( |
1220
|
1 |
|
"%s expects collection of integers or collection of arrays of integers", |
1221
|
|
|
__METHOD__ |
1222
|
1 |
|
)); |
1223
|
|
|
} |
1224
|
17 |
|
} |
1225
|
|
|
|
1226
|
127 |
|
protected function assertArrayOrIterator($data) |
1227
|
|
|
{ |
1228
|
127 |
|
if (is_null($data) || is_array($data) || $data instanceof Iterator) { |
1229
|
126 |
|
return; |
1230
|
|
|
} |
1231
|
1 |
|
throw new InvalidArgumentException("Invalid type for collection data: " . gettype($data)); |
1232
|
|
|
} |
1233
|
|
|
} |
1234
|
|
|
|
This check looks at variables that have been passed in as parameters and are passed out again to other methods.
If the outgoing method call has stricter type requirements than the method itself, an issue is raised.
An additional type check may prevent trouble.