diff --git a/docs/source/components/dataset/concepts.rst b/docs/source/components/dataset/concepts.rst index 94973a4..5048390 100644 --- a/docs/source/components/dataset/concepts.rst +++ b/docs/source/components/dataset/concepts.rst @@ -2,9 +2,9 @@ Concepts ======== -Library defines couple of building blocks for processing streams of data. It is -advisable to introduce yourself with concepts defined in this document and then -read about how to use and extend the library. +The library defines several building blocks for processing data streams. It is +recommended that you familiarize yourself with the concepts described in this +document before reading about how to use and extend the library. .. contents:: :depth: 1 @@ -17,36 +17,33 @@ Stream source .. _iterable: https://www.php.net/manual/en/language.types.iterable.php -Stream source (or collection) is any iterable which can be iterated through, -which means either an ``array`` or instance of ``\Traversable``. In short, -any iterable_. +A stream source (or collection) is any iterable that can be iterated over, which +means either an ``array`` or an instance of ``\Traversable``. In short, it is +any iterable_. -Each stream source emits some value, indexed by key. Key is usually associated -with ``int`` or ``string`` as we rely on arrays in PHP a lot. However, library -assumes any ``iterable``, which includes, but not limits to: +Each stream source emits values indexed by a key. The key is usually an ``int`` +or ``string``, as PHP arrays are commonly used. However, the library assumes any +``iterable``, including, but not limited to: -* ``\Generator``, which may emit anything as a key, +* ``\Generator``, which may emit values with any type of key, * ``\WeakMap``, which emits objects as a key, -* and so on... +* and so on. -Common denominator for stream source is that it is not rewindable. Generators, -per example, can not be rewind, you can not iterate them twice. For that reason, -even if you use an arrays (or any rewindable stream source), library assumes -that stream source is not rewindable. +The common characteristic of a stream source is that it is not rewindable. +Generators, for example, cannot be rewound; you cannot iterate over them twice. +For this reason, even when using arrays (or any other rewindable stream source), +the library assumes that the stream source is not rewindable. Data stream, or stream wrapper ------------------------------ -Data stream (or stream wrapper) is ``RunOpenCode\Component\Dataset\Stream`` -class which wraps stream source providing stream processing using operators, -reducers and collectors. +A data stream (or stream wrapper) is the +``RunOpenCode\Component\Dataset\Stream`` class, which wraps a stream source and +provides stream processing using operators, reducers, collectors, and +aggregators (which will be discussed later in the document). -Class is deliberately not final and allow extension in order for you to be able -to integrate your own custom operators, reducers and collectors - should you -need to do so. - -Using object oriented approach, with instance of data stream, you may apply -various operations on your source of data utilizing fluent API. +Using an object-oriented approach, you can apply various operations to your data +source through the fluent API provided by the data stream instance. .. code-block:: php :linenos: @@ -57,14 +54,14 @@ various operations on your source of data utilizing fluent API. Stream::create(/* ... */) ->map(/* ... */) - ->batch(/* ... */) + ->tap(/* ... */) ->takeUntil(/* ... */) ->finally(/* ... */); .. _pipe operator: https://wiki.php.net/rfc/pipe-operator-v3 -Having in mind PHP 8.5, library provides a functions as well to support -functional approach using `pipe operator`_: +With PHP 8.5 in mind, the library also provides functions to support a +functional approach using the `pipe operator`_. .. code-block:: php :linenos: @@ -73,44 +70,43 @@ functional approach using `pipe operator`_: use function RunOpenCode\Component\Dataset\stream; use function RunOpenCode\Component\Dataset\map; - use function RunOpenCode\Component\Dataset\batch; + use function RunOpenCode\Component\Dataset\tap; use function RunOpenCode\Component\Dataset\takeUntil; use function RunOpenCode\Component\Dataset\finally; stream(/* ... */) |> map(/* ... */) - |> batch(/* ... */) + |> tap(/* ... */) |> takeUntil(/* ... */) |> finally(/* ... */); -Data stream is, of course, iterable and none of the operators are applied until -stream is being iterated. +A data stream is, of course, iterable, and none of the operators are applied +until the stream is iterated. Operators --------- -You use operators to execute some "operations" against the stream of data. -Operators operate on yielded value, one by one, and they yield result of their -operations. +Operators are used to perform specific operations on a data stream. They process +each yielded value one by one and yield the result of their operation. -Library delivers a set of commonly used operators, such as ``map()``, -``filter()``, ``take()``, etc. However, you may expand set of operators by -writing your own. +The library provides a set of commonly used operators, such as ``map()``, +``filter()``, ``take()``, and others. However, you can extend the available set +of operators by implementing your own. -General idea is that with operators, you execute various operations reading -from and/or modifying original stream. +The general idea behind operators is to execute various operations that read +from and/or modify the original stream as it is being iterated. Reducers -------- -Reducers iterate over the stream of data and reduce all of them into one single -value of any kind. Common examples of reducers are ``sum()``, ``average()``, -``min()``, ``max()``, etc. which are delivered with this library. +Reducers iterate over a data stream and reduce all elements into a single value +of any kind. Common examples of reducers include ``sum()``, ``average()``, +``min()``, ``max()``, all of which are provided by this library. -However, reducers are design to be iterable as well, and may be applied as -aggregators (which is a new concept defined by this library) which enables you -to apply reducer on stream and get both reduced value as well as iterate through -stream. +However, reducers are designed to be iterable as well and can be applied as +aggregators (a concept introduced by this library, explained later in this +document). This allows you to apply a reducer to a stream while still being able +to iterate over it and obtain the reduced value at the same time. .. code-block:: php :linenos: @@ -126,17 +122,17 @@ stream. Collectors ---------- -When operators (and aggregators) are applied on stream, you can get to stream -data just by iterating. +When operators (and aggregators) are applied to a stream, you can access the +stream data simply by iterating over it. -Sometimes you want to collect all of that data into some data structure to -continue with processing using some other method. +Sometimes, however, you may want to collect all the data into a specific data +structure for further processing using other methods. -Library, in that matter, supports such concept and provides common collectors -such as ``RunOpenCode\Component\Dataset\Collector\ArrayCollector`` which -collects everything into array, or -``RunOpenCode\Component\Dataset\Collector\ListCollector`` which collects -everything into numeric ordered array and so on. +The library supports this concept and provides common collectors, such as +``RunOpenCode\Component\Dataset\Collector\ArrayCollector``, which collects all +items into an array, or +``RunOpenCode\Component\Dataset\Collector\ListCollector``, which collects items +into a numerically ordered array, and more. .. code-block:: php :linenos: @@ -161,16 +157,16 @@ everything into numeric ordered array and so on. Aggregators ----------- -Aggregators are concept introduced with this library. General idea is that you -can both iterate stream with applied operators and calculate reduced value -simultaneously. +Aggregators are a concept introduced by this library. The general idea is that +you can iterate over a stream with applied operators while simultaneously +calculating a reduced value in a single pass. -This is useful when, per example, you are rendering a table of financial data -and at the bottom of table you want to render total and/or average sum, or -similar. +This is useful, for example, when rendering a table of financial data and you +want to display totals, averages, or similar summary values at the bottom of the +table. -Aggregators are "attached" reducers to a stream and can be accessed when stream -is fully iterated. +Aggregators are essentially "attached" reducers to a stream and can be accessed +once the stream has been fully iterated. .. code-block:: php :linenos: @@ -188,8 +184,7 @@ is fully iterated. echo "\n"; } - echo $stream->aggregators('sum'); + echo $stream->aggregated['sum']; -Knowing the concepts applied within this library, you may proceed with further -reading of documentation for this library. - \ No newline at end of file +With an understanding of the concepts used in this library, you can now proceed +with the rest of the documentation. diff --git a/docs/source/components/dataset/index.rst b/docs/source/components/dataset/index.rst index fd7594d..142d9e3 100644 --- a/docs/source/components/dataset/index.rst +++ b/docs/source/components/dataset/index.rst @@ -2,29 +2,28 @@ Dataset component ================= -This library is heavily inspired by `Java Stream API`_ for dealing with -collections in functionali(ish), declarative way. In some way, it is inspired -with `ReactiveX`_ as well, only with much, much simpler approach and with less -features, of course. +This library is heavily inspired by the `Java Stream API`_ for working with +collections in a functional(ish), declarative way. In some aspects, it is also +inspired by `ReactiveX`_, but with a much simpler approach and far fewer +features. If your problem can be described as: - I have a data stream from some source (file, database query result, etc.) and - I want to iterate through its records and do some processing with small - memory footprint. + I have a data stream from some source (file, database query result, etc.), + and I want to iterate over its records and process them using a small memory + footprint. -this is the library which can help you achieve that goal by using declarative -approach. +then this library can help you achieve that goal using a declarative approach. -There are several PHP implementations of same idea, however, this implementation -focuses on PHP ``iterable`` assuming that underlying implementation is most -probably instance of `Generator`_. Of course, it will work with ``array`` data -type, or anything which implements ``\Traversable``, however, power of this -library is in its focus of simple declarative data stream processing with small -memory footprint. +There are several PHP implementations of this idea; however, this implementation +focuses on PHP iterable values, assuming that the underlying implementation +is most likely an instance of `Generator`_. Of course, it also works with the +``array`` data type or anything that implements ``\Traversable``. The real +strength of this library lies in its focus on simple, declarative data stream +processing with minimal memory usage. If you need full fledged `ReactiveX`_ in PHP, please take a look at the official -implementation of the specification at `RxPHP`_. +implementation of the specification: `RxPHP`_. .. _Java Stream API: https://docs.oracle.com/javase/8/docs/api/java/util/stream/Stream.html .. _ReactiveX: https://reactivex.io @@ -35,13 +34,14 @@ Features -------- * Declarative approach to process data streams. -* Designed to work with any ``iterable`` using as less as possible memory. -* Provides bunch of operators, reducers and collectors, which can be easily +* Designed to work with any ``iterable`` while using as little memory as + possible. +* Provides a set of operators, reducers, and collectors that can be easily extended or added as needed. * Focused on small memory consumption during processing. -* Introduces concept of **aggregators** allowing you to simultaneously process - stream and reduce (aggregate) values during processing without breaking the - data stream. +* Introduces the concept of **aggregators**, allowing you to process a stream + and reduce (aggregate) values simultaneously without interrupting the data + stream. Table of Contents ----------------- @@ -60,10 +60,11 @@ Table of Contents Quick example ------------- -A simple example of using this library for listing online transactions is given -below. Assume that we want to display list of online transactions executed in -some time period, and we want to show total amount for each individual currency -as well as in total, this would be a way to do that using this library: +A simple example of using this library to list online transactions is shown +below. Assume that we want to display a list of online transactions executed +within a certain time period, and we also want to calculate the total amount for +each currency as well as the overall total. This is how it can be done using +this library: .. code-block:: php :linenos: @@ -120,29 +121,28 @@ as well as in total, this would be a way to do that using this library: } } -**Explanation of the code:** On line no 35 we fetch data from database. PHP -returns iterable which is pointer on the first row of the returned dataset, -which means that no rows are loaded into memory of the PHP virtual machine. +**Explanation of the code:** On line 35 we fetch data from database. PHP returns +an iterable that points to the first row of the result set, which means that no +rows are loaded into the PHP virtual machine's memory upfront. Line 41 wraps that iterable into instance of -``RunOpenCode\Component\Dataset\Stream`` and then we apply operations which we -want to conduct against the stream during its iteration. +``RunOpenCode\Component\Dataset\Stream``. We then apply the operations that +should be executed while iterating over the stream. -Line 42 applies aggregator which will sum all transactions executed using -``EUR`` as currency, line 43 does that for ``USD``. +Line 42 applies aggregator that sums all transactions executed in ``EUR``. Line +43 does the same for ``USD``. -Line 44 will add new column to the row, ``converted`` which will convert all -amounts to ``EUR`` using given conversion rate. +Line 44 adds a new column to each row, ``converted`` which will convert all +amounts to ``EUR`` using the provided conversion rate. -Lastly, lines 48 and 49 will apply aggregator which will provide us with total -sum of all transactions in ``EUR`` as well as average transaction amount in -``EUR``. +Finally, lines 48 and 49 apply aggregators that calculate the total sum of all +transactions in ``EUR`` as well as the average transaction amount in ``EUR``. -**None of the processing is executed, until you iterate stream**. Iterable is -just wrapped with processing logic, to execute it, you need to iterate it. You -will probably do that in some templating language. However, example below will -just use ``echo`` to demonstrate concept: +**None of the processing is executed, until the stream is iterated**. The +iterable is only wrapped with processing logic. To execute it, you must iterate +over it. In practice, this will often be done in a templating engine. +The example below uses ``echo`` to demonstrate the concept: .. code-block:: php :linenos: @@ -162,9 +162,9 @@ just use ``echo`` to demonstrate concept: } // Since we iterated, our aggregated values are available too. - echo \sprint('Total in EUR: %d', $stream->aggregators['total_converted']); + echo \sprintf('Total in EUR: %d', $stream->aggregated['total_converted']); echo "\n"; - echo \sprint('Average in EUR: %d', $stream->aggregators['average_converted']); + echo \sprintf('Average in EUR: %d', $stream->aggregated['average_converted']); So, during this process, memory footprint is almost as low as amount of memory required for storing one row. diff --git a/docs/source/components/dataset/installation.rst b/docs/source/components/dataset/installation.rst index 8fb2abd..84db03c 100644 --- a/docs/source/components/dataset/installation.rst +++ b/docs/source/components/dataset/installation.rst @@ -9,5 +9,5 @@ following command in your terminal: composer require runopencode/dataset -Nothing more is required, no additional initialization and/or configration. Just -use the library classes. +Nothing more is required, no additional initialization and/or configuration. +Just use the library classes/functions. diff --git a/docs/source/components/dataset/stream/index.rst b/docs/source/components/dataset/stream/index.rst index 1a178ea..29ed628 100644 --- a/docs/source/components/dataset/stream/index.rst +++ b/docs/source/components/dataset/stream/index.rst @@ -1,3 +1,58 @@ ====== Stream ====== + +The main purpose of the ``RunOpenCode\Component\Dataset\Stream`` class is to +wrap an ``iterable`` and provide a convenient abstraction for processing streams +of data. By wrapping an ``iterable``, the class allows you to perform multiple +operations (mapping, filtering, tapping into stream, etc.) in a declarative and +composable manner without loading the entire collection into memory. + +Further on, stream can simultaneously aggregate data, reduced it to single value +or collect data into some convenient data structure using collectors. + +Fluent API +---------- + +With an instance of ``RunOpenCode\Component\Dataset\Stream``, you can apply a +variety of operators to transform or filter the data. Operators process each +item in the stream lazily, meaning that no computation is performed until the +stream is iterated. + +Examples of some of the available operators include: + +* ``map()`` – transform each value in the stream. +* ``filter()`` – include only values that meet a given condition. +* ``take()``, ``skip()``, ``distinct()`` – control which items are emitted. +* ``sort()``, ``reverse()`` – ordering operators (note: these load the entire stream into memory). +* etc. + +Additionally, ``RunOpenCode\Component\Dataset\Stream`` supports aggregators, +which are attached reducers that compute a reduced value while the stream is +being iterated. This allows you to process a stream and calculate totals, +averages, or other summary values simultaneously without breaking the data flow. + + **Applying operators and aggregators on stream DOES NOT break the stream and + its fluent API.** + +With the fluent API, you are able to write stream processing code in declarative +manner: + +.. code-block:: php + :linenos: + + map(/* ... */) + ->tap(/* ... */) + ->takeUntil(/* ... */) + ->finally(/* ... */); + +Internals +--------- + +Class ``RunOpenCode\Component\Dataset\Stream`` implements +``RunOpenCode\Component\Dataset\Contract\StreamInterface``. While \ No newline at end of file diff --git a/src/RunOpenCode/Component/Dataset/src/AbstractStream.php b/src/RunOpenCode/Component/Dataset/src/AbstractStream.php index 4153af2..0743570 100644 --- a/src/RunOpenCode/Component/Dataset/src/AbstractStream.php +++ b/src/RunOpenCode/Component/Dataset/src/AbstractStream.php @@ -38,7 +38,19 @@ abstract class AbstractStream implements StreamInterface } /** - * Check if stream has been iterated through. + * {@inheritdoc} + */ + final public array $aggregated { + get { + return \array_map( + static fn(AggregatorInterface $aggregator): mixed => $aggregator->value, + $this->registry->aggregators, + ); + } + } + + /** + * {@inheritdoc} */ final public bool $closed = false { get { diff --git a/src/RunOpenCode/Component/Dataset/src/Aggregator/Aggregator.php b/src/RunOpenCode/Component/Dataset/src/Aggregator/Aggregator.php index e8daa20..0130129 100644 --- a/src/RunOpenCode/Component/Dataset/src/Aggregator/Aggregator.php +++ b/src/RunOpenCode/Component/Dataset/src/Aggregator/Aggregator.php @@ -6,7 +6,7 @@ use RunOpenCode\Component\Dataset\AbstractStream; use RunOpenCode\Component\Dataset\Contract\AggregatorInterface; -use RunOpenCode\Component\Dataset\Contract\ReducerInterface; +use RunOpenCode\Component\Dataset\Operator\Reduce; /** * Aggregator. @@ -34,12 +34,12 @@ final class Aggregator extends AbstractStream implements AggregatorInterface /** * Create new instance of aggregator. * - * @param non-empty-string $name Name of the aggregator. - * @param ReducerInterface $reducer Reducer instance. + * @param non-empty-string $name Name of the aggregator. + * @param Reduce $reducer Instance of reduce operator. */ public function __construct( - public readonly string $name, - private readonly ReducerInterface $reducer + public readonly string $name, + private readonly Reduce $reducer ) { parent::__construct($this->reducer); } diff --git a/src/RunOpenCode/Component/Dataset/src/Collector/ArrayCollector.php b/src/RunOpenCode/Component/Dataset/src/Collector/ArrayCollector.php index e76a07a..17a2484 100644 --- a/src/RunOpenCode/Component/Dataset/src/Collector/ArrayCollector.php +++ b/src/RunOpenCode/Component/Dataset/src/Collector/ArrayCollector.php @@ -4,7 +4,6 @@ namespace RunOpenCode\Component\Dataset\Collector; -use RunOpenCode\Component\Dataset\Contract\AggregatorInterface; use RunOpenCode\Component\Dataset\Contract\CollectorInterface; use RunOpenCode\Component\Dataset\Contract\StreamInterface; use RunOpenCode\Component\Dataset\Exception\LogicException; @@ -32,7 +31,9 @@ final class ArrayCollector implements \IteratorAggregate, \Countable, \ArrayAcce /** * {@inheritdoc} */ - public private(set) array $aggregators; + public array $aggregated { + get => $this->collection instanceof StreamInterface ? $this->collection->aggregated : []; + } /** * {@inheritdoc} @@ -47,11 +48,7 @@ final class ArrayCollector implements \IteratorAggregate, \Countable, \ArrayAcce public function __construct( public readonly iterable $collection, ) { - $this->value = iterable_to_array($this->collection); - $this->aggregators = $this->collection instanceof StreamInterface ? \array_map( - static fn(AggregatorInterface $aggregator): mixed => $aggregator->value, - $this->collection->aggregators, - ) : []; + $this->value = iterable_to_array($this->collection); } /** diff --git a/src/RunOpenCode/Component/Dataset/src/Collector/CursoredCollector.php b/src/RunOpenCode/Component/Dataset/src/Collector/CursoredCollector.php index 093a3a6..4b894da 100644 --- a/src/RunOpenCode/Component/Dataset/src/Collector/CursoredCollector.php +++ b/src/RunOpenCode/Component/Dataset/src/Collector/CursoredCollector.php @@ -4,7 +4,6 @@ namespace RunOpenCode\Component\Dataset\Collector; -use RunOpenCode\Component\Dataset\Contract\AggregatorInterface; use RunOpenCode\Component\Dataset\Contract\CollectorInterface; use RunOpenCode\Component\Dataset\Contract\StreamInterface; use RunOpenCode\Component\Dataset\Exception\LogicException; @@ -38,9 +37,13 @@ final class CursoredCollector implements \IteratorAggregate, CollectorInterface /** * {@inheritdoc} */ - public array $aggregators { + public array $aggregated { get { - return $this->aggregators ?? []; + if (!$this->closed) { + throw new LogicException('Collector must be iterated first.'); + } + + return $this->aggregated; } } @@ -54,10 +57,6 @@ final class CursoredCollector implements \IteratorAggregate, CollectorInterface */ public ?int $previous { get { - if (!$this->closed) { - throw new LogicException('Collector must be fully iterated first.'); - } - if ($this->offset <= 0) { return null; } @@ -71,7 +70,7 @@ final class CursoredCollector implements \IteratorAggregate, CollectorInterface */ public ?int $next { get { - if (!$this->closed) { + if (!$this->exhausted) { throw new LogicException('Collector must be fully iterated first.'); } @@ -90,9 +89,9 @@ final class CursoredCollector implements \IteratorAggregate, CollectorInterface /** * Indicates whether there are more items available after current collection is fully iterated. */ - private bool $hasMore { + public bool $hasMore { get { - if (!$this->closed) { + if (!$this->exhausted) { throw new LogicException('Collector must be fully iterated first.'); } @@ -100,6 +99,11 @@ final class CursoredCollector implements \IteratorAggregate, CollectorInterface } } + /** + * Denotes if collection has been fully iterated. + */ + private bool $exhausted = false; + /** * @param iterable $collection Collection to collect. */ @@ -116,29 +120,30 @@ public function __construct( */ public function getIterator(): \Traversable { - $iteration = 0; - $this->closed = true; + $iteration = 0; + $this->closed = true; + $this->aggregated = []; foreach ($this->collection as $key => $value) { $iteration++; if (null !== $this->limit && $iteration === $this->limit) { yield $key => $value; - $this->aggregators = \array_map( - static fn(AggregatorInterface $aggregator): mixed => $aggregator->value, - $this->collection instanceof StreamInterface ? $this->collection->aggregators : [], - ); + $this->aggregated = $this->collection instanceof StreamInterface ? $this->collection->aggregated : []; continue; } if (null !== $this->limit && $iteration > $this->limit) { - $this->hasMore = true; + $this->hasMore = true; + $this->exhausted = true; return; } + $this->aggregated = $this->collection instanceof StreamInterface ? $this->collection->aggregated : []; yield $key => $value; } - $this->hasMore = false; + $this->hasMore = false; + $this->exhausted = true; } } diff --git a/src/RunOpenCode/Component/Dataset/src/Collector/IterableCollector.php b/src/RunOpenCode/Component/Dataset/src/Collector/IterableCollector.php index eeabda5..07a1454 100644 --- a/src/RunOpenCode/Component/Dataset/src/Collector/IterableCollector.php +++ b/src/RunOpenCode/Component/Dataset/src/Collector/IterableCollector.php @@ -5,12 +5,14 @@ namespace RunOpenCode\Component\Dataset\Collector; use RunOpenCode\Component\Dataset\Contract\CollectorInterface; +use RunOpenCode\Component\Dataset\Contract\StreamInterface; +use RunOpenCode\Component\Dataset\Exception\LogicException; /** * Collect as original iterable. * * Allows you to iterate through whole dataset providing you the access to - * aggregators when collection is fully iterated. + * aggregators when collection is iterated. * * @template TKey * @template TValue @@ -32,9 +34,13 @@ final class IterableCollector implements \IteratorAggregate, CollectorInterface /** * {@inheritdoc} */ - public array $aggregators { + public array $aggregated { get { - return $this->aggregators ?? []; + if (!$this->closed) { + throw new LogicException('Collector must be iterated first.'); + } + + return $this->collection instanceof StreamInterface ? $this->collection->aggregated : []; } } diff --git a/src/RunOpenCode/Component/Dataset/src/Collector/ListCollector.php b/src/RunOpenCode/Component/Dataset/src/Collector/ListCollector.php index 94de501..3269a60 100644 --- a/src/RunOpenCode/Component/Dataset/src/Collector/ListCollector.php +++ b/src/RunOpenCode/Component/Dataset/src/Collector/ListCollector.php @@ -4,7 +4,6 @@ namespace RunOpenCode\Component\Dataset\Collector; -use RunOpenCode\Component\Dataset\Contract\AggregatorInterface; use RunOpenCode\Component\Dataset\Contract\CollectorInterface; use RunOpenCode\Component\Dataset\Contract\StreamInterface; use RunOpenCode\Component\Dataset\Exception\LogicException; @@ -32,7 +31,9 @@ final class ListCollector implements \IteratorAggregate, \Countable, \ArrayAcces /** * {@inheritdoc} */ - public private(set) array $aggregators; + public array $aggregated { + get => $this->collection instanceof StreamInterface ? $this->collection->aggregated : []; + } /** * {@inheritdoc} @@ -47,11 +48,7 @@ final class ListCollector implements \IteratorAggregate, \Countable, \ArrayAcces public function __construct( private readonly iterable $collection, ) { - $this->value = iterable_to_list($this->collection); - $this->aggregators = $this->collection instanceof StreamInterface ? \array_map( - static fn(AggregatorInterface $aggregator): mixed => $aggregator->value, - $this->collection->aggregators, - ) : []; + $this->value = iterable_to_list($this->collection); } /** diff --git a/src/RunOpenCode/Component/Dataset/src/Contract/CollectorInterface.php b/src/RunOpenCode/Component/Dataset/src/Contract/CollectorInterface.php index 518c5a9..fa54143 100644 --- a/src/RunOpenCode/Component/Dataset/src/Contract/CollectorInterface.php +++ b/src/RunOpenCode/Component/Dataset/src/Contract/CollectorInterface.php @@ -25,7 +25,7 @@ interface CollectorInterface * * @var array */ - public array $aggregators { + public array $aggregated { get; } diff --git a/src/RunOpenCode/Component/Dataset/src/Contract/ReducerInterface.php b/src/RunOpenCode/Component/Dataset/src/Contract/ReducerInterface.php index 5f42840..a8e6dc1 100644 --- a/src/RunOpenCode/Component/Dataset/src/Contract/ReducerInterface.php +++ b/src/RunOpenCode/Component/Dataset/src/Contract/ReducerInterface.php @@ -7,20 +7,30 @@ /** * Interface for dataset reducers. * + * Each reducer is a simple, stateful class instance which does + * data reduction. For each iteration, aggregates the value and + * stores it into value property. + * * @template TKey * @template TValue * @template TReducedValue - * - * @extends \IteratorAggregate */ -interface ReducerInterface extends \IteratorAggregate +interface ReducerInterface { /** - * Get reduced value. + * Reduced value. * * @var TReducedValue */ public mixed $value { get; } + + /** + * Provide key and value from next iteration for reduction. + * + * @param TValue $value Value from current iteration. + * @param TKey $key Key from current iteration. + */ + public function next(mixed $value, mixed $key): void; } diff --git a/src/RunOpenCode/Component/Dataset/src/Contract/StreamInterface.php b/src/RunOpenCode/Component/Dataset/src/Contract/StreamInterface.php index cd889a7..d76aa3d 100644 --- a/src/RunOpenCode/Component/Dataset/src/Contract/StreamInterface.php +++ b/src/RunOpenCode/Component/Dataset/src/Contract/StreamInterface.php @@ -33,7 +33,23 @@ interface StreamInterface extends \IteratorAggregate } /** - * Check if stream has been iterated through. + * Get aggregated values collected during iteration process. + * + * @var array + */ + public array $aggregated { + get; + } + + /** + * Check if stream has been iterated. + * + * Do note that this denotes only if iteration started, not + * if stream has been fully iterated. + * + * This information may be used to determine if stream can + * be iterated or not as implementation assumes that all + * streams can not be rewound. */ public bool $closed { get; diff --git a/src/RunOpenCode/Component/Dataset/src/Operator/Distinct.php b/src/RunOpenCode/Component/Dataset/src/Operator/Distinct.php index 5c2f267..be46d7c 100644 --- a/src/RunOpenCode/Component/Dataset/src/Operator/Distinct.php +++ b/src/RunOpenCode/Component/Dataset/src/Operator/Distinct.php @@ -39,7 +39,7 @@ * @template TKey * @template TValue * - * @phpstan-type IdentityCallable = callable(TValue, TKey): string + * @phpstan-type IdentityCallable = callable(TValue, TKey=): string * * @extends AbstractStream * @implements OperatorInterface diff --git a/src/RunOpenCode/Component/Dataset/src/Operator/Filter.php b/src/RunOpenCode/Component/Dataset/src/Operator/Filter.php index 4c76d8d..961dab2 100644 --- a/src/RunOpenCode/Component/Dataset/src/Operator/Filter.php +++ b/src/RunOpenCode/Component/Dataset/src/Operator/Filter.php @@ -32,7 +32,7 @@ * @template TKey * @template TValue * - * @phpstan-type FilterCallable = callable(TValue, TKey): bool + * @phpstan-type FilterCallable = callable(TValue, TKey=): bool * * @extends AbstractStream * @implements OperatorInterface diff --git a/src/RunOpenCode/Component/Dataset/src/Operator/Map.php b/src/RunOpenCode/Component/Dataset/src/Operator/Map.php index 1cca791..10508f3 100644 --- a/src/RunOpenCode/Component/Dataset/src/Operator/Map.php +++ b/src/RunOpenCode/Component/Dataset/src/Operator/Map.php @@ -6,7 +6,6 @@ use RunOpenCode\Component\Dataset\AbstractStream; use RunOpenCode\Component\Dataset\Contract\OperatorInterface; -use RunOpenCode\Component\Dataset\Stream; /** * Map operator. @@ -35,8 +34,8 @@ * @template TModifiedKey * @template TModifiedValue * - * @phpstan-type MapValueTransform = callable(TValue, TKey): TModifiedValue - * @phpstan-type MapKeyTransform = callable(TKey, TValue): TModifiedKey + * @phpstan-type ValueTransformCallable = callable(TValue, TKey=): TModifiedValue + * @phpstan-type KeyTransformCallable = callable(TKey, TValue=): TModifiedKey * * @extends AbstractStream * @implements OperatorInterface @@ -48,9 +47,9 @@ final class Map extends AbstractStream implements OperatorInterface private readonly \Closure $keyTransform; /** - * @param iterable $collection Collection to iterate over. - * @param MapValueTransform $valueTransform User defined callable to transform item values. - * @param MapKeyTransform|null $keyTransform User defined callable to transform item keys. If null, original keys are preserved. + * @param iterable $collection Collection to iterate over. + * @param ValueTransformCallable $valueTransform User defined callable to transform item values. + * @param KeyTransformCallable|null $keyTransform User defined callable to transform item keys. If null, original keys are preserved. */ public function __construct( private readonly iterable $collection, diff --git a/src/RunOpenCode/Component/Dataset/src/Operator/Reduce.php b/src/RunOpenCode/Component/Dataset/src/Operator/Reduce.php new file mode 100644 index 0000000..3661f02 --- /dev/null +++ b/src/RunOpenCode/Component/Dataset/src/Operator/Reduce.php @@ -0,0 +1,62 @@ + + * @implements OperatorInterface + * + * @internal + */ +final class Reduce extends AbstractStream implements OperatorInterface +{ + /** + * Current reduced value. + * + * @var TReducedValue + */ + public mixed $value { + get => $this->closed ? $this->reducer->value : throw new LogicException('Stream is not iterated.'); + } + + /** + * Create reducing operator. + * + * @param iterable $collection Collection of values to reduce. + * @param ReducerInterface $reducer Reducer to use. + */ + public function __construct( + private readonly iterable $collection, + private readonly ReducerInterface $reducer, + ) { + parent::__construct($this->collection); + } + + /** + * {@inheritdoc} + */ + protected function iterate(): \Traversable + { + foreach ($this->collection as $key => $value) { + $this->reducer->next($value, $key); + + yield $key => $value; + } + } +} diff --git a/src/RunOpenCode/Component/Dataset/src/Operator/Sort.php b/src/RunOpenCode/Component/Dataset/src/Operator/Sort.php index 03c9d41..25b8e88 100644 --- a/src/RunOpenCode/Component/Dataset/src/Operator/Sort.php +++ b/src/RunOpenCode/Component/Dataset/src/Operator/Sort.php @@ -40,8 +40,8 @@ * @template TKey * @template TValue * - * @phpstan-type KeyComparator = callable(TKey, TKey): int - * @phpstan-type ValueComparator = callable(TValue, TValue): int + * @phpstan-type KeyComparatorCallable = callable(TKey, TKey): int + * @phpstan-type ValueComparatorCallable = callable(TValue, TValue): int * * @extends AbstractStream * @implements OperatorInterface @@ -51,9 +51,9 @@ final class Sort extends AbstractStream implements OperatorInterface private readonly \Closure $sorter; /** - * @param iterable $collection Collection to iterate over. - * @param ($byKeys is true ? ValueComparator : KeyComparator)|null $comparator User defined callable to compare two items. If null, spaceship operator (<=>) is used. - * @param bool $byKeys If `byKeys` is true, keys will be compared instead of values. + * @param iterable $collection Collection to iterate over. + * @param ($byKeys is true ? ValueComparatorCallable : KeyComparatorCallable)|null $comparator User defined callable to compare two items. If null, spaceship operator (<=>) is used. + * @param bool $byKeys If `byKeys` is true, keys will be compared instead of values. */ public function __construct( private readonly iterable $collection, diff --git a/src/RunOpenCode/Component/Dataset/src/Reducer/Average.php b/src/RunOpenCode/Component/Dataset/src/Reducer/Average.php index dab981f..e568df1 100644 --- a/src/RunOpenCode/Component/Dataset/src/Reducer/Average.php +++ b/src/RunOpenCode/Component/Dataset/src/Reducer/Average.php @@ -4,9 +4,7 @@ namespace RunOpenCode\Component\Dataset\Reducer; -use RunOpenCode\Component\Dataset\AbstractStream; use RunOpenCode\Component\Dataset\Contract\ReducerInterface; -use RunOpenCode\Component\Dataset\Exception\LogicException; /** * Reducer which calculates average of values from a collection of values. @@ -17,69 +15,63 @@ * @template TKey * @template TValue * - * @phpstan-type ValueExtractor = callable(TValue, TKey): (int|float|null) + * @phpstan-type ExtractorCallable = callable(TValue, TKey): (int|float|null) * - * @extends AbstractStream * @implements ReducerInterface */ -final class Average extends AbstractStream implements ReducerInterface +final class Average implements ReducerInterface { /** * {@inheritdoc} */ public mixed $value { - get { - if (!$this->closed) { - throw new LogicException('Stream is not closed (iterated).'); - } - - return null !== $this->value ? (float)$this->value : 0; - } + get => 0 !== $this->count && null !== $this->total ? $this->total / $this->count : null; } - private \Closure $extractor; + /** + * Extractor to extract reducible value. + */ + private readonly \Closure $extractor; + + /** + * Current total of aggregated values. + */ + private float|null $total; + + /** + * Current count of aggregated values. + */ + private int $count = 0; /** - * @param iterable $collection Collection of values to reduce. - * @param ValueExtractor|null $extractor Optional value extractor. If not provided, values are used as is. - * @param bool $countNull Whether null values should be counted when calculating average. + * Create new average reducer. + * + * @param int|float|null $initial Initial value to start with. + * @param ExtractorCallable|null $extractor Optional function to extract reducible value. + * @param bool $countNull Should NULL values be accounted for, ignored by default. */ public function __construct( - private readonly iterable $collection, - ?callable $extractor = null, - private readonly bool $countNull = false, + int|float|null $initial = null, + ?callable $extractor = null, + private readonly bool $countNull = false, ) { - parent::__construct($collection); - $this->extractor = ( - $extractor ?? - static fn(mixed $value): int|float|null => \is_numeric($value) ? $value + 0 : null - )(...); + $this->extractor = null !== $extractor ? $extractor(...) : static fn(mixed $value): mixed => $value; + $this->total = null !== $initial ? (float)$initial : null; } /** * {@inheritdoc} */ - protected function iterate(): \Traversable + public function next(mixed $value, mixed $key): void { - $total = 0; - $count = 0; - - foreach ($this->collection as $key => $value) { - /** @var int|float|null $extracted */ - $extracted = ($this->extractor)($value, $key); + /** @var int|float|null $value */ + $value = ($this->extractor)($value, $key); + $this->count += null !== $value || $this->countNull ? 1 : 0; - if (null === $extracted) { - $count = $this->countNull ? $count + 1 : $count; - $this->value = (0 === $count) ? 0 : ($total / $count); - yield $key => $value; - continue; - } - - $total += $extracted; - $count++; - $this->value = $total / $count; - - yield $key => $value; + if (null === $value) { + return; } + + $this->total = ($this->total ?? 0.0) + (float)$value; } } diff --git a/src/RunOpenCode/Component/Dataset/src/Reducer/Callback.php b/src/RunOpenCode/Component/Dataset/src/Reducer/Callback.php index c0b0a0b..7778f75 100644 --- a/src/RunOpenCode/Component/Dataset/src/Reducer/Callback.php +++ b/src/RunOpenCode/Component/Dataset/src/Reducer/Callback.php @@ -4,9 +4,7 @@ namespace RunOpenCode\Component\Dataset\Reducer; -use RunOpenCode\Component\Dataset\AbstractStream; use RunOpenCode\Component\Dataset\Contract\ReducerInterface; -use RunOpenCode\Component\Dataset\Exception\LogicException; /** * Reducer which uses custom callback function to reduce items to value. @@ -15,49 +13,41 @@ * @template TValue * @template TReducedValue = mixed * - * @phpstan-type ReducerCallable = callable(TReducedValue, TValue, TKey): TReducedValue + * @phpstan-type ReducerCallable = callable(TReducedValue, TValue, TKey=): TReducedValue * - * @extends AbstractStream * @implements ReducerInterface */ -final class Callback extends AbstractStream implements ReducerInterface +final class Callback implements ReducerInterface { /** * {@inheritdoc} */ - public mixed $value { - get => $this->closed ? $this->value : throw new LogicException('Stream is not closed (iterated).'); - } + public private(set) mixed $value; + /** + * Reducer function to apply. + */ private readonly \Closure $callback; /** - * @param iterable $collection Collection of values to reduce. - * @param ReducerCallable $callback Callback function used to reduce values. - * @param mixed $initial Initial value. + * Create new callback reducer. + * + * @param ReducerCallable $callback Callback function used to reduce values. + * @param mixed $initial Initial value. */ public function __construct( - private readonly iterable $collection, - callable $callback, - private readonly mixed $initial = null + callable $callback, + mixed $initial = null ) { - parent::__construct($collection); $this->callback = $callback(...); + $this->value = $initial; } /** * {@inheritdoc} */ - protected function iterate(): \Traversable + public function next(mixed $value, mixed $key): void { - $this->value = $this->initial; - $carry = $this->initial; - - foreach ($this->collection as $key => $value) { - $carry = ($this->callback)($carry, $value, $key); - $this->value = $carry; - - yield $key => $value; - } + $this->value = ($this->callback)($this->value, $value, $key); } } diff --git a/src/RunOpenCode/Component/Dataset/src/Reducer/Count.php b/src/RunOpenCode/Component/Dataset/src/Reducer/Count.php index 5325453..a4e38be 100644 --- a/src/RunOpenCode/Component/Dataset/src/Reducer/Count.php +++ b/src/RunOpenCode/Component/Dataset/src/Reducer/Count.php @@ -4,9 +4,7 @@ namespace RunOpenCode\Component\Dataset\Reducer; -use RunOpenCode\Component\Dataset\AbstractStream; use RunOpenCode\Component\Dataset\Contract\ReducerInterface; -use RunOpenCode\Component\Dataset\Exception\LogicException; /** * Reducer which counts number of items. @@ -14,47 +12,42 @@ * @template TKey * @template TValue * - * @extends AbstractStream + * @phpstan-type FilterCallable = callable(TValue, TKey): bool + * * @implements ReducerInterface */ -final class Count extends AbstractStream implements ReducerInterface +final class Count implements ReducerInterface { /** * {@inheritdoc} */ - public mixed $value { - get => $this->closed ? $this->value : throw new LogicException('Stream is not closed (iterated).'); - } + public private(set) mixed $value = 0; - private \Closure $callback; + /** + * Filter callable. + */ + private readonly \Closure $filter; /** - * @param iterable $collection Collection of values to reduce. - * @param callable(TValue, TKey): bool|null $filter Optional filter callback to count only items that match the filter. + * Create new count reducer. + * + * @param FilterCallable|null $filter Optional filter callback to count only items that match the filter. */ public function __construct( - private readonly iterable $collection, - ?callable $filter = null, + ?callable $filter = null, ) { - parent::__construct($this->collection); - $this->callback = $filter ? $filter(...) : static fn(): bool => true; + $this->filter = $filter ? $filter(...) : static fn(): bool => true; } /** * {@inheritdoc} */ - protected function iterate(): \Traversable + public function next(mixed $value, mixed $key): void { - $this->value = 0; - - foreach ($this->collection as $key => $value) { - if (!($this->callback)($value, $key)) { - yield $key => $value; - continue; - } - - $this->value++; - yield $key => $value; + if (false === ($this->filter)($value, $key)) { + return; } + + $this->value++; } } diff --git a/src/RunOpenCode/Component/Dataset/src/Reducer/Max.php b/src/RunOpenCode/Component/Dataset/src/Reducer/Max.php index 71dcf0e..79fe9ca 100644 --- a/src/RunOpenCode/Component/Dataset/src/Reducer/Max.php +++ b/src/RunOpenCode/Component/Dataset/src/Reducer/Max.php @@ -4,9 +4,7 @@ namespace RunOpenCode\Component\Dataset\Reducer; -use RunOpenCode\Component\Dataset\AbstractStream; use RunOpenCode\Component\Dataset\Contract\ReducerInterface; -use RunOpenCode\Component\Dataset\Exception\LogicException; /** * Reducer which calculates maximum value from a collection of values. @@ -17,60 +15,55 @@ * @template TValue * @template TReducedValue * - * @phpstan-type ValueExtractor = callable(TValue, TKey): TReducedValue|null + * @phpstan-type ExtractorCallable = callable(TValue, TKey): TReducedValue|null + * @phpstan-type ComparatorCallable = callable(TReducedValue, TReducedValue): (0|1|-1) * - * @extends AbstractStream * @implements ReducerInterface */ -final class Max extends AbstractStream implements ReducerInterface +final class Max implements ReducerInterface { /** * {@inheritdoc} */ - public mixed $value { - get => $this->closed ? $this->value : throw new LogicException('Stream is not closed (iterated).'); - } + public private(set) mixed $value; + + private \Closure $extractor; - private readonly \Closure $extractor; + private \Closure $comparator; /** - * @param iterable $collection Collection of values to reduce. - * @param ValueExtractor|null $extractor Optional value extractor. If not provided, values are used as is. + * Create new max reducer. + * + * @param TReducedValue|null $initial Initial value to start with. + * @param ExtractorCallable|null $extractor Optional reducible value extractor. + * @param ComparatorCallable|null $comparator Optional comparator. */ public function __construct( - private readonly iterable $collection, - ?callable $extractor = null + mixed $initial = null, + ?callable $extractor = null, + ?callable $comparator = null, ) { - parent::__construct($this->collection); - $this->extractor = ( - $extractor ?? - static fn(mixed $value): mixed => $value - )(...); + $this->value = $initial; + $this->extractor = null !== $extractor ? $extractor(...) : static fn(mixed $value): mixed => $value; + $this->comparator = null !== $comparator ? $comparator(...) : static fn(mixed $first, mixed $second): int => $first <=> $second; } /** * {@inheritdoc} */ - protected function iterate(): \Traversable + public function next(mixed $value, mixed $key): void { - $this->value = null; - $current = null; - - foreach ($this->collection as $key => $value) { - /** @var TReducedValue|null $extracted */ - $extracted = ($this->extractor)($value, $key); + $value = ($this->extractor)($value, $key); - if (null === $extracted) { - yield $key => $value; - continue; - } - - if (null === $current || $extracted > $current) { - $current = $extracted; - $this->value = $current; - } + if (null === $value) { + return; + } - yield $key => $value; + if (null === $this->value) { + $this->value = $value; + return; } + + $this->value = 1 === ($this->comparator)($value, $this->value) ? $value : $this->value; } } diff --git a/src/RunOpenCode/Component/Dataset/src/Reducer/Min.php b/src/RunOpenCode/Component/Dataset/src/Reducer/Min.php index 6f50877..65f248e 100644 --- a/src/RunOpenCode/Component/Dataset/src/Reducer/Min.php +++ b/src/RunOpenCode/Component/Dataset/src/Reducer/Min.php @@ -4,9 +4,7 @@ namespace RunOpenCode\Component\Dataset\Reducer; -use RunOpenCode\Component\Dataset\AbstractStream; use RunOpenCode\Component\Dataset\Contract\ReducerInterface; -use RunOpenCode\Component\Dataset\Exception\LogicException; /** * Reducer which calculates minimum value from a collection of values. @@ -15,61 +13,57 @@ * * @template TKey * @template TValue + * @template TReducedValue * - * @phpstan-type ValueExtractor = callable(TValue, TKey): (int|float|null) + * @phpstan-type ExtractorCallable = callable(TValue, TKey): TReducedValue|null + * @phpstan-type ComparatorCallable = callable(TReducedValue, TReducedValue): (0|1|-1) * - * @extends AbstractStream - * @implements ReducerInterface + * @implements ReducerInterface */ -final class Min extends AbstractStream implements ReducerInterface +final class Min implements ReducerInterface { /** * {@inheritdoc} */ - public mixed $value { - get => $this->closed ? $this->value : throw new LogicException('Stream is not closed (iterated).'); - } + public private(set) mixed $value; private \Closure $extractor; + private \Closure $comparator; + /** - * @param iterable $collection Collection of values to reduce. - * @param ValueExtractor|null $extractor Optional value extractor. If not provided, values are used as is. + * Create new min reducer. + * + * @param TReducedValue|null $initial Initial value to start with. + * @param ExtractorCallable|null $extractor Optional reducible value extractor. + * @param ComparatorCallable|null $comparator Optional comparator. */ public function __construct( - private readonly iterable $collection, - ?callable $extractor = null + mixed $initial = null, + ?callable $extractor = null, + ?callable $comparator = null, ) { - parent::__construct($this->collection); - $this->extractor = ( - $extractor ?? - static fn(mixed $value): int|float|null => \is_numeric($value) ? $value + 0 : null - )(...); + $this->value = $initial; + $this->extractor = null !== $extractor ? $extractor(...) : static fn(mixed $value): mixed => $value; + $this->comparator = null !== $comparator ? $comparator(...) : static fn(mixed $first, mixed $second): int => $first <=> $second; } /** * {@inheritdoc} */ - protected function iterate(): \Traversable + public function next(mixed $value, mixed $key): void { - $this->value = null; - $current = null; - - foreach ($this->collection as $key => $value) { - /** @var int|float|null $extracted */ - $extracted = ($this->extractor)($value, $key); + $value = ($this->extractor)($value, $key); - if (null === $extracted) { - yield $key => $value; - continue; - } - - if (null === $current || $extracted < $current) { - $current = $extracted; - $this->value = $current; - } + if (null === $value) { + return; + } - yield $key => $value; + if (null === $this->value) { + $this->value = $value; + return; } + + $this->value = -1 === ($this->comparator)($value, $this->value) ? $value : $this->value; } } diff --git a/src/RunOpenCode/Component/Dataset/src/Reducer/Sum.php b/src/RunOpenCode/Component/Dataset/src/Reducer/Sum.php index d743993..6b5e303 100644 --- a/src/RunOpenCode/Component/Dataset/src/Reducer/Sum.php +++ b/src/RunOpenCode/Component/Dataset/src/Reducer/Sum.php @@ -4,9 +4,7 @@ namespace RunOpenCode\Component\Dataset\Reducer; -use RunOpenCode\Component\Dataset\AbstractStream; use RunOpenCode\Component\Dataset\Contract\ReducerInterface; -use RunOpenCode\Component\Dataset\Exception\LogicException; /** * Reducer which calculates sum of values from a collection of values. @@ -16,56 +14,48 @@ * @template TKey * @template TValue * - * @phpstan-type ValueExtractor = callable(TValue, TKey): (int|float|null) + * @phpstan-type ExtractorCallable = callable(TValue, TKey): (int|float|null) * - * @extends AbstractStream * @implements ReducerInterface */ -final class Sum extends AbstractStream implements ReducerInterface +final class Sum implements ReducerInterface { /** * {@inheritdoc} */ - public mixed $value { - get => $this->closed ? $this->value : throw new LogicException('Stream is not closed (iterated).'); - } + public private(set) mixed $value; private \Closure $extractor; /** - * @param iterable $collection Collection of values to reduce. - * @param ValueExtractor|null $extractor Optional value extractor. If not provided, values are used as is. + * @param int|float|null $initial Initial value. + * @param ExtractorCallable|null $extractor Optional reducible value extractor. */ public function __construct( - private readonly iterable $collection, - ?callable $extractor = null + int|float|null $initial = null, + ?callable $extractor = null, ) { - parent::__construct($this->collection); - $this->extractor = ( - $extractor ?? - static fn(mixed $value): int|float|null => \is_numeric($value) ? $value + 0 : null - )(...); + $this->value = $initial; + $this->extractor = null !== $extractor ? $extractor(...) : static fn(mixed $value): mixed => $value; } /** * {@inheritdoc} */ - protected function iterate(): \Traversable + public function next(mixed $value, mixed $key): void { - $this->value = null; - - foreach ($this->collection as $key => $value) { - /** @var int|float|null $extracted */ - $extracted = ($this->extractor)($value, $key); - - if (null === $extracted) { - yield $key => $value; - continue; - } + /** @var int|float|null $value */ + $value = ($this->extractor)($value, $key); - $this->value = $extracted + ($this->value ?? 0); + if (null === $value) { + return; + } - yield $key => $value; + if (null === $this->value) { + $this->value = $value; + return; } + + $this->value += $value; } } diff --git a/src/RunOpenCode/Component/Dataset/src/Stream.php b/src/RunOpenCode/Component/Dataset/src/Stream.php index 9a300f2..f90b229 100644 --- a/src/RunOpenCode/Component/Dataset/src/Stream.php +++ b/src/RunOpenCode/Component/Dataset/src/Stream.php @@ -32,14 +32,12 @@ /** * Dataset iterable stream. * - * You may extend this class to add your own custom operators. - * * @template TKey * @template TValue * * @extends AbstractStream */ -class Stream extends AbstractStream +final class Stream extends AbstractStream { /** * @param iterable $collection @@ -82,7 +80,7 @@ public function bufferCount(int $count): self /** * Applies buffer while operator on current stream. * - * @param callable(Buffer, TValue=, TKey=): bool $predicate Callable predicate function to evaluate. + * @param callable(Buffer, TValue=, TKey=): bool $predicate Callable predicate function to evaluate. * * @return Stream> * @@ -96,7 +94,7 @@ public function bufferWhile(callable $predicate): self /** * Applies distinct operator on current stream. * - * @param callable(TValue, TKey): string|null $identity User defined callable to determine item identity. If null, strict comparison (===) is used. + * @param callable(TValue, TKey=): string|null $identity User defined callable to determine item identity. If null, strict comparison (===) is used. * * @return self * @@ -110,7 +108,7 @@ public function distinct(?callable $identity = null): self /** * Applies filter operator on current stream. * - * @param callable(TValue, TKey): bool $filter User defined callable to filter items. + * @param callable(TValue, TKey=): bool $filter User defined callable to filter items. * * @return self * @@ -180,8 +178,8 @@ public function ifEmpty(\Exception|callable $action): self * @template TModifiedKey * @template TModifiedValue * - * @param callable(TValue, TKey): TModifiedValue $valueTransform User defined callable to be called on each item. - * @param callable(TKey, TValue): TModifiedKey|null $keyTransform User defined callable to be called on each item key. If null, original keys are preserved. + * @param callable(TValue, TKey=): TModifiedValue $valueTransform User defined callable to be called on each item. + * @param callable(TKey, TValue=): TModifiedKey|null $keyTransform User defined callable to be called on each item key. If null, original keys are preserved. * * @return self<($keyTransform is null ? TKey : TModifiedKey), TModifiedValue> * @@ -315,13 +313,13 @@ public function tap(callable $callback): self * @template TReducedValue * @template TReducer of ReducerInterface * - * @param non-empty-string $name Name of the aggregation. - * @param class-string $reducer Reducer to user for aggregation. - * @param mixed ...$args Arguments passed to reducer. + * @param non-empty-string $name Name of the aggregation. + * @param class-string|callable(TReducedValue, TValue, TKey=): TReducedValue $reducer Reducer to user for aggregation. + * @param mixed ...$args Arguments passed to reducer. * * @return self */ - public function aggregate(string $name, string $reducer, mixed ...$args): self + public function aggregate(string $name, callable|string $reducer, mixed ...$args): self { return dataset_aggregate($name, $this, $reducer, ...$args); } @@ -350,8 +348,8 @@ public function collect(string $collector, mixed ...$args): CollectorInterface * @template TReducedValue * @template TReducer of ReducerInterface * - * @param class-string|callable(TReducedValue|null, TValue, TKey): TReducedValue $reducer Reducer class name or callable. - * @param mixed ...$args Arguments passed to reducer. + * @param class-string|callable(TReducedValue, TValue, TKey=): TReducedValue $reducer Reducer to use. + * @param mixed ...$args Arguments passed to reducer. * * @return TReducedValue * diff --git a/src/RunOpenCode/Component/Dataset/src/functions.php b/src/RunOpenCode/Component/Dataset/src/functions.php index ccf05e3..8a1f90c 100644 --- a/src/RunOpenCode/Component/Dataset/src/functions.php +++ b/src/RunOpenCode/Component/Dataset/src/functions.php @@ -9,9 +9,10 @@ use RunOpenCode\Component\Dataset\Contract\ReducerInterface; use RunOpenCode\Component\Dataset\Contract\StreamInterface; use RunOpenCode\Component\Dataset\Model\Buffer; -use RunOpenCode\Component\Dataset\Reducer\Callback; /** + * Transform iterable to array. + * * @template TKey of array-key * @template TValue * @@ -29,6 +30,8 @@ function iterable_to_array(iterable $iterable, bool $preserveKeys = true): array } /** + * Transform iterable to list. + * * @template TKey * @template TValue * @@ -106,8 +109,8 @@ function buffer_while(iterable $collection, callable $predicate): Stream * @template TKey * @template TValue * - * @param iterable $collection Collection to iterate over. - * @param callable(TValue, TKey): string|null $identity User defined callable to determine item identity. If null, strict comparison (===) is used. + * @param iterable $collection Collection to iterate over. + * @param (callable(TValue, TKey=): string)|null $identity User defined callable to determine item identity. If null, strict comparison (===) is used. * * @return Stream * @@ -126,8 +129,8 @@ function distinct(iterable $collection, ?callable $identity = null): Stream * @template TKey * @template TValue * - * @param iterable $collection Collection to iterate over. - * @param callable(TValue, TKey): bool $filter User defined callable to filter items. + * @param iterable $collection Collection to iterate over. + * @param callable(TValue, TKey=): bool $filter User defined callable to filter items. * * @return Stream * @@ -233,9 +236,9 @@ function if_empty(iterable $collection, \Exception|callable $action): Stream * @template TModifiedKey * @template TModifiedValue * - * @param iterable $collection Collection to iterate over. - * @param callable(TValue, TKey): TModifiedValue $valueTransform User defined callable to be called on each item. - * @param callable(TKey, TValue): TModifiedKey|null $keyTransform User defined callable to be called on each item key. If null, original keys are preserved. + * @param iterable $collection Collection to iterate over. + * @param callable(TValue, TKey=): TModifiedValue $valueTransform User defined callable to be called on each item. + * @param callable(TKey, TValue=): TModifiedKey|null $keyTransform User defined callable to be called on each item key. If null, original keys are preserved. * * @return Stream<($keyTransform is null ? TModifiedKey : TKey), TModifiedValue> * @@ -414,7 +417,6 @@ function tap(iterable $collection, callable $callback): Stream ); } - /** * Attach reducer as an aggregator. * @@ -423,23 +425,22 @@ function tap(iterable $collection, callable $callback): Stream * @template TReducedValue * @template TReducer of ReducerInterface * - * @param non-empty-string $name Name of the aggregator. - * @param iterable $collection Collection to collect from. - * @param class-string $reducer Reducer to attach - * @param mixed ...$args Arguments passed to reducer. + * @param non-empty-string $name Name of the aggregator. + * @param iterable $collection Collection to collect from. + * @param class-string|callable(TReducedValue, TValue, TKey=): TReducedValue $reducer Reducer to attach. + * @param mixed ...$args Arguments passed to reducer. * * @return Stream */ -function aggregate(string $name, iterable $collection, string $reducer, mixed ...$args): Stream +function aggregate(string $name, iterable $collection, callable|string $reducer, mixed ...$args): Stream { - /** @var TReducer $reducer */ - $reducer = new \ReflectionClass($reducer)->newInstanceArgs(\array_merge( - [$collection], - $args - )); + /** @var TReducer $instance */ + $instance = \is_string($reducer) && \is_a($reducer, ReducerInterface::class, true) + ? new \ReflectionClass($reducer)->newInstanceArgs($args) + : new Reducer\Callback($reducer, ...$args); return new Stream( - new Aggregator($name, $reducer), + new Aggregator($name, new Operator\Reduce($collection, $instance)), ); } @@ -475,9 +476,9 @@ function collect(iterable $collection, string $collector, mixed ...$args): Colle * @template TReducedValue * @template TReducer of ReducerInterface * - * @param iterable $collection Collection to collect from. - * @param class-string|callable(TReducedValue|null, TValue, TKey): TReducedValue $reducer Reducer class name or callable. - * @param mixed ...$args Arguments passed to reducer. + * @param iterable $collection Collection to collect from. + * @param class-string|callable(TReducedValue, TValue, TKey=): TReducedValue $reducer Reducer to use. + * @param mixed ...$args Arguments passed to reducer. * * @return TReducedValue * @@ -485,15 +486,14 @@ function collect(iterable $collection, string $collector, mixed ...$args): Colle */ function reduce(iterable $collection, callable|string $reducer, mixed ...$args): mixed { - $isClassString = \is_string($reducer) && \is_a($reducer, ReducerInterface::class, true); - $reducer = $isClassString ? new \ReflectionClass($reducer)->newInstanceArgs(\array_merge( - [$collection], - $args - )) : new Callback($collection, $reducer, ...$args); + /** @var TReducer $instance */ + $instance = \is_string($reducer) && \is_a($reducer, ReducerInterface::class, true) + ? new \ReflectionClass($reducer)->newInstanceArgs($args) + : new Reducer\Callback($reducer, ...$args); - foreach ($reducer as $_) { - // noop. - } + $operator = new Operator\Reduce($collection, $instance); + + flush($operator); - return $reducer->value; + return $operator->value; } diff --git a/src/RunOpenCode/Component/Dataset/tests/Aggregator/AggregatorTest.php b/src/RunOpenCode/Component/Dataset/tests/Aggregator/AggregatorTest.php index 3b01361..9456584 100644 --- a/src/RunOpenCode/Component/Dataset/tests/Aggregator/AggregatorTest.php +++ b/src/RunOpenCode/Component/Dataset/tests/Aggregator/AggregatorTest.php @@ -6,10 +6,11 @@ use PHPUnit\Framework\Attributes\Test; use PHPUnit\Framework\MockObject\MockObject; -use PHPUnit\Framework\MockObject\Runtime\PropertyHook; use PHPUnit\Framework\TestCase; use RunOpenCode\Component\Dataset\Aggregator\Aggregator; use RunOpenCode\Component\Dataset\Contract\ReducerInterface; +use RunOpenCode\Component\Dataset\Operator\Reduce; +use RunOpenCode\Component\Dataset\Reducer\Callback; use function RunOpenCode\Component\Dataset\iterable_to_array; @@ -20,12 +21,15 @@ public function iterates(): void { /** @var ReducerInterface&MockObject $reducer */ $reducer = $this->createMock(ReducerInterface::class); - $aggregator = new Aggregator('foo', $reducer); + $aggregator = new Aggregator('foo', new Reduce([ + 'foo', + 'bar', + 'baz', + ], $reducer)); $reducer - ->expects($this->once()) - ->method('getIterator') - ->willReturn(new \ArrayIterator(['foo', 'bar', 'baz'])); + ->expects($this->exactly(3)) + ->method('next'); $this->assertSame([ 'foo', @@ -37,15 +41,15 @@ public function iterates(): void #[Test] public function provides_value(): void { - /** @var ReducerInterface&MockObject $reducer */ - $reducer = $this->createMock(ReducerInterface::class); - $aggregator = new Aggregator('foo', $reducer); + $reducer = new Callback(static fn(string $carry, string $value): string => \sprintf('%s/%s', $carry, $value), ''); + $aggregator = new Aggregator('foo', new Reduce([ + 'foo', + 'bar', + 'baz', + ], $reducer)); - $reducer - ->expects($this->once()) - ->method(PropertyHook::get('value')) - ->willReturn(42); + iterable_to_array($aggregator); // @phpstan-ignore-line - $this->assertSame(42, $aggregator->value); + $this->assertSame('/foo/bar/baz', $aggregator->value); } } diff --git a/src/RunOpenCode/Component/Dataset/tests/Collector/ArrayCollectorTest.php b/src/RunOpenCode/Component/Dataset/tests/Collector/ArrayCollectorTest.php index 50b11ab..c0a6653 100644 --- a/src/RunOpenCode/Component/Dataset/tests/Collector/ArrayCollectorTest.php +++ b/src/RunOpenCode/Component/Dataset/tests/Collector/ArrayCollectorTest.php @@ -78,9 +78,9 @@ public function aggregates(): void ->aggregate('average', Average::class) ->collect(ArrayCollector::class); - $this->assertSame(2, $collector->aggregators['count']); - $this->assertSame(12, $collector->aggregators['sum']); - $this->assertEqualsWithDelta(6, $collector->aggregators['average'], 0.0001); + $this->assertSame(2, $collector->aggregated['count']); + $this->assertSame(12, $collector->aggregated['sum']); + $this->assertEqualsWithDelta(6, $collector->aggregated['average'], 0.0001); } #[Test] diff --git a/src/RunOpenCode/Component/Dataset/tests/Collector/CursoredCollectorTest.php b/src/RunOpenCode/Component/Dataset/tests/Collector/CursoredCollectorTest.php index ef2cf5b..6412f3e 100644 --- a/src/RunOpenCode/Component/Dataset/tests/Collector/CursoredCollectorTest.php +++ b/src/RunOpenCode/Component/Dataset/tests/Collector/CursoredCollectorTest.php @@ -76,8 +76,8 @@ public function aggregates(): void $this->assertSame(4, $collector->previous); $this->assertSame(8, $collector->next); - $this->assertSame(2, $collector->aggregators['count']); - $this->assertSame(12, $collector->aggregators['sum']); - $this->assertEqualsWithDelta(6, $collector->aggregators['average'], 0.0001); + $this->assertSame(2, $collector->aggregated['count']); + $this->assertSame(12, $collector->aggregated['sum']); + $this->assertEqualsWithDelta(6, $collector->aggregated['average'], 0.0001); } } diff --git a/src/RunOpenCode/Component/Dataset/tests/Collector/ListCollectorTest.php b/src/RunOpenCode/Component/Dataset/tests/Collector/ListCollectorTest.php index eb67a59..fa48862 100644 --- a/src/RunOpenCode/Component/Dataset/tests/Collector/ListCollectorTest.php +++ b/src/RunOpenCode/Component/Dataset/tests/Collector/ListCollectorTest.php @@ -76,9 +76,9 @@ public function aggregates(): void ->aggregate('average', Average::class) ->collect(ListCollector::class); - $this->assertSame(2, $collector->aggregators['count']); - $this->assertSame(12, $collector->aggregators['sum']); - $this->assertEqualsWithDelta(6, $collector->aggregators['average'], 0.0001); + $this->assertSame(2, $collector->aggregated['count']); + $this->assertSame(12, $collector->aggregated['sum']); + $this->assertEqualsWithDelta(6, $collector->aggregated['average'], 0.0001); } #[Test] diff --git a/src/RunOpenCode/Component/Dataset/tests/Operator/BufferCountTest.php b/src/RunOpenCode/Component/Dataset/tests/Operator/BufferCountTest.php index bcc9965..876ea0f 100644 --- a/src/RunOpenCode/Component/Dataset/tests/Operator/BufferCountTest.php +++ b/src/RunOpenCode/Component/Dataset/tests/Operator/BufferCountTest.php @@ -42,7 +42,7 @@ public function buffers(): void 'processed_e' => 14, ], \iterator_to_array($stream)); - $this->assertSame(3, $stream->aggregators['count']->value); + $this->assertSame(3, $stream->aggregated['count']); } #[Test] @@ -65,7 +65,7 @@ public function buffers_one_element(): void 'processed_a' => 4, ], \iterator_to_array($stream)); - $this->assertSame(1, $stream->aggregators['count']->value); + $this->assertSame(1, $stream->aggregated['count']); } #[Test] @@ -75,6 +75,6 @@ public function buffers_empty_stream(): void $this->assertSame([], \iterator_to_array($stream)); - $this->assertSame(0, $stream->aggregators['count']->value); + $this->assertSame(0, $stream->aggregated['count']); } } diff --git a/src/RunOpenCode/Component/Dataset/tests/Operator/BufferWhileTest.php b/src/RunOpenCode/Component/Dataset/tests/Operator/BufferWhileTest.php index 589e5eb..6a30b18 100644 --- a/src/RunOpenCode/Component/Dataset/tests/Operator/BufferWhileTest.php +++ b/src/RunOpenCode/Component/Dataset/tests/Operator/BufferWhileTest.php @@ -45,7 +45,7 @@ public function buffers(): void 'processed_e' => 6, ], \iterator_to_array($stream)); - $this->assertSame(2, $stream->aggregators['count']->value); + $this->assertSame(2, $stream->aggregated['count']); } #[Test] @@ -71,7 +71,7 @@ public function buffers_one_element(): void 'processed_a' => 4, ], \iterator_to_array($stream)); - $this->assertSame(1, $stream->aggregators['count']->value); + $this->assertSame(1, $stream->aggregated['count']); } #[Test] @@ -84,6 +84,6 @@ public function buffers_empty_stream(): void $this->assertSame([], \iterator_to_array($stream)); - $this->assertSame(0, $stream->aggregators['count']->value); + $this->assertSame(0, $stream->aggregated['count']); } } diff --git a/src/RunOpenCode/Component/Dataset/tests/Operator/DistinctTest.php b/src/RunOpenCode/Component/Dataset/tests/Operator/DistinctTest.php index 10b89e5..8cc25b9 100644 --- a/src/RunOpenCode/Component/Dataset/tests/Operator/DistinctTest.php +++ b/src/RunOpenCode/Component/Dataset/tests/Operator/DistinctTest.php @@ -19,7 +19,7 @@ public function distinct_by_identity(): void 'b' => [10], 'c' => [2], 'd' => [10], - ], static fn(array $value, string $key): string => (string)$value[0]); + ], static fn(array $value, string $key): string => (string)$value[0]); // @phpstan-ignore-line $this->assertSame([ 'a' => [2], diff --git a/src/RunOpenCode/Component/Dataset/tests/Operator/FilterTest.php b/src/RunOpenCode/Component/Dataset/tests/Operator/FilterTest.php index f76685d..d4003c7 100644 --- a/src/RunOpenCode/Component/Dataset/tests/Operator/FilterTest.php +++ b/src/RunOpenCode/Component/Dataset/tests/Operator/FilterTest.php @@ -19,7 +19,7 @@ public function filters(): void 'b' => 10, 'c' => 5, 'd' => 1, - ], static fn(int $value, string $key): bool => $value > 2 && 'c' !== $key); + ], static fn(int $value, string $key): bool => $value > 2 && 'c' !== $key); // @phpstan-ignore-line $this->assertSame([ 'b' => 10, diff --git a/src/RunOpenCode/Component/Dataset/tests/Reducer/AverageTest.php b/src/RunOpenCode/Component/Dataset/tests/Reducer/AverageTest.php index fc21c5c..9aaff7f 100644 --- a/src/RunOpenCode/Component/Dataset/tests/Reducer/AverageTest.php +++ b/src/RunOpenCode/Component/Dataset/tests/Reducer/AverageTest.php @@ -6,79 +6,81 @@ use PHPUnit\Framework\Attributes\Test; use PHPUnit\Framework\TestCase; -use RunOpenCode\Component\Dataset\Exception\LogicException; use RunOpenCode\Component\Dataset\Reducer\Average; +use function RunOpenCode\Component\Dataset\reduce; + final class AverageTest extends TestCase { #[Test] public function average_value(): void { - $reducer = new Average([ + $dataset = [ 'a' => 2, 'b' => 4, 'c' => 6, 'd' => null, 'e' => 8, - ]); + ]; - $this->assertSame([ - 'a' => 2, - 'b' => 4, - 'c' => 6, - 'd' => null, - 'e' => 8, - ], \iterator_to_array($reducer)); - $this->assertEqualsWithDelta(5.0, $reducer->value, 0.0001); + $this->assertEqualsWithDelta( + 5.0, + reduce($dataset, Average::class), + 0.0001 + ); } #[Test] public function average_from_extracted_value(): void { - $reducer = new Average([ + $dataset = [ 'a' => [2], 'b' => [4], 'c' => [6], 'd' => [null], 'e' => [8], - ], static fn(array $item): ?int => $item[0]); + ]; - $this->assertSame([ - 'a' => [2], - 'b' => [4], - 'c' => [6], - 'd' => [null], - 'e' => [8], - ], \iterator_to_array($reducer)); - $this->assertEqualsWithDelta(5.0, $reducer->value, 0.0001); + $this->assertEqualsWithDelta( + 5.0, + reduce($dataset, Average::class, extractor: static fn(array $item): ?int => $item[0]), // @phpstan-ignore-line + 0.0001 + ); } #[Test] - public function average_skips_nulls(): void + public function average_with_initial_value(): void { - $reducer = new Average([ + $dataset = [ 'a' => 2, 'b' => 4, 'c' => 6, 'd' => null, 'e' => 8, - ], countNull: true); + ]; + + $this->assertEqualsWithDelta( + 10.0, + reduce($dataset, Average::class, initial: 20), + 0.0001 + ); + } - $this->assertSame([ + #[Test] + public function average_skips_nulls(): void + { + $dataset = [ 'a' => 2, 'b' => 4, 'c' => 6, 'd' => null, 'e' => 8, - ], \iterator_to_array($reducer)); - $this->assertEqualsWithDelta(4.0, $reducer->value, 0.0001); - } - - #[Test] - public function get_value_throws_exception_when_not_iterated(): void - { - $this->expectException(LogicException::class); + ]; - new Average([])->value; + $this->assertEqualsWithDelta( + 4.0, + reduce($dataset, Average::class, countNull: true), + 0.0001 + ); } } diff --git a/src/RunOpenCode/Component/Dataset/tests/Reducer/CallbackTest.php b/src/RunOpenCode/Component/Dataset/tests/Reducer/CallbackTest.php index 89f7ec6..32a515c 100644 --- a/src/RunOpenCode/Component/Dataset/tests/Reducer/CallbackTest.php +++ b/src/RunOpenCode/Component/Dataset/tests/Reducer/CallbackTest.php @@ -6,33 +6,24 @@ use PHPUnit\Framework\Attributes\Test; use PHPUnit\Framework\TestCase; -use RunOpenCode\Component\Dataset\Exception\LogicException; -use RunOpenCode\Component\Dataset\Reducer\Callback; + +use function RunOpenCode\Component\Dataset\reduce; final class CallbackTest extends TestCase { #[Test] public function reduces(): void { - $reducer = new Callback([ - 'a' => 1, - 'b' => 2, - 'c' => 3, - ], static fn(int $carry, int $value): int => $carry + $value, 0); - - $this->assertSame([ + $dataset = [ 'a' => 1, 'b' => 2, 'c' => 3, - ], \iterator_to_array($reducer)); - $this->assertEqualsWithDelta(6, $reducer->value, 0.0001); - } - - #[Test] - public function get_value_throws_exception_when_not_iterated(): void - { - $this->expectException(LogicException::class); + ]; - new Callback([], static fn(): int => 0)->value; + $this->assertEqualsWithDelta( + 6, + reduce($dataset, static fn(int $carry, int $value): int => $carry + $value, initial: 0), + 0.0001 + ); } } diff --git a/src/RunOpenCode/Component/Dataset/tests/Reducer/CountTest.php b/src/RunOpenCode/Component/Dataset/tests/Reducer/CountTest.php index 5eebad3..d971eb5 100644 --- a/src/RunOpenCode/Component/Dataset/tests/Reducer/CountTest.php +++ b/src/RunOpenCode/Component/Dataset/tests/Reducer/CountTest.php @@ -6,50 +6,39 @@ use PHPUnit\Framework\Attributes\Test; use PHPUnit\Framework\TestCase; -use RunOpenCode\Component\Dataset\Exception\LogicException; use RunOpenCode\Component\Dataset\Reducer\Count; +use function RunOpenCode\Component\Dataset\reduce; + final class CountTest extends TestCase { #[Test] public function counts_everything(): void { - $reducer = new Count([ + $dataset = [ 'a' => 1, 'b' => 2, 'c' => 3, - ]); + ]; - $this->assertSame([ - 'a' => 1, - 'b' => 2, - 'c' => 3, - ], \iterator_to_array($reducer)); - $this->assertSame(3, $reducer->value); + $this->assertSame( + 3, + reduce($dataset, Count::class) + ); } #[Test] public function counts_filtered_only(): void { - $reducer = new Count([ - 'a' => 1, - 'b' => 2, - 'c' => 3, - ], static fn(int $value, string $key): bool => $key !== 'b' && $value !== 1); - - $this->assertSame([ + $dataset = [ 'a' => 1, 'b' => 2, 'c' => 3, - ], \iterator_to_array($reducer)); - $this->assertSame(1, $reducer->value); - } - - #[Test] - public function get_value_throws_exception_when_not_iterated(): void - { - $this->expectException(LogicException::class); + ]; - new Count([])->value; + $this->assertSame( + 1, + reduce($dataset, Count::class, filter: static fn(int $value, string $key): bool => $key !== 'b' && $value !== 1) + ); } } diff --git a/src/RunOpenCode/Component/Dataset/tests/Reducer/MaxTest.php b/src/RunOpenCode/Component/Dataset/tests/Reducer/MaxTest.php index 2f6d4bf..45952ba 100644 --- a/src/RunOpenCode/Component/Dataset/tests/Reducer/MaxTest.php +++ b/src/RunOpenCode/Component/Dataset/tests/Reducer/MaxTest.php @@ -6,43 +6,35 @@ use PHPUnit\Framework\Attributes\Test; use PHPUnit\Framework\TestCase; -use RunOpenCode\Component\Dataset\Exception\LogicException; use RunOpenCode\Component\Dataset\Reducer\Max; +use function RunOpenCode\Component\Dataset\reduce; + final class MaxTest extends TestCase { #[Test] public function max_value(): void { - $reducer = new Max([ + $dataset = [ 'a' => 1, 'b' => 3, 'c' => null, - ]); - $this->assertSame([ - 'a' => 1, - 'b' => 3, - 'c' => null, - ], \iterator_to_array($reducer)); - $this->assertEquals(3, $reducer->value); - } + ]; - #[Test] - public function max_extracted_value(): void - { - $reducer = new Max([ - 3, 2, 1 - ], static fn(int $value, int $key): int => $value * $key); - - $this->assertSame([3, 2, 1], \iterator_to_array($reducer)); - $this->assertEquals(2, $reducer->value); + $this->assertEquals( + 3, + reduce($dataset, Max::class), + ); } #[Test] - public function get_value_throws_exception_when_not_iterated(): void + public function max_extracted_value(): void { - $this->expectException(LogicException::class); + $dataset = [3, 2, 1]; - new Max([])->value; + $this->assertEquals( + 2, + reduce($dataset, Max::class, extractor: static fn(int $value, int $key): int => $value * $key), + ); } } diff --git a/src/RunOpenCode/Component/Dataset/tests/Reducer/MinTest.php b/src/RunOpenCode/Component/Dataset/tests/Reducer/MinTest.php index 8115597..e8b2376 100644 --- a/src/RunOpenCode/Component/Dataset/tests/Reducer/MinTest.php +++ b/src/RunOpenCode/Component/Dataset/tests/Reducer/MinTest.php @@ -6,43 +6,35 @@ use PHPUnit\Framework\Attributes\Test; use PHPUnit\Framework\TestCase; -use RunOpenCode\Component\Dataset\Exception\LogicException; use RunOpenCode\Component\Dataset\Reducer\Min; +use function RunOpenCode\Component\Dataset\reduce; + final class MinTest extends TestCase { #[Test] public function min_value(): void { - $reducer = new Min([ + $dataset = [ 'a' => null, 'b' => 3, 'c' => 2, - ]); - $this->assertSame([ - 'a' => null, - 'b' => 3, - 'c' => 2, - ], \iterator_to_array($reducer)); - $this->assertEquals(2, $reducer->value); - } + ]; - #[Test] - public function max_extracted_value(): void - { - $reducer = new Min([ - 3, 2, 1 - ], static fn(int $value, int $key): int => $value * $key); - - $this->assertSame([3, 2, 1], \iterator_to_array($reducer)); - $this->assertEquals(0, $reducer->value); + $this->assertEquals( + 2, + reduce($dataset, Min::class), + ); } #[Test] - public function get_value_throws_exception_when_not_iterated(): void + public function max_extracted_value(): void { - $this->expectException(LogicException::class); + $dataset = [3, 2, 1]; - new Min([])->value; + $this->assertEquals( + 0, + reduce($dataset, Min::class, extractor: static fn(int $value, int $key): int => $value * $key), + ); } } diff --git a/src/RunOpenCode/Component/Dataset/tests/Reducer/SumTest.php b/src/RunOpenCode/Component/Dataset/tests/Reducer/SumTest.php index 4197950..34cfd65 100644 --- a/src/RunOpenCode/Component/Dataset/tests/Reducer/SumTest.php +++ b/src/RunOpenCode/Component/Dataset/tests/Reducer/SumTest.php @@ -6,42 +6,35 @@ use PHPUnit\Framework\Attributes\Test; use PHPUnit\Framework\TestCase; -use RunOpenCode\Component\Dataset\Exception\LogicException; use RunOpenCode\Component\Dataset\Reducer\Sum; +use function RunOpenCode\Component\Dataset\reduce; + final class SumTest extends TestCase { #[Test] public function sums_values(): void { - $reducer = new Sum([ + $dataset = [ 'a' => 1, 'b' => 2, 'c' => null, - ]); + ]; - $this->assertEquals([ - 'a' => 1, - 'b' => 2, - 'c' => null, - ], \iterator_to_array($reducer)); - $this->assertEquals(3, $reducer->value); + $this->assertEquals( + 3, + reduce($dataset, Sum::class), + ); } #[Test] public function sums_extracted_values(): void { - $reducer = new Sum([1, 2, 3], static fn(int $value, int $key): int => $value * $key); - - $this->assertSame([1, 2, 3], \iterator_to_array($reducer)); - $this->assertEquals(8, $reducer->value); - } - - #[Test] - public function get_value_throws_exception_when_not_iterated(): void - { - $this->expectException(LogicException::class); + $dataset = [1, 2, 3]; - new Sum([])->value; + $this->assertEquals( + 8, + reduce($dataset, Sum::class, extractor: static fn(int $value, int $key): int => $value * $key), + ); } } diff --git a/src/RunOpenCode/Component/Dataset/tests/StreamTest.php b/src/RunOpenCode/Component/Dataset/tests/StreamTest.php index 05a8f0c..1a08ea8 100644 --- a/src/RunOpenCode/Component/Dataset/tests/StreamTest.php +++ b/src/RunOpenCode/Component/Dataset/tests/StreamTest.php @@ -58,7 +58,7 @@ public function buffer_count(): void 'processed_e' => 14, ], \iterator_to_array($stream)); - $this->assertSame(3, $stream->aggregators['count']->value); + $this->assertSame(3, $stream->aggregated['count']); } #[Test] @@ -92,7 +92,7 @@ public function buffer_while(): void 'processed_e' => 6, ], \iterator_to_array($stream)); - $this->assertSame(2, $stream->aggregators['count']->value); + $this->assertSame(2, $stream->aggregated['count']); } #[Test] @@ -380,9 +380,9 @@ public function aggregate(): void 'f' => 4, ], $dataset->value); - $this->assertSame(12, $dataset->aggregators['middle_sum']); - $this->assertSame(16, $dataset->aggregators['inner_sum']); - $this->assertSame(28, $dataset->aggregators['total_sum']); + $this->assertSame(12, $dataset->aggregated['middle_sum']); + $this->assertSame(16, $dataset->aggregated['inner_sum']); + $this->assertSame(28, $dataset->aggregated['total_sum']); } #[Test] @@ -400,7 +400,7 @@ public function reduce(): void $this->assertSame(10, new Stream($dataset)->reduce(Max::class)); $this->assertSame(1, new Stream($dataset)->reduce(Min::class)); $this->assertSame(18, new Stream($dataset)->reduce(Sum::class)); - $this->assertSame(36, new Stream($dataset)->reduce(static fn(?int $carry, int $value, string $key): int => $value * 2 + ($carry ?? 0))); + $this->assertSame(36, new Stream($dataset)->reduce(static fn(?int $carry, int $value, string $key): int => $value * 2 + ($carry ?? 0))); // @phpstan-ignore-line } #[Test] @@ -417,7 +417,7 @@ public function flush(): void ->aggregate('count', Count::class) ->flush(); - $this->assertSame(4, $stream->aggregators['count']->value); + $this->assertSame(4, $stream->aggregated['count']); $this->assertTrue($stream->closed); }