Skip to content

Commit a68e44e

Browse files
committed
Html Reader/Writer Better Handling of Booleans
When Html Writer outputs a cell with a boolean value, the result will either be 1 or null-string; neither of these is optimal for anyone looking at the resulting html. Html Reader already has the ability to recognize data types using the html `data-type` attribute, but Html Writer doesn't use it. This PR adds the ability to generate that attribute for booleans. It will generate a string value appropriate for the locale when it encounters a boolean. Html Reader, when it encounters `data-type="b"`, will interpret the result as true if the value is 1 or a string value recognized as true in any locale; it will interpret the result as false if the value is 0, null-string, null, or a string value recognized as false in any locale; if none of the above, it will leave the value as an unchanged string. So, Reader will wind up with the correct result even if its locale is different than what Writer used. Because this is a breaking change, it is opt-in. You need to call `Writer::setBetterBoolean(true)` in order for it take effect. The current default value for that property is false. When it is time to introduce breaking changes (see PR #4240), the default will be changed to true.
1 parent f37b119 commit a68e44e

File tree

4 files changed

+266
-2
lines changed

4 files changed

+266
-2
lines changed

src/PhpSpreadsheet/Calculation/Calculation.php

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3141,6 +3141,53 @@ private function getLocaleFile(string $localeDir, string $locale, string $langua
31413141
return $localeFileName;
31423142
}
31433143

