scratch – Blame information for rev 87

Subversion Repositories:
Rev:
Rev Author Line No. Line
87 office 1 <?php
2  
3 /*
4 * This file is part of Fusonic-linq.
5 *
6 * (c) Fusonic GmbH
7 *
8 * For the full copyright and license information, please view the LICENSE
9 * file that was distributed with this source code.
10 */
11  
12 namespace Fusonic\Linq;
13  
14 use Countable;
15 use Fusonic\Linq\Iterator\ExceptIterator;
16 use Fusonic\Linq\Iterator\DistinctIterator;
17 use Fusonic\Linq\Iterator\GroupIterator;
18 use Fusonic\Linq\Iterator\IntersectIterator;
19 use Fusonic\Linq\Iterator\OfTypeIterator;
20 use Fusonic\Linq\Iterator\OrderIterator;
21 use Fusonic\Linq\Iterator\SelectIterator;
22 use Fusonic\Linq\Iterator\SelectManyIterator;
23 use Fusonic\Linq\Iterator\WhereIterator;
24 use Fusonic\Linq\Helper\LinqHelper;
25 use IteratorAggregate;
26 use Traversable;
27 use UnexpectedValueException;
28 use InvalidArgumentException;
29 use OutOfRangeException;
30  
31 /**
32 * Linq is a simple, powerful and consistent library for querying, projecting and aggregating data in php.
33 *
34 * @author David Roth <david.roth@fusonic.net>.
35 */
36 class Linq implements IteratorAggregate, Countable
37 {
38 private $iterator;
39  
40 /**
41 * Creates a new Linq object using the provided dataSource.
42 *
43 * @param array|\Iterator|IteratorAggregate $dataSource A Traversable sequence as data source.
44 */
45 public function __construct($dataSource)
46 {
47 LinqHelper::assertArgumentIsIterable($dataSource, "dataSource");
48 $dataSource = LinqHelper::getIteratorOrThrow($dataSource);
49  
50 $this->iterator = $dataSource;
51 }
52  
53 /**
54 * Creates a new Linq object using the provided dataDataSource.
55 * This is the recommended way for getting a new Linq instance.
56 *
57 * @param array|\Iterator|IteratorAggregate $dataSource A Traversable sequence as data source.
58 * @return Linq
59 */
60 public static function from($dataSource)
61 {
62 return new Linq($dataSource);
63 }
64  
65 /**
66 * Generates a sequence of integral numbers within a specified range.
67 *
68 * @param $start The value of the first integer in the sequence.
69 * @param $count The number of sequential integers to generate.
70 * @return Linq An sequence that contains a range of sequential int numbers.
71 * @throws \OutOfRangeException
72 */
73 public static function range($start, $count)
74 {
75 if ($count < 0) {
76 throw new OutOfRangeException('$count must be not be negative.');
77 }
78  
79 return new Linq(range($start, $start + $count - 1));
80 }
81  
82 /**
83 * Filters the Linq object according to func return result.
84 *
85 * @param callback $func A func that returns boolean
86 * @return Linq Filtered results according to $func
87 */
88 public function where($func)
89 {
90 return new Linq(new WhereIterator($this->iterator, $func));
91 }
92  
93 /**
94 * Filters the Linq object according to type.
95 *
96 * @param string $type
97 *
98 * @return Linq Filtered results according to $func
99 */
100 public function ofType($type)
101 {
102 return new Linq(new OfTypeIterator($this->iterator, $type));
103 }
104  
105 /**
106 * Bypasses a specified number of elements and then returns the remaining elements.
107 *
108 * @param int $count The number of elements to skip before returning the remaining elements.
109 * @return Linq A Linq object that contains the elements that occur after the specified index.
110 */
111 public function skip($count)
112 {
113 // If its an array iterator we must check the arrays bounds are greater than the skip count.
114 // This is because the LimitIterator will use the seek() method which will throw an exception if $count > array.bounds.
115 $innerIterator = $this->iterator;
116 if ($innerIterator instanceof \ArrayIterator) {
117 if ($count >= $innerIterator->count()) {
118 return new Linq(array());
119 }
120 }
121  
122 return new Linq(new \LimitIterator($innerIterator, $count, -1));
123 }
124  
125 /**
126 * Returns a specified number of contiguous elements from the start of a sequence
127 *
128 * @param int $count The number of elements to return.
129 * @return Linq A Linq object that contains the specified number of elements from the start.
130 */
131 public function take($count)
132 {
133 if ($count == 0) {
134 return new Linq(array());
135 }
136  
137 return new Linq(new \LimitIterator($this->iterator, 0, $count));
138 }
139  
140 /**
141 * Applies an accumulator function over a sequence.
142 * The aggregate method makes it simple to perform a calculation over a sequence of values.
143 * This method works by calling $func one time for each element.
144 * The first element of source is used as the initial aggregate value if $seed parameter is not specified.
145 * If $seed is specified, this value will be used as the first value.
146 *
147 * @param callback $func An accumulator function to be invoked on each element.
148 * @param mixed $seed
149 * @throws \RuntimeException if the input sequence contains no elements.
150 * @return mixed Returns the final result of $func.
151 */
152 public function aggregate($func, $seed = null)
153 {
154 $result = null;
155 $first = true;
156  
157 if ($seed !== null) {
158 $result = $seed;
159 $first = false;
160 }
161  
162 foreach ($this->iterator as $current) {
163 if (!$first) {
164 $result = $func($result, $current);
165 } else {
166 $result = $current;
167 $first = false;
168 }
169 }
170 if ($first) {
171 throw new \RuntimeException("The input sequence contains no elements.");
172 }
173 return $result;
174 }
175  
176 /**
177 * Splits the sequence in chunks according to $chunksize.
178 *
179 * @param $chunksize Specifies how many elements are grouped together per chunk.
180 * @throws \InvalidArgumentException
181 * @return Linq
182 */
183 public function chunk($chunksize)
184 {
185 if ($chunksize < 1) {
186 throw new \InvalidArgumentException("chunksize", $chunksize);
187 }
188  
189 $i = -1;
190 return $this->select(
191 function ($x) use (&$i) {
192 $i++;
193 return array("index" => $i, "value" => $x);
194 }
195 )
196 ->groupBy(
197 function ($pair) use ($chunksize) {
198 return $pair["index"] / $chunksize;
199 }
200 )
201 ->select(
202 function (GroupedLinq $group) {
203 return $group->select(
204 function ($v) {
205 return $v["value"];
206 }
207 );
208 }
209 );
210 }
211  
212 /**
213 * Determines whether all elements satisfy a condition.
214 *
215 * @param callback $func A function to test each element for a condition.
216 * @return bool True if every element passes the test in the specified func, or if the sequence is empty; otherwise, false.
217 */
218 public function all($func)
219 {
220 foreach ($this->iterator as $current) {
221 $match = LinqHelper::getBoolOrThrowException($func($current));
222 if (!$match) {
223 return false;
224 }
225 }
226 return true;
227 }
228  
229 /**
230 * Determines whether any element exists or satisfies a condition by invoking $func.
231 *
232 * @param callback $func A function to test each element for a condition or NULL to determine if any element exists.
233 * @return bool True if no $func given and the source sequence contains any elements or True if any elements passed the test in the specified func; otherwise, false.
234 */
235 public function any($func = null)
236 {
237 foreach ($this->iterator as $current) {
238 if ($func === null) {
239 return true;
240 }
241  
242 $match = LinqHelper::getBoolOrThrowException($func($current));
243 if ($match) {
244 return true;
245 }
246 }
247 return false;
248 }
249  
250 /**
251 * Counts the elements of this Linq sequence.
252 * @return int
253 */
254 public function count()
255 {
256 if ($this->iterator instanceof Countable) {
257 return $this->iterator->count();
258 }
259  
260 return iterator_count($this->iterator);
261 }
262  
263 /**
264 * Computes the average of all numeric values. Uses $func to obtain the value on each element.
265 *
266 * @param callback $func A func that returns any numeric type (int, float etc.)
267 * @throws \UnexpectedValueException if an item of the sequence is not a numeric value.
268 * @return double Average of items
269 */
270 public function average($func = null)
271 {
272 $resultTotal = 0;
273 $itemCount = 0;
274  
275 $source = $this->getSelectIteratorOrInnerIterator($func);
276  
277 foreach ($source as $item) {
278 if (!is_numeric($item)) {
279 throw new UnexpectedValueException("Cannot calculate an average on a none numeric value");
280 }
281  
282 $resultTotal += $item;
283 $itemCount++;
284 }
285 return $itemCount == 0 ? 0 : ($resultTotal / $itemCount);
286 }
287  
288 /**
289 * Sorts the elements in ascending order according to a key provided by $func.
290 *
291 * @param callback $func A function to extract a key from an element.
292 * @return Linq A new Linq instance whose elements are sorted ascending according to a key.
293 */
294 public function orderBy($func)
295 {
296 return $this->order($func, LinqHelper::LINQ_ORDER_ASC);
297 }
298  
299 /**
300 * Sorts the elements in descending order according to a key provided by $func.
301 *
302 * @param callback $func A function to extract a key from an element.
303 * @return Linq A new Linq instance whose elements are sorted descending according to a key.
304 */
305 public function orderByDescending($func)
306 {
307 return $this->order($func, LinqHelper::LINQ_ORDER_DESC);
308 }
309  
310 private function order($func, $direction = LinqHelper::LINQ_ORDER_ASC)
311 {
312 return new Linq(new OrderIterator($this->iterator, $func, $direction));
313 }
314  
315 /**
316 * Gets the sum of all items or by invoking a transform function on each item to get a numeric value.
317 *
318 * @param callback $func A func that returns any numeric type (int, float etc.) from the given element, or NULL to use the element itself.
319 * @throws \UnexpectedValueException if any element is not a numeric value.
320 * @return double The sum of all items.
321 */
322 public function sum($func = null)
323 {
324 $sum = 0;
325 $iterator = $this->getSelectIteratorOrInnerIterator($func);
326 foreach ($iterator as $value) {
327 if (!is_numeric($value)) {
328 throw new UnexpectedValueException("sum() only works on numeric values.");
329 }
330  
331 $sum += $value;
332 }
333 return $sum;
334 }
335  
336 /**
337 * Gets the minimum item value of all items or by invoking a transform function on each item to get a numeric value.
338 *
339 * @param callback $func A func that returns any numeric type (int, float etc.) from the given element, or NULL to use the element itself.
340 * @throws \RuntimeException if the sequence contains no elements
341 * @throws \UnexpectedValueException
342 * @return double Minimum item value
343 */
344 public function min($func = null)
345 {
346 $min = null;
347 $iterator = $this->getSelectIteratorOrInnerIterator($func);
348 foreach ($iterator as $value) {
349 if (!is_numeric($value) && !is_string($value) && !($value instanceof \DateTime)) {
350 throw new UnexpectedValueException("min() only works on numeric values, strings and DateTime objects.");
351 }
352  
353 if (is_null($min)) {
354 $min = $value;
355 } elseif ($min > $value) {
356 $min = $value;
357 }
358 }
359  
360 if ($min === null) {
361 throw new \RuntimeException("Cannot calculate min() as the Linq sequence contains no elements.");
362 }
363  
364 return $min;
365 }
366  
367 /**
368 * Returns the maximum item value according to $func
369 *
370 * @param callback $func A func that returns any numeric type (int, float etc.)
371 * @throws \RuntimeException if the sequence contains no elements
372 * @throws \UnexpectedValueException if any element is not a numeric value or a string.
373 * @return double Maximum item value
374 */
375 public function max($func = null)
376 {
377 $max = null;
378 $iterator = $this->getSelectIteratorOrInnerIterator($func);
379 foreach ($iterator as $value) {
380 if (!is_numeric($value) && !is_string($value) && !($value instanceof \DateTime)) {
381 throw new UnexpectedValueException("max() only works on numeric values, strings and DateTime objects.");
382 }
383  
384 if (is_null($max)) {
385 $max = $value;
386 } elseif ($max < $value) {
387 $max = $value;
388 }
389 }
390  
391 if ($max === null) {
392 throw new \RuntimeException("Cannot calculate max() as the Linq sequence contains no elements.");
393 }
394  
395 return $max;
396 }
397  
398 /**
399 * Projects each element into a new form by invoking the selector function.
400 *
401 * @param callback $func A transform function to apply to each element.
402 * @return Linq A new Linq object whose elements are the result of invoking the transform function on each element of the original Linq object.
403 */
404 public function select($func)
405 {
406 return new Linq(new SelectIterator($this->iterator, $func));
407 }
408  
409 /**
410 * Projects each element of a sequence to a new Linq and flattens the resulting sequences into one sequence.
411 *
412 * @param callback $func A func that returns a sequence (array, Linq, Iterator).
413 * @throws \UnexpectedValueException if an element is not a traversable sequence.
414 * @return Linq A new Linq object whose elements are the result of invoking the one-to-many transform function on each element of the input sequence.
415 */
416 public function selectMany($func)
417 {
418 return new Linq(new SelectManyIterator(new SelectIterator($this->iterator, $func)));
419 }
420  
421 /**
422 * Performs the specified action on each element of the Linq sequence and returns the Linq sequence.
423 * @param callback $func A func that will be evaluated for each item in the linq sequence.
424 * @return Linq The original Linq sequence that was used to perform the foreach.
425 */
426 public function each($func)
427 {
428 foreach ($this->iterator as $item) {
429 $func($item);
430 }
431 return $this;
432 }
433  
434 /**
435 * Determines whether a sequence contains a specified element.
436 * This function will use php strict comparison (===). If you need custom comparison use the Linq::any($func) method.
437 *
438 * @param mixed $value The value to locate in the sequence.
439 * @return bool True if $value is found within the sequence; otherwise false.
440 */
441 public function contains($value)
442 {
443 return $this->any(
444 function ($x) use ($value) {
445 return $x === $value;
446 }
447 );
448 }
449  
450 /**
451 * Concatenates this Linq object with the given sequence.
452 *
453 * @param array|\Iterator $second A sequence which will be concatenated with this Linq object.
454 * @throws InvalidArgumentException if the given sequence is not traversable.
455 * @return Linq A new Linq object that contains the concatenated elements of the input sequence and the original Linq sequence.
456 */
457 public function concat($second)
458 {
459 LinqHelper::assertArgumentIsIterable($second, "second");
460  
461 $allItems = new \ArrayIterator(array($this->iterator, $second));
462  
463 return new Linq(new SelectManyIterator($allItems));
464 }
465  
466 /**
467 * Returns distinct item values of this
468 *
469 * @param callback $func
470 * @return Linq Distinct item values of this
471 */
472 public function distinct($func = null)
473 {
474 return new Linq(new DistinctIterator($this->getSelectIteratorOrInnerIterator($func)));
475 }
476  
477 /**
478 * Intersects the Linq sequence with second Iterable sequence.
479 *
480 * @param \Iterator|array An iterator to intersect with:
481 * @return Linq intersected items
482 */
483 public function intersect($second)
484 {
485 LinqHelper::assertArgumentIsIterable($second, "second");
486 return new Linq(new IntersectIterator($this->iterator, LinqHelper::getIteratorOrThrow($second)));
487 }
488  
489 /**
490 * Returns all elements except the ones of the given sequence.
491 *
492 * @param array|\Iterator $second
493 * @return Linq Returns all items of this not occuring in $second
494 */
495 public function except($second)
496 {
497 LinqHelper::assertArgumentIsIterable($second, "second");
498 return new Linq(new ExceptIterator($this->iterator, LinqHelper::getIteratorOrThrow($second)));
499 }
500  
501 /**
502 * Returns the element at a specified index.
503 * This method throws an exception if index is out of range.
504 * To instead return NULL when the specified index is out of range, use the elementAtOrNull method.
505 *
506 * @throws \OutOfRangeException if index is less than 0 or greater than or equal to the number of elements in the sequence.
507 * @param int $index
508 * @return mixed Item at $index
509 */
510 public function elementAt($index)
511 {
512 return $this->getValueAt($index, true);
513 }
514  
515 /**
516 * Returns the element at a specified index or NULL if the index is out of range.
517 *
518 * @param $index
519 * @return mixed Item at $index
520 */
521 public function elementAtOrNull($index)
522 {
523 return $this->getValueAt($index, false);
524 }
525  
526 private function getValueAt($index, $throwEx)
527 {
528 $i = 0;
529 foreach ($this->iterator as $value) {
530 if ($i == $index) {
531 return $value;
532 }
533 $i++;
534 }
535  
536 if ($throwEx) {
537 throw new OutOfRangeException("Index is less than 0 or greater than or equal to the number of elements in the sequence.");
538 }
539  
540 return null;
541 }
542  
543 /**
544 * Groups the object according to the $func generated key
545 *
546 * @param callback $keySelector a func that returns an item as key, item can be any type.
547 * @return GroupedLinq
548 */
549 public function groupBy($keySelector)
550 {
551 return new Linq(new GroupIterator($this->iterator, $keySelector));
552 }
553  
554 /**
555 * Returns the last element that satisfies a specified condition.
556 * @throws \RuntimeException if no element satisfies the condition in predicate or the source sequence is empty.
557 *
558 * @param callback $func a func that returns boolean.
559 * @return Object Last item in this
560 */
561 public function last($func = null)
562 {
563 return $this->getLast($func, true);
564 }
565  
566 /**
567 * Returns the last element that satisfies a condition or NULL if no such element is found.
568 *
569 * @param callback $func a func that returns boolean.
570 * @return mixed
571 */
572 public function lastOrNull($func = null)
573 {
574 return $this->getLast($func, false);
575 }
576  
577 /**
578 * Returns the first element that satisfies a specified condition
579 * @throws \RuntimeException if no element satisfies the condition in predicate -or- the source sequence is empty / does not match any elements.
580 *
581 * @param callback $func a func that returns boolean.
582 * @return mixed
583 */
584 public function first($func = null)
585 {
586 return $this->getFirst($func, true);
587 }
588  
589 /**
590 * Returns the first element, or NULL if the sequence contains no elements.
591 *
592 * @param callback $func a func that returns boolean.
593 * @return mixed
594 */
595 public function firstOrNull($func = null)
596 {
597 return $this->getFirst($func, false);
598 }
599  
600 /**
601 * Returns the only element that satisfies a specified condition.
602 *
603 * @throws \RuntimeException if no element exists or if more than one element exists.
604 * @param callback $func a func that returns boolean.
605 * @return mixed
606 */
607 public function single($func = null)
608 {
609 return $this->getSingle($func, true);
610 }
611  
612 /**
613 * Returns the only element that satisfies a specified condition or NULL if no such element exists.
614 *
615 * @throws \RuntimeException if more than one element satisfies the condition.
616 * @param callback $func a func that returns boolean.
617 * @return mixed
618 */
619 public function singleOrNull($func = null)
620 {
621 return $this->getSingle($func, false);
622 }
623  
624  
625 private function getWhereIteratorOrInnerIterator($func)
626 {
627 return $func === null ? $this->iterator : new WhereIterator($this->iterator, $func);
628 }
629  
630 private function getSelectIteratorOrInnerIterator($func)
631 {
632 return $func === null ? $this->iterator : new SelectIterator($this->iterator, $func);
633 }
634  
635 private function getSingle($func, $throw)
636 {
637 $source = $this->getWhereIteratorOrInnerIterator($func);
638  
639 $count = 0;
640 $single = null;
641  
642 foreach ($source as $stored) {
643 $count++;
644  
645 if ($count > 1) {
646 throw new \RuntimeException("The input sequence contains more than 1 elements.");
647 }
648  
649 $single = $stored;
650 }
651  
652 if ($count == 0 && $throw) {
653 throw new \RuntimeException("The input sequence contains no matching element.");
654 }
655  
656 return $single;
657 }
658  
659 private function getFirst($func, $throw)
660 {
661 $source = $this->getWhereIteratorOrInnerIterator($func);
662  
663 $count = 0;
664 $first = null;
665  
666 foreach ($source as $stored) {
667 $count++;
668 $first = $stored;
669 break;
670 }
671  
672 if ($count == 0 && $throw) {
673 throw new \RuntimeException("The input sequence contains no matching element.");
674 }
675  
676 return $first;
677 }
678  
679 private function getLast($func, $throw)
680 {
681 $source = $this->getWhereIteratorOrInnerIterator($func);
682  
683 $count = 0;
684 $last = null;
685  
686 foreach ($source as $stored) {
687 $count++;
688 $last = $stored;
689 }
690  
691 if ($count == 0 && $throw) {
692 throw new \RuntimeException("The input sequence contains no matching element.");
693 }
694  
695 return $last;
696 }
697  
698 /**
699 * Creates an Array from this Linq object with key/value selector(s).
700 *
701 * @param callback $keySelector a func that returns the array-key for each element.
702 * @param callback $valueSelector a func that returns the array-value for each element.
703 *
704 * @return Array An array with all values.
705 */
706 public function toArray($keySelector = null, $valueSelector = null)
707 {
708 if ($keySelector === null && $valueSelector === null) {
709 return iterator_to_array($this, false);
710 } elseif ($keySelector == null) {
711 return iterator_to_array(new SelectIterator($this->getIterator(), $valueSelector), false);
712 } else {
713 $array = array();
714 foreach ($this as $value) {
715 $key = $keySelector($value);
716 $array[$key] = $valueSelector == null ? $value : $valueSelector($value);
717 }
718 return $array;
719 }
720 }
721  
722 /**
723 * Retrieves the iterator of this Linq class.
724 * @link http://php.net/manual/en/iteratoraggregate.getiterator.php
725 * @return Traversable An instance of an object implementing <b>Iterator</b> or
726 * <b>Traversable</b>
727 */
728 public function getIterator()
729 {
730 return $this->iterator;
731 }
732 }