From f6e39a5f052ea21a80d06e216920a66d0870e6c8 Mon Sep 17 00:00:00 2001 From: Tim MacDonald Date: Wed, 22 Jun 2022 16:37:20 +1000 Subject: [PATCH 1/5] add lottery helper --- src/Illuminate/Support/Lottery.php | 117 +++++++++++++++++++++++++++++ tests/Support/LotteryTest.php | 77 +++++++++++++++++++ 2 files changed, 194 insertions(+) create mode 100644 src/Illuminate/Support/Lottery.php create mode 100644 tests/Support/LotteryTest.php diff --git a/src/Illuminate/Support/Lottery.php b/src/Illuminate/Support/Lottery.php new file mode 100644 index 000000000000..a1b80ce6c2a3 --- /dev/null +++ b/src/Illuminate/Support/Lottery.php @@ -0,0 +1,117 @@ +chances = $chances; + + $this->outOf = $outOf; + } + + /** + * @param int $chances + * @param int $outOf + * @return static + */ + public static function odds($chances, $outOf) + { + return new static($chances, $outOf); + } + + /** + * @param callable $callback + * @return $this + */ + public function winner($callback) + { + $this->winnerCallback = $callback; + + return $this; + } + + /** + * @param callable $callback + * @return $this + */ + public function loser($callback) + { + $this->loserCallback = $callback; + + return $this; + } + + /** + * @param mixed ...$args + * @return mixed + */ + public function __invoke(...$args) + { + return ($this->pickCallback())(...$args); + } + + /** + * @param null|int $times + * @return mixed + */ + public function choose($times = null) + { + if ($times === null) { + return ($this->pickCallback())(); + } + + $results = []; + + for ($i = 0; $i < $times; $i++) { + $results[] = ($this->pickCallback())(); + } + + return $results; + } + + /** + * @return callable + */ + protected function pickCallback() + { + $callback = $this->wins() + ? ($this->winnerCallback ?? fn () => true) + : ($this->loserCallback ?? fn () => false); + + return $callback; + } + + /** + * @return bool + */ + protected function wins() + { + return random_int(1, $this->outOf) <= $this->chances; + } +} diff --git a/tests/Support/LotteryTest.php b/tests/Support/LotteryTest.php new file mode 100644 index 000000000000..92e9fa6a91a4 --- /dev/null +++ b/tests/Support/LotteryTest.php @@ -0,0 +1,77 @@ +winner(function () use (&$wins) { + $wins = true; + })->choose(); + + $this->assertTrue($wins); + } + + public function testItCanLose() + { + $wins = false; + $loses = false; + + Lottery::odds(0, 1) + ->winner(function () use (&$wins) { + $wins = true; + })->loser(function () use (&$loses) { + $loses = true; + })->choose(); + + $this->assertFalse($wins); + $this->assertTrue($loses); + } + + public function testItCanReturnValues() + { + $win = Lottery::odds(1, 1)->winner(fn () => 'win')->choose(); + $this->assertSame('win', $win); + + $lose = Lottery::odds(0, 1)->loser(fn () => 'lose')->choose(); + $this->assertSame('lose', $lose); + } + + public function testItCanChooseSeveralTimes() + { + $results = Lottery::odds(1, 1)->winner(fn () => 'win')->choose(2); + $this->assertSame(['win', 'win'], $results); + + $results = Lottery::odds(0, 1)->loser(fn () => 'lose')->choose(2); + $this->assertSame(['lose', 'lose'], $results); + } + + public function testItCanBePassedAsCallable() + { + // Exmaple... + // DB::whenQueryingForLongerThan(Interval::seconds(5), Lottery::odds(1, 5)->winner(function ($connection) { + // Alert the team + // }))2; + $result = (function (callable $callable) { + return $callable('winner-chicken', '-dinner'); + })(Lottery::odds(1, 1)->winner(fn ($first, $second) => 'winner-'.$first.$second)); + + $this->assertSame('winner-winner-chicken-dinner', $result); + } + + public function testWithoutSpecifiedClosuresBooleansAreReturned() + { + $win = Lottery::odds(1, 1)->choose(); + $this->assertTrue($win); + + $lose = Lottery::odds(0, 1)->choose(); + $this->assertFalse($lose); + } +} From 27a1474d0b1d867b909950c463cfd02da5292771 Mon Sep 17 00:00:00 2001 From: Tim MacDonald Date: Thu, 23 Jun 2022 13:49:42 +1000 Subject: [PATCH 2/5] code style --- tests/Support/LotteryTest.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/Support/LotteryTest.php b/tests/Support/LotteryTest.php index 92e9fa6a91a4..b6fff0d5174b 100644 --- a/tests/Support/LotteryTest.php +++ b/tests/Support/LotteryTest.php @@ -57,8 +57,8 @@ public function testItCanBePassedAsCallable() { // Exmaple... // DB::whenQueryingForLongerThan(Interval::seconds(5), Lottery::odds(1, 5)->winner(function ($connection) { - // Alert the team - // }))2; + // Alert the team + // })); $result = (function (callable $callable) { return $callable('winner-chicken', '-dinner'); })(Lottery::odds(1, 1)->winner(fn ($first, $second) => 'winner-'.$first.$second)); From 8738d3a4aa42c56aa2a613d14db86a9b3d258669 Mon Sep 17 00:00:00 2001 From: Tim MacDonald Date: Thu, 23 Jun 2022 13:59:19 +1000 Subject: [PATCH 3/5] refactor --- src/Illuminate/Support/Lottery.php | 49 ++++++++++++++++++++++-------- 1 file changed, 36 insertions(+), 13 deletions(-) diff --git a/src/Illuminate/Support/Lottery.php b/src/Illuminate/Support/Lottery.php index a1b80ce6c2a3..584a64622979 100644 --- a/src/Illuminate/Support/Lottery.php +++ b/src/Illuminate/Support/Lottery.php @@ -5,26 +5,36 @@ class Lottery { /** + * The number of expected wins. + * * @var int */ protected $chances; /** + * The number of potential opportunities to win. + * * @var int */ protected $outOf; /** + * The winning callback. + * * @var null|callable */ - protected $winnerCallback; + protected $winner; /** + * The losing callback. + * * @var null|callable */ - protected $loserCallback; + protected $loser; /** + * Create a new Lottery instance. + * * @param int $chances * @param int $outOf */ @@ -36,6 +46,8 @@ public function __construct($chances, $outOf) } /** + * Create a new Lottery instance. + * * @param int $chances * @param int $outOf * @return static @@ -46,68 +58,79 @@ public static function odds($chances, $outOf) } /** + * Set the winner callback. + * * @param callable $callback * @return $this */ public function winner($callback) { - $this->winnerCallback = $callback; + $this->winner = $callback; return $this; } /** + * Set the loser callback. + * * @param callable $callback * @return $this */ public function loser($callback) { - $this->loserCallback = $callback; + $this->loser = $callback; return $this; } /** + * Run the lottery. + * * @param mixed ...$args * @return mixed */ public function __invoke(...$args) { - return ($this->pickCallback())(...$args); + return $this->runCallback(...$args); } /** + * Run the lottery. + * * @param null|int $times * @return mixed */ public function choose($times = null) { if ($times === null) { - return ($this->pickCallback())(); + return $this->runCallback(); } $results = []; for ($i = 0; $i < $times; $i++) { - $results[] = ($this->pickCallback())(); + $results[] = $this->runCallback(); } return $results; } /** + * Run the winner or loser callback, randomly. + * + * @param mixed ...$args * @return callable */ - protected function pickCallback() + protected function runCallback(...$args) { - $callback = $this->wins() - ? ($this->winnerCallback ?? fn () => true) - : ($this->loserCallback ?? fn () => false); - - return $callback; + return $this->wins() + ? ($this->winner ?? fn () => true)(...$args) + : ($this->loser ?? fn () => false)(...$args); } /** + * Determine if the lottery "wins" or "loses". + * * @return bool */ protected function wins() From 52d7628718f276ee619079744cf84a33ee04cd72 Mon Sep 17 00:00:00 2001 From: Tim MacDonald Date: Fri, 24 Jun 2022 11:01:27 +1000 Subject: [PATCH 4/5] add testing helpers --- src/Illuminate/Support/Lottery.php | 112 ++++++++++++++++++++++++++++- tests/Support/LotteryTest.php | 64 +++++++++++++++++ 2 files changed, 175 insertions(+), 1 deletion(-) diff --git a/src/Illuminate/Support/Lottery.php b/src/Illuminate/Support/Lottery.php index 584a64622979..005e19872a14 100644 --- a/src/Illuminate/Support/Lottery.php +++ b/src/Illuminate/Support/Lottery.php @@ -32,6 +32,13 @@ class Lottery */ protected $loser; + /** + * The factory that should be used to generate results. + * + * @var callable|null + */ + protected static $resultFactory; + /** * Create a new Lottery instance. * @@ -135,6 +142,109 @@ protected function runCallback(...$args) */ protected function wins() { - return random_int(1, $this->outOf) <= $this->chances; + return static::resultFactory()($this->chances, $this->outOf); + } + + /** + * The factory that determines the lottery result. + * + * @return callable + */ + protected static function resultFactory() + { + return static::$resultFactory ?? fn ($chances, $outOf) => random_int(1, $outOf) <= $chances; + } + + /** + * Force the lottery to always result in a win. + * + * @param callable|null $callback + * @return void + */ + public static function alwaysWin($callback = null) + { + self::setResultFactory(fn () => true); + + if ($callback === null) { + return; + } + + $callback(); + + static::determineResultNormally(); + } + + /** + * Force the lottery to always result in a lose. + * + * @param callable|null $callback + * @return void + */ + public static function alwaysLose($callback = null) + { + self::setResultFactory(fn () => false); + + if ($callback === null) { + return; + } + + $callback(); + + static::determineResultNormally(); + } + + /** + * Set the sequence that will be used to determine lottery results. + * + * @param array $sequence + * @param callable|null $whenMissing + * @return void + */ + public static function forceResultWithSequence($sequence, $whenMissing = null) + { + $next = 0; + + $whenMissing ??= function ($chances, $outOf) use (&$next) { + $factoryCache = static::$resultFactory; + + static::$resultFactory = null; + + $result = static::resultFactory()($chances, $outOf); + + static::$resultFactory = $factoryCache; + + $next++; + + return $result; + }; + + static::setResultFactory(function ($chances, $outOf) use (&$next, $sequence, $whenMissing) { + if (array_key_exists($next, $sequence)) { + return $sequence[$next++]; + } + + return $whenMissing($chances, $outOf); + }); + } + + /** + * Indicate that the lottery results should be determined normally. + * + * @return void + */ + public static function determineResultNormally() + { + static::$resultFactory = null; + } + + /** + * Set the factory that should be used to deterine the lottery results. + * + * @param callable $factory + * @return void + */ + public static function setResultFactory($factory) + { + self::$resultFactory = $factory; } } diff --git a/tests/Support/LotteryTest.php b/tests/Support/LotteryTest.php index b6fff0d5174b..3e703583cc0f 100644 --- a/tests/Support/LotteryTest.php +++ b/tests/Support/LotteryTest.php @@ -4,6 +4,7 @@ use Illuminate\Support\Lottery; use PHPUnit\Framework\TestCase; +use RuntimeException; class LotteryTest extends TestCase { @@ -74,4 +75,67 @@ public function testWithoutSpecifiedClosuresBooleansAreReturned() $lose = Lottery::odds(0, 1)->choose(); $this->assertFalse($lose); } + + public function testItCanForceWinningResultInTests() + { + $result = null; + Lottery::alwaysWin(function () use (&$result) { + $result = Lottery::odds(1, 2)->winner(fn () => 'winner')->choose(10); + }); + + $this->assertSame([ + 'winner', 'winner', 'winner', 'winner', 'winner', + 'winner', 'winner', 'winner', 'winner', 'winner', + ], $result); + } + + public function testItCanForceLosingResultInTests() + { + $result = null; + Lottery::alwaysLose(function () use (&$result) { + $result = Lottery::odds(1, 2)->loser(fn () => 'loser')->choose(10); + }); + + $this->assertSame([ + 'loser', 'loser', 'loser', 'loser', 'loser', + 'loser', 'loser', 'loser', 'loser', 'loser', + ], $result); + } + + public function testItCanForceTheResultViaSequence() + { + $result = null; + Lottery::forceResultWithSequence([ + true, false, true, false, true, + false, true, false, true, false, + ]); + + $result = Lottery::odds(1, 100)->winner(fn () => 'winner')->loser(fn () => 'loser')->choose(10); + + $this->assertSame([ + 'winner', 'loser', 'winner', 'loser', 'winner', + 'loser', 'winner', 'loser', 'winner', 'loser', + ], $result); + } + + public function testItCanHandleMissingSequenceItems() + { + $result = null; + Lottery::forceResultWithSequence([ + 0 => true, + 1 => true, + // 2 => ... + 3 => true, + ], fn () => throw new RuntimeException('Missing key in sequence.')); + + $result = Lottery::odds(1, 10000)->winner(fn () => 'winner')->loser(fn () => 'loser')->choose(); + $this->assertSame('winner', $result); + + $result = Lottery::odds(1, 10000)->winner(fn () => 'winner')->loser(fn () => 'loser')->choose(); + $this->assertSame('winner', $result); + + $this->expectException(RuntimeException::class); + $this->expectErrorMessage('Missing key in sequence.'); + Lottery::odds(1, 10000)->winner(fn () => 'winner')->loser(fn () => 'loser')->choose(); + } } From 78a15fb364fee305810bdbf63d6b64ec3a31d3d8 Mon Sep 17 00:00:00 2001 From: Tim MacDonald Date: Thu, 10 Nov 2022 16:49:17 +1100 Subject: [PATCH 5/5] Support single float value --- src/Illuminate/Support/Lottery.php | 26 +++++++++++------- tests/Support/LotteryTest.php | 43 ++++++++++++++++++++++++++++++ 2 files changed, 60 insertions(+), 9 deletions(-) diff --git a/src/Illuminate/Support/Lottery.php b/src/Illuminate/Support/Lottery.php index 005e19872a14..1cc87d489765 100644 --- a/src/Illuminate/Support/Lottery.php +++ b/src/Illuminate/Support/Lottery.php @@ -2,19 +2,21 @@ namespace Illuminate\Support; +use RuntimeException; + class Lottery { /** * The number of expected wins. * - * @var int + * @var int|float */ protected $chances; /** * The number of potential opportunities to win. * - * @var int + * @var int|null */ protected $outOf; @@ -42,11 +44,15 @@ class Lottery /** * Create a new Lottery instance. * - * @param int $chances - * @param int $outOf + * @param int|float $chances + * @param ?int $outOf */ - public function __construct($chances, $outOf) + public function __construct($chances, $outOf = null) { + if ($outOf === null && is_float($chances) && $chances > 1) { + throw new RuntimeException('Float must not be greater than 1.'); + } + $this->chances = $chances; $this->outOf = $outOf; @@ -55,11 +61,11 @@ public function __construct($chances, $outOf) /** * Create a new Lottery instance. * - * @param int $chances - * @param int $outOf + * @param int|float $chances + * @param ?int $outOf * @return static */ - public static function odds($chances, $outOf) + public static function odds($chances, $outOf = null) { return new static($chances, $outOf); } @@ -152,7 +158,9 @@ protected function wins() */ protected static function resultFactory() { - return static::$resultFactory ?? fn ($chances, $outOf) => random_int(1, $outOf) <= $chances; + return static::$resultFactory ?? fn ($chances, $outOf) => $outOf === null + ? random_int(0, PHP_INT_MAX) / PHP_INT_MAX <= $chances + : random_int(1, $outOf) <= $chances; } /** diff --git a/tests/Support/LotteryTest.php b/tests/Support/LotteryTest.php index 3e703583cc0f..ca0d5638b890 100644 --- a/tests/Support/LotteryTest.php +++ b/tests/Support/LotteryTest.php @@ -8,6 +8,13 @@ class LotteryTest extends TestCase { + protected function tearDown(): void + { + parent::tearDown(); + + Lottery::determineResultNormally(); + } + public function testItCanWin() { $wins = false; @@ -138,4 +145,40 @@ public function testItCanHandleMissingSequenceItems() $this->expectErrorMessage('Missing key in sequence.'); Lottery::odds(1, 10000)->winner(fn () => 'winner')->loser(fn () => 'loser')->choose(); } + + public function testItThrowsForFloatsOverOne() + { + $this->expectException(RuntimeException::class); + $this->expectErrorMessage('Float must not be greater than 1.'); + + new Lottery(1.1); + } + + public function testItCanWinWithFloat() + { + $wins = false; + + Lottery::odds(1.0) + ->winner(function () use (&$wins) { + $wins = true; + })->choose(); + + $this->assertTrue($wins); + } + + public function testItCanLoseWithFloat() + { + $wins = false; + $loses = false; + + Lottery::odds(0.0) + ->winner(function () use (&$wins) { + $wins = true; + })->loser(function () use (&$loses) { + $loses = true; + })->choose(); + + $this->assertFalse($wins); + $this->assertTrue($loses); + } }