I do have a collection and I want to do either one of the following: the sum or the average of a property in specific relation in that collection so for example I want to get the average from the total property in the following relation orders.invoice
so in the code I have to loop on all the orders (as the orders here are an array) and then go to each invoice (as the order here has one invoice) in each order and then calculate the average. so if we have 3 orders and those orders has an invoice with those values (in the total property) 50, 200 and 100, the return value will be (50 + 200 + 100) / 3 = 116.7 if we are doing an average if we are doing sum it would be 50 + 200 + 100 = 350
note that the collection could be nested and we don’t know the type of the relation between the nested relations ex: “relation1.relation2.relatoin3” so if relation 1 is a collection we will need to loop on it and if relation2 is an object we just go directly to the next relation.
so far I have managed to do the sum but what’s left is the average, here is the function:
/**
* calculate the sum/AVG based on the chosen property on a single relation or nested relations.
*
* @var IlluminateSupportCollection | array $entity
* @var string $path
* @var string $property
* @var 'sum'|'avg' $operation
* @return float
*/
public static function compute($entity, string $path, string $property, string $operation = 'sum'): float
{
$parts = explode('.', $path, 2); // Limit to 2 parts
$relation = $parts[0];
$new_path = isset($parts[1]) ? $parts[1] : '';
$result = 0;
$count = 0;
if (is_iterable($entity)) {
$entity->each(function ($item) use (&$result, &$count, $path, $property, $operation) {
$result += self::compute($item, $path, $property, $operation);
$count++;
});
return $result;
}
if (is_iterable($entity->{$relation})) {
collect($entity->{$relation})->each(function ($item) use (&$result, &$count, $new_path, $property, $operation) {
if($new_path == '') {
$result += $item->{$property};
$count++;
} else {
$result += self::compute($item, $new_path, $property, $operation);
}
});
} else if($entity->{$relation}) {
if($new_path == '') {
$result += $entity->{$relation}->{$property};
$count++;
} else {
$result += self::compute($entity->{$relation}, $new_path, $property, $operation);
}
}
if($operation == 'avg' && $count > 0) {
// dd($count);
$result /= 4; // here is the problem in the `avg` as we can't save the value of the $count on every function call as we do with $result
}
return $result;
}
here is the test:
<?php
namespace TestsUnit;
use TestsTestCase;
use IlluminateSupportFacadesFile;
use AppCollectionNestedCollection;
class CollectionNestedComputationTest extends TestCase {
protected $data;
protected function setup(): void
{
parent::setup();
$this->data = collect(json_decode(file_get_contents(base_path('tests/Unit/data.json')))->results);
}
/**
* single path.
*/
public function test_that_single_path_is_calculated_correctly(): void
{
$path = 'devis';
$property = 'id';
$this->assertEquals(292763, NestedCollection::compute($this->data, $path, $property, 'sum'));
$this->assertEquals(73190.75, NestedCollection::compute($this->data, $path, $property, 'avg'));
}
/**
* nested path.
*/
public function test_that_nested_path_is_calculated_correctly(): void
{
$path = 'devis.data.trajets.brands';
$property = '1';
$this->assertEquals(1940, NestedCollection::compute($this->data, $path, $property, 'sum'));
$this->assertEquals(485, NestedCollection::compute($this->data, $path, $property, 'avg'));
}
}
and here is the data used in the test:
{
"results": [
{
"id": 10575,
"devis": {
"id": 73258,
"data": {
"trajets": [
{
"brands": {
"1": -990
}
}
]
}
}
},
{
"id": 10574,
"devis": {
"id": 73235,
"data": {
"trajets": [
{
"brands": {
"1": "990"
}
}
]
}
}
},
{
"id": 10573,
"devis": {
"id": 73143,
"data": {
"trajets": [
{
"brands": {
"1": "1250"
}
}
]
}
}
},
{
"id": 10572,
"devis": {
"id": 73127,
"data": {
"trajets": [
{
"brands": {
"1": "690"
}
}
]
}
}
}
]
}
as I mentioned above the sum works as excepted but the average is not working as expected