From 93ce541ef868edb07f202e4ae4e0217bd88a1b27 Mon Sep 17 00:00:00 2001 From: Freek Van der Herten Date: Fri, 18 Mar 2022 08:52:42 +0100 Subject: [PATCH] Add `weightedRandom` macro (#224) * wip * Fix styling * wip * wip * wip * wip Co-authored-by: freekmurze --- README.md | 29 ++++++++++ composer.json | 1 + src/CollectionMacroServiceProvider.php | 1 + src/Macros/WeightedRandom.php | 50 +++++++++++++++++ tests/Macros/WeightedRandomTest.php | 75 ++++++++++++++++++++++++++ tests/TestCase.php | 2 + 6 files changed, 158 insertions(+) create mode 100644 src/Macros/WeightedRandom.php create mode 100644 tests/Macros/WeightedRandomTest.php diff --git a/README.md b/README.md index d9b7e76..fb7471f 100644 --- a/README.md +++ b/README.md @@ -82,6 +82,7 @@ The package will automatically register itself. - [`toPairs`](#topairs) - [`transpose`](#transpose) - [`validate`](#validate) +- [`weightedRandom`](#weightedRandom) - [`withSize`](#withsize) ### `after` @@ -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. diff --git a/composer.json b/composer.json index 3d3f557..7d0dbde 100644 --- a/composer.json +++ b/composer.json @@ -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": { diff --git a/src/CollectionMacroServiceProvider.php b/src/CollectionMacroServiceProvider.php index facc34f..2fb9468 100644 --- a/src/CollectionMacroServiceProvider.php +++ b/src/CollectionMacroServiceProvider.php @@ -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, ]; } diff --git a/src/Macros/WeightedRandom.php b/src/Macros/WeightedRandom.php new file mode 100644 index 0000000..2a2e9ee --- /dev/null +++ b/src/Macros/WeightedRandom.php @@ -0,0 +1,50 @@ +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; + }; + } +} diff --git a/tests/Macros/WeightedRandomTest.php b/tests/Macros/WeightedRandomTest.php new file mode 100644 index 0000000..ae09c03 --- /dev/null +++ b/tests/Macros/WeightedRandomTest.php @@ -0,0 +1,75 @@ + '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']); + } +} diff --git a/tests/TestCase.php b/tests/TestCase.php index f9cc6a2..79750de 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -11,6 +11,8 @@ abstract class TestCase extends BaseTestCase protected function setUp(): void { $this->createDummyprovider()->register(); + + ray()->newScreen($this->getName()); } protected function createDummyprovider(): CollectionMacroServiceProvider