Skip to content

Commit

Permalink
Add weightedRandom macro (#224)
Browse files Browse the repository at this point in the history
* wip

* Fix styling

* wip

* wip

* wip

* wip

Co-authored-by: freekmurze <freekmurze@users.noreply.github.com>
  • Loading branch information
freekmurze and freekmurze committed Mar 18, 2022
1 parent 0692d4b commit 93ce541
Show file tree
Hide file tree
Showing 6 changed files with 158 additions and 0 deletions.
29 changes: 29 additions & 0 deletions README.md
Expand Up @@ -82,6 +82,7 @@ The package will automatically register itself.
- [`toPairs`](#topairs)
- [`transpose`](#transpose)
- [`validate`](#validate)
- [`weightedRandom`](#weightedRandom)
- [`withSize`](#withsize)

### `after`
Expand Down Expand Up @@ -841,6 +842,34 @@ collect(['sebastian@spatie.be', 'bla'])->validate('email'); // returns false
collect(['sebastian@spatie.be', 'freek@spatie.be'])->validate('email'); // returns true
```

### `weightedRandom`

Returns a random item by a weight. In this example, the item with `a` has the most chance to get picked, and the item with `c` the least.

```php
// pass the field name that should be used as a weight

$randomItem = collect([
['value' => 'a', 'weight' => 30],
['value' => 'b', 'weight' => 20],
['value' => 'c', 'weight' => 10],
])->weightedRandom('weight');
```

Alternatively, you can pass a callable to get the weight.

```php
$randomItem = collect([
['value' => 'a', 'weight' => 30],
['value' => 'b', 'weight' => 20],
['value' => 'c', 'weight' => 10],
])->weightedRandom(function(array $item) {
return $item['weight'];
});
```



### `withSize`

Create a new collection with the specified amount of items.
Expand Down
1 change: 1 addition & 0 deletions composer.json
Expand Up @@ -31,6 +31,7 @@
"mockery/mockery": "^1.4.2",
"orchestra/testbench": "^6.23|^7.0",
"phpunit/phpunit": "^9.4.4",
"spatie/laravel-ray": "^1.29",
"symfony/stopwatch": "^5.2|^6.0"
},
"suggest": {
Expand Down
1 change: 1 addition & 0 deletions src/CollectionMacroServiceProvider.php
Expand Up @@ -66,6 +66,7 @@ private function macros(): array
'transpose' => \Spatie\CollectionMacros\Macros\Transpose::class,
'try' => \Spatie\CollectionMacros\Macros\TryCatch::class,
'validate' => \Spatie\CollectionMacros\Macros\Validate::class,
'weightedRandom' => \Spatie\CollectionMacros\Macros\WeightedRandom::class,
'withSize' => \Spatie\CollectionMacros\Macros\WithSize::class,
];
}
Expand Down
50 changes: 50 additions & 0 deletions src/Macros/WeightedRandom.php
@@ -0,0 +1,50 @@
<?php

namespace Spatie\CollectionMacros\Macros;

class WeightedRandom
{
public function __invoke()
{
return function (callable|string $weightAttribute, $default = null) {
if (is_string($weightAttribute)) {
$attributeName = $weightAttribute;

$weightAttribute = function ($item) use ($attributeName) {
return data_get($item, $attributeName);
};
}

$range = 0;

$weightedItems = collect($this->items)
->map(function ($item) use ($weightAttribute, &$range) {
$weightAttribute = $weightAttribute($item);
$range += $weightAttribute;

return [
'range' => $range,
'weight' => $weightAttribute,
'item' => $item,
];
})
->filter(function (array $weightedItem) {
return $weightedItem['weight'] > 0;
});

if ($weightedItems->isEmpty()) {
return $this->random();
}


$randomNumber = rand(1, $range);

$itemAndRange = $weightedItems
->first(function ($weightedItem) use ($randomNumber) {
return $weightedItem['range'] >= $randomNumber;
});

return $itemAndRange['item'] ?? $default;
};
}
}
75 changes: 75 additions & 0 deletions tests/Macros/WeightedRandomTest.php
@@ -0,0 +1,75 @@
<?php

namespace Spatie\CollectionMacros\Test\Macros;

use Illuminate\Support\Collection;
use Spatie\CollectionMacros\Test\TestCase;

class WeightedRandomTest extends TestCase
{
/** @test */
public function it_will_probably_return_the_heaviest_item_most()
{
$items = collect([
['value' => 'a', 'weight' => 1],
['value' => 'b', 'weight' => 10],
['value' => 'c', 'weight' => 1],
]);

$mostPopularValue = Collection::range(0, 1000)
->map(function () use ($items) {
return $items->weightedRandom(function (array $item) {
return $item['weight'];
});
})
->groupBy('value')
->map
->count()
->sortDesc()
->flip()
->first();

$this->assertEquals('b', $mostPopularValue);
}

/** @test */
public function it_will_not_pick_a_value_without_a_weight()
{
$items = collect([
['value' => 'a', 'weight' => 0],
['value' => 'b', 'weight' => 0],
['value' => 'c', 'weight' => 1],
['value' => 'c', 'weight' => 0],
]);

$pickedItem = $items->weightedRandom(fn (array $item) => $item['weight']);

$this->assertEquals('c', $pickedItem['value']);
}

/** @test */
public function it_will_pick_a_random_value_when_all_values_are_zero()
{
$items = collect([
['value' => 'a', 'weight' => 0],
['value' => 'b', 'weight' => 0],
['value' => 'c', 'weight' => 0],
]);

$this->assertIsArray($items->weightedRandom(fn (array $item) => $item['weight']));
}

/** @test */
public function it_can_pick_a_weighted_random_by_attribute_name()
{
$items = collect([
['value' => 'a', 'weight' => 0],
['value' => 'b', 'weight' => 1],
['value' => 'c', 'weight' => 0],
]);

$item = ($items->weightedRandom('weight'));

$this->assertEquals('b', $item['value']);
}
}
2 changes: 2 additions & 0 deletions tests/TestCase.php
Expand Up @@ -11,6 +11,8 @@ abstract class TestCase extends BaseTestCase
protected function setUp(): void
{
$this->createDummyprovider()->register();

ray()->newScreen($this->getName());
}

protected function createDummyprovider(): CollectionMacroServiceProvider
Expand Down

0 comments on commit 93ce541

Please sign in to comment.