Skip to content

Commit 68fd711

Browse files
authored
Merge pull request #4479 from oleibman/issue4476
More Precision for Float to String Casts
2 parents 9da522f + 1700100 commit 68fd711

File tree

11 files changed

+237
-27
lines changed

11 files changed

+237
-27
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ and this project adheres to [Semantic Versioning](https://semver.org).
3434
- Print Area and Row Break. [Issue #1275](https://github.com/PHPOffice/PhpSpreadsheet/issues/1275) [PR #4450](https://github.com/PHPOffice/PhpSpreadsheet/pull/4450)
3535
- Copy Styles after insertNewColumnBefore. [Issue #1425](https://github.com/PHPOffice/PhpSpreadsheet/issues/1425) [PR #4468](https://github.com/PHPOffice/PhpSpreadsheet/pull/4468)
3636
- 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)
37+
- 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)
3738
- Hyperlink Styles. [Issue #1632](https://github.com/PHPOffice/PhpSpreadsheet/issues/1632) [PR #4478](https://github.com/PHPOffice/PhpSpreadsheet/pull/4478)
3839
- ODS Handling of Ceiling and Floor. [Issue #477](https://github.com/PHPOffice/PhpSpreadsheet/issues/407) [PR #4466](https://github.com/PHPOffice/PhpSpreadsheet/pull/4466)
3940
- 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)

src/PhpSpreadsheet/Shared/Date.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -471,7 +471,7 @@ public static function stringToExcel(string $dateValue): bool|float
471471
if (strlen($dateValue) < 2) {
472472
return false;
473473
}
474-
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)) {
474+
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)) {
475475
return false;
476476
}
477477

src/PhpSpreadsheet/Shared/StringHelper.php

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
namespace PhpOffice\PhpSpreadsheet\Shared;
44

5+
use Composer\Pcre\Preg;
56
use PhpOffice\PhpSpreadsheet\Calculation\Calculation;
67
use PhpOffice\PhpSpreadsheet\Exception as SpreadsheetException;
78
use Stringable;
@@ -494,9 +495,9 @@ public static function mbStrSplit(string $string): array
494495
{
495496
// Split at all position not after the start: ^
496497
// and not before the end: $
497-
$split = preg_split('/(?<!^)(?!$)/u', $string);
498+
$split = Preg::split('/(?<!^)(?!$)/u', $string);
498499

499-
return ($split === false) ? [] : $split;
500+
return $split;
500501
}
501502

502503
/**
@@ -530,6 +531,7 @@ private static function getLocaleValue(string $key, string $altKey, string $defa
530531
$localeconv = localeconv();
531532
$rslt = $localeconv[$key];
532533
// win-1252 implements Euro as 0x80 plus other symbols
534+
// Not suitable for Composer\Pcre\Preg
533535
if (preg_match('//u', $rslt) !== 1) {
534536
$rslt = '';
535537
}
@@ -659,6 +661,22 @@ public static function convertToString(mixed $value, bool $throw = true, string
659661
if ($convertBool && is_bool($value)) {
660662
return $value ? Calculation::getTRUE() : Calculation::getFALSE();
661663
}
664+
if (is_float($value)) {
665+
$string = (string) $value;
666+
// look out for scientific notation
667+
if (!Preg::isMatch('/[^-+0-9.]/', $string)) {
668+
$minus = $value < 0 ? '-' : '';
669+
$positive = abs($value);
670+
$floor = floor($positive);
671+
$oldFrac = (string) ($positive - $floor);
672+
$frac = Preg::replace('/^0[.](\d+)$/', '$1', $oldFrac);
673+
if ($frac !== $oldFrac) {
674+
return "$minus$floor.$frac";
675+
}
676+
}
677+
678+
return $string;
679+
}
662680
if ($value === null || is_scalar($value) || $value instanceof Stringable) {
663681
return (string) $value;
664682
}

src/PhpSpreadsheet/Writer/Xlsx/Worksheet.php

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1463,15 +1463,11 @@ private function writeCellString(XMLWriter $objWriter, string $mappedType, RichT
14631463

14641464
private function writeCellNumeric(XMLWriter $objWriter, float|int $cellValue): void
14651465
{
1466-
//force a decimal to be written if the type is float
1467-
if (is_float($cellValue)) {
1468-
// force point as decimal separator in case current locale uses comma
1469-
$cellValue = str_replace(',', '.', (string) $cellValue);
1470-
if (!str_contains($cellValue, '.')) {
1471-
$cellValue = $cellValue . '.0';
1472-
}
1466+
$result = StringHelper::convertToString($cellValue);
1467+
if (is_float($cellValue) && !str_contains($result, '.')) {
1468+
$result .= '.0';
14731469
}
1474-
$objWriter->writeElement('v', "$cellValue");
1470+
$objWriter->writeElement('v', $result);
14751471
}
14761472

14771473
private function writeCellBoolean(XMLWriter $objWriter, string $mappedType, bool $cellValue): void

tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/TruncTest.php

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,8 +54,9 @@ public function testTooMuchPrecision(mixed $expectedResult, float|int|string $va
5454
$sheet = $this->getSheet();
5555
$sheet->getCell('E1')->setValue($value);
5656
$sheet->getCell('E2')->setValue("=TRUNC(E1,$digits)");
57-
$result = $sheet->getCell('E2')->getCalculatedValueString();
58-
self::assertSame($expectedResult, $result);
57+
/** @var float|string */
58+
$result = $sheet->getCell('E2')->getCalculatedValue();
59+
self::assertSame($expectedResult, (string) $result);
5960
}
6061

