Skip to content

Commit 170b058

Browse files
committed
WIP Minor Breaking Change to DefaultValueBinder
Fix #4522. Although this is technically a breaking change, it is expected that very few, if any, existing programs will be affected. Nevertheless, DefaultValueBinder is implicitly used by the vast majority of programs out there, and I am reluctant to install a breaking change for something so widespread. So I will save this change for the next breaking release, which should be PhpSpreadsheet 5.0.0. There is no current schedule for that release. For strings consisting entirely of digits, DefaultValueBinder treats the value as a string if greater than PHP_INT_MAX, or an int otherwise. This prevents the loss of precision in large integers. This treatment dates back to PHPExcel, and has been in place since 2014. There are several problems with this approach. Excel itself maintains [15 digits of precision](https://support.microsoft.com/en-us/office/excel-specifications-and-limits-1672b34d-7043-467e-8e27-269d656771c3). So, string-vs-int should be decided at 999_999_999_999_999. This is much lower than PHP_INT_MAX for 64-bit, and much higher than for 32-bit. DefaultValueBinder is changed to use that new limit. A second problem is that DefaultValueBinder is only making this adjustment for positive integers. It is changed to test absolute value. A third problem is that DefaultValueBinder is making this adjustment only for strings, so that if you pass the number in as an int, it will not be adjusted (and will lose precision). It is changed to apply the same test for int.
1 parent 747ccd1 commit 170b058

File tree

2 files changed

+33
-12
lines changed

2 files changed

+33
-12
lines changed

src/PhpSpreadsheet/Cell/DefaultValueBinder.php

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

33
namespace PhpOffice\PhpSpreadsheet\Cell;
44

5+
use Composer\Pcre\Preg;
56
use DateTimeInterface;
67
use PhpOffice\PhpSpreadsheet\Calculation\Calculation;
78
use PhpOffice\PhpSpreadsheet\Calculation\Exception as CalculationException;
@@ -12,6 +13,9 @@
1213

1314
class DefaultValueBinder implements IValueBinder
1415
{
16+
// 123 456 789 012 345
17+
private const FIFTEEN_NINES = 999_999_999_999_999;
18+
1519
/**
1620
* Bind value to a cell.
1721
*
@@ -49,6 +53,9 @@ public static function dataTypeForValue(mixed $value): string
4953
if ($value === null) {
5054
return DataType::TYPE_NULL;
5155
}
56+
if (is_int($value) && abs($value) > self::FIFTEEN_NINES) {
57+
return DataType::TYPE_STRING;
58+
}
5259
if (is_float($value) || is_int($value)) {
5360
return DataType::TYPE_NUMERIC;
5461
}
@@ -89,13 +96,18 @@ public static function dataTypeForValue(mixed $value): string
8996

9097
return DataType::TYPE_FORMULA;
9198
}
92-
if (preg_match('/^[\+\-]?(\d+\.?\d*|\d*\.?\d+)([Ee][\-\+]?[0-2]?\d{1,3})?$/', $value)) {
99+
if (Preg::isMatch('/^[\+\-]?(\d+\.?\d*|\d*\.?\d+)([Ee][\-\+]?[0-2]?\d{1,3})?$/', $value)) {
93100
$tValue = ltrim($value, '+-');
94101
if (strlen($tValue) > 1 && $tValue[0] === '0' && $tValue[1] !== '.') {
95102
return DataType::TYPE_STRING;
96-
} elseif ((!str_contains($value, '.')) && ($value > PHP_INT_MAX)) {
97-
return DataType::TYPE_STRING;
98-
} elseif (!is_numeric($value)) {
103+
}
104+
if (!Preg::isMatch('/[eE.]/', $value)) {
105+
$aValue = abs((float) $value);
106+
if ($aValue > self::FIFTEEN_NINES) {
107+
return DataType::TYPE_STRING;
108+
}
109+
}
110+
if (!is_numeric($value)) {
99111
return DataType::TYPE_STRING;
100112
}
101113

tests/PhpSpreadsheetTests/Writer/Xlsx/FloatsRetainedTest.php

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,16 +8,21 @@
88
use PhpOffice\PhpSpreadsheet\Shared\File;
99
use PhpOffice\PhpSpreadsheet\Spreadsheet;
1010
use PhpOffice\PhpSpreadsheet\Writer\Xlsx as Writer;
11+
use PHPUnit\Framework\Attributes\DataProvider;
1112
use PHPUnit\Framework\TestCase;
1213

1314
class FloatsRetainedTest extends TestCase
1415
{
15-
#[\PHPUnit\Framework\Attributes\DataProvider('providerIntyFloatsRetainedByWriter')]
16-
public function testIntyFloatsRetainedByWriter(float|int $value): void
16+
#[DataProvider('providerIntyFloatsRetainedByWriter')]
17+
public function testIntyFloatsRetainedByWriter(float|int $value, mixed $expected = null): void
1718
{
19+
if ($expected === null) {
20+
$expected = $value;
21+
}
1822
$outputFilename = File::temporaryFilename();
1923
$spreadsheet = new Spreadsheet();
20-
$spreadsheet->getActiveSheet()->getCell('A1')->setValue($value);
24+
$spreadsheet->getActiveSheet()
25+
->getCell('A1')->setValue($value);
2126

2227
$writer = new Writer($spreadsheet);
2328
$writer->save($outputFilename);
@@ -27,7 +32,11 @@ public function testIntyFloatsRetainedByWriter(float|int $value): void
2732
$spreadsheet2 = $reader->load($outputFilename);
2833
unlink($outputFilename);
2934

30-
self::assertSame($value, $spreadsheet2->getActiveSheet()->getCell('A1')->getValue());
35+
self::assertSame(
36+
$expected,
37+
$spreadsheet2->getActiveSheet()
38+
->getCell('A1')->getValue()
39+
);
3140
$spreadsheet2->disconnectWorksheets();
3241
}
3342

@@ -44,10 +53,10 @@ public static function providerIntyFloatsRetainedByWriter(): array
4453
[1.3e-10],
4554
[1e10],
4655
[3.00000000000000000001],
47-
[99999999999999999],
48-
[99999999999999999.0],
49-
[999999999999999999999999999999999999999999],
50-
[999999999999999999999999999999999999999999.0],
56+
'int but too much precision for Excel' => [99_999_999_999_999_999, '99999999999999999'],
57+
[99_999_999_999_999_999.0],
58+
'int > PHP_INT_MAX so stored as float' => [999_999_999_999_999_999_999_999_999_999_999_999_999_999],
59+
[999_999_999_999_999_999_999_999_999_999_999_999_999_999.0],
5160
];
5261
}
5362
}

0 commit comments

Comments
 (0)