3144+
/** @var array<int, array<int, string>> */
3145+
private static array $falseTrueArray = [];
3146+
3147+
/** @return array<int, array<int, string>> */
3148+
public function getFalseTrueArray(): array
3149+
{
3150+
if (!empty(self::$falseTrueArray)) {
3151+
return self::$falseTrueArray;
3152+
}
3153+
if (count(self::$validLocaleLanguages) == 1) {
3154+
self::loadLocales();
3155+
}
3156+
$falseTrueArray = [['FALSE'], ['TRUE']];
3157+
foreach (self::$validLocaleLanguages as $language) {
3158+
if (str_starts_with($language, 'en')) {
3159+
continue;
3160+
}
3161+
$locale = $language;
3162+
if (str_contains($locale, '_')) {
3163+
[$language] = explode('_', $locale);
3164+
}
3165+
$localeDir = implode(DIRECTORY_SEPARATOR, [__DIR__, 'locale', null]);
3166+
3167+
try {
3168+
$functionNamesFile = $this->getLocaleFile($localeDir, $locale, $language, 'functions');
3169+
} catch (Exception $e) {
3170+
continue;
3171+
}
3172+
// Retrieve the list of locale or language specific function names
3173+
$localeFunctions = file($functionNamesFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) ?: [];
3174+
foreach ($localeFunctions as $localeFunction) {
3175+
[$localeFunction] = explode('##', $localeFunction); // Strip out comments
3176+
if (str_contains($localeFunction, '=')) {
3177+
[$fName, $lfName] = array_map('trim', explode('=', $localeFunction));
3178+
if ($fName === 'FALSE') {
3179+
$falseTrueArray[0][] = $lfName;
3180+
} elseif ($fName === 'TRUE') {
3181+
$falseTrueArray[1][] = $lfName;
3182+
}
3183+
}
3184+
}
3185+
}
3186+
self::$falseTrueArray = $falseTrueArray;
3187+
3188+
return $falseTrueArray;
3189+
}
3190+
31443191
/**
31453192
* Set the locale code.
31463193
*

src/PhpSpreadsheet/Reader/Html.php

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
use DOMElement;
88
use DOMNode;
99
use DOMText;
10+
use PhpOffice\PhpSpreadsheet\Calculation\Calculation;
1011
use PhpOffice\PhpSpreadsheet\Cell\Coordinate;
1112
use PhpOffice\PhpSpreadsheet\Cell\DataType;
1213
use PhpOffice\PhpSpreadsheet\Comment;
@@ -271,6 +272,12 @@ protected function flushCell(Worksheet $sheet, string $column, int|string $row,
271272
->setQuotePrefix(true);
272273
}
273274
}
275+
if ($datatype === DataType::TYPE_BOOL) {
276+
$cellContent = self::convertBoolean($cellContent);
277+
if (!is_bool($cellContent)) {
278+
$attributeArray['data-type'] = DataType::TYPE_STRING;
279+
}
280+
}
274281

275282
//catching the Exception and ignoring the invalid data types
276283
try {
@@ -291,6 +298,31 @@ protected function flushCell(Worksheet $sheet, string $column, int|string $row,
291298
$cellContent = (string) '';
292299
}
293300

301+
/** @var array<int, array<int, string>> */
302+
private static array $falseTrueArray = [];
303+
304+
private function convertBoolean(?string $cellContent): bool|string
305+
{
306+
if ($cellContent === '1') {
307+
return true;
308+
}
309+
if ($cellContent === '0' || $cellContent === '' || $cellContent === null) {
310+
return false;
311+
}
312+
if (empty(self::$falseTrueArray)) {
313+
$calc = Calculation::getInstance();
314+
self::$falseTrueArray = $calc->getFalseTrueArray();
315+
}
316+
if (in_array(mb_strtoupper($cellContent), self::$falseTrueArray[1], true)) {
317+
return true;
318+
}
319+
if (in_array(mb_strtoupper($cellContent), self::$falseTrueArray[0], true)) {
320+
return false;
321+
}
322+
323+
return $cellContent;
324+
}
325+
294326
private function processDomElementBody(Worksheet $sheet, int &$row, string &$column, string &$cellContent, DOMElement $child): void
295327
{
296328
$attributeArray = [];

src/PhpSpreadsheet/Writer/Html.php

Lines changed: 47 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
use PhpOffice\PhpSpreadsheet\Calculation\Calculation;
66
use PhpOffice\PhpSpreadsheet\Cell\Cell;
77
use PhpOffice\PhpSpreadsheet\Cell\Coordinate;
8+
use PhpOffice\PhpSpreadsheet\Cell\DataType;
89
use PhpOffice\PhpSpreadsheet\Chart\Chart;
910
use PhpOffice\PhpSpreadsheet\Comment;
1011
use PhpOffice\PhpSpreadsheet\Document\Properties;
@@ -36,6 +37,9 @@ class Html extends BaseWriter
3637

3738
private const DEFAULT_CELL_WIDTH_PIXELS = 56;
3839

40+
private const TRUE_SUBSTITUTE = "\u{fffe}";
41+
private const FALSE_SUBSTITUTE = "\u{feff}";
42+
3943
/**
4044
* Migration aid to tell if html tags will be treated as plaintext in comments.
4145
* if (
@@ -141,13 +145,22 @@ class Html extends BaseWriter
141145
/** @var Chart[] */
142146
private $sheetCharts;
143147

148+
private bool $betterBoolean = false;
149+
150+
private string $getTrue = 'TRUE';
151+
152+
private string $getFalse = 'FALSE';
153+
144154
/**
145155
* Create a new HTML.
146156
*/
147157
public function __construct(Spreadsheet $spreadsheet)
148158
{
149159
$this->spreadsheet = $spreadsheet;
150160
$this->defaultFont = $this->spreadsheet->getDefaultStyle()->getFont();
161+
$calc = Calculation::getInstance($this->spreadsheet);
162+
$this->getTrue = $calc->getTRUE();
163+
$this->getFalse = $calc->getFALSE();
151164
}
152165

153166
/**
@@ -1346,8 +1359,21 @@ private function generateRowCellDataValue(Worksheet $worksheet, Cell $cell, stri
13461359
if ($cell->getValue() instanceof RichText) {
13471360
$cellData .= $this->generateRowCellDataValueRich($cell->getValue());
13481361
} else {
1349-
$origData = $this->preCalculateFormulas ? $cell->getCalculatedValue() : $cell->getValue();
1350-
$origData2 = $this->preCalculateFormulas ? $cell->getCalculatedValueString() : $cell->getValueString();
1362+
if ($this->preCalculateFormulas) {
1363+
$origData = $cell->getCalculatedValue();
1364+
if ($this->betterBoolean && is_bool($origData)) {
1365+
$origData2 = $origData ? self::TRUE_SUBSTITUTE : self::FALSE_SUBSTITUTE;
1366+
} else {
1367+
$origData2 = $cell->getCalculatedValueString();
1368+
}
1369+
} else {
1370+
$origData = $cell->getValue();
1371+
if ($this->betterBoolean && is_bool($origData)) {
1372+
$origData2 = $origData ? self::TRUE_SUBSTITUTE : self::FALSE_SUBSTITUTE;
1373+
} else {
1374+
$origData2 = $cell->getValueString();
1375+
}
1376+
}
13511377
$formatCode = $worksheet->getParentOrThrow()->getCellXfByIndex($cell->getXfIndex())->getNumberFormat()->getFormatCode();
13521378

13531379
$cellData = NumberFormat::toFormattedString(
@@ -1448,6 +1474,13 @@ private function generateRowWriteCell(
14481474
$htmlx .= $this->generateRowIncludeCharts($worksheet, $coordinate);
14491475
// Column start
14501476
$html .= ' <' . $cellType;
1477+
if ($cellData === self::TRUE_SUBSTITUTE) {
1478+
$html .= ' data-type="' . DataType::TYPE_BOOL . '"';
1479+
$cellData = $this->getTrue;
1480+
} elseif ($cellData === self::FALSE_SUBSTITUTE) {
1481+
$html .= ' data-type="' . DataType::TYPE_BOOL . '"';
1482+
$cellData = $this->getFalse;
1483+
}
14511484
if (!$this->useInlineCss && !$this->isPdf && is_string($cssClass)) {
14521485
$html .= ' class="' . $cssClass . '"';
14531486
if ($htmlx) {
@@ -1903,4 +1936,16 @@ private function shouldGenerateColumn(Worksheet $sheet, string $colStr): bool
19031936

19041937
return $sheet->getColumnDimension($colStr)->getVisible();
19051938
}
1939+
1940+
public function getBetterBoolean(): bool
1941+
{
1942+
return $this->betterBoolean;
1943+
}
1944+
1945+
public function setBetterBoolean(bool $betterBoolean): self
1946+
{
1947+
$this->betterBoolean = $betterBoolean;
1948+
1949+
return $this;
1950+
}
19061951
}
Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace PhpOffice\PhpSpreadsheetTests\Writer\Html;
6+
7+
use PhpOffice\PhpSpreadsheet\Calculation\Calculation;
8+
use PhpOffice\PhpSpreadsheet\Reader\Html as HtmlReader;
9+
use PhpOffice\PhpSpreadsheet\Spreadsheet;
10+
use PhpOffice\PhpSpreadsheet\Writer\Html as HtmlWriter;
11+
use PhpOffice\PhpSpreadsheetTests\Functional;
12+
13+
class BetterBooleanTest extends Functional\AbstractFunctional
14+
{
15+
private string $locale;
16+
17+
protected function setUp(): void
18+
{
19+
$calculation = Calculation::getInstance();
20+
$this->locale = $calculation->getLocale();
21+
}
22+
23+
protected function tearDown(): void
24+
{
25+
$calculation = Calculation::getInstance();
26+
$calculation->setLocale($this->locale);
27+
}
28+
29+
public function testDefault(): void
30+
{
31+
$spreadsheet = new Spreadsheet();
32+
$writer = new HtmlWriter($spreadsheet);
33+
// Default will change with next PhpSpreadsheet release
34+
self::assertFalse($writer->getBetterBoolean());
35+
$spreadsheet->disconnectWorksheets();
36+
}
37+
38+
public function setBetter(HtmlWriter $writer): void
39+
{
40+
$writer->setBetterBoolean(true);
41+
}
42+
43+
public function setNotBetter(HtmlWriter $writer): void
44+
{
45+
$writer->setBetterBoolean(false);
46+
}
47+
48+
public function testBetterBoolean(): void
49+
{
50+
$spreadsheet = new Spreadsheet();
51+
$sheet = $spreadsheet->getActiveSheet();
52+
$sheet->getCell('A1')->setValue(10);
53+
$sheet->getCell('B1')->setValue('Hello');
54+
$sheet->getCell('C1')->setValue(true);
55+
$sheet->getCell('D1')->setValue('=IF(1>2, TRUE, FALSE)');
56+
57+
/** @var callable */
58+
$callableWriter = [$this, 'setBetter'];
59+
$reloaded = $this->writeAndReload($spreadsheet, 'Html', null, $callableWriter);
60+
$spreadsheet->disconnectWorksheets();
61+
62+
$rsheet = $reloaded->getActiveSheet();
63+
self::assertSame(10, $rsheet->getCell('A1')->getValue());
64+
self::assertSame('Hello', $rsheet->getCell('B1')->getValue());
65+
self::assertTrue($rsheet->getCell('C1')->getValue());
66+
self::assertFalse($rsheet->getCell('D1')->getValue());
67+
$reloaded->disconnectWorksheets();
68+
}
69+
70+
public function testNotBetterBoolean(): void
71+
{
72+
$spreadsheet = new Spreadsheet();
73+
$sheet = $spreadsheet->getActiveSheet();
74+
$sheet->getCell('A1')->setValue(10);
75+
$sheet->getCell('B1')->setValue('Hello');
76+
$sheet->getCell('C1')->setValue(true);
77+
$sheet->getCell('D1')->setValue('=IF(1>2, TRUE, FALSE)');
78+
79+
/** @var callable */
80+
$callableWriter = [$this, 'setNotBetter'];
81+
$reloaded = $this->writeAndReload($spreadsheet, 'Html', null, $callableWriter);
82+
$spreadsheet->disconnectWorksheets();
83+
84+
$rsheet = $reloaded->getActiveSheet();
85+
self::assertSame(10, $rsheet->getCell('A1')->getValue());
86+
self::assertSame('Hello', $rsheet->getCell('B1')->getValue());
87+
self::assertSame(1, $rsheet->getCell('C1')->getValue());
88+
self::assertNull($rsheet->getCell('D1')->getValue());
89+
$reloaded->disconnectWorksheets();
90+
}
91+
92+
public function testLocale(): void
93+
{
94+
$spreadsheet = new Spreadsheet();
95+
$sheet = $spreadsheet->getActiveSheet();
96+
$sheet->getCell('A1')->setValue(10);
97+
$sheet->getCell('B1')->setValue('Hello');
98+
$sheet->getCell('C1')->setValue(true);
99+
$sheet->getCell('D1')->setValue('=IF(1>2, TRUE, FALSE)');
100+
$calc = Calculation::getInstance();
101+
$calc->setLocale('fr');
102+
$writer = new HtmlWriter($spreadsheet);
103+
$writer->setBetterBoolean(true);
104+
$html = $writer->generateHtmlAll();
105+
self::assertStringContainsString('VRAI', $html);
106+
self::assertStringNotContainsString('TRUE', $html);
107+
108+
/** @var callable */
109+
$callableWriter = [$this, 'setBetter'];
110+
$reloaded = $this->writeAndReload($spreadsheet, 'Html', null, $callableWriter);
111+
$spreadsheet->disconnectWorksheets();
112+
113+
$rsheet = $reloaded->getActiveSheet();
114+
self::assertSame(10, $rsheet->getCell('A1')->getValue());
115+
self::assertSame('Hello', $rsheet->getCell('B1')->getValue());
116+
self::assertTrue($rsheet->getCell('C1')->getValue());
117+
self::assertFalse($rsheet->getCell('D1')->getValue());
118+
$reloaded->disconnectWorksheets();
119+
}
120+
121+
public function testForeignNoLocale(): void
122+
{
123+
$fragment = '<table><tbody><tr>'
124+
. '<td>10</td>'
125+
. '<td>Hello</td>'
126+
. '<td data-type="b">ИСТИНА</td>' // Bulgarian TRUE
127+
. '<td data-type="b">EPÄTOSI</td>' // Finnish FALSE
128+
. '<td data-type="b">whatever</td>'
129+
. '<td data-type="b">tRuE</td>'
130+
. '</tr></tbody></table>';
131+
$reader = new HtmlReader();
132+
$spreadsheet = $reader->loadFromString($fragment);
133+
$sheet = $spreadsheet->getActiveSheet();
134+
self::assertTrue($sheet->getCell('C1')->getValue());
135+
self::assertFalse($sheet->getCell('D1')->getValue());
136+
self::assertSame('whatever', $sheet->getCell('E1')->getValue());
137+
self::assertTrue($sheet->getCell('F1')->getValue());
138+
$spreadsheet->disconnectWorksheets();
139+
}
140+
}

0 commit comments

Comments
 (0)