diff --git a/CHANGELOG.md b/CHANGELOG.md index c5753337b2..0b0bfe17b2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,6 +25,9 @@ and this project adheres to [Semantic Versioning](https://semver.org). ### Fixed +- Html Reader/Writer Better Handling of Booleans. [PR #4257](https://github.com/PHPOffice/PhpSpreadsheet/pull/4257) +- Fill Patterns/Colors When Xml Attributes are Missing. [Issue #4248](https://github.com/PHPOffice/PhpSpreadsheet/issues/4248) [PR #4250](https://github.com/PHPOffice/PhpSpreadsheet/pull/4250) +- Remove Unneccesary files from Composer Package. [PR #4262](https://github.com/PHPOffice/PhpSpreadsheet/pull/4262) - Swapped row and column indexes in ReferenceHelper. [Issue #4246](https://github.com/PHPOffice/PhpSpreadsheet/issues/4246) [PR #4247](https://github.com/PHPOffice/PhpSpreadsheet/pull/4247) - Fix minor break handling drawings. [Issue #4241](https://github.com/PHPOffice/PhpSpreadsheet/issues/4241) [PR #4244](https://github.com/PHPOffice/PhpSpreadsheet/pull/4244) - Ignore cell formatting when the format is a single @. [Issue #4242](https://github.com/PHPOffice/PhpSpreadsheet/issues/4242) [PR #4243](https://github.com/PHPOffice/PhpSpreadsheet/pull/4243) diff --git a/src/PhpSpreadsheet/Calculation/Calculation.php b/src/PhpSpreadsheet/Calculation/Calculation.php index 0cc64c32e8..ab52fb1af9 100644 --- a/src/PhpSpreadsheet/Calculation/Calculation.php +++ b/src/PhpSpreadsheet/Calculation/Calculation.php @@ -3141,6 +3141,53 @@ private function getLocaleFile(string $localeDir, string $locale, string $langua return $localeFileName; } + /** @var array> */ + private static array $falseTrueArray = []; + + /** @return array> */ + public function getFalseTrueArray(): array + { + if (!empty(self::$falseTrueArray)) { + return self::$falseTrueArray; + } + if (count(self::$validLocaleLanguages) == 1) { + self::loadLocales(); + } + $falseTrueArray = [['FALSE'], ['TRUE']]; + foreach (self::$validLocaleLanguages as $language) { + if (str_starts_with($language, 'en')) { + continue; + } + $locale = $language; + if (str_contains($locale, '_')) { + [$language] = explode('_', $locale); + } + $localeDir = implode(DIRECTORY_SEPARATOR, [__DIR__, 'locale', null]); + + try { + $functionNamesFile = $this->getLocaleFile($localeDir, $locale, $language, 'functions'); + } catch (Exception $e) { + continue; + } + // Retrieve the list of locale or language specific function names + $localeFunctions = file($functionNamesFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) ?: []; + foreach ($localeFunctions as $localeFunction) { + [$localeFunction] = explode('##', $localeFunction); // Strip out comments + if (str_contains($localeFunction, '=')) { + [$fName, $lfName] = array_map('trim', explode('=', $localeFunction)); + if ($fName === 'FALSE') { + $falseTrueArray[0][] = $lfName; + } elseif ($fName === 'TRUE') { + $falseTrueArray[1][] = $lfName; + } + } + } + } + self::$falseTrueArray = $falseTrueArray; + + return $falseTrueArray; + } + /** * Set the locale code. * diff --git a/src/PhpSpreadsheet/Reader/Html.php b/src/PhpSpreadsheet/Reader/Html.php index 4ca781a87d..431a08ffff 100644 --- a/src/PhpSpreadsheet/Reader/Html.php +++ b/src/PhpSpreadsheet/Reader/Html.php @@ -7,6 +7,7 @@ use DOMElement; use DOMNode; use DOMText; +use PhpOffice\PhpSpreadsheet\Calculation\Calculation; use PhpOffice\PhpSpreadsheet\Cell\Coordinate; use PhpOffice\PhpSpreadsheet\Cell\DataType; use PhpOffice\PhpSpreadsheet\Comment; @@ -271,6 +272,12 @@ protected function flushCell(Worksheet $sheet, string $column, int|string $row, ->setQuotePrefix(true); } } + if ($datatype === DataType::TYPE_BOOL) { + $cellContent = self::convertBoolean($cellContent); + if (!is_bool($cellContent)) { + $attributeArray['data-type'] = DataType::TYPE_STRING; + } + } //catching the Exception and ignoring the invalid data types try { @@ -291,6 +298,31 @@ protected function flushCell(Worksheet $sheet, string $column, int|string $row, $cellContent = (string) ''; } + /** @var array> */ + private static array $falseTrueArray = []; + + private static function convertBoolean(?string $cellContent): bool|string + { + if ($cellContent === '1') { + return true; + } + if ($cellContent === '0' || $cellContent === '' || $cellContent === null) { + return false; + } + if (empty(self::$falseTrueArray)) { + $calc = Calculation::getInstance(); + self::$falseTrueArray = $calc->getFalseTrueArray(); + } + if (in_array(mb_strtoupper($cellContent), self::$falseTrueArray[1], true)) { + return true; + } + if (in_array(mb_strtoupper($cellContent), self::$falseTrueArray[0], true)) { + return false; + } + + return $cellContent; + } + private function processDomElementBody(Worksheet $sheet, int &$row, string &$column, string &$cellContent, DOMElement $child): void { $attributeArray = []; diff --git a/src/PhpSpreadsheet/Writer/Html.php b/src/PhpSpreadsheet/Writer/Html.php index ad95efd18c..df1353ea08 100644 --- a/src/PhpSpreadsheet/Writer/Html.php +++ b/src/PhpSpreadsheet/Writer/Html.php @@ -5,6 +5,7 @@ use PhpOffice\PhpSpreadsheet\Calculation\Calculation; use PhpOffice\PhpSpreadsheet\Cell\Cell; use PhpOffice\PhpSpreadsheet\Cell\Coordinate; +use PhpOffice\PhpSpreadsheet\Cell\DataType; use PhpOffice\PhpSpreadsheet\Chart\Chart; use PhpOffice\PhpSpreadsheet\Comment; use PhpOffice\PhpSpreadsheet\Document\Properties; @@ -141,6 +142,12 @@ class Html extends BaseWriter /** @var Chart[] */ private $sheetCharts; + private bool $betterBoolean = false; + + private string $getTrue = 'TRUE'; + + private string $getFalse = 'FALSE'; + /** * Create a new HTML. */ @@ -148,6 +155,9 @@ public function __construct(Spreadsheet $spreadsheet) { $this->spreadsheet = $spreadsheet; $this->defaultFont = $this->spreadsheet->getDefaultStyle()->getFont(); + $calc = Calculation::getInstance($this->spreadsheet); + $this->getTrue = $calc->getTRUE(); + $this->getFalse = $calc->getFALSE(); } /** @@ -1353,8 +1363,21 @@ private function generateRowCellDataValue(Worksheet $worksheet, Cell $cell, stri if ($cell->getValue() instanceof RichText) { $cellData .= $this->generateRowCellDataValueRich($cell->getValue()); } else { - $origData = $this->preCalculateFormulas ? $cell->getCalculatedValue() : $cell->getValue(); - $origData2 = $this->preCalculateFormulas ? $cell->getCalculatedValueString() : $cell->getValueString(); + if ($this->preCalculateFormulas) { + $origData = $cell->getCalculatedValue(); + if ($this->betterBoolean && is_bool($origData)) { + $origData2 = $origData ? $this->getTrue : $this->getFalse; + } else { + $origData2 = $cell->getCalculatedValueString(); + } + } else { + $origData = $cell->getValue(); + if ($this->betterBoolean && is_bool($origData)) { + $origData2 = $origData ? $this->getTrue : $this->getFalse; + } else { + $origData2 = $cell->getValueString(); + } + } $formatCode = $worksheet->getParentOrThrow()->getCellXfByIndex($cell->getXfIndex())->getNumberFormat()->getFormatCode(); $cellData = NumberFormat::toFormattedString( @@ -1394,9 +1417,20 @@ private function generateRowCellData(Worksheet $worksheet, null|Cell|string $cel $cellData = nl2br($cellData); // Extend CSS class? + $dataType = $cell->getDataType(); + if ($this->betterBoolean && $this->preCalculateFormulas && $dataType === DataType::TYPE_FORMULA) { + $calculatedValue = $cell->getCalculatedValue(); + if (is_bool($calculatedValue)) { + $dataType = DataType::TYPE_BOOL; + } elseif (is_numeric($calculatedValue)) { + $dataType = DataType::TYPE_NUMERIC; + } elseif (is_string($calculatedValue)) { + $dataType = DataType::TYPE_STRING; + } + } if (!$this->useInlineCss && is_string($cssClass)) { $cssClass .= ' style' . $cell->getXfIndex(); - $cssClass .= ' ' . $cell->getDataType(); + $cssClass .= ' ' . $dataType; } elseif (is_array($cssClass)) { $index = $cell->getXfIndex(); $styleIndex = 'td.style' . $index . ', th.style' . $index; @@ -1410,7 +1444,7 @@ private function generateRowCellData(Worksheet $worksheet, null|Cell|string $cel $sharedStyle->getAlignment()->getHorizontal() == Alignment::HORIZONTAL_GENERAL && isset($this->cssStyles['.' . $cell->getDataType()]['text-align']) ) { - $cssClass['text-align'] = $this->cssStyles['.' . $cell->getDataType()]['text-align']; + $cssClass['text-align'] = $this->cssStyles['.' . $dataType]['text-align']; } } } else { @@ -1455,6 +1489,16 @@ private function generateRowWriteCell( $htmlx .= $this->generateRowIncludeCharts($worksheet, $coordinate); // Column start $html .= ' <' . $cellType; + if ($this->betterBoolean) { + $dataType = $worksheet->getCell($coordinate)->getDataType(); + if ($dataType === DataType::TYPE_BOOL) { + $html .= ' data-type="' . DataType::TYPE_BOOL . '"'; + } elseif ($dataType === DataType::TYPE_FORMULA && is_bool($worksheet->getCell($coordinate)->getCalculatedValue())) { + $html .= ' data-type="' . DataType::TYPE_BOOL . '"'; + } elseif (is_numeric($cellData) && $worksheet->getCell($coordinate)->getDataType() === DataType::TYPE_STRING) { + $html .= ' data-type="' . DataType::TYPE_STRING . '"'; + } + } if (!$this->useInlineCss && !$this->isPdf && is_string($cssClass)) { $html .= ' class="' . $cssClass . '"'; if ($htmlx) { @@ -1910,4 +1954,16 @@ private function shouldGenerateColumn(Worksheet $sheet, string $colStr): bool return $sheet->getColumnDimension($colStr)->getVisible(); } + + public function getBetterBoolean(): bool + { + return $this->betterBoolean; + } + + public function setBetterBoolean(bool $betterBoolean): self + { + $this->betterBoolean = $betterBoolean; + + return $this; + } } diff --git a/tests/PhpSpreadsheetTests/Writer/Html/BetterBooleanTest.php b/tests/PhpSpreadsheetTests/Writer/Html/BetterBooleanTest.php new file mode 100644 index 0000000000..0eac757d4d --- /dev/null +++ b/tests/PhpSpreadsheetTests/Writer/Html/BetterBooleanTest.php @@ -0,0 +1,211 @@ +locale = $calculation->getLocale(); + } + + protected function tearDown(): void + { + $calculation = Calculation::getInstance(); + $calculation->setLocale($this->locale); + } + + public function testDefault(): void + { + $spreadsheet = new Spreadsheet(); + $writer = new HtmlWriter($spreadsheet); + // Default will change with next PhpSpreadsheet release + self::assertFalse($writer->getBetterBoolean()); + $spreadsheet->disconnectWorksheets(); + } + + public function setBetter(HtmlWriter $writer): void + { + $writer->setBetterBoolean(true); + } + + public function setNotBetter(HtmlWriter $writer): void + { + $writer->setBetterBoolean(false); + } + + public function testBetterBoolean(): void + { + $spreadsheet = new Spreadsheet(); + $sheet = $spreadsheet->getActiveSheet(); + $sheet->getCell('A1')->setValue(1); + $sheet->getCell('B1')->setValue('Hello'); + $sheet->getCell('C1')->setValue(true); + $sheet->getCell('D1')->setValue('=IF(1>2, TRUE, FALSE)'); + $sheet->getCell('E1')->setValueExplicit(1, DataType::TYPE_STRING); + $sheet->getCell('F1')->setValue('="A"&"B"'); + $sheet->getCell('G1')->setValue('=1+2'); + + /** @var callable */ + $callableWriter = [$this, 'setBetter']; + $reloaded = $this->writeAndReload($spreadsheet, 'Html', null, $callableWriter); + $spreadsheet->disconnectWorksheets(); + + $rsheet = $reloaded->getActiveSheet(); + self::assertSame(1, $rsheet->getCell('A1')->getValue()); + self::assertSame('Hello', $rsheet->getCell('B1')->getValue()); + self::assertTrue($rsheet->getCell('C1')->getValue()); + self::assertFalse($rsheet->getCell('D1')->getValue()); + self::assertSame('1', $rsheet->getCell('E1')->getValue()); + self::assertSame('AB', $rsheet->getCell('F1')->getValue()); + self::assertSame(3, $rsheet->getCell('G1')->getValue()); + $reloaded->disconnectWorksheets(); + } + + public function testNotBetterBoolean(): void + { + $spreadsheet = new Spreadsheet(); + $sheet = $spreadsheet->getActiveSheet(); + $sheet->getCell('A1')->setValue(1); + $sheet->getCell('B1')->setValue('Hello'); + $sheet->getCell('C1')->setValue(true); + $sheet->getCell('D1')->setValue('=IF(1>2, TRUE, FALSE)'); + $sheet->getCell('E1')->setValueExplicit(1, DataType::TYPE_STRING); + $sheet->getCell('F1')->setValue('="A"&"B"'); + $sheet->getCell('G1')->setValue('=1+2'); + + /** @var callable */ + $callableWriter = [$this, 'setNotBetter']; + $reloaded = $this->writeAndReload($spreadsheet, 'Html', null, $callableWriter); + $spreadsheet->disconnectWorksheets(); + + $rsheet = $reloaded->getActiveSheet(); + self::assertSame(1, $rsheet->getCell('A1')->getValue()); + self::assertSame('Hello', $rsheet->getCell('B1')->getValue()); + self::assertSame(1, $rsheet->getCell('C1')->getValue()); + self::assertNull($rsheet->getCell('D1')->getValue()); + self::assertSame(1, $rsheet->getCell('E1')->getValue()); + self::assertSame('AB', $rsheet->getCell('F1')->getValue()); + self::assertSame(3, $rsheet->getCell('G1')->getValue()); + $reloaded->disconnectWorksheets(); + } + + public function testLocale(): void + { + $spreadsheet = new Spreadsheet(); + $sheet = $spreadsheet->getActiveSheet(); + $sheet->getCell('A1')->setValue(1); + $sheet->getCell('B1')->setValue('Hello'); + $sheet->getCell('C1')->setValue(true); + $sheet->getCell('D1')->setValue('=IF(1>2, TRUE, FALSE)'); + $sheet->getCell('E1')->setValueExplicit(1, DataType::TYPE_STRING); + $sheet->getCell('F1')->setValue('="A"&"B"'); + $sheet->getCell('G1')->setValue('=1+2'); + $calc = Calculation::getInstance(); + $calc->setLocale('fr'); + $writer = new HtmlWriter($spreadsheet); + $writer->setBetterBoolean(true); + $html = $writer->generateHtmlAll(); + self::assertStringNotContainsString('TRUE', $html); + self::assertStringContainsString('VRAI', $html); + self::assertStringContainsString('FAUX', $html); + self::assertStringContainsString('1', $html); + self::assertStringContainsString('AB', $html); + self::assertStringContainsString('3', $html); + + /** @var callable */ + $callableWriter = [$this, 'setBetter']; + $reloaded = $this->writeAndReload($spreadsheet, 'Html', null, $callableWriter); + $spreadsheet->disconnectWorksheets(); + + $rsheet = $reloaded->getActiveSheet(); + self::assertSame(1, $rsheet->getCell('A1')->getValue()); + self::assertSame('Hello', $rsheet->getCell('B1')->getValue()); + self::assertTrue($rsheet->getCell('C1')->getValue()); + self::assertFalse($rsheet->getCell('D1')->getValue()); + self::assertSame('1', $rsheet->getCell('E1')->getValue()); + self::assertSame('AB', $rsheet->getCell('F1')->getValue()); + self::assertSame(3, $rsheet->getCell('G1')->getValue()); + $reloaded->disconnectWorksheets(); + } + + public function testInline(): void + { + $spreadsheet = new Spreadsheet(); + $sheet = $spreadsheet->getActiveSheet(); + $sheet->getCell('A1')->setValue(1); + $sheet->getCell('B1')->setValue('Hello'); + $sheet->getCell('C1')->setValue(true); + $sheet->getCell('D1')->setValue('=IF(1>2, TRUE, FALSE)'); + $sheet->getCell('E1')->setValueExplicit(1, DataType::TYPE_STRING); + $sheet->getCell('F1')->setValue('="A"&"B"'); + $sheet->getCell('G1')->setValue('=1+2'); + $calc = Calculation::getInstance(); + $calc->setLocale('fr'); + $writer = new HtmlWriter($spreadsheet); + $writer->setBetterBoolean(true); + $writer->setUseInlineCss(true); + $html = $writer->generateHtmlAll(); + $html = str_replace('vertical-align:bottom; color:#000000; font-family:\'Calibri\'; font-size:11pt; ', '', $html); + $html = str_replace(' width:42pt" class="gridlines gridlinesp"', '"', $html); + self::assertStringNotContainsString('TRUE', $html); + self::assertStringContainsString('1', $html); + self::assertStringContainsString('Hello', $html); + self::assertStringContainsString('VRAI', $html); + self::assertStringContainsString('FAUX', $html); + self::assertStringContainsString('1', $html); + self::assertStringContainsString('AB', $html); + self::assertStringContainsString('3', $html); + + /** @var callable */ + $callableWriter = [$this, 'setBetter']; + $reloaded = $this->writeAndReload($spreadsheet, 'Html', null, $callableWriter); + $spreadsheet->disconnectWorksheets(); + + $rsheet = $reloaded->getActiveSheet(); + self::assertSame(1, $rsheet->getCell('A1')->getValue()); + self::assertSame('Hello', $rsheet->getCell('B1')->getValue()); + self::assertTrue($rsheet->getCell('C1')->getValue()); + self::assertFalse($rsheet->getCell('D1')->getValue()); + self::assertSame('1', $rsheet->getCell('E1')->getValue()); + self::assertSame('AB', $rsheet->getCell('F1')->getValue()); + self::assertSame(3, $rsheet->getCell('G1')->getValue()); + $reloaded->disconnectWorksheets(); + } + + public function testForeignNoLocale(): void + { + $fragment = '' + . '' + . '' + . '' // Bulgarian TRUE + . '' // Finnish FALSE + . '' + . '' + . '' + . '
1HelloИСТИНАEPÄTOSIwhatevertRuE1
'; + $reader = new HtmlReader(); + $spreadsheet = $reader->loadFromString($fragment); + $sheet = $spreadsheet->getActiveSheet(); + self::assertSame(1, $sheet->getCell('A1')->getValue()); + self::assertSame('Hello', $sheet->getCell('B1')->getValue()); + self::assertTrue($sheet->getCell('C1')->getValue()); + self::assertFalse($sheet->getCell('D1')->getValue()); + self::assertSame('whatever', $sheet->getCell('E1')->getValue()); + self::assertTrue($sheet->getCell('F1')->getValue()); + self::assertSame('1', $sheet->getCell('G1')->getValue()); + $spreadsheet->disconnectWorksheets(); + } +}