From bc74f3aa1d76f191c6c7c3631e286abb25c38759 Mon Sep 17 00:00:00 2001 From: oleibman <10341515+oleibman@users.noreply.github.com> Date: Sun, 8 Sep 2024 15:40:25 -0700 Subject: [PATCH] Validate POST Data in Sample 45 Errors can result if not validated. --- .../Basic4/45_Quadratic_equation_solver.php | 6 ++-- .../Calculation/Financial/Amortization.php | 4 +-- .../Calculation/MathTrig/Round.php | 33 +++++++++++++++---- .../Calculation/MathTrig/Trunc.php | 29 ++++++++++++---- src/PhpSpreadsheet/Helper/Sample.php | 7 +++- .../Functions/MathTrig/TruncTest.php | 32 ++++++++++++++++++ .../Functional/StreamTest.php | 12 ++++++- .../Writer/Dompdf/PaperSizeArrayTest.php | 18 ++++++++-- tests/data/Calculation/MathTrig/ROUNDDOWN.php | 1 + tests/data/Calculation/MathTrig/ROUNDUP.php | 1 + tests/data/Calculation/MathTrig/TRUNC.php | 20 +++++++++++ 11 files changed, 142 insertions(+), 21 deletions(-) diff --git a/samples/Basic4/45_Quadratic_equation_solver.php b/samples/Basic4/45_Quadratic_equation_solver.php index 30aabf60f5..d681a21092 100644 --- a/samples/Basic4/45_Quadratic_equation_solver.php +++ b/samples/Basic4/45_Quadratic_equation_solver.php @@ -14,7 +14,7 @@ } ?>
- Enter the coefficients for the Ax2 + Bx + C = 0 + Enter the coefficients for Ax2 + Bx + C = 0
@@ -47,7 +47,9 @@ log('Non-numeric input'); + } elseif ($_POST['A'] == 0) { $helper->log('The equation is not quadratic'); } else { // Calculate and Display the results diff --git a/src/PhpSpreadsheet/Calculation/Financial/Amortization.php b/src/PhpSpreadsheet/Calculation/Financial/Amortization.php index e56fabe913..b53829b767 100644 --- a/src/PhpSpreadsheet/Calculation/Financial/Amortization.php +++ b/src/PhpSpreadsheet/Calculation/Financial/Amortization.php @@ -9,8 +9,6 @@ class Amortization { - private const ROUNDING_ADJUSTMENT = (PHP_VERSION_ID < 80400) ? 0 : 1e-14; - /** * AMORDEGRC. * @@ -82,7 +80,7 @@ public static function AMORDEGRC( $amortiseCoeff = self::getAmortizationCoefficient($rate); $rate *= $amortiseCoeff; - $rate += self::ROUNDING_ADJUSTMENT; + $rate = (float) (string) $rate; // ugly way to avoid rounding problem $fNRate = round($yearFrac * $rate * $cost, 0); $cost -= $fNRate; $fRest = $cost - $salvage; diff --git a/src/PhpSpreadsheet/Calculation/MathTrig/Round.php b/src/PhpSpreadsheet/Calculation/MathTrig/Round.php index a573f2afdf..d2aa1c0b13 100644 --- a/src/PhpSpreadsheet/Calculation/MathTrig/Round.php +++ b/src/PhpSpreadsheet/Calculation/MathTrig/Round.php @@ -10,8 +10,6 @@ class Round { use ArrayEnabled; - private const ROUNDING_ADJUSTMENT = (PHP_VERSION_ID < 80400) ? 0 : 1e-14; - /** * ROUND. * @@ -69,11 +67,22 @@ public static function up($number, $digits): array|string|float return 0.0; } + $digitsPlus1 = $digits + 1; if ($number < 0.0) { - return round($number - 0.5 * 0.1 ** $digits + self::ROUNDING_ADJUSTMENT, $digits, PHP_ROUND_HALF_DOWN); + if ($digitsPlus1 < 0) { + return round($number - 0.5 * 0.1 ** $digits, $digits, PHP_ROUND_HALF_DOWN); + } + $result = sprintf("%.{$digitsPlus1}F", $number - 0.5 * 0.1 ** $digits); + + return round((float) $result, $digits, PHP_ROUND_HALF_DOWN); + } + + if ($digitsPlus1 < 0) { + return round($number + 0.5 * 0.1 ** $digits, $digits, PHP_ROUND_HALF_DOWN); } + $result = sprintf("%.{$digitsPlus1}F", $number + 0.5 * 0.1 ** $digits); - return round($number + 0.5 * 0.1 ** $digits - self::ROUNDING_ADJUSTMENT, $digits, PHP_ROUND_HALF_DOWN); + return round((float) $result, $digits, PHP_ROUND_HALF_DOWN); } /** @@ -105,11 +114,23 @@ public static function down($number, $digits): array|string|float return 0.0; } + $digitsPlus1 = $digits + 1; if ($number < 0.0) { - return round($number + 0.5 * 0.1 ** $digits - self::ROUNDING_ADJUSTMENT, $digits, PHP_ROUND_HALF_UP); + if ($digitsPlus1 < 0) { + return round($number + 0.5 * 0.1 ** $digits, $digits, PHP_ROUND_HALF_UP); + } + $result = sprintf("%.{$digitsPlus1}F", $number + 0.5 * 0.1 ** $digits); + + return round((float) $result, $digits, PHP_ROUND_HALF_UP); + } + + if ($digitsPlus1 < 0) { + return round($number - 0.5 * 0.1 ** $digits, $digits, PHP_ROUND_HALF_UP); } - return round($number - 0.5 * 0.1 ** $digits + self::ROUNDING_ADJUSTMENT, $digits, PHP_ROUND_HALF_UP); + $result = sprintf("%.{$digitsPlus1}F", $number - 0.5 * 0.1 ** $digits); + + return round((float) $result, $digits, PHP_ROUND_HALF_UP); } /** diff --git a/src/PhpSpreadsheet/Calculation/MathTrig/Trunc.php b/src/PhpSpreadsheet/Calculation/MathTrig/Trunc.php index 44aedd2ca0..f36f14d704 100644 --- a/src/PhpSpreadsheet/Calculation/MathTrig/Trunc.php +++ b/src/PhpSpreadsheet/Calculation/MathTrig/Trunc.php @@ -13,6 +13,11 @@ class Trunc * TRUNC. * * Truncates value to the number of fractional digits by number_digits. + * This will probably not be the precise result in the unlikely + * event that the number of digits to the left of the decimal + * plus the number of digits to the right exceeds PHP_FLOAT_DIG + * (or possibly that value minus 1). + * Excel is unlikely to do any better. * * @param array|float $value Or can be an array of values * @param array|int $digits Or can be an array of values @@ -34,15 +39,27 @@ public static function evaluate(array|float|string|null $value = 0, array|int|st return $e->getMessage(); } - $digits = floor($digits); + if ($value == 0) { + return $value; + } - // Truncate - $adjust = 10 ** $digits; + if ($value >= 0) { + $minusSign = ''; + } else { + $minusSign = '-'; + $value = -$value; + } + $digits = (int) floor($digits); + if ($digits < 0) { + $result = (float) (substr(sprintf('%.0F', $value), 0, $digits) . str_repeat('0', -$digits)); - if (($digits > 0) && (rtrim((string) (int) ((abs($value) - abs((int) $value)) * $adjust), '0') < $adjust / 10)) { - return $value; + return ($minusSign === '') ? $result : -$result; } + $decimals = (floor($value) == (int) $value) ? (PHP_FLOAT_DIG - strlen((string) (int) $value)) : $digits; + $resultString = ($decimals < 0) ? sprintf('%F', $value) : sprintf('%.' . $decimals . 'F', $value); + $regExp = '/([.]\\d{' . $digits . '})\\d+$/'; + $result = $minusSign . (preg_replace($regExp, '$1', $resultString) ?? $resultString); - return ((int) ($value * $adjust)) / $adjust; + return (float) $result; } } diff --git a/src/PhpSpreadsheet/Helper/Sample.php b/src/PhpSpreadsheet/Helper/Sample.php index c772aface1..e0d2cfee24 100644 --- a/src/PhpSpreadsheet/Helper/Sample.php +++ b/src/PhpSpreadsheet/Helper/Sample.php @@ -9,6 +9,7 @@ use PhpOffice\PhpSpreadsheet\Spreadsheet; use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet; use PhpOffice\PhpSpreadsheet\Writer\IWriter; +use PhpOffice\PhpSpreadsheet\Writer\Pdf\Dompdf; use RecursiveDirectoryIterator; use RecursiveIteratorIterator; use RecursiveRegexIterator; @@ -129,7 +130,11 @@ public function write(Spreadsheet $spreadsheet, string $filename, array $writers $writerCallback($writer); } $callStartTime = microtime(true); - $writer->save($path); + if (PHP_VERSION_ID >= 80400 && $writer instanceof Dompdf) { + @$writer->save($path); + } else { + $writer->save($path); + } $this->logWrite($writer, $path, $callStartTime); if ($this->isCli() === false) { // @codeCoverageIgnoreStart diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/TruncTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/TruncTest.php index beaf0a8313..e82d7ccd9b 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/TruncTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/TruncTest.php @@ -47,4 +47,36 @@ public static function providerTruncArray(): array 'matrix' => [[[3.14, 3.141], [3.14159, 3.14159265]], '3.1415926536', '{2, 3; 5, 8}'], ]; } + + /** + * @dataProvider providerTooMuchPrecision + */ + public function testTooMuchPrecision(mixed $expectedResult, float|int|string $value, int $digits = 1): void + { + // This test is pretty screwy. Possibly shouldn't even attempt it. + // At any rate, these results seem to indicate that PHP + // maximum precision is PHP_FLOAT_DIG - 1 digits, not PHP_FLOAT_DIG. + // If that changes, at least one of these tests will have to change. + $sheet = $this->getSheet(); + $sheet->getCell('E1')->setValue($value); + $sheet->getCell('E2')->setValue("=TRUNC(E1,$digits)"); + $result = $sheet->getCell('E2')->getCalculatedValue(); + self::assertSame($expectedResult, (string) $result); + } + + public static function providerTooMuchPrecision(): array + { + $max64Plus1 = 9223372036854775808; + $stringMax = (string) $max64Plus1; + + return [ + '2 digits less than PHP_FLOAT_DIG' => ['1' . str_repeat('0', PHP_FLOAT_DIG - 4) . '1.2', 10.0 ** (PHP_FLOAT_DIG - 3) + 1.2, 1], + '1 digit less than PHP_FLOAT_DIG' => ['1' . str_repeat('0', PHP_FLOAT_DIG - 3) . '1', 10.0 ** (PHP_FLOAT_DIG - 2) + 1.2, 1], + 'PHP_FLOAT_DIG' => ['1.0E+' . (PHP_FLOAT_DIG - 1), 10.0 ** (PHP_FLOAT_DIG - 1) + 1.2, 1], + '1 digit more than PHP_FLOAT_DIG' => ['1.0E+' . PHP_FLOAT_DIG, 10.0 ** PHP_FLOAT_DIG + 1.2, 1], + '32bit exceed int max' => ['3123456780', 3123456789, -1], + '64bit exceed int max neg decimals' => [$stringMax, $max64Plus1, -1], + '64bit exceed int max pos decimals' => [$stringMax, $max64Plus1, 1], + ]; + } } diff --git a/tests/PhpSpreadsheetTests/Functional/StreamTest.php b/tests/PhpSpreadsheetTests/Functional/StreamTest.php index 3430b7a31b..c67c1ab22c 100644 --- a/tests/PhpSpreadsheetTests/Functional/StreamTest.php +++ b/tests/PhpSpreadsheetTests/Functional/StreamTest.php @@ -8,6 +8,12 @@ use PhpOffice\PhpSpreadsheet\Spreadsheet; use PHPUnit\Framework\TestCase; +/** + * Not clear that Dompdf will be Php8.4 compatible in time. + * Run in separate process and add version test till it is ready. + * + * @runTestsInSeparateProcesses + */ class StreamTest extends TestCase { public static function providerFormats(): array @@ -42,7 +48,11 @@ public function testAllWritersCanWriteToStream(string $format): void } else { self::assertSame(0, $stat['size']); - $writer->save($stream); + if ($format === 'Dompdf' && PHP_VERSION_ID >= 80400) { + @$writer->save($stream); + } else { + $writer->save($stream); + } self::assertIsResource($stream, 'should not close the stream for further usage out of PhpSpreadsheet'); $stat = fstat($stream); diff --git a/tests/PhpSpreadsheetTests/Writer/Dompdf/PaperSizeArrayTest.php b/tests/PhpSpreadsheetTests/Writer/Dompdf/PaperSizeArrayTest.php index 4c0b86b637..ccd0e84f05 100644 --- a/tests/PhpSpreadsheetTests/Writer/Dompdf/PaperSizeArrayTest.php +++ b/tests/PhpSpreadsheetTests/Writer/Dompdf/PaperSizeArrayTest.php @@ -10,6 +10,12 @@ use PhpOffice\PhpSpreadsheet\Writer\Pdf\Dompdf; use PHPUnit\Framework\TestCase; +/** + * Not clear that Dompdf will be Php8.4 compatible in time. + * Run in separate process and add version test till it is ready. + * + * @runTestsInSeparateProcesses + */ class PaperSizeArrayTest extends TestCase { private string $outfile = ''; @@ -36,7 +42,11 @@ public function testPaperSizeArray(): void $sheet->setCellValue('A7', 'Lorem Ipsum'); $writer = new Dompdf($spreadsheet); $this->outfile = File::temporaryFilename(); - $writer->save($this->outfile); + if (PHP_VERSION_ID >= 80400) { // hopefully temporary + @$writer->save($this->outfile); + } else { + $writer->save($this->outfile); + } $spreadsheet->disconnectWorksheets(); unset($spreadsheet); $contents = file_get_contents($this->outfile); @@ -56,7 +66,11 @@ public function testPaperSizeNotArray(): void $sheet->setCellValue('A7', 'Lorem Ipsum'); $writer = new Dompdf($spreadsheet); $this->outfile = File::temporaryFilename(); - $writer->save($this->outfile); + if (PHP_VERSION_ID >= 80400) { // hopefully temporary + @$writer->save($this->outfile); + } else { + $writer->save($this->outfile); + } $spreadsheet->disconnectWorksheets(); unset($spreadsheet); $contents = file_get_contents($this->outfile); diff --git a/tests/data/Calculation/MathTrig/ROUNDDOWN.php b/tests/data/Calculation/MathTrig/ROUNDDOWN.php index 3a65c13f24..fed45cd449 100644 --- a/tests/data/Calculation/MathTrig/ROUNDDOWN.php +++ b/tests/data/Calculation/MathTrig/ROUNDDOWN.php @@ -33,4 +33,5 @@ [0, 'B1, 0'], ['exception', ''], ['exception', '35.51'], + 'negative number and precision' => [-31400, '-31415.92654, -2'], ]; diff --git a/tests/data/Calculation/MathTrig/ROUNDUP.php b/tests/data/Calculation/MathTrig/ROUNDUP.php index 9014a9e3c0..9683d54d4a 100644 --- a/tests/data/Calculation/MathTrig/ROUNDUP.php +++ b/tests/data/Calculation/MathTrig/ROUNDUP.php @@ -33,4 +33,5 @@ [0, 'B1, 0'], ['exception', ''], ['exception', '35.51'], + 'negative number and precision' => [-31500, '-31415.92654, -2'], ]; diff --git a/tests/data/Calculation/MathTrig/TRUNC.php b/tests/data/Calculation/MathTrig/TRUNC.php index 40fa521603..a92d502b26 100644 --- a/tests/data/Calculation/MathTrig/TRUNC.php +++ b/tests/data/Calculation/MathTrig/TRUNC.php @@ -11,6 +11,7 @@ [-31415.92654, '-31415.92654, 10'], [31415.92, '31415.92654, 2'], [31400, '31415.92654, -2'], + 'negative number and precision' => [-31400, '-31415.92654, -2'], [0, '31415.92654, -10'], [0, '-31415.92654, -10'], [12000, '12345.6789, -3'], @@ -32,4 +33,23 @@ [-3, 'A4'], [-5, 'A5'], [0, 'B1'], + 'issue4113' => [1.0, '1.01, 1'], + 'issue4113 negative' => [-1.0, '-1.01, 1'], + 'issue4113 additional' => [10.04, '10.04, 2'], + 'issue4113 additional negative' => [-10.04, '-10.04, 2'], + 'issue4113 small fraction keep all' => [0.04, '0.04, 2'], + 'issue4113 small negative fraction keep all' => [-0.04, '-0.04, 2'], + 'issue4113 small fraction lose some' => [0.0, '0.01, 1'], + 'issue4113 small negative fraction lose some' => [0.0, '-0.001, 1'], + 'issue4113 example 3' => [-43747, '-43747.99122596, 0'], + 'issue4113 example 3 positive' => [43747, '43747.99122596, 0'], + 'issue4113 example 4' => [-9.11, '-9.1196419, 2'], + 'issue4113 example 5' => [-42300.65, '-42300.65099338, 3'], + 'issue4113 example 6 variant 1' => [0.000012, '0.0000123, 6'], + 'issue4113 example 6 variant 2' => [0.0000123, '0.0000123, 8'], + 'issue4113 example 6 variant 3' => [-0.000012, '-0.0000123, 6'], + 'issue4113 example 6 variant 4' => [-0.0000123, '-0.0000123, 8'], + 'issue4113 example 6 variant 5' => [0.000012, '1.23E-5, 6'], + 'issue4113 example 6 variant 6' => [-0.0000123, '-1.23E-5, 8'], + 'exceed 32-bit int max' => [3123456780, '3123456789, -1'], ];