Skip to content

Commit 394b10a

Browse files
committed
feature [8.4][bcmath] implement bcround, bcceil and bcfloor
1 parent 41ace00 commit 394b10a

File tree

4 files changed

+1179
-0
lines changed

4 files changed

+1179
-0
lines changed

src/Php84/Php84.php

Lines changed: 204 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -205,6 +205,14 @@ public static function grapheme_str_split(string $string, int $length)
205205
return $chunks;
206206
}
207207

208+
public static function bcceil(string $num): string
209+
{
210+
if (!is_numeric($num)) {
211+
throw new \ValueError('bcceil(): Argument #1 ($num) is not well-formed');
212+
}
213+
return self::bcround($num, 0, \RoundingMode::PositiveInfinity);
214+
}
215+
208216
public static function bcdivmod(string $num1, string $num2, ?int $scale = null): ?array
209217
{
210218
if (null === $quot = \bcdiv($num1, $num2, 0)) {
@@ -214,4 +222,200 @@ public static function bcdivmod(string $num1, string $num2, ?int $scale = null):
214222

215223
return [$quot, \bcmod($num1, $num2, $scale)];
216224
}
225+
226+
public static function bcfloor(string $num): string
227+
{
228+
if (!is_numeric($num)) {
229+
throw new \ValueError('bcfloor(): Argument #1 ($num) is not well-formed');
230+
}
231+
return self::bcround($num, 0, \RoundingMode::NegativeInfinity);
232+
}
233+
234+
/**
235+
* @param \RoundingMode::* $mode
236+
*/
237+
public static function bcround(string $num, int $precision = 0, int $mode = \RoundingMode::HalfAwayFromZero): string
238+
{
239+
if (!is_numeric($num)) {
240+
throw new \ValueError('bcround(): Argument #1 ($num) is not well-formed');
241+
}
242+
243+
$sign = 1;
244+
if ('' !== $num && ('-' === $num[0] || '+' === $num[0])) {
245+
if ('-' === $num[0]) {
246+
$sign = -1;
247+
}
248+
249+
$num = substr($num, 1);
250+
}
251+
252+
if (false !== strpos($num, '.')) {
253+
[$intPart, $fracPart] = array_pad(explode('.', $num, 2), 2, '');
254+
} else {
255+
$intPart = $num;
256+
$fracPart = '';
257+
}
258+
259+
if ('' === $intPart) {
260+
$intPart = '0';
261+
}
262+
263+
$intPart = self::trimLeadingZeros($intPart);
264+
$fracPart = (string) $fracPart;
265+
266+
if ($precision >= 0) {
267+
$fracLength = \strlen($fracPart);
268+
269+
if ($precision <= $fracLength) {
270+
$scaledInt = $intPart.(string) substr($fracPart, 0, $precision);
271+
$scaledFrac = (string) substr($fracPart, $precision);
272+
} else {
273+
$scaledInt = $intPart.$fracPart.str_repeat('0', $precision - $fracLength);
274+
$scaledFrac = '';
275+
}
276+
} else {
277+
$shift = -$precision;
278+
$intLength = \strlen($intPart);
279+
280+
if ($shift <= $intLength) {
281+
$splitPos = $intLength - $shift;
282+
$scaledInt = substr($intPart, 0, $splitPos);
283+
$scaledInt = '' === $scaledInt ? '0' : $scaledInt;
284+
$scaledFrac = substr($intPart, $splitPos).$fracPart;
285+
} else {
286+
$scaledInt = '0';
287+
$scaledFrac = str_repeat('0', $shift - $intLength).$intPart.$fracPart;
288+
}
289+
}
290+
291+
$roundedInt = self::roundIntegerPart($scaledInt, $scaledFrac, $sign, $mode);
292+
$isZero = '' === trim($roundedInt, '0');
293+
$absResult = self::formatRoundedDigits($roundedInt, $precision);
294+
295+
if (-1 === $sign && !$isZero) {
296+
$absResult = '-'.$absResult;
297+
}
298+
299+
return $absResult;
300+
}
301+
302+
private static function roundIntegerPart(string $intPart, string $fracPart, int $sign, int $mode): string
303+
{
304+
$intPart = self::trimLeadingZeros($intPart);
305+
306+
if ('' === $fracPart || '' === trim($fracPart, '0')) {
307+
return $intPart;
308+
}
309+
310+
$firstDigit = $fracPart[0];
311+
$tail = (string) substr($fracPart, 1);
312+
$tailNonZero = '' !== trim($tail, '0');
313+
$isGreaterThanHalf = $firstDigit > '5' || ('5' === $firstDigit && $tailNonZero);
314+
$isExactlyHalf = '5' === $firstDigit && !$tailNonZero;
315+
$shouldIncrease = false;
316+
317+
switch ($mode) {
318+
case \RoundingMode::TowardsZero:
319+
break;
320+
321+
case \RoundingMode::AwayFromZero:
322+
$shouldIncrease = true;
323+
break;
324+
325+
case \RoundingMode::PositiveInfinity:
326+
$shouldIncrease = $sign > 0;
327+
break;
328+
329+
case \RoundingMode::NegativeInfinity:
330+
$shouldIncrease = $sign < 0;
331+
break;
332+
333+
case \RoundingMode::HalfAwayFromZero:
334+
$shouldIncrease = $isGreaterThanHalf || $isExactlyHalf;
335+
break;
336+
337+
case \RoundingMode::HalfTowardsZero:
338+
$shouldIncrease = $isGreaterThanHalf;
339+
break;
340+
341+
case \RoundingMode::HalfEven:
342+
if ($isGreaterThanHalf) {
343+
$shouldIncrease = true;
344+
} elseif ($isExactlyHalf && self::lastDigit($intPart) % 2 === 1) {
345+
$shouldIncrease = true;
346+
}
347+
break;
348+
349+
case \RoundingMode::HalfOdd:
350+
if ($isGreaterThanHalf) {
351+
$shouldIncrease = true;
352+
} elseif ($isExactlyHalf && self::lastDigit($intPart) % 2 === 0) {
353+
$shouldIncrease = true;
354+
}
355+
break;
356+
}
357+
358+
if ($shouldIncrease) {
359+
$intPart = self::incrementDigits($intPart);
360+
}
361+
362+
return self::trimLeadingZeros($intPart);
363+
}
364+
365+
private static function formatRoundedDigits(string $roundedInt, int $precision): string
366+
{
367+
if ($precision > 0) {
368+
if (\strlen($roundedInt) <= $precision) {
369+
$roundedInt = str_pad($roundedInt, $precision + 1, '0', STR_PAD_LEFT);
370+
}
371+
372+
$intDigits = substr($roundedInt, 0, -$precision);
373+
$fracDigits = substr($roundedInt, -$precision);
374+
375+
$intDigits = self::trimLeadingZeros('' === $intDigits ? '0' : $intDigits);
376+
$fracDigits = str_pad($fracDigits, $precision, '0', STR_PAD_LEFT);
377+
378+
return $intDigits.'.'.$fracDigits;
379+
}
380+
381+
if (0 === $precision) {
382+
return self::trimLeadingZeros($roundedInt);
383+
}
384+
385+
$shift = -$precision;
386+
$digits = $roundedInt.str_repeat('0', $shift);
387+
388+
return self::trimLeadingZeros($digits);
389+
}
390+
391+
private static function incrementDigits(string $digits): string
392+
{
393+
$digits = '' === $digits ? '0' : $digits;
394+
$index = \strlen($digits) - 1;
395+
$result = $digits;
396+
$carry = 1;
397+
398+
while ($index >= 0 && $carry) {
399+
$value = ord($result[$index]) - 48 + $carry;
400+
$carry = $value >= 10 ? 1 : 0;
401+
$result[$index] = chr(48 + ($value % 10));
402+
--$index;
403+
}
404+
405+
return $carry ? '1'.$result : $result;
406+
}
407+
408+
private static function trimLeadingZeros(string $digits): string
409+
{
410+
$digits = ltrim($digits, '0');
411+
412+
return '' === $digits ? '0' : $digits;
413+
}
414+
415+
private static function lastDigit(string $digits): int
416+
{
417+
$length = \strlen($digits);
418+
419+
return $length ? ord($digits[$length - 1]) - 48 : 0;
420+
}
217421
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
if (\PHP_VERSION_ID < 80400) {
13+
// @author Thomas Durand <[email protected]>
14+
final class RoundingMode {
15+
const HalfAwayFromZero = 0;
16+
const HalfTowardsZero = 1;
17+
const HalfEven = 2;
18+
const HalfOdd = 3;
19+
const TowardsZero = 4;
20+
const AwayFromZero = 5;
21+
const NegativeInfinity = 6;
22+
const PositiveInfinity = 7;
23+
24+
private function __construct() {}
25+
26+
public static function cases(): array
27+
{
28+
return [
29+
self::HalfAwayFromZero,
30+
self::HalfTowardsZero,
31+
self::HalfEven,
32+
self::HalfOdd,
33+
self::TowardsZero,
34+
self::AwayFromZero,
35+
self::NegativeInfinity,
36+
self::PositiveInfinity,
37+
];
38+
}
39+
}
40+
}

src/Php84/bootstrap.php

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,9 +68,18 @@ function mb_rtrim(string $string, ?string $characters = null, ?string $encoding
6868
}
6969

7070
if (extension_loaded('bcmath')) {
71+
if (!function_exists('bcceil')) {
72+
function bcceil(string $num): string { return p\Php84::bcceil($num); }
73+
}
7174
if (!function_exists('bcdivmod')) {
7275
function bcdivmod(string $num1, string $num2, ?int $scale = null): ?array { return p\Php84::bcdivmod($num1, $num2, $scale); }
7376
}
77+
if (!function_exists('bcfloor')) {
78+
function bcfloor(string $num): string { return p\Php84::bcfloor($num); }
79+
}
80+
if (!function_exists('bcround')) {
81+
function bcround(string $num, int $precision = 0, int $mode = RoundingMode::HalfAwayFromZero): string { return p\Php84::bcround($num, $precision, $mode); }
82+
}
7483
}
7584

7685
if (\PHP_VERSION_ID >= 80200) {

0 commit comments

Comments
 (0)