6162
public static function providerTooMuchPrecision(): array

tests/PhpSpreadsheetTests/Calculation/Functions/TextData/ArrayToTextTest.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
namespace PhpOffice\PhpSpreadsheetTests\Calculation\Functions\TextData;
66

7+
use PhpOffice\PhpSpreadsheet\Shared\StringHelper;
78
use PHPUnit\Framework\Attributes\DataProvider;
89

910
class ArrayToTextTest extends AllSetupTeardown
@@ -17,6 +18,9 @@ public function testArrayToText(string $expectedResult, array $testData, int $mo
1718
$worksheet->getCell('H1')->setValue("=ARRAYTOTEXT(A1:C5, {$mode})");
1819

1920
$result = $worksheet->getCell('H1')->getCalculatedValue();
21+
$b1SimpleCast = '12345.6789';
22+
$b1AccurateCast = StringHelper::convertToString(12345.6789);
23+
$expectedResult = str_replace($b1SimpleCast, $b1AccurateCast, $expectedResult);
2024
self::assertSame($expectedResult, $result);
2125
}
2226

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace PhpOffice\PhpSpreadsheetTests;
6+
7+
use PHPUnit\Framework\Attributes\DataProvider;
8+
use PHPUnit\Framework\TestCase;
9+
10+
class FloatImprecisionTest extends TestCase
11+
{
12+
#[DataProvider('providerFloats')]
13+
public function testCompareFloats(float $float1, float $float2): void
14+
{
15+
self::assertSame($float1, $float2);
16+
}
17+
18+
public static function providerFloats(): array
19+
{
20+
return [
21+
[12345.6789, 12345.67890000000079],
22+
];
23+
}
24+
}

tests/PhpSpreadsheetTests/Writer/Ods/ContentTest.php

Lines changed: 36 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,9 @@ protected function setUp(): void
3232
parent::setUp();
3333

3434
$this->compatibilityMode = Functions::getCompatibilityMode();
35-
Functions::setCompatibilityMode(Functions::COMPATIBILITY_OPENOFFICE);
35+
Functions::setCompatibilityMode(
36+
Functions::COMPATIBILITY_OPENOFFICE
37+
);
3638
}
3739

3840
protected function tearDown(): void
@@ -57,6 +59,8 @@ public function testWriteSpreadsheet(): void
5759
$worksheet1 = $workbook->getActiveSheet();
5860
$worksheet1->setCellValue('A1', 1); // Number
5961
$worksheet1->setCellValue('B1', 12345.6789); // Number
62+
$b1SimpleCast = '12345.6789';
63+
$b1AccurateCast = StringHelper::convertToString(12345.6789);
6064
$worksheet1->setCellValue('C1', '1'); // Number without cast
6165
$worksheet1->setCellValueExplicit('D1', '01234', DataType::TYPE_STRING); // Number casted to string
6266
$worksheet1->setCellValue('E1', 'Lorem ipsum'); // String
@@ -74,6 +78,11 @@ public function testWriteSpreadsheet(): void
7478
$worksheet1->getStyle('D2')
7579
->getNumberFormat()
7680
->setFormatCode(NumberFormat::FORMAT_DATE_DATETIME);
81+
/** @var float */
82+
$d2SimpleCast = $worksheet1->getCell('D2')->getValue();
83+
$d2SimpleCast = (string) $d2SimpleCast;
84+
$d2AccurateCast = $worksheet1
85+
->getCell('D2')->getValueString();
7786

