diff --git a/src/Money.php b/src/Money.php index a9b0defd..7d808ad7 100644 --- a/src/Money.php +++ b/src/Money.php @@ -9,13 +9,12 @@ use Money\Exception\InvalidArgumentException; use function array_fill; -use function array_keys; -use function array_map; -use function array_sum; +use function array_key_first; use function count; use function filter_var; -use function floor; +use function is_float; use function is_int; +use function ltrim; use function max; use function str_pad; use function strlen; @@ -320,41 +319,64 @@ public function allocate(array $ratios): array { $remainder = $this->amount; $results = []; - $total = array_sum($ratios); - - if ($total <= 0) { - throw new InvalidArgumentException('Cannot allocate to none, sum of ratios must be greater than zero'); - } + $scale = 0; + $numbers = []; foreach ($ratios as $key => $ratio) { if ($ratio < 0) { throw new InvalidArgumentException('Cannot allocate to none, ratio must be zero or positive'); } - $share = self::$calculator::share($this->amount, (string) $ratio, (string) $total); + $number = is_float($ratio) ? Number::fromFloat($ratio) : Number::fromNumber($ratio); + $numbers[$key] = $number; + $scale = max($scale, strlen($number->getFractionalPart())); + } + + $total = '0'; + $normalizedRatios = []; + + foreach ($numbers as $key => $number) { + $normalized = $number->getIntegerPart() . str_pad($number->getFractionalPart(), $scale, '0'); + $normalizedRatios[$key] = ltrim($normalized, '0') ?: '0'; + $total = self::$calculator::add($total, $normalizedRatios[$key]); + } + + $ratios = $normalizedRatios; + + if (self::$calculator::compare($total, '0') <= 0) { + throw new InvalidArgumentException('Cannot allocate to none, sum of ratios must be greater than zero'); + } + + $fractions = []; + + foreach ($ratios as $key => $ratio) { + $product = self::$calculator::multiply($this->amount, $ratio); + $share = self::$calculator::share($this->amount, $ratio, $total); + $fractions[$key] = self::$calculator::subtract($product, self::$calculator::multiply($share, $total)); + $results[$key] = new self($share, $this->currency); $remainder = self::$calculator::subtract($remainder, $share); } - if (self::$calculator::compare($remainder, '0') === 0) { - return $results; - } + while (true) { + if (self::$calculator::compare($remainder, '0') <= 0) { + return $results; + } - $amount = $this->amount; - $fractions = array_map(static function (float|int $ratio) use ($total, $amount) { - $share = (float) $ratio / $total * (float) $amount; + $index = array_key_first($fractions); - return $share - floor($share); - }, $ratios); + foreach ($fractions as $key => $fraction) { + if (self::$calculator::compare($fraction, $fractions[$index]) <= 0) { + continue; + } + + $index = $key; + } - while (self::$calculator::compare($remainder, '0') > 0) { - $index = $fractions !== [] ? array_keys($fractions, max($fractions))[0] : 0; $results[$index] = new self(self::$calculator::add($results[$index]->amount, '1'), $results[$index]->currency); $remainder = self::$calculator::subtract($remainder, '1'); unset($fractions[$index]); } - - return $results; } /** diff --git a/tests/MoneyTest.php b/tests/MoneyTest.php index 0f0fb2de..5bb5ae9d 100644 --- a/tests/MoneyTest.php +++ b/tests/MoneyTest.php @@ -132,14 +132,14 @@ public function it_divides_the_amount(int|string $divisor, int $roundingMode, st } /** - * @phpstan-param int $amount + * @phpstan-param int|numeric-string $amount * @phpstan-param non-empty-array $ratios - * @phpstan-param non-empty-array $results + * @phpstan-param non-empty-array $results * * @dataProvider allocationExamples * @test */ - public function itAllocatesAmount(int $amount, array $ratios, array $results): void + public function itAllocatesAmount(int|string $amount, array $ratios, array $results): void { $money = new Money($amount, new Currency(self::CURRENCY)); @@ -483,9 +483,9 @@ public static function comparisonExamples(): array /** * @phpstan-return non-empty-list, - * non-empty-array + * non-empty-array * }> */ public static function allocationExamples(): array @@ -505,6 +505,8 @@ public static function allocationExamples(): array [2, [1, 1, 1], [1, 1, 0]], [1, [1, 1], [1, 0]], [1, [0.33, 0.66], [0, 1]], + [4, [1, 2], [1, 3]], + ['7000000000000000', [1, 2], ['2333333333333333', '4666666666666667']], [101, [3, 7], [30, 71]], [101, [7, 3], [71, 30]], [101, ['foo' => 7, 'bar' => 3], ['foo' => 71, 'bar' => 30]],