From 7689037b606fd20ba7ab1a89c58edf1cfe5c9ff0 Mon Sep 17 00:00:00 2001 From: oleibman <10341515+oleibman@users.noreply.github.com> Date: Wed, 6 Aug 2025 17:53:04 -0700 Subject: [PATCH 1/2] Additional Floating Point Precision Changes Fix #1324, which went stale many years ago, and is now re-opened. It can be considered a follow-on to PR #4479 which was installed in May. The user complains of a *very* small loss of precision. This can be corrected by using `StringHelper::convertToString` in lieu of a simple cast from float to string in two places. `Style\NumberFormat\Formatter` is quite straightforward. `Writer\Csv` is a bit more complicated. If you put a float with more than 15 digits precision in a Csv, on opening it Excel will give a pop-up, saying something inaccurate like "We've corrected this for you - is that okay?" Better to avoid that by making sure to avoid too much precision when writing to Csv. --- .../Style/NumberFormat/Formatter.php | 4 +- src/PhpSpreadsheet/Writer/Csv.php | 37 +++++++------- .../Shared/Issue1324Test.php | 48 +++++++++++++++++++ 3 files changed, 70 insertions(+), 19 deletions(-) create mode 100644 tests/PhpSpreadsheetTests/Shared/Issue1324Test.php diff --git a/src/PhpSpreadsheet/Style/NumberFormat/Formatter.php b/src/PhpSpreadsheet/Style/NumberFormat/Formatter.php index 74ae4bd41d..86cedc38ae 100644 --- a/src/PhpSpreadsheet/Style/NumberFormat/Formatter.php +++ b/src/PhpSpreadsheet/Style/NumberFormat/Formatter.php @@ -159,7 +159,9 @@ public static function toFormattedString($value, string $format, null|array|call // For 'General' format code, we just pass the value although this is not entirely the way Excel does it, // it seems to round numbers to a total of 10 digits. if (($format === NumberFormat::FORMAT_GENERAL) || ($format === NumberFormat::FORMAT_TEXT)) { - return self::adjustSeparators((string) $value); + return self::adjustSeparators( + StringHelper::convertToString($value) + ); } // Ignore square-$-brackets prefix in format string, like "[$-411]ge.m.d", "[$-010419]0%", etc diff --git a/src/PhpSpreadsheet/Writer/Csv.php b/src/PhpSpreadsheet/Writer/Csv.php index f8a65426b6..0ec8aa4724 100644 --- a/src/PhpSpreadsheet/Writer/Csv.php +++ b/src/PhpSpreadsheet/Writer/Csv.php @@ -2,10 +2,10 @@ namespace PhpOffice\PhpSpreadsheet\Writer; +use Composer\Pcre\Preg; use PhpOffice\PhpSpreadsheet\Calculation\Calculation; use PhpOffice\PhpSpreadsheet\Cell\Coordinate; use PhpOffice\PhpSpreadsheet\Spreadsheet; -use Stringable; class Csv extends BaseWriter { @@ -133,6 +133,7 @@ public function save($filename, int $flags = 0): void } } } + /** @var string[] $cellsArray */ $this->writeLine($this->fileHandle, $cellsArray); } @@ -271,25 +272,11 @@ public function getEnclosureRequired(): bool return $this->enclosureRequired; } - /** - * Convert boolean to TRUE/FALSE; otherwise return element cast to string. - * - * @param null|bool|float|int|string|Stringable $element element to be converted - */ - private static function elementToString(mixed $element): string - { - if (is_bool($element)) { - return $element ? 'TRUE' : 'FALSE'; - } - - return (string) $element; - } - /** * Write line to CSV file. * * @param resource $fileHandle PHP filehandle - * @param mixed[] $values Array containing values in a row + * @param string[] $values Array containing values in a row */ private function writeLine($fileHandle, array $values): void { @@ -299,9 +286,23 @@ private function writeLine($fileHandle, array $values): void // Build the line $line = ''; - /** @var null|bool|float|int|string|Stringable $element */ foreach ($values as $element) { - $element = self::elementToString($element); + if (Preg::isMatch('/^([+-])?(\d+)[.](\d+)/', $element, $matches)) { + // Excel will "convert" file with pop-up + // if there are more than 15 digits precision. + $whole = $matches[2]; + if ($whole !== '0') { + $wholeLen = strlen($whole); + $frac = $matches[3]; + $maxFracLen = 15 - $wholeLen; + if ($maxFracLen >= 0 && strlen($frac) > $maxFracLen) { + $result = sprintf("%.{$maxFracLen}F", $element); + if (str_contains($result, '.')) { + $element = Preg::replace('/[.]?0+$/', '', $result); // strip trailing zeros + } + } + } + } // Add delimiter $line .= $delimiter; $delimiter = $this->delimiter; diff --git a/tests/PhpSpreadsheetTests/Shared/Issue1324Test.php b/tests/PhpSpreadsheetTests/Shared/Issue1324Test.php new file mode 100644 index 0000000000..f2797d2f1b --- /dev/null +++ b/tests/PhpSpreadsheetTests/Shared/Issue1324Test.php @@ -0,0 +1,48 @@ +getActiveSheet(); + $sheet->getCell('A1')->setValue($s1); + self::assertNotSame(753.15, $sheet->getCell('A1')->getValue()); + $formats = ['Csv', 'Xlsx', 'Xls', 'Ods', 'Html']; + foreach ($formats as $format) { + $reloadedSpreadsheet = $this->writeAndReload($spreadsheet, $format); + $rsheet = $reloadedSpreadsheet->getActiveSheet(); + $s2 = $rsheet->getCell('A1')->getValue(); + self::assertSame($s1, $s2, "difference for $format"); + $reloadedSpreadsheet->disconnectWorksheets(); + } + $spreadsheet->disconnectWorksheets(); + } + + public function testCsv(): void + { + $string1 = '753.149999999999'; + $s1 = (float) $string1; + $spreadsheet = new Spreadsheet(); + $sheet = $spreadsheet->getActiveSheet(); + $sheet->getCell('A1')->setValue($s1); + $writer = new CsvWriter($spreadsheet); + ob_start(); + $writer->save('php://output'); + $output = (string) ob_get_clean(); + self::assertStringContainsString($string1, $output); + $spreadsheet->disconnectWorksheets(); + } +} From d1db7845cce1d0ef47795424a0922d56ac90e1dd Mon Sep 17 00:00:00 2001 From: oleibman <10341515+oleibman@users.noreply.github.com> Date: Thu, 7 Aug 2025 20:30:24 -0700 Subject: [PATCH 2/2] Update CHANGELOG.md --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index c6b3a2ccbd..679e2761b9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -37,6 +37,7 @@ and this project adheres to [Semantic Versioning](https://semver.org). Thia is a ### Fixed - Xlsx Writer eliminate xml:space from non-text nodes. [Issue #4542](https://github.com/PHPOffice/PhpSpreadsheet/issues/4542) [PR #4556](https://github.com/PHPOffice/PhpSpreadsheet/pull/4556) +- Xlsx Writer eliminate xml:space from non-text nodes. [Issue #1324](https://github.com/PHPOffice/PhpSpreadsheet/issues/1324) [PR #4575](https://github.com/PHPOffice/PhpSpreadsheet/pull/4575) ## 2025-07-23 - 4.5.0