7887
$worksheet1->setCellValueExplicit('F1', null, DataType::TYPE_ERROR);
7988
$worksheet1->setCellValueExplicit('G1', 'Lorem ipsum', DataType::TYPE_INLINE);
@@ -85,12 +94,17 @@ public function testWriteSpreadsheet(): void
8594
$worksheet1->getStyle('C1')->getFont()->setSize(14);
8695
$worksheet1->getStyle('C1')->getFont()->setColor(new Color(Color::COLOR_BLUE));
8796

88-
$worksheet1->getStyle('C1')->getFill()->setFillType(Fill::FILL_SOLID);
89-
$worksheet1->getStyle('C1')->getFill()->setStartColor(new Color(Color::COLOR_RED));
97+
$worksheet1->getStyle('C1')
98+
->getFill()->setFillType(Fill::FILL_SOLID);
99+
$worksheet1->getStyle('C1')
100+
->getFill()->setStartColor(new Color(Color::COLOR_RED));
90101

91-
$worksheet1->getStyle('C1')->getFont()->setUnderline(Font::UNDERLINE_SINGLE);
92-
$worksheet1->getStyle('C2')->getFont()->setUnderline(Font::UNDERLINE_DOUBLE);
93-
$worksheet1->getStyle('D2')->getFont()->setUnderline(Font::UNDERLINE_NONE);
102+
$worksheet1->getStyle('C1')->getFont()
103+
->setUnderline(Font::UNDERLINE_SINGLE);
104+
$worksheet1->getStyle('C2')->getFont()
105+
->setUnderline(Font::UNDERLINE_DOUBLE);
106+
$worksheet1->getStyle('D2')->getFont()
107+
->setUnderline(Font::UNDERLINE_NONE);
94108

95109
// Worksheet 2
96110
$worksheet2 = $workbook->createSheet();
@@ -101,7 +115,12 @@ public function testWriteSpreadsheet(): void
101115
$content = new Content(new Ods($workbook));
102116
$xml = $content->write();
103117

104-
self::assertXmlStringEqualsXmlFile($this->samplesPath . '/content-with-data.xml', $xml);
118+
$xmlFile = $this->samplesPath . '/content-with-data.xml';
119+
$xmlContents = (string) file_get_contents($xmlFile);
120+
$xmlContents = str_replace($b1SimpleCast, $b1AccurateCast, $xmlContents);
121+
$xmlContents = str_replace($d2SimpleCast, $d2AccurateCast, $xmlContents);
122+
self::assertXmlStringEqualsXmlString($xmlContents, $xml);
123+
$workbook->disconnectWorksheets();
105124
}
106125

107126
public function testWriteWithHiddenWorksheet(): void
@@ -124,19 +143,21 @@ public function testWriteWithHiddenWorksheet(): void
124143
$xml = $content->write();
125144

126145
self::assertXmlStringEqualsXmlFile($this->samplesPath . '/content-hidden-worksheet.xml', $xml);
146+
$workbook->disconnectWorksheets();
127147
}
128148

