@@ -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}
0 commit comments