TextGrid Improvements

Helper/TextGrid was intended to assist with some samples. However, it has emerged recently on two tickets. PR #4342 intended to introduce functionality very similar to TextGrid, and was closed for that reason. Issue #1640 was closed as stale over 4 years ago, despite the fact that TextGrid seems an adequate resolution for it.

Since it seems that there is a use for this function beyond its original intended usage, I added a few parameters to give it some flexibility - the ability to omit row and/or column headers, and the ability to add a divider line between rows.

A description of this function is added to the formal documentation.
This commit is contained in:
oleibman
2025-03-22 20:37:28 -07:00
parent 74dca30c89
commit ca71ae77ac
4 changed files with 243 additions and 20 deletions
+34 -2
View File
@@ -1169,6 +1169,38 @@ One benefit of flags is that you can pass several flags in a single method call.
Two or more flags can be passed together using PHP's `|` operator.
```php
$reader = \PhpOffice\PhpSpreadsheet\IOFactory::createReaderForFile("myExampleFile.xlsx");
$reader->load("spreadsheetWithCharts.xlsx", $reader::READ_DATA_ONLY | $reader::IGNORE_EMPTY_CELLS);
$reader = \PhpOffice\PhpSpreadsheet\IOFactory::createReaderForFile('myExampleFile.xlsx');
$reader->load(
'spreadsheetWithCharts.xlsx',
$reader::READ_DATA_ONLY | $reader::IGNORE_EMPTY_CELLS
);
```
## Writing Data as a Plaintext Grid
Although not really a spreadsheet format, it can be useful to write data in grid format to a plaintext file.
Code like the following can be used:
```php
$array = $sheet->toArray(null, true, true, true);
$textGrid = new \PhpOffice\PhpSpreadsheet\Shared\TextGrid(
$array,
true, // true for cli, false for html
// Starting with release 4.2,
// the output format can be tweaked by uncommenting
// any of the following 3 optional parameters.
// rowDividers: true,
// rowHeaders: false,
// columnHeaders: false,
);
$result = $textGrid->render();
```
You can then echo `$result` to a terminal, or write it to a file with `file_put_contents`. The result will resemble:
```
+-----+------------------+---+----------+
| A | B | C | D |
+---+-----+------------------+---+----------+
| 1 | 6 | 1900-01-06 00:00 | | 0.572917 |
| 2 | 6 | 1900-01-06 00:00 | | 1<>2 |
| 3 | xyz | xyz | | |
+---+-----+------------------+---+----------+
```
+44 -17
View File
@@ -2,6 +2,8 @@
namespace PhpOffice\PhpSpreadsheet\Helper;
use PhpOffice\PhpSpreadsheet\Shared\StringHelper;
class TextGrid
{
private bool $isCli;
@@ -14,7 +16,13 @@ class TextGrid
private string $gridDisplay;
public function __construct(array $matrix, bool $isCli = true)
private bool $rowDividers = false;
private bool $rowHeaders = true;
private bool $columnHeaders = true;
public function __construct(array $matrix, bool $isCli = true, bool $rowDividers = false, bool $rowHeaders = true, bool $columnHeaders = true)
{
$this->rows = array_keys($matrix);
$this->columns = array_keys($matrix[$this->rows[0]]);
@@ -29,11 +37,14 @@ class TextGrid
$this->matrix = $matrix;
$this->isCli = $isCli;
$this->rowDividers = $rowDividers;
$this->rowHeaders = $rowHeaders;
$this->columnHeaders = $columnHeaders;
}
public function render(): string
{
$this->gridDisplay = $this->isCli ? '' : '<pre>';
$this->gridDisplay = $this->isCli ? '' : ('<pre>' . PHP_EOL);
if (!empty($this->rows)) {
$maxRow = max($this->rows);
@@ -42,7 +53,9 @@ class TextGrid
$this->renderColumnHeader($maxRowLength, $columnWidths);
$this->renderRows($maxRowLength, $columnWidths);
$this->renderFooter($maxRowLength, $columnWidths);
if (!$this->rowDividers) {
$this->renderFooter($maxRowLength, $columnWidths);
}
}
$this->gridDisplay .= $this->isCli ? '' : '</pre>';
@@ -53,30 +66,48 @@ class TextGrid
private function renderRows(int $maxRowLength, array $columnWidths): void
{
foreach ($this->matrix as $row => $rowData) {
$this->gridDisplay .= '|' . str_pad((string) $this->rows[$row], $maxRowLength, ' ', STR_PAD_LEFT) . ' ';
if ($this->rowHeaders) {
$this->gridDisplay .= '|' . str_pad((string) $this->rows[$row], $maxRowLength, ' ', STR_PAD_LEFT) . ' ';
}
$this->renderCells($rowData, $columnWidths);
$this->gridDisplay .= '|' . PHP_EOL;
if ($this->rowDividers) {
$this->renderFooter($maxRowLength, $columnWidths);
}
}
}
private function renderCells(array $rowData, array $columnWidths): void
{
foreach ($rowData as $column => $cell) {
$displayCell = ($this->isCli) ? (string) $cell : htmlentities((string) $cell);
$valueForLength = StringHelper::convertToString($cell, convertBool: true);
$displayCell = $this->isCli ? $valueForLength : htmlentities($valueForLength);
$this->gridDisplay .= '| ';
$this->gridDisplay .= $displayCell . str_repeat(' ', $columnWidths[$column] - mb_strlen($cell ?? '') + 1);
$this->gridDisplay .= $displayCell . str_repeat(' ', $columnWidths[$column] - mb_strlen($valueForLength) + 1);
}
}
private function renderColumnHeader(int $maxRowLength, array $columnWidths): void
private function renderColumnHeader(int $maxRowLength, array &$columnWidths): void
{
$this->gridDisplay .= str_repeat(' ', $maxRowLength + 2);
if (!$this->columnHeaders) {
$this->renderFooter($maxRowLength, $columnWidths);
return;
}
foreach ($this->columns as $column => $reference) {
$columnWidths[$column] = max($columnWidths[$column], mb_strlen($reference));
}
if ($this->rowHeaders) {
$this->gridDisplay .= str_repeat(' ', $maxRowLength + 2);
}
foreach ($this->columns as $column => $reference) {
$this->gridDisplay .= '+-' . str_repeat('-', $columnWidths[$column] + 1);
}
$this->gridDisplay .= '+' . PHP_EOL;
$this->gridDisplay .= str_repeat(' ', $maxRowLength + 2);
if ($this->rowHeaders) {
$this->gridDisplay .= str_repeat(' ', $maxRowLength + 2);
}
foreach ($this->columns as $column => $reference) {
$this->gridDisplay .= '| ' . str_pad((string) $reference, $columnWidths[$column] + 1, ' ');
}
@@ -87,7 +118,9 @@ class TextGrid
private function renderFooter(int $maxRowLength, array $columnWidths): void
{
$this->gridDisplay .= '+' . str_repeat('-', $maxRowLength + 1);
if ($this->rowHeaders) {
$this->gridDisplay .= '+' . str_repeat('-', $maxRowLength + 1);
}
foreach ($this->columns as $column => $reference) {
$this->gridDisplay .= '+-';
$this->gridDisplay .= str_pad((string) '', $columnWidths[$column] + 1, '-');
@@ -112,13 +145,7 @@ class TextGrid
$columnData = array_values($columnData);
foreach ($columnData as $columnValue) {
if (is_string($columnValue)) {
$columnWidth = max($columnWidth, mb_strlen($columnValue));
} elseif (is_bool($columnValue)) {
$columnWidth = max($columnWidth, mb_strlen($columnValue ? 'TRUE' : 'FALSE'));
}
$columnWidth = max($columnWidth, mb_strlen((string) $columnWidth));
$columnWidth = max($columnWidth, mb_strlen(StringHelper::convertToString($columnValue, convertBool: true)));
}
return $columnWidth;
+6 -1
View File
@@ -2,6 +2,7 @@
namespace PhpOffice\PhpSpreadsheet\Shared;
use PhpOffice\PhpSpreadsheet\Calculation\Calculation;
use PhpOffice\PhpSpreadsheet\Exception as SpreadsheetException;
use Stringable;
@@ -647,8 +648,12 @@ class StringHelper
return strlen("$string");
}
public static function convertToString(mixed $value, bool $throw = true, string $default = ''): string
/** @param bool $convertBool If true, convert bool to locale-aware TRUE/FALSE rather than 1/null-string */
public static function convertToString(mixed $value, bool $throw = true, string $default = '', bool $convertBool = false): string
{
if ($convertBool && is_bool($value)) {
return $value ? Calculation::getTRUE() : Calculation::getFALSE();
}
if ($value === null || is_scalar($value) || $value instanceof Stringable) {
return (string) $value;
}
@@ -0,0 +1,159 @@
<?php
declare(strict_types=1);
namespace PhpOffice\PhpSpreadsheetTests\Helper;
use PhpOffice\PhpSpreadsheet\Helper\TextGrid;
use PhpOffice\PhpSpreadsheet\Spreadsheet;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\TestCase;
class TextGridTest extends TestCase
{
#[DataProvider('providerTextGrid')]
public function testTextGrid(
bool $cli,
bool $rowDividers,
bool $rowHeaders,
bool $columnHeaders,
array $expected
): void {
$spreadsheet = new Spreadsheet();
$sheet = $spreadsheet->getActiveSheet();
$sheet->fromArray([
[6, '=TEXT(A1,"yyyy-mm-dd hh:mm")', null, 0.572917],
['="6"', '=TEXT(A2,"yyyy-mm-dd hh:mm")', null, '1<>2'],
['xyz', '=TEXT(A3,"yyyy-mm-dd hh:mm")'],
], strictNullComparison: true);
$textGrid = new TextGrid(
$sheet->toArray(null, true, true, true),
$cli,
rowDividers: $rowDividers,
rowHeaders: $rowHeaders,
columnHeaders: $columnHeaders
);
$result = $textGrid->render();
// Note that, for cli, string will end with PHP_EOL,
// so explode will add an extra null-string element
// to its array output.
$lines = explode(PHP_EOL, $result);
self::assertSame($expected, $lines);
$spreadsheet->disconnectWorksheets();
}
public static function providerTextGrid(): array
{
return [
'cli default values' => [
true, false, true, true,
[
' +-----+------------------+---+----------+',
' | A | B | C | D |',
'+---+-----+------------------+---+----------+',
'| 1 | 6 | 1900-01-06 00:00 | | 0.572917 |',
'| 2 | 6 | 1900-01-06 00:00 | | 1<>2 |',
'| 3 | xyz | xyz | | |',
'+---+-----+------------------+---+----------+',
'',
],
],
'html default values' => [
false, false, true, true,
[
'<pre>',
' +-----+------------------+---+----------+',
' | A | B | C | D |',
'+---+-----+------------------+---+----------+',
'| 1 | 6 | 1900-01-06 00:00 | | 0.572917 |',
'| 2 | 6 | 1900-01-06 00:00 | | 1&lt;&gt;2 |',
'| 3 | xyz | xyz | | |',
'+---+-----+------------------+---+----------+',
'</pre>',
],
],
'cli rowDividers' => [
true, true, true, true,
[
' +-----+------------------+---+----------+',
' | A | B | C | D |',
'+---+-----+------------------+---+----------+',
'| 1 | 6 | 1900-01-06 00:00 | | 0.572917 |',
'+---+-----+------------------+---+----------+',
'| 2 | 6 | 1900-01-06 00:00 | | 1<>2 |',
'+---+-----+------------------+---+----------+',
'| 3 | xyz | xyz | | |',
'+---+-----+------------------+---+----------+',
'',
],
],
'cli no columnHeaders' => [
true, false, true, false,
[
'+---+-----+------------------+--+----------+',
'| 1 | 6 | 1900-01-06 00:00 | | 0.572917 |',
'| 2 | 6 | 1900-01-06 00:00 | | 1<>2 |',
'| 3 | xyz | xyz | | |',
'+---+-----+------------------+--+----------+',
'',
],
],
'cli no row headers' => [
true, false, false, true,
[
'+-----+------------------+---+----------+',
'| A | B | C | D |',
'+-----+------------------+---+----------+',
'| 6 | 1900-01-06 00:00 | | 0.572917 |',
'| 6 | 1900-01-06 00:00 | | 1<>2 |',
'| xyz | xyz | | |',
'+-----+------------------+---+----------+',
'',
],
],
'cli row dividers, no row nor column headers' => [
true, true, false, false,
[
'+-----+------------------+--+----------+',
'| 6 | 1900-01-06 00:00 | | 0.572917 |',
'+-----+------------------+--+----------+',
'| 6 | 1900-01-06 00:00 | | 1<>2 |',
'+-----+------------------+--+----------+',
'| xyz | xyz | | |',
'+-----+------------------+--+----------+',
'',
],
],
];
}
public function testBool(): void
{
$spreadsheet = new Spreadsheet();
$sheet = $spreadsheet->getActiveSheet();
$sheet->fromArray([
[0, 1],
[true, false],
[true, true],
], strictNullComparison: true);
$textGrid = new TextGrid(
$sheet->toArray(null, true, false, true),
true,
rowDividers: false,
rowHeaders: false,
columnHeaders: false,
);
$expected = [
'+------+-------+',
'| 0 | 1 |',
'| TRUE | FALSE |',
'| TRUE | TRUE |',
'+------+-------+',
'',
];
$result = $textGrid->render();
$lines = explode(PHP_EOL, $result);
self::assertSame($expected, $lines);
$spreadsheet->disconnectWorksheets();
}
}