Skip to content

Commit cd5836a

Browse files
authored
Merge pull request #4418 from oleibman/gridimprovements
TextGrid Improvements
2 parents 902d4bd + cc1357c commit cd5836a

File tree

5 files changed

+262
-22
lines changed

5 files changed

+262
-22
lines changed

CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ and this project adheres to [Semantic Versioning](https://semver.org).
1111

1212
- Add ability to add custom functions to Calculation. [PR #4390](https://github.com/PHPOffice/PhpSpreadsheet/pull/4390)
1313
- Add FormulaRange to IgnoredErrors. [PR #4393](https://github.com/PHPOffice/PhpSpreadsheet/pull/4393)
14+
- TextGrid improvements. [PR #4418](https://github.com/PHPOffice/PhpSpreadsheet/pull/4418)
1415
- Permit read to class which extends Spreadsheet. [Discussion #4402](https://github.com/PHPOffice/PhpSpreadsheet/discussions/4402) [PR #4404](https://github.com/PHPOffice/PhpSpreadsheet/pull/4404)
1516

1617
### Removed

docs/topics/reading-and-writing-to-file.md

+41-3
Original file line numberDiff line numberDiff line change
@@ -1169,6 +1169,44 @@ One benefit of flags is that you can pass several flags in a single method call.
11691169
Two or more flags can be passed together using PHP's `|` operator.
11701170

11711171
```php
1172-
$reader = \PhpOffice\PhpSpreadsheet\IOFactory::createReaderForFile("myExampleFile.xlsx");
1173-
$reader->load("spreadsheetWithCharts.xlsx", $reader::READ_DATA_ONLY | $reader::IGNORE_EMPTY_CELLS);
1174-
```
1172+
$reader = \PhpOffice\PhpSpreadsheet\IOFactory::createReaderForFile('myExampleFile.xlsx');
1173+
$reader->load(
1174+
'spreadsheetWithCharts.xlsx',
1175+
$reader::READ_DATA_ONLY | $reader::IGNORE_EMPTY_CELLS
1176+
);
1177+
```
1178+
1179+
## Writing Data as a Plaintext Grid
1180+
1181+
Although not really a spreadsheet format, it can be useful to write data in grid format to a plaintext file.
1182+
Code like the following can be used:
1183+
```php
1184+
$array = $sheet->toArray(null, true, true, true);
1185+
$textGrid = new \PhpOffice\PhpSpreadsheet\Shared\TextGrid(
1186+
$array,
1187+
true, // true for cli, false for html
1188+
// Starting with release 4.2,
1189+
// the output format can be tweaked by uncommenting
1190+
// any of the following 3 optional parameters.
1191+
// rowDividers: true,
1192+
// rowHeaders: false,
1193+
// columnHeaders: false,
1194+
);
1195+
$result = $textGrid->render();
1196+
```
1197+
You can then echo `$result` to a terminal, or write it to a file with `file_put_contents`. The result will resemble:
1198+
```
1199+
+-----+------------------+---+----------+
1200+
| A | B | C | D |
1201+
+---+-----+------------------+---+----------+
1202+
| 1 | 6 | 1900-01-06 00:00 | | 0.572917 |
1203+
| 2 | 6 | TRUE | | 1<>2 |
1204+
| 3 | xyz | xyz | | |
1205+
+---+-----+------------------+---+----------+
1206+
```
1207+
Please note that this may produce sub-optimal results for situations such as:
1208+
1209+
- use of accents as combining characters rather than using pre-composed characters (may be handled by extending the class to override the `getString` or `strlen` methods)
1210+
- Fullwidth characters
1211+
- right-to-left characters (better display in a browser than a terminal on a non-RTL system)
1212+
- multi-line strings

src/PhpSpreadsheet/Helper/TextGrid.php

+55-18
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
namespace PhpOffice\PhpSpreadsheet\Helper;
44

5+
use PhpOffice\PhpSpreadsheet\Shared\StringHelper;
6+
57
class TextGrid
68
{
79
private bool $isCli;
@@ -14,7 +16,13 @@ class TextGrid
1416

1517
private string $gridDisplay;
1618

17-
public function __construct(array $matrix, bool $isCli = true)
19+
private bool $rowDividers = false;
20+
21+
private bool $rowHeaders = true;
22+
23+
private bool $columnHeaders = true;
24+
25+
public function __construct(array $matrix, bool $isCli = true, bool $rowDividers = false, bool $rowHeaders = true, bool $columnHeaders = true)
1826
{
1927
$this->rows = array_keys($matrix);
2028
$this->columns = array_keys($matrix[$this->rows[0]]);
@@ -29,20 +37,25 @@ function (&$row): void {
2937

3038
$this->matrix = $matrix;
3139
$this->isCli = $isCli;
40+
$this->rowDividers = $rowDividers;
41+
$this->rowHeaders = $rowHeaders;
42+
$this->columnHeaders = $columnHeaders;
3243
}
3344

3445
public function render(): string
3546
{
36-
$this->gridDisplay = $this->isCli ? '' : '<pre>';
47+
$this->gridDisplay = $this->isCli ? '' : ('<pre>' . PHP_EOL);
3748

3849
if (!empty($this->rows)) {
3950
$maxRow = max($this->rows);
40-
$maxRowLength = mb_strlen((string) $maxRow) + 1;
51+
$maxRowLength = strlen((string) $maxRow) + 1;
4152
$columnWidths = $this->getColumnWidths();
4253

4354
$this->renderColumnHeader($maxRowLength, $columnWidths);
4455
$this->renderRows($maxRowLength, $columnWidths);
45-
$this->renderFooter($maxRowLength, $columnWidths);
56+
if (!$this->rowDividers) {
57+
$this->renderFooter($maxRowLength, $columnWidths);
58+
}
4659
}
4760

4861
$this->gridDisplay .= $this->isCli ? '' : '</pre>';
@@ -53,30 +66,48 @@ public function render(): string
5366
private function renderRows(int $maxRowLength, array $columnWidths): void
5467
{
5568
foreach ($this->matrix as $row => $rowData) {
56-
$this->gridDisplay .= '|' . str_pad((string) $this->rows[$row], $maxRowLength, ' ', STR_PAD_LEFT) . ' ';
69+
if ($this->rowHeaders) {
70+
$this->gridDisplay .= '|' . str_pad((string) $this->rows[$row], $maxRowLength, ' ', STR_PAD_LEFT) . ' ';
71+
}
5772
$this->renderCells($rowData, $columnWidths);
5873
$this->gridDisplay .= '|' . PHP_EOL;
74+
if ($this->rowDividers) {
75+
$this->renderFooter($maxRowLength, $columnWidths);
76+
}
5977
}
6078
}
6179

6280
private function renderCells(array $rowData, array $columnWidths): void
6381
{
6482
foreach ($rowData as $column => $cell) {
65-
$displayCell = ($this->isCli) ? (string) $cell : htmlentities((string) $cell);
83+
$valueForLength = $this->getString($cell);
84+
$displayCell = $this->isCli ? $valueForLength : htmlentities($valueForLength);
6685
$this->gridDisplay .= '| ';
67-
$this->gridDisplay .= $displayCell . str_repeat(' ', $columnWidths[$column] - mb_strlen($cell ?? '') + 1);
86+
$this->gridDisplay .= $displayCell . str_repeat(' ', $columnWidths[$column] - $this->strlen($valueForLength) + 1);
6887
}
6988
}
7089

71-
private function renderColumnHeader(int $maxRowLength, array $columnWidths): void
90+
private function renderColumnHeader(int $maxRowLength, array &$columnWidths): void
7291
{
73-
$this->gridDisplay .= str_repeat(' ', $maxRowLength + 2);
92+
if (!$this->columnHeaders) {
93+
$this->renderFooter($maxRowLength, $columnWidths);
94+
95+
return;
96+
}
97+
foreach ($this->columns as $column => $reference) {
98+
$columnWidths[$column] = max($columnWidths[$column], $this->strlen($reference));
99+
}
100+
if ($this->rowHeaders) {
101+
$this->gridDisplay .= str_repeat(' ', $maxRowLength + 2);
102+
}
74103
foreach ($this->columns as $column => $reference) {
75104
$this->gridDisplay .= '+-' . str_repeat('-', $columnWidths[$column] + 1);
76105
}
77106
$this->gridDisplay .= '+' . PHP_EOL;
78107

79-
$this->gridDisplay .= str_repeat(' ', $maxRowLength + 2);
108+
if ($this->rowHeaders) {
109+
$this->gridDisplay .= str_repeat(' ', $maxRowLength + 2);
110+
}
80111
foreach ($this->columns as $column => $reference) {
81112
$this->gridDisplay .= '| ' . str_pad((string) $reference, $columnWidths[$column] + 1, ' ');
82113
}
@@ -87,7 +118,9 @@ private function renderColumnHeader(int $maxRowLength, array $columnWidths): voi
87118

88119
private function renderFooter(int $maxRowLength, array $columnWidths): void
89120
{
90-
$this->gridDisplay .= '+' . str_repeat('-', $maxRowLength + 1);
121+
if ($this->rowHeaders) {
122+
$this->gridDisplay .= '+' . str_repeat('-', $maxRowLength + 1);
123+
}
91124
foreach ($this->columns as $column => $reference) {
92125
$this->gridDisplay .= '+-';
93126
$this->gridDisplay .= str_pad((string) '', $columnWidths[$column] + 1, '-');
@@ -112,15 +145,19 @@ private function getColumnWidth(array $columnData): int
112145
$columnData = array_values($columnData);
113146

114147
foreach ($columnData as $columnValue) {
115-
if (is_string($columnValue)) {
116-
$columnWidth = max($columnWidth, mb_strlen($columnValue));
117-
} elseif (is_bool($columnValue)) {
118-
$columnWidth = max($columnWidth, mb_strlen($columnValue ? 'TRUE' : 'FALSE'));
119-
}
120-
121-
$columnWidth = max($columnWidth, mb_strlen((string) $columnWidth));
148+
$columnWidth = max($columnWidth, $this->strlen($this->getString($columnValue)));
122149
}
123150

124151
return $columnWidth;
125152
}
153+
154+
protected function getString(mixed $value): string
155+
{
156+
return StringHelper::convertToString($value, convertBool: true);
157+
}
158+
159+
protected function strlen(string $value): int
160+
{
161+
return mb_strlen($value);
162+
}
126163
}

src/PhpSpreadsheet/Shared/StringHelper.php

+6-1
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
namespace PhpOffice\PhpSpreadsheet\Shared;
44

5+
use PhpOffice\PhpSpreadsheet\Calculation\Calculation;
56
use PhpOffice\PhpSpreadsheet\Exception as SpreadsheetException;
67
use Stringable;
78

@@ -647,8 +648,12 @@ public static function strlenAllowNull(?string $string): int
647648
return strlen("$string");
648649
}
649650

650-
public static function convertToString(mixed $value, bool $throw = true, string $default = ''): string
651+
/** @param bool $convertBool If true, convert bool to locale-aware TRUE/FALSE rather than 1/null-string */
652+
public static function convertToString(mixed $value, bool $throw = true, string $default = '', bool $convertBool = false): string
651653
{
654+
if ($convertBool && is_bool($value)) {
655+
return $value ? Calculation::getTRUE() : Calculation::getFALSE();
656+
}
652657
if ($value === null || is_scalar($value) || $value instanceof Stringable) {
653658
return (string) $value;
654659
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace PhpOffice\PhpSpreadsheetTests\Helper;
6+
7+
use PhpOffice\PhpSpreadsheet\Helper\TextGrid;
8+
use PhpOffice\PhpSpreadsheet\Spreadsheet;
9+
use PHPUnit\Framework\Attributes\DataProvider;
10+
use PHPUnit\Framework\TestCase;
11+
12+
class TextGridTest extends TestCase
13+
{
14+
#[DataProvider('providerTextGrid')]
15+
public function testTextGrid(
16+
bool $cli,
17+
bool $rowDividers,
18+
bool $rowHeaders,
19+
bool $columnHeaders,
20+
array $expected
21+
): void {
22+
$spreadsheet = new Spreadsheet();
23+
$sheet = $spreadsheet->getActiveSheet();
24+
$sheet->fromArray([
25+
[6, '=TEXT(A1,"yyyy-mm-dd hh:mm")', null, 0.572917],
26+
['="6"', '=TEXT(A2,"yyyy-mm-dd hh:mm")', null, '1<>2'],
27+
['xyz', '=TEXT(A3,"yyyy-mm-dd hh:mm")'],
28+
], strictNullComparison: true);
29+
$textGrid = new TextGrid(
30+
$sheet->toArray(null, true, true, true),
31+
$cli,
32+
rowDividers: $rowDividers,
33+
rowHeaders: $rowHeaders,
34+
columnHeaders: $columnHeaders
35+
);
36+
$result = $textGrid->render();
37+
// Note that, for cli, string will end with PHP_EOL,
38+
// so explode will add an extra null-string element
39+
// to its array output.
40+
$lines = explode(PHP_EOL, $result);
41+
self::assertSame($expected, $lines);
42+
$spreadsheet->disconnectWorksheets();
43+
}
44+
45+
public static function providerTextGrid(): array
46+
{
47+
return [
48+
'cli default values' => [
49+
true, false, true, true,
50+
[
51+
' +-----+------------------+---+----------+',
52+
' | A | B | C | D |',
53+
'+---+-----+------------------+---+----------+',
54+
'| 1 | 6 | 1900-01-06 00:00 | | 0.572917 |',
55+
'| 2 | 6 | 1900-01-06 00:00 | | 1<>2 |',
56+
'| 3 | xyz | xyz | | |',
57+
'+---+-----+------------------+---+----------+',
58+
'',
59+
],
60+
],
61+
'html default values' => [
62+
false, false, true, true,
63+
[
64+
'<pre>',
65+
' +-----+------------------+---+----------+',
66+
' | A | B | C | D |',
67+
'+---+-----+------------------+---+----------+',
68+
'| 1 | 6 | 1900-01-06 00:00 | | 0.572917 |',
69+
'| 2 | 6 | 1900-01-06 00:00 | | 1&lt;&gt;2 |',
70+
'| 3 | xyz | xyz | | |',
71+
'+---+-----+------------------+---+----------+',
72+
'</pre>',
73+
],
74+
],
75+
'cli rowDividers' => [
76+
true, true, true, true,
77+
[
78+
' +-----+------------------+---+----------+',
79+
' | A | B | C | D |',
80+
'+---+-----+------------------+---+----------+',
81+
'| 1 | 6 | 1900-01-06 00:00 | | 0.572917 |',
82+
'+---+-----+------------------+---+----------+',
83+
'| 2 | 6 | 1900-01-06 00:00 | | 1<>2 |',
84+
'+---+-----+------------------+---+----------+',
85+
'| 3 | xyz | xyz | | |',
86+
'+---+-----+------------------+---+----------+',
87+
'',
88+
],
89+
],
90+
'cli no columnHeaders' => [
91+
true, false, true, false,
92+
[
93+
'+---+-----+------------------+--+----------+',
94+
'| 1 | 6 | 1900-01-06 00:00 | | 0.572917 |',
95+
'| 2 | 6 | 1900-01-06 00:00 | | 1<>2 |',
96+
'| 3 | xyz | xyz | | |',
97+
'+---+-----+------------------+--+----------+',
98+
'',
99+
],
100+
],
101+
'cli no row headers' => [
102+
true, false, false, true,
103+
[
104+
'+-----+------------------+---+----------+',
105+
'| A | B | C | D |',
106+
'+-----+------------------+---+----------+',
107+
'| 6 | 1900-01-06 00:00 | | 0.572917 |',
108+
'| 6 | 1900-01-06 00:00 | | 1<>2 |',
109+
'| xyz | xyz | | |',
110+
'+-----+------------------+---+----------+',
111+
'',
112+
],
113+
],
114+
'cli row dividers, no row nor column headers' => [
115+
true, true, false, false,
116+
[
117+
'+-----+------------------+--+----------+',
118+
'| 6 | 1900-01-06 00:00 | | 0.572917 |',
119+
'+-----+------------------+--+----------+',
120+
'| 6 | 1900-01-06 00:00 | | 1<>2 |',
121+
'+-----+------------------+--+----------+',
122+
'| xyz | xyz | | |',
123+
'+-----+------------------+--+----------+',
124+
'',
125+
],
126+
],
127+
];
128+
}
129+
130+
public function testBool(): void
131+
{
132+
$spreadsheet = new Spreadsheet();
133+
$sheet = $spreadsheet->getActiveSheet();
134+
$sheet->fromArray([
135+
[0, 1],
136+
[true, false],
137+
[true, true],
138+
], strictNullComparison: true);
139+
$textGrid = new TextGrid(
140+
$sheet->toArray(null, true, false, true),
141+
true,
142+
rowDividers: false,
143+
rowHeaders: false,
144+
columnHeaders: false,
145+
);
146+
$expected = [
147+
'+------+-------+',
148+
'| 0 | 1 |',
149+
'| TRUE | FALSE |',
150+
'| TRUE | TRUE |',
151+
'+------+-------+',
152+
'',
153+
];
154+
$result = $textGrid->render();
155+
$lines = explode(PHP_EOL, $result);
156+
self::assertSame($expected, $lines);
157+
$spreadsheet->disconnectWorksheets();
158+
}
159+
}

0 commit comments

Comments
 (0)