129149
public function testWriteBorderStyle(): void
130150
{
131151
$spreadsheet = new Spreadsheet();
132-
$spreadsheet->getActiveSheet()->getStyle('A1:B2')->applyFromArray([
133-
'borders' => [
134-
'outline' => [
135-
'borderStyle' => Border::BORDER_THICK,
136-
'color' => ['argb' => 'AA22DD00'],
152+
$spreadsheet->getActiveSheet()
153+
->getStyle('A1:B2')->applyFromArray([
154+
'borders' => [
155+
'outline' => [
156+
'borderStyle' => Border::BORDER_THICK,
157+
'color' => ['argb' => 'AA22DD00'],
158+
],
137159
],
138-
],
139-
]);
160+
]);
140161

141162
$content = new Content(new Ods($spreadsheet));
142163
$xml = $content->write();
@@ -161,5 +182,6 @@ public function testWriteBorderStyle(): void
161182
}
162183
}
163184
}
185+
$spreadsheet->disconnectWorksheets();
164186
}
165187
}
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace PhpOffice\PhpSpreadsheetTests\Writer\Ods;
6+
7+
use DateTime;
8+
use PhpOffice\PhpSpreadsheet\Shared\Date;
9+
use PhpOffice\PhpSpreadsheet\Spreadsheet;
10+
use PhpOffice\PhpSpreadsheetTests\Functional\AbstractFunctional;
11+
12+
class MicrosecondsTest extends AbstractFunctional
13+
{
14+
/**
15+
* Test save and load XLSX file for round-trip DateTime.
16+
* Ods Writer does not support date formats,
17+
* and Reader does not support styles, so this
18+
* test is slightly different than its Xls/Xlsx counterparts.
19+
*/
20+
public function testIssue4476(): void
21+
{
22+
$date = '2020-10-21';
23+
$time = '14:55:31';
24+
$originalDateTime = new DateTime("{$date}T{$time}");
25+
$spreadsheet = new Spreadsheet();
26+
$sheet = $spreadsheet->getActiveSheet();
27+
$sheet->setCellValue('A1', Date::dateTimeToExcel($originalDateTime));
28+
$reloadedSpreadsheet = $this->writeAndReload($spreadsheet, 'Ods');
29+
$spreadsheet->disconnectWorksheets();
30+
31+
$rsheet = $reloadedSpreadsheet->getActiveSheet();
32+
$rsheet->getStyle('A1')
33+
->getNumberFormat()
34+
->setFormatCode('yyyy-mm-dd hh:mm:ss.000');
35+
/** @var float */
36+
$reread = $rsheet->getCell('A1')->getValue();
37+
$temp = Date::excelToDateTimeObject($reread)
38+
->format('Y-m-d H:i:s.u');
39+
self::assertSame("{$date} {$time}.000000", $temp, 'round trip works with float value');
40+
$formatted = $rsheet->getCell('A1')->getFormattedValue();
41+
self::assertSame("{$date} {$time}.000", $formatted, 'round trip works with formatted value');
42+
/** @var float */
43+
$temp = Date::stringToExcel($formatted);
44+
$temp = Date::excelToDateTimeObject($temp)
45+
->format('Y-m-d H:i:s.u');
46+
self::assertSame("{$date} {$time}.000000", $temp, 'round trip works using stringToExcel on formatted value');
47+
48+
$reloadedSpreadsheet->disconnectWorksheets();
49+
}
50+
}
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace PhpOffice\PhpSpreadsheetTests\Writer\Xls;
6+
7+
use DateTime;
8+
use PhpOffice\PhpSpreadsheet\Shared\Date;
9+
use PhpOffice\PhpSpreadsheet\Spreadsheet;
10+
use PhpOffice\PhpSpreadsheetTests\Functional\AbstractFunctional;
11+
12+
class MicrosecondsTest extends AbstractFunctional
13+
{
14+
/**
15+
* Test save and load XLSX file for round-trip DateTime.
16+
*/
17+
public function testIssue4476(): void
18+
{
19+
$date = '2020-10-21';
20+
$time = '14:55:31';
21+
$originalDateTime = new DateTime("{$date}T{$time}");
22+
$spreadsheet = new Spreadsheet();
23+
$sheet = $spreadsheet->getActiveSheet();
24+
$sheet->setCellValue('A1', Date::dateTimeToExcel($originalDateTime));
25+
$sheet->getStyle('A1')
26+
->getNumberFormat()
27+
->setFormatCode('yyyy-mm-dd hh:mm:ss.000');
28+
$reloadedSpreadsheet = $this->writeAndReload($spreadsheet, 'Xls');
29+
$spreadsheet->disconnectWorksheets();
30+
31+
$rsheet = $reloadedSpreadsheet->getActiveSheet();
32+
/** @var float */
33+
$reread = $rsheet->getCell('A1')->getValue();
34+
$temp = Date::excelToDateTimeObject($reread)
35+
->format('Y-m-d H:i:s.u');
36+
self::assertSame("{$date} {$time}.000000", $temp, 'round trip works with float value');
37+
$formatted = $rsheet->getCell('A1')->getFormattedValue();
38+
self::assertSame("{$date} {$time}.000", $formatted, 'round trip works with formatted value');
39+
/** @var float */
40+
$temp = Date::stringToExcel($formatted);
41+
$temp = Date::excelToDateTimeObject($temp)
42+
->format('Y-m-d H:i:s.u');
43+
self::assertSame("{$date} {$time}.000000", $temp, 'round trip works using stringToExcel on formatted value');
44+
45+
$reloadedSpreadsheet->disconnectWorksheets();
46+
}
47+
}

0 commit comments

Comments
 (0)