diff --git a/CHANGELOG.md b/CHANGELOG.md index b2cc4d6c9a..d284cd8092 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -34,6 +34,7 @@ and this project adheres to [Semantic Versioning](https://semver.org). - Print Area and Row Break. [Issue #1275](https://github.com/PHPOffice/PhpSpreadsheet/issues/1275) [PR #4450](https://github.com/PHPOffice/PhpSpreadsheet/pull/4450) - Copy Styles after insertNewColumnBefore. [Issue #1425](https://github.com/PHPOffice/PhpSpreadsheet/issues/1425) [PR #4468](https://github.com/PHPOffice/PhpSpreadsheet/pull/4468) - Xls Writer Treat Hyperlink Starting with # as Internal. [Issue #56](https://github.com/PHPOffice/PhpSpreadsheet/issues/56) [PR #4453](https://github.com/PHPOffice/PhpSpreadsheet/pull/4453) +- More Precision for Float to String Casts. [Issue #3899](https://github.com/PHPOffice/PhpSpreadsheet/issues/3899) [PR #4479](https://github.com/PHPOffice/PhpSpreadsheet/pull/4479) - Hyperlink Styles. [Issue #1632](https://github.com/PHPOffice/PhpSpreadsheet/issues/1632) [PR #4478](https://github.com/PHPOffice/PhpSpreadsheet/pull/4478) - ODS Handling of Ceiling and Floor. [Issue #477](https://github.com/PHPOffice/PhpSpreadsheet/issues/407) [PR #4466](https://github.com/PHPOffice/PhpSpreadsheet/pull/4466) - Xlsx Reader Do Not Process Printer Settings for Dataonly. [Issue #4477](https://github.com/PHPOffice/PhpSpreadsheet/issues/4477) [PR #4480](https://github.com/PHPOffice/PhpSpreadsheet/pull/4480) diff --git a/src/PhpSpreadsheet/Shared/Date.php b/src/PhpSpreadsheet/Shared/Date.php index e8f23164f3..3767a60118 100644 --- a/src/PhpSpreadsheet/Shared/Date.php +++ b/src/PhpSpreadsheet/Shared/Date.php @@ -471,7 +471,7 @@ public static function stringToExcel(string $dateValue): bool|float if (strlen($dateValue) < 2) { return false; } - if (!preg_match('/^(\d{1,4}[ \.\/\-][A-Z]{3,9}([ \.\/\-]\d{1,4})?|[A-Z]{3,9}[ \.\/\-]\d{1,4}([ \.\/\-]\d{1,4})?|\d{1,4}[ \.\/\-]\d{1,4}([ \.\/\-]\d{1,4})?)( \d{1,2}:\d{1,2}(:\d{1,2})?)?$/iu', $dateValue)) { + if (!preg_match('/^(\d{1,4}[ \.\/\-][A-Z]{3,9}([ \.\/\-]\d{1,4})?|[A-Z]{3,9}[ \.\/\-]\d{1,4}([ \.\/\-]\d{1,4})?|\d{1,4}[ \.\/\-]\d{1,4}([ \.\/\-]\d{1,4})?)( \d{1,2}:\d{1,2}(:\d{1,2}([.]\d+))?)?$/iu', $dateValue)) { return false; } diff --git a/src/PhpSpreadsheet/Shared/StringHelper.php b/src/PhpSpreadsheet/Shared/StringHelper.php index 40adfd8c96..3e4311832c 100644 --- a/src/PhpSpreadsheet/Shared/StringHelper.php +++ b/src/PhpSpreadsheet/Shared/StringHelper.php @@ -2,6 +2,7 @@ namespace PhpOffice\PhpSpreadsheet\Shared; +use Composer\Pcre\Preg; use PhpOffice\PhpSpreadsheet\Calculation\Calculation; use PhpOffice\PhpSpreadsheet\Exception as SpreadsheetException; use Stringable; @@ -494,9 +495,9 @@ public static function mbStrSplit(string $string): array { // Split at all position not after the start: ^ // and not before the end: $ - $split = preg_split('/(?writeElement('v', "$cellValue"); + $objWriter->writeElement('v', $result); } private function writeCellBoolean(XMLWriter $objWriter, string $mappedType, bool $cellValue): void diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/TruncTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/TruncTest.php index 0f6bd0b977..31a1607945 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/TruncTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/TruncTest.php @@ -54,8 +54,9 @@ public function testTooMuchPrecision(mixed $expectedResult, float|int|string $va $sheet = $this->getSheet(); $sheet->getCell('E1')->setValue($value); $sheet->getCell('E2')->setValue("=TRUNC(E1,$digits)"); - $result = $sheet->getCell('E2')->getCalculatedValueString(); - self::assertSame($expectedResult, $result); + /** @var float|string */ + $result = $sheet->getCell('E2')->getCalculatedValue(); + self::assertSame($expectedResult, (string) $result); } public static function providerTooMuchPrecision(): array diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/TextData/ArrayToTextTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/TextData/ArrayToTextTest.php index 3ec22ddda4..2979463327 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/TextData/ArrayToTextTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/TextData/ArrayToTextTest.php @@ -4,6 +4,7 @@ namespace PhpOffice\PhpSpreadsheetTests\Calculation\Functions\TextData; +use PhpOffice\PhpSpreadsheet\Shared\StringHelper; use PHPUnit\Framework\Attributes\DataProvider; class ArrayToTextTest extends AllSetupTeardown @@ -17,6 +18,9 @@ public function testArrayToText(string $expectedResult, array $testData, int $mo $worksheet->getCell('H1')->setValue("=ARRAYTOTEXT(A1:C5, {$mode})"); $result = $worksheet->getCell('H1')->getCalculatedValue(); + $b1SimpleCast = '12345.6789'; + $b1AccurateCast = StringHelper::convertToString(12345.6789); + $expectedResult = str_replace($b1SimpleCast, $b1AccurateCast, $expectedResult); self::assertSame($expectedResult, $result); } diff --git a/tests/PhpSpreadsheetTests/FloatImprecisionTest.php b/tests/PhpSpreadsheetTests/FloatImprecisionTest.php new file mode 100644 index 0000000000..aa960486b3 --- /dev/null +++ b/tests/PhpSpreadsheetTests/FloatImprecisionTest.php @@ -0,0 +1,24 @@ +compatibilityMode = Functions::getCompatibilityMode(); - Functions::setCompatibilityMode(Functions::COMPATIBILITY_OPENOFFICE); + Functions::setCompatibilityMode( + Functions::COMPATIBILITY_OPENOFFICE + ); } protected function tearDown(): void @@ -57,6 +59,8 @@ public function testWriteSpreadsheet(): void $worksheet1 = $workbook->getActiveSheet(); $worksheet1->setCellValue('A1', 1); // Number $worksheet1->setCellValue('B1', 12345.6789); // Number + $b1SimpleCast = '12345.6789'; + $b1AccurateCast = StringHelper::convertToString(12345.6789); $worksheet1->setCellValue('C1', '1'); // Number without cast $worksheet1->setCellValueExplicit('D1', '01234', DataType::TYPE_STRING); // Number casted to string $worksheet1->setCellValue('E1', 'Lorem ipsum'); // String @@ -74,6 +78,11 @@ public function testWriteSpreadsheet(): void $worksheet1->getStyle('D2') ->getNumberFormat() ->setFormatCode(NumberFormat::FORMAT_DATE_DATETIME); + /** @var float */ + $d2SimpleCast = $worksheet1->getCell('D2')->getValue(); + $d2SimpleCast = (string) $d2SimpleCast; + $d2AccurateCast = $worksheet1 + ->getCell('D2')->getValueString(); $worksheet1->setCellValueExplicit('F1', null, DataType::TYPE_ERROR); $worksheet1->setCellValueExplicit('G1', 'Lorem ipsum', DataType::TYPE_INLINE); @@ -85,12 +94,17 @@ public function testWriteSpreadsheet(): void $worksheet1->getStyle('C1')->getFont()->setSize(14); $worksheet1->getStyle('C1')->getFont()->setColor(new Color(Color::COLOR_BLUE)); - $worksheet1->getStyle('C1')->getFill()->setFillType(Fill::FILL_SOLID); - $worksheet1->getStyle('C1')->getFill()->setStartColor(new Color(Color::COLOR_RED)); + $worksheet1->getStyle('C1') + ->getFill()->setFillType(Fill::FILL_SOLID); + $worksheet1->getStyle('C1') + ->getFill()->setStartColor(new Color(Color::COLOR_RED)); - $worksheet1->getStyle('C1')->getFont()->setUnderline(Font::UNDERLINE_SINGLE); - $worksheet1->getStyle('C2')->getFont()->setUnderline(Font::UNDERLINE_DOUBLE); - $worksheet1->getStyle('D2')->getFont()->setUnderline(Font::UNDERLINE_NONE); + $worksheet1->getStyle('C1')->getFont() + ->setUnderline(Font::UNDERLINE_SINGLE); + $worksheet1->getStyle('C2')->getFont() + ->setUnderline(Font::UNDERLINE_DOUBLE); + $worksheet1->getStyle('D2')->getFont() + ->setUnderline(Font::UNDERLINE_NONE); // Worksheet 2 $worksheet2 = $workbook->createSheet(); @@ -101,7 +115,12 @@ public function testWriteSpreadsheet(): void $content = new Content(new Ods($workbook)); $xml = $content->write(); - self::assertXmlStringEqualsXmlFile($this->samplesPath . '/content-with-data.xml', $xml); + $xmlFile = $this->samplesPath . '/content-with-data.xml'; + $xmlContents = (string) file_get_contents($xmlFile); + $xmlContents = str_replace($b1SimpleCast, $b1AccurateCast, $xmlContents); + $xmlContents = str_replace($d2SimpleCast, $d2AccurateCast, $xmlContents); + self::assertXmlStringEqualsXmlString($xmlContents, $xml); + $workbook->disconnectWorksheets(); } public function testWriteWithHiddenWorksheet(): void @@ -124,19 +143,21 @@ public function testWriteWithHiddenWorksheet(): void $xml = $content->write(); self::assertXmlStringEqualsXmlFile($this->samplesPath . '/content-hidden-worksheet.xml', $xml); + $workbook->disconnectWorksheets(); } public function testWriteBorderStyle(): void { $spreadsheet = new Spreadsheet(); - $spreadsheet->getActiveSheet()->getStyle('A1:B2')->applyFromArray([ - 'borders' => [ - 'outline' => [ - 'borderStyle' => Border::BORDER_THICK, - 'color' => ['argb' => 'AA22DD00'], + $spreadsheet->getActiveSheet() + ->getStyle('A1:B2')->applyFromArray([ + 'borders' => [ + 'outline' => [ + 'borderStyle' => Border::BORDER_THICK, + 'color' => ['argb' => 'AA22DD00'], + ], ], - ], - ]); + ]); $content = new Content(new Ods($spreadsheet)); $xml = $content->write(); @@ -161,5 +182,6 @@ public function testWriteBorderStyle(): void } } } + $spreadsheet->disconnectWorksheets(); } } diff --git a/tests/PhpSpreadsheetTests/Writer/Ods/MicrosecondsTest.php b/tests/PhpSpreadsheetTests/Writer/Ods/MicrosecondsTest.php new file mode 100644 index 0000000000..70db6fd806 --- /dev/null +++ b/tests/PhpSpreadsheetTests/Writer/Ods/MicrosecondsTest.php @@ -0,0 +1,50 @@ +getActiveSheet(); + $sheet->setCellValue('A1', Date::dateTimeToExcel($originalDateTime)); + $reloadedSpreadsheet = $this->writeAndReload($spreadsheet, 'Ods'); + $spreadsheet->disconnectWorksheets(); + + $rsheet = $reloadedSpreadsheet->getActiveSheet(); + $rsheet->getStyle('A1') + ->getNumberFormat() + ->setFormatCode('yyyy-mm-dd hh:mm:ss.000'); + /** @var float */ + $reread = $rsheet->getCell('A1')->getValue(); + $temp = Date::excelToDateTimeObject($reread) + ->format('Y-m-d H:i:s.u'); + self::assertSame("{$date} {$time}.000000", $temp, 'round trip works with float value'); + $formatted = $rsheet->getCell('A1')->getFormattedValue(); + self::assertSame("{$date} {$time}.000", $formatted, 'round trip works with formatted value'); + /** @var float */ + $temp = Date::stringToExcel($formatted); + $temp = Date::excelToDateTimeObject($temp) + ->format('Y-m-d H:i:s.u'); + self::assertSame("{$date} {$time}.000000", $temp, 'round trip works using stringToExcel on formatted value'); + + $reloadedSpreadsheet->disconnectWorksheets(); + } +} diff --git a/tests/PhpSpreadsheetTests/Writer/Xls/MicrosecondsTest.php b/tests/PhpSpreadsheetTests/Writer/Xls/MicrosecondsTest.php new file mode 100644 index 0000000000..130d6a401e --- /dev/null +++ b/tests/PhpSpreadsheetTests/Writer/Xls/MicrosecondsTest.php @@ -0,0 +1,47 @@ +getActiveSheet(); + $sheet->setCellValue('A1', Date::dateTimeToExcel($originalDateTime)); + $sheet->getStyle('A1') + ->getNumberFormat() + ->setFormatCode('yyyy-mm-dd hh:mm:ss.000'); + $reloadedSpreadsheet = $this->writeAndReload($spreadsheet, 'Xls'); + $spreadsheet->disconnectWorksheets(); + + $rsheet = $reloadedSpreadsheet->getActiveSheet(); + /** @var float */ + $reread = $rsheet->getCell('A1')->getValue(); + $temp = Date::excelToDateTimeObject($reread) + ->format('Y-m-d H:i:s.u'); + self::assertSame("{$date} {$time}.000000", $temp, 'round trip works with float value'); + $formatted = $rsheet->getCell('A1')->getFormattedValue(); + self::assertSame("{$date} {$time}.000", $formatted, 'round trip works with formatted value'); + /** @var float */ + $temp = Date::stringToExcel($formatted); + $temp = Date::excelToDateTimeObject($temp) + ->format('Y-m-d H:i:s.u'); + self::assertSame("{$date} {$time}.000000", $temp, 'round trip works using stringToExcel on formatted value'); + + $reloadedSpreadsheet->disconnectWorksheets(); + } +} diff --git a/tests/PhpSpreadsheetTests/Writer/Xlsx/MicrosecondsTest.php b/tests/PhpSpreadsheetTests/Writer/Xlsx/MicrosecondsTest.php new file mode 100644 index 0000000000..bdc50bf8cd --- /dev/null +++ b/tests/PhpSpreadsheetTests/Writer/Xlsx/MicrosecondsTest.php @@ -0,0 +1,47 @@ +getActiveSheet(); + $sheet->setCellValue('A1', Date::dateTimeToExcel($originalDateTime)); + $sheet->getStyle('A1') + ->getNumberFormat() + ->setFormatCode('yyyy-mm-dd hh:mm:ss.000'); + $reloadedSpreadsheet = $this->writeAndReload($spreadsheet, 'Xlsx'); + $spreadsheet->disconnectWorksheets(); + + $rsheet = $reloadedSpreadsheet->getActiveSheet(); + /** @var float */ + $reread = $rsheet->getCell('A1')->getValue(); + $temp = Date::excelToDateTimeObject($reread) + ->format('Y-m-d H:i:s.u'); + self::assertSame("{$date} {$time}.000000", $temp, 'round trip works with float value'); + $formatted = $rsheet->getCell('A1')->getFormattedValue(); + self::assertSame("{$date} {$time}.000", $formatted, 'round trip works with formatted value'); + /** @var float */ + $temp = Date::stringToExcel($formatted); + $temp = Date::excelToDateTimeObject($temp) + ->format('Y-m-d H:i:s.u'); + self::assertSame("{$date} {$time}.000000", $temp, 'round trip works using stringToExcel on formatted value'); + + $reloadedSpreadsheet->disconnectWorksheets(); + } +}