@@ -238,18 +238,57 @@ private static function checkEarlyZero(string $num1, string $num2, int $scale):
238238 }
239239
240240 /**
241- * Check for division by zero and throw exception .
241+ * Check if a numeric string represents zero .
242242 *
243- * @throws \DivisionByZeroError If divisor is zero
243+ * Handles various zero formats: '0', '0.00', '-0.00', '+0.000', etc.
244+ * Uses consistent normalization logic for reliable zero detection.
245+ *
246+ * Examples of inputs that return true:
247+ * - '0', '0.0', '0.00', '0.000'
248+ * - '-0', '-0.0', '-0.00', '-0.000'
249+ * - '+0', '+0.0', '+0.00', '+0.000'
250+ * - '00', '00.00', '000.000'
251+ *
252+ * Examples of inputs that return false:
253+ * - '1', '0.1', '0.001', '-0.001'
254+ * - 'abc', '', '.'
255+ *
256+ * @param string $number The numeric string to check
257+ *
258+ * @return bool True if the number is zero, false otherwise
244259 */
245- private static function checkDivisionByZero (string $ divisor ): void
260+ private static function isZero (string $ number ): bool
246261 {
247- // Normalize and check for zero - handle '0', '0.00', '-0.00', etc.
248- $ normalized = ltrim ($ divisor , '+- ' );
262+ $ normalized = ltrim ($ number , '+- ' );
249263 $ normalized = ltrim ($ normalized , '0 ' );
250264 $ normalized = ltrim ($ normalized , '. ' );
251265 $ normalized = rtrim ($ normalized , '0 ' );
252- if ($ normalized === '' || $ normalized === '. ' ) {
266+
267+ return $ normalized === '' || $ normalized === '. ' ;
268+ }
269+
270+ /**
271+ * Check if a string starts with a negative sign after trimming whitespace.
272+ *
273+ * @param string $num The string to check
274+ *
275+ * @return bool True if the string starts with '-' after trimming, false otherwise
276+ */
277+ private static function startsWithNegativeSign (string $ num ): bool
278+ {
279+ $ trimmed = ltrim ($ num );
280+
281+ return $ trimmed !== '' && $ trimmed [0 ] === '- ' ;
282+ }
283+
284+ /**
285+ * Check for division by zero and throw exception.
286+ *
287+ * @throws \DivisionByZeroError If divisor is zero
288+ */
289+ private static function checkDivisionByZero (string $ divisor ): void
290+ {
291+ if (self ::isZero ($ divisor )) {
253292 throw new \DivisionByZeroError (self ::DIVISION_BY_ZERO_MESSAGE );
254293 }
255294 }
@@ -292,14 +331,15 @@ protected static function validateIntegerInputs(array $numbers, array $names = [
292331 }
293332
294333 // Check non-negative constraint
295- if (isset ($ constraints ['non_negative ' ]) && in_array ($ index , $ constraints ['non_negative ' ], true ) && isset ($ intPart[ 0 ]) && $ intPart [ 0 ] === ' - ' ) {
334+ if (isset ($ constraints ['non_negative ' ]) && in_array ($ index , $ constraints ['non_negative ' ], true ) && self :: startsWithNegativeSign ($ intPart) ) {
296335 throw new \ValueError ("{$ paramName } must be greater than or equal to 0 " );
297336 }
298337
299338 // Handle negative numbers by removing the sign if allowed
300- if ($ intPart[ 0 ] === ' - '
339+ if (self :: startsWithNegativeSign ( $ intPart)
301340 && (!isset ($ constraints ['non_negative ' ]) || !in_array ($ index , $ constraints ['non_negative ' ], true ))) {
302- $ intPart = substr ($ intPart , 1 );
341+ $ trimmedIntPart = ltrim ($ intPart );
342+ $ intPart = substr ($ trimmedIntPart , 1 );
303343 }
304344
305345 $ results [] = $ intPart ;
@@ -399,6 +439,8 @@ public static function isNegative(BigInteger $x): bool
399439
400440 /**
401441 * Add two arbitrary precision numbers.
442+ *
443+ * @throws \ValueError if inputs are not well-formed
402444 */
403445 public static function add (string $ num1 , string $ num2 , ?int $ scale = null ): string
404446 {
@@ -421,6 +463,8 @@ public static function add(string $num1, string $num2, ?int $scale = null): stri
421463
422464 /**
423465 * Subtract one arbitrary precision number from another.
466+ *
467+ * @throws \ValueError if inputs are not well-formed
424468 */
425469 public static function sub (string $ num1 , string $ num2 , ?int $ scale = null ): string
426470 {
@@ -443,6 +487,8 @@ public static function sub(string $num1, string $num2, ?int $scale = null): stri
443487
444488 /**
445489 * Multiply two arbitrary precision numbers.
490+ *
491+ * @throws \ValueError if inputs are not well-formed
446492 */
447493 public static function mul (string $ num1 , string $ num2 , ?int $ scale = null ): string
448494 {
@@ -474,6 +520,7 @@ public static function mul(string $num1, string $num2, ?int $scale = null): stri
474520 * Divide two arbitrary precision numbers.
475521 *
476522 * @throws \DivisionByZeroError When divisor is zero
523+ * @throws \ValueError if inputs are not well-formed
477524 */
478525 public static function div (string $ num1 , string $ num2 , ?int $ scale = null ): string
479526 {
@@ -505,6 +552,7 @@ public static function div(string $num1, string $num2, ?int $scale = null): stri
505552 * Uses the PHP 7.2+ behavior
506553 *
507554 * @throws \DivisionByZeroError When divisor is zero
555+ * @throws \ValueError if inputs are not well-formed
508556 */
509557 public static function mod (string $ num1 , string $ num2 , ?int $ scale = null ): string
510558 {
@@ -530,6 +578,8 @@ public static function mod(string $num1, string $num2, ?int $scale = null): stri
530578
531579 /**
532580 * Compare two arbitrary precision numbers.
581+ *
582+ * @throws \ValueError if inputs are not well-formed
533583 */
534584 public static function comp (string $ num1 , string $ num2 , ?int $ scale = null ): int
535585 {
@@ -578,12 +628,7 @@ public static function pow(string $base, string $exponent, ?int $scale = null):
578628 [$ base , $ exponent ] = self ::validateAndNormalizeInputs ($ base , $ exponent , 'bcpow ' );
579629
580630 // Handle special case: 0 to any power is 0 (except 0^0 which is handled above)
581- // Check for zero using same logic as division by zero check
582- $ normalized = ltrim ($ base , '+- ' );
583- $ normalized = ltrim ($ normalized , '0 ' );
584- $ normalized = ltrim ($ normalized , '. ' );
585- $ normalized = rtrim ($ normalized , '0 ' );
586- if ($ normalized === '' || $ normalized === '. ' ) {
631+ if (self ::isZero ($ base )) {
587632 $ result = '0 ' ;
588633 if ($ scale !== 0 ) {
589634 $ result .= '. ' .str_repeat ('0 ' , $ scale );
@@ -688,6 +733,8 @@ public static function powmod(string $base, string $exponent, string $modulus, ?
688733
689734 /**
690735 * Get the square root of an arbitrary precision number.
736+ *
737+ * @throws \ValueError if inputs are not well-formed
691738 */
692739 public static function sqrt (string $ num , ?int $ scale = null ): string
693740 {
@@ -697,7 +744,7 @@ public static function sqrt(string $num, ?int $scale = null): string
697744 // Argument validation
698745 self ::validateNumberString ($ num , 'bcsqrt ' , 1 , 'num ' );
699746
700- // Use default scale if not provided
747+ // Use default scale if not provided (needed for early zero return)
701748 if ($ scale === null ) {
702749 if (!isset (self ::$ scale )) {
703750 $ defaultScale = ini_get ('bcmath.scale ' );
@@ -707,21 +754,78 @@ public static function sqrt(string $num, ?int $scale = null): string
707754 }
708755 self ::validateScale ($ scale , 'bcsqrt ' , 2 );
709756
757+ // Check for negative numbers (except negative zero)
758+ if (self ::startsWithNegativeSign ($ num ) && !self ::isZero ($ num )) {
759+ throw new \ValueError ('bcsqrt(): Argument #1 ($num) must be greater than or equal to 0 ' );
760+ }
761+
762+ // Handle zero case early (including negative zero)
763+ if (self ::isZero ($ num )) {
764+ return $ scale !== 0 ? '0. ' .str_repeat ('0 ' , $ scale ) : '0 ' ;
765+ }
766+
710767 $ temp = explode ('. ' , $ num );
711- $ numStr = implode ('' , $ temp );
712- $ wasPadded = strlen ($ numStr ) % 2 !== 0 ;
713- if ($ wasPadded ) {
714- $ numStr = "0 {$ numStr }" ;
715- }
716- // Calculate decimal start position: original integer length + padding, divided by 2
717- $ integerLength = strlen ($ temp [0 ]) + ($ wasPadded ? 1 : 0 );
718- $ decStart = $ integerLength / 2 ;
768+ $ integerPart = $ temp [0 ];
769+ $ decimalPart = $ temp [1 ] ?? '' ;
770+
771+ // Special handling for numbers < 1
772+ $ leadingZeroPairs = 0 ;
773+ $ skipIntegerPart = false ;
774+ if ($ integerPart === '0 ' && $ decimalPart !== '' ) {
775+ $ skipIntegerPart = true ;
776+ // Count leading zeros in decimal part
777+ $ leadingZeros = strspn ($ decimalPart , '0 ' );
778+ // For decimals < 1, each pair of leading zeros in the input produces one leading zero in the result.
779+ $ leadingZeroPairs = (int ) floor ($ leadingZeros / 2 );
780+
781+ // Now we need to create proper pairs from the decimal part
782+ // If odd number of leading zeros, the last zero pairs with first non-zero digit
783+ if ($ leadingZeros % 2 === 1 ) {
784+ // Skip the paired zeros, keep the odd zero with remaining digits
785+ $ decimalPart = substr ($ decimalPart , $ leadingZeros - 1 );
786+ } else {
787+ // Skip all the leading zeros as they're all paired
788+ $ decimalPart = substr ($ decimalPart , $ leadingZeros );
789+ }
790+
791+ // Now pad the decimal part if needed
792+ if (strlen ($ decimalPart ) % 2 !== 0 ) {
793+ $ decimalPart .= '0 ' ;
794+ }
795+
796+ // For numbers < 1, we only process the decimal part
797+ $ numStr = $ decimalPart ;
798+ } else {
799+ // For numbers >= 1, process normally
800+ // Pad integer part on the left if odd length
801+ if (strlen ($ integerPart ) % 2 !== 0 ) {
802+ $ integerPart = '0 ' .$ integerPart ;
803+ }
804+
805+ // Pad decimal part on the right if odd length
806+ if (strlen ($ decimalPart ) % 2 !== 0 ) {
807+ $ decimalPart .= '0 ' ;
808+ }
809+
810+ // Create combined string
811+ $ numStr = $ integerPart .$ decimalPart ;
812+ }
813+
814+ // Calculate how many digits the integer part of the result should have
815+ // For numbers >= 1: ceil(n/2) where n is the number of integer digits
816+ // For numbers < 1: 0 (the result will also be < 1)
817+ $ integerResultDigits = ($ temp [0 ] === '0 ' ) ? 0 : (int ) ceil (strlen ($ temp [0 ]) / 2 );
818+
819+ // Create array of digit pairs
719820 $ parts = str_split ($ numStr , 2 );
720821 $ parts = array_map ('intval ' , $ parts );
822+
721823 $ i = 0 ;
722824 $ p = 0 ; // for the first step, p = 0
723825 $ c = $ parts [$ i ];
724826 $ result = '' ;
827+ $ digitCount = 0 ; // Track how many result digits we've generated
828+
725829 while (true ) {
726830 // determine the greatest digit x such that x(20p+x) <= c
727831 for ($ x = 1 ; $ x <= 10 ; $ x ++) {
@@ -732,17 +836,40 @@ public static function sqrt(string $num, ?int $scale = null): string
732836 }
733837 }
734838 $ result .= $ x ;
839+ $ digitCount ++;
840+
841+ // Add decimal point after we've generated all integer digits
842+ if ($ digitCount === $ integerResultDigits && $ scale > 0 ) {
843+ $ result .= '. ' ;
844+ }
845+
735846 $ y = $ x * (20 * $ p + $ x );
736847 $ p = 10 * $ p + $ x ;
737848 $ c = 100 * ($ c - $ y );
738849 if (isset ($ parts [++$ i ])) {
739850 $ c += $ parts [$ i ];
740851 }
741- if ((!$ c && $ i >= $ decStart ) || $ i - $ decStart === $ scale ) {
852+
853+ // Check if we should stop
854+ $ decimalDigits = $ digitCount - $ integerResultDigits ;
855+ if ((!$ c && $ digitCount >= $ integerResultDigits ) || ($ decimalDigits >= $ scale && $ scale >= 0 )) {
742856 break ;
743857 }
744- if ($ decStart === $ i ) {
745- $ result .= '. ' ;
858+ }
859+
860+ // For numbers < 1, format the result properly
861+ if ($ integerResultDigits === 0 ) {
862+ // If scale is 0 and result would be < 1, return '0'
863+ if ($ scale === 0 ) {
864+ return '0 ' ;
865+ }
866+
867+ if ($ leadingZeroPairs > 0 ) {
868+ // Result should be 0.{leadingZeroPairs zeros}{result}
869+ $ result = '0. ' .str_repeat ('0 ' , $ leadingZeroPairs ).$ result ;
870+ } else {
871+ // No leading zeros, but still < 1, so add '0.' prefix
872+ $ result = '0. ' .$ result ;
746873 }
747874 }
748875
@@ -758,9 +885,13 @@ public static function sqrt(string $num, ?int $scale = null): string
758885
759886 /**
760887 * Round down to the nearest integer.
888+ *
889+ * @throws \ValueError if inputs are not well-formed
761890 */
762891 public static function floor (string $ num ): string
763892 {
893+ self ::validateNumberString ($ num , 'bcfloor ' , 1 , 'num ' );
894+
764895 if (!is_numeric ($ num )) {
765896 if (version_compare (PHP_VERSION , '8.4 ' , '>= ' )) {
766897 throw new \ValueError ('bcfloor(): Argument #1 ($num) is not well-formed ' );
@@ -777,7 +908,7 @@ public static function floor(string $num): string
777908 $ fractionalPart = substr ($ num , $ dotPos + 1 );
778909
779910 // For negative numbers with fractional parts, we need to subtract 1
780- if ($ num[ 0 ] === ' - ' && ltrim ($ fractionalPart , '0 ' ) !== '' ) {
911+ if (self :: startsWithNegativeSign ( $ num) && ltrim ($ fractionalPart , '0 ' ) !== '' ) {
781912 return self ::sub ($ integerPart , '1 ' , 0 );
782913 }
783914
@@ -789,9 +920,13 @@ public static function floor(string $num): string
789920
790921 /**
791922 * Round up to the nearest integer.
923+ *
924+ * @throws \ValueError if inputs are not well-formed
792925 */
793926 public static function ceil (string $ num ): string
794927 {
928+ self ::validateNumberString ($ num , 'bcceil ' , 1 , 'num ' );
929+
795930 if (!is_numeric ($ num )) {
796931 if (version_compare (PHP_VERSION , '8.4 ' , '>= ' )) {
797932 throw new \ValueError ('bcceil(): Argument #1 ($num) is not well-formed ' );
@@ -808,7 +943,7 @@ public static function ceil(string $num): string
808943 $ fractionalPart = substr ($ num , $ dotPos + 1 );
809944
810945 // For positive numbers with fractional parts, we need to add 1
811- if ($ num[ 0 ] !== ' - ' && ltrim ($ fractionalPart , '0 ' ) !== '' ) {
946+ if (! self :: startsWithNegativeSign ( $ num) && ltrim ($ fractionalPart , '0 ' ) !== '' ) {
812947 $ integerPart = $ integerPart === '' ? '0 ' : $ integerPart ;
813948
814949 return self ::add ($ integerPart , '1 ' , 0 );
@@ -822,9 +957,13 @@ public static function ceil(string $num): string
822957
823958 /**
824959 * Round to a given decimal place.
960+ *
961+ * @throws \ValueError if inputs are not well-formed
825962 */
826963 public static function round (string $ num , int $ precision = 0 , int $ mode = PHP_ROUND_HALF_UP ): string
827964 {
965+ self ::validateNumberString ($ num , 'bcround ' , 1 , 'num ' );
966+
828967 if (!is_numeric ($ num )) {
829968 if (version_compare (PHP_VERSION , '8.4 ' , '>= ' )) {
830969 throw new \ValueError ('bcround(): Argument #1 ($num) is not well-formed ' );
@@ -862,9 +1001,12 @@ public static function bcroundHelper(string $number, int $precision, int $mode =
8621001
8631002 // Extract sign
8641003 $ sign = '' ;
865- if ($ number[ 0 ] === ' - ' ) {
1004+ if (self :: startsWithNegativeSign ( $ number) ) {
8661005 $ sign = '- ' ;
867- $ number = substr ($ number , 1 );
1006+ $ trimmedNumber = ltrim ($ number );
1007+ $ number = substr ($ trimmedNumber , 1 );
1008+ } else {
1009+ $ number = ltrim ($ number );
8681010 }
8691011
8701012 // Add 0.5 * 10^(-$precision) for rounding (for HALF_UP mode)
0 commit comments