diff --git a/CHANGELOG.md b/CHANGELOG.md index 24470f2..4a6f704 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,36 +1,46 @@ -0.3.2 / - - Adjusted is1D and is2D to support int|float|string|bool|null - - Extracted some functions to separate helper class - - Created test class for Helper +0.4.0 / 2025-11-28 + - Renamed `Helper` to `Validator` and moved formatting logic to new `Formatter` class + - Added `is3D` support in `Validator` + - Updated tests for `Validator` and introduced `FormatterTest` + - Adjusted autoload configuration in `composer.json` + - Improved `PrettyPrintTest` with proper `ob` level handling + - Updated `CHANGELOG` for version `0.4.0` + - Refactor PrettyPrint: remove unused `is2DNumeric` method and simplify array handling logic + - Refactor PrettyPrint: delegate 2D matrix formatting to `Formatter::format2DAligned`, added tests in `FormatterTest` + +0.3.2 / 2025-11-28 + - Adjusted is1D and is2D to support `int|float|string|bool|null` + - Extracted some functions to separate helper class + - Created test class for Helper 0.3.1 / 2025-11-27 - - Adjust default formatting dimensions in PrettyPrint tensor methods - - README.md example updates - - Add precision option to PrettyPrint for float formatting - - Refactor PrettyPrint for coding standards compliance and improve script configurations in composer.json - - Added MAX limitation for arguments - - Allowed formatted printing of numbers and strings together + - Adjust default formatting dimensions in `PrettyPrint` tensor methods + - `README.md` example updates + - Add precision option to PrettyPrint for float formatting + - Refactor `PrettyPrint` for coding standards compliance and improve script configurations in composer.json + - Added MAX limitation for arguments + - Allowed formatted printing of numbers and strings together 0.3.0 / 2025-11-24 - - Improved documentation - - Added "start" option to PrettyPrint for prefix control - - Added custom "label" support for tensor formatting in PrettyPrint - - Added web environment `
` wrapping support
+ - Improved documentation
+ - Added "start" option to PrettyPrint for prefix control
+ - Added custom "label" support for tensor formatting in PrettyPrint
+ - Added web environment `` wrapping support
0.2.1 / 2025-11-22
- - Added Github Actions
- - Added PHP Lint
- - Add usage examples and options reference to README.md
+ - Added Github Actions
+ - Added PHP Lint
+ - Add usage examples and options reference to `README.md`
0.2.0 / 2025-11-22
- - Added Unit Tests
- - Improved documentation
+ - Added Unit Tests
+ - Improved documentation
0.1.0 / 2025-11-21
- - Updated project structure
- - Added README and License
- - Implemented basic functionality
- - Extracted pprint, pp and ppd function to separate functions.php file
+ - Updated project structure
+ - Added README and License
+ - Implemented basic functionality
+ - Extracted `pprint`, `pp` and `ppd` function to separate functions.php file
0.0.1 / 2025-11-12
-- Initial commit
+ - Initial commit
diff --git a/composer.json b/composer.json
index f1e0584..e4103e6 100644
--- a/composer.json
+++ b/composer.json
@@ -12,7 +12,8 @@
],
"autoload": {
"psr-4": {
- "Apphp\\PrettyPrint\\": "src/"
+ "Apphp\\PrettyPrint\\": "src/",
+ "Apphp\\PrettyPrint\\Tests\\": "tests/"
},
"files": [
"src/functions.php"
diff --git a/src/Formatter.php b/src/Formatter.php
new file mode 100644
index 0000000..4cb950d
--- /dev/null
+++ b/src/Formatter.php
@@ -0,0 +1,98 @@
+ $row) {
+ $frow = [];
+ for ($c = 0; $c < $cols; $c++) {
+ $s = '';
+ if (array_key_exists($c, $row)) {
+ $cell = $row[$c];
+ if (is_int($cell) || is_float($cell)) {
+ $s = self::formatNumber($cell, $precision);
+ } elseif (is_string($cell)) {
+ $s = "'" . addslashes($cell) . "'";
+ } elseif (is_bool($cell)) {
+ $s = $cell ? 'True' : 'False';
+ } elseif (is_null($cell)) {
+ $s = 'None';
+ } elseif (is_array($cell)) {
+ $s = 'Array';
+ } elseif (is_object($cell)) {
+ $s = 'Object';
+ } elseif (is_resource($cell)) {
+ $s = 'Resource';
+ } else {
+ $s = 'Unknown';
+ }
+ }
+ $frow[$c] = $s;
+ $widths[$c] = max($widths[$c], strlen($s));
+ }
+ $formatted[$r] = $frow;
+ }
+
+ // Build lines using precomputed widths
+ $lines = [];
+ foreach ($formatted as $frow) {
+ $cells = [];
+ for ($c = 0; $c < $cols; $c++) {
+ $cells[] = str_pad($frow[$c] ?? '', $widths[$c], ' ', STR_PAD_LEFT);
+ }
+ $lines[] = '[' . implode(', ', $cells) . ']';
+ }
+
+ if (count($lines) === 1) {
+ return '[' . $lines[0] . ']';
+ }
+ return '[' . implode(",\n ", $lines) . ']';
+ }
+}
diff --git a/src/PrettyPrint.php b/src/PrettyPrint.php
index 7e555ab..ceaef11 100644
--- a/src/PrettyPrint.php
+++ b/src/PrettyPrint.php
@@ -159,7 +159,7 @@ public function __invoke(...$args)
}
// Label + single 3D tensor
- if (count($args) === 2 && !is_array($args[0]) && is_array($args[1]) && $this->is3D($args[1])) {
+ if (count($args) === 2 && !is_array($args[0]) && is_array($args[1]) && Validator::is3D($args[1])) {
$out = $this->format3DTorch(
$args[1],
(int)($fmt['headB'] ?? 5),
@@ -176,9 +176,9 @@ public function __invoke(...$args)
}
// Label + 2D matrix (supports numeric and string matrices)
- if (count($args) === 2 && !is_array($args[0]) && is_array($args[1]) && Helper::is2D($args[1])) {
+ if (count($args) === 2 && !is_array($args[0]) && is_array($args[1]) && Validator::is2D($args[1])) {
$label = is_bool($args[0]) ? ($args[0] ? 'True' : 'False') : (is_null($args[0]) ? 'None' : (string)$args[0]);
- $out = $this->format2DAligned($args[1]);
+ $out = Formatter::format2DAligned($args[1], $this->precision);
echo $start . ($label . "\n" . $out) . $end;
$this->precision = $prevPrecision;
return;
@@ -195,7 +195,7 @@ public function __invoke(...$args)
}
$allRows = true;
for ($i = $startIndex; $i < count($args); $i++) {
- if (Helper::is1D($args[$i])) {
+ if (Validator::is1D($args[$i])) {
$rows[] = $args[$i];
} else {
$allRows = false;
@@ -203,7 +203,7 @@ public function __invoke(...$args)
}
}
if ($allRows && count($rows) > 1) {
- $out = $this->format2DAligned($rows);
+ $out = Formatter::format2DAligned($rows, $this->precision);
echo $start . ((($label !== null) ? ($label . "\n" . $out) : $out)) . $end;
$this->precision = $prevPrecision;
return;
@@ -212,17 +212,10 @@ public function __invoke(...$args)
// Default formatting
$parts = [];
- $containsArray = false;
- foreach ($args as $a) {
- if (is_array($a)) {
- $containsArray = true;
- break;
- }
- }
foreach ($args as $arg) {
if (is_array($arg)) {
- if ($this->is3D($arg)) {
+ if (Validator::is3D($arg)) {
$parts[] = $this->format3DTorch(
$arg,
(int)($fmt['headB'] ?? 5),
@@ -233,7 +226,7 @@ public function __invoke(...$args)
(int)($fmt['tailCols'] ?? 5),
(string)($fmt['label'] ?? 'tensor')
);
- } elseif (Helper::is2D($arg)) {
+ } elseif (Validator::is2D($arg)) {
$parts[] = $this->format2DTorch(
$arg,
(int)($fmt['headRows'] ?? 5),
@@ -251,9 +244,17 @@ public function __invoke(...$args)
} elseif (is_null($arg)) {
$parts[] = 'None';
} elseif (is_int($arg) || is_float($arg)) {
- $parts[] = Helper::formatNumber($arg, $this->precision);
+ $parts[] = Formatter::formatNumber($arg, $this->precision);
+ } elseif (is_string($arg)) {
+ $parts[] = "'" . addslashes($arg) . "'";
+ } elseif (is_array($arg)) {
+ $parts[] = 'Array';
+ } elseif (is_object($arg)) {
+ $parts[] = 'Class';
+ } elseif (is_resource($arg)) {
+ $parts[] = 'Resource';
} else {
- $parts[] = (string)$arg;
+ $parts[] = 'Unknown';
}
}
}
@@ -264,114 +265,6 @@ public function __invoke(...$args)
// ---- Private helpers ----
- // TODO: >>>>>>>>
- /**
- * Determine if the given value is a 3D tensor of numeric matrices.
- *
- * @param mixed $value
- * @return bool True if $value is an array of 2D numeric arrays.
- */
- private function is3D($value): bool
- {
- if (!is_array($value)) {
- return false;
- }
- foreach ($value as $matrix) {
- if (!$this->is2DNumeric($matrix)) {
- return false;
- }
- }
- return true;
- }
-
- /**
- * Determine if the given value is a 2D numeric matrix (ints/floats only).
- *
- * @param mixed $value
- * @return bool
- */
- private function is2DNumeric($value): bool
- {
- if (!is_array($value)) {
- return false;
- }
- if (empty($value)) {
- return true;
- }
- foreach ($value as $row) {
- if (!is_array($row)) {
- return false;
- }
- foreach ($row as $cell) {
- if (!is_int($cell) && !is_float($cell)) {
- return false;
- }
- }
- }
- return true;
- }
-
- /**
- * Format a 2D numeric matrix with aligned columns.
- *
- * @param array $matrix 2D array of ints/floats.
- * @return string
- */
- private function format2DAligned(array $matrix): string
- {
- $cols = 0;
- foreach ($matrix as $row) {
- if (is_array($row)) {
- $cols = max($cols, count($row));
- }
- }
- if ($cols === 0) {
- return '[]';
- }
-
- // Pre-format all cells (numbers and strings) and compute widths in one pass
- $widths = array_fill(0, $cols, 0);
- $formatted = [];
- foreach ($matrix as $r => $row) {
- $frow = [];
- for ($c = 0; $c < $cols; $c++) {
- $s = '';
- if (array_key_exists($c, $row)) {
- $cell = $row[$c];
- if (is_int($cell) || is_float($cell)) {
- $s = Helper::formatNumber($cell, $this->precision);
- } elseif (is_string($cell)) {
- $s = "'" . addslashes($cell) . "'";
- } elseif (is_bool($cell)) {
- $s = $cell ? 'True' : 'False';
- } elseif (is_null($cell)) {
- $s = 'None';
- } else {
- $s = (string)$cell;
- }
- }
- $frow[$c] = $s;
- $widths[$c] = max($widths[$c], strlen($s));
- }
- $formatted[$r] = $frow;
- }
-
- // Build lines using precomputed widths
- $lines = [];
- foreach ($formatted as $frow) {
- $cells = [];
- for ($c = 0; $c < $cols; $c++) {
- $cells[] = str_pad($frow[$c] ?? '', $widths[$c], ' ', STR_PAD_LEFT);
- }
- $lines[] = '[' . implode(', ', $cells) . ']';
- }
-
- if (count($lines) === 1) {
- return '[' . $lines[0] . ']';
- }
- return '[' . implode(",\n ", $lines) . ']';
- }
-
/**
* Format a 2D matrix showing head/tail rows and columns with ellipses in-between.
*
@@ -431,10 +324,10 @@ private function format2DSummarized(array $matrix, int $headRows = 5, int $tailR
$s = '';
if ($pos === '...') {
$s = '...';
- } elseif (isset($matrix[$rIndex][$pos])) {
+ } elseif (array_key_exists($pos, $matrix[$rIndex])) {
$cell = $matrix[$rIndex][$pos];
if (is_int($cell) || is_float($cell)) {
- $s = Helper::formatNumber($cell, $this->precision);
+ $s = Formatter::formatNumber($cell, $this->precision);
} elseif (is_string($cell)) {
$s = "'" . addslashes($cell) . "'";
} elseif (is_bool($cell)) {
@@ -496,14 +389,14 @@ private function format2DSummarized(array $matrix, int $headRows = 5, int $tailR
private function formatForArray($value): string
{
if (is_array($value)) {
- if (Helper::is2D($value)) {
- return $this->format2DAligned($value);
+ if (Validator::is2D($value)) {
+ return Formatter::format2DAligned($value, $this->precision);
}
$formattedItems = array_map(fn ($v) => $this->formatForArray($v), $value);
return '[' . implode(', ', $formattedItems) . ']';
}
if (is_int($value) || is_float($value)) {
- return Helper::formatNumber($value, $this->precision);
+ return Formatter::formatNumber($value, $this->precision);
}
if (is_bool($value)) {
return $value ? 'True' : 'False';
@@ -603,4 +496,4 @@ private function format2DTorch(array $matrix, int $headRows = 5, int $tailRows =
}
}
-// 672/605==
+// 672/605/499/==
diff --git a/src/Helper.php b/src/Validator.php
similarity index 71%
rename from src/Helper.php
rename to src/Validator.php
index 971209b..67e16fc 100644
--- a/src/Helper.php
+++ b/src/Validator.php
@@ -2,29 +2,11 @@
namespace Apphp\PrettyPrint;
-class Helper
+/**
+ * Validator for pretty-printing arrays.
+ */
+class Validator
{
- /**
- * Format a value as a number when possible.
- *
- * Integers are returned verbatim; floats are rendered with 4 decimal places;
- * non-numeric values are cast to string.
- *
- * @param mixed $v
- * @param int $precision
- * @return string
- */
- public static function formatNumber(mixed $v, int $precision = 2): string
- {
- if (is_int($v)) {
- return (string)$v;
- }
- if (is_float($v)) {
- return number_format($v, $precision, '.', '');
- }
- return (string)$v;
- }
-
/**
* Determine if a value is a 1D array of int|float|string|bool|null.
*
@@ -72,4 +54,23 @@ public static function is2D(mixed $value): bool
}
return true;
}
+
+ /**
+ * Determine if the given value is a 3D tensor of matrices.
+ *
+ * @param mixed $value
+ * @return bool True if $value is an array of 2D arrays.
+ */
+ public static function is3D(mixed $value): bool
+ {
+ if (!is_array($value)) {
+ return false;
+ }
+ foreach ($value as $matrix) {
+ if (!self::is2D($matrix)) {
+ return false;
+ }
+ }
+ return true;
+ }
}
diff --git a/tests/HelperTest.php b/tests/FormatterTest.php
similarity index 52%
rename from tests/HelperTest.php
rename to tests/FormatterTest.php
index 610c887..f410588 100644
--- a/tests/HelperTest.php
+++ b/tests/FormatterTest.php
@@ -4,8 +4,7 @@
namespace Apphp\PrettyPrint\Tests;
-use Apphp\PrettyPrint\Helper;
-use Apphp\PrettyPrint\PrettyPrint;
+use Apphp\PrettyPrint\Formatter;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\Group;
use PHPUnit\Framework\TestCase;
@@ -13,9 +12,9 @@
use PHPUnit\Framework\Attributes\TestDox;
use PHPUnit\Framework\Attributes\DataProvider;
-#[CoversClass(PrettyPrint::class)]
-#[Group('Helper')]
-final class HelperTest extends TestCase
+#[CoversClass(Formatter::class)]
+#[Group('PrettyPrint')]
+final class FormatterTest extends TestCase
{
public static function formatNumberProvider(): array
{
@@ -57,7 +56,7 @@ public static function defaultFormatNumberProvider(): array
#[DataProvider('formatNumberProvider')]
public function testFormatNumber(mixed $value, int $precision, string $expected): void
{
- self::assertSame($expected, Helper::formatNumber($value, $precision));
+ self::assertSame($expected, Formatter::formatNumber($value, $precision));
}
#[Test]
@@ -65,54 +64,74 @@ public function testFormatNumber(mixed $value, int $precision, string $expected)
#[DataProvider('defaultFormatNumberProvider')]
public function testFormatNumberDefaultPrecision(mixed $value, string $expected): void
{
- self::assertSame($expected, Helper::formatNumber($value));
+ self::assertSame($expected, Formatter::formatNumber($value));
}
- public static function is1DProvider(): array
+ public static function format2DAlignedProvider(): array
{
return [
- 'ints only' => [[1, 2, 3], true],
- 'floats only' => [[1.0, 2.5, -3.14], true],
- 'ints and floats' => [[1, 2.0, -3.5], true],
- 'empty array' => [[], true],
- 'contains string' => [[1, '2', 3], true],
- 'contains bool' => [[1, true, 3], true],
- 'contains null' => [[1, null, 3], true],
- 'nested array' => [[1, [2], 3], false],
- 'non-array int' => [123, false],
- 'non-array string' => ['abc', false],
+ 'two numeric rows align' => [
+ [[1, 23, 456], [12, 3, 45]],
+ 2,
+ "[[ 1, 23, 456],\n [12, 3, 45]]",
+ ],
+ 'ragged rows pad missing cells' => [
+ [[1, 23], [12, 3, 45]],
+ 2,
+ "[[ 1, 23, ],\n [12, 3, 45]]",
+ ],
+ 'strings quoted and escaped' => [
+ [["a'b", 'c']],
+ 2,
+ "[['a\'b', 'c']]",
+ ],
+ 'booleans and null rendered' => [
+ [[true, false, null]],
+ 2,
+ '[[True, False, None]]',
+ ],
+ 'floats obey precision' => [
+ [[1.2, 3.4567], [9.0, 10.9999]],
+ 2,
+ "[[1.20, 3.46],\n [9.00, 11.00]]",
+ ],
+ 'empty matrix' => [
+ [],
+ 2,
+ '[]',
+ ],
+ 'array cell casts via (string)$cell' => [
+ [[[1, 2]]],
+ 2,
+ '[[Array]]',
+ ],
+ 'object cell renders as Object' => [
+ [[(object)['a' => 1]]],
+ 2,
+ '[[Object]]',
+ ],
+ 'resource cell renders as Resource' => [
+ [[fopen('php://memory', 'r')]],
+ 2,
+ '[[Resource]]',
+ ],
+ 'closed resource renders as Unknown' => (function () {
+ $h = fopen('php://temp', 'r+');
+ fclose($h);
+ return [
+ [[$h]],
+ 2,
+ '[[Unknown]]',
+ ];
+ })(),
];
}
#[Test]
- #[TestDox('is1D returns true only for 1D arrays of int|float|string|bool|null')]
- #[DataProvider('is1DProvider')]
- public function testIs1D(mixed $value, bool $expected): void
+ #[TestDox('format2DAligned formats 2D arrays with alignment, quoting, and precision')]
+ #[DataProvider('format2DAlignedProvider')]
+ public function testFormat2DAligned(array $matrix, int $precision, string $expected): void
{
- self::assertSame($expected, Helper::is1D($value));
- }
-
- public static function is2DProvider(): array
- {
- return [
- 'empty outer array' => [[], true],
- 'single empty row' => [[[ ]], true],
- 'multiple empty rows' => [[[ ], [ ]], true],
- 'numeric matrix' => [[[1,2,3],[4,5,6]], true],
- 'mixed scalars' => [[[1,'2',true,null],[3.5, 'x', false, 0]], true],
- 'ragged rows allowed' => [[[1,2],[3,4,5]], true],
- 'assoc rows allowed' => [[['a' => 1,'b' => 2], ['c' => 3]], true],
- 'row contains array (nested) invalid' => [[[1,[2],3]], false],
- 'row is not array invalid' => [[1,2,3], false],
- 'outer non-array invalid' => [123, false],
- ];
- }
-
- #[Test]
- #[TestDox('is2D returns true only for arrays-of-arrays with scalar|null cells')]
- #[DataProvider('is2DProvider')]
- public function testIs2D(mixed $value, bool $expected): void
- {
- self::assertSame($expected, Helper::is2D($value));
+ self::assertSame($expected, Formatter::format2DAligned($matrix, $precision));
}
}
diff --git a/tests/FunctionsTest.php b/tests/FunctionsTest.php
index b932948..18a912a 100644
--- a/tests/FunctionsTest.php
+++ b/tests/FunctionsTest.php
@@ -23,7 +23,7 @@ public function pprintPrintsOutput(): void
ob_start();
pprint('Hello');
$out = ob_get_clean();
- self::assertSame("Hello\n", $out);
+ self::assertSame("'Hello'\n", $out);
}
#[Test]
diff --git a/tests/PrettyPrintTest.php b/tests/PrettyPrintTest.php
index 27916d8..b10396a 100644
--- a/tests/PrettyPrintTest.php
+++ b/tests/PrettyPrintTest.php
@@ -15,6 +15,23 @@
#[Group('PrettyPrint')]
final class PrettyPrintTest extends TestCase
{
+ private int $obLevel = 0;
+
+ protected function setUp(): void
+ {
+ parent::setUp();
+ $this->obLevel = ob_get_level();
+ }
+
+ protected function tearDown(): void
+ {
+ // Ensure any stray buffers opened during a test are closed
+ while (ob_get_level() > $this->obLevel) {
+ @ob_end_clean();
+ }
+ parent::tearDown();
+ }
+
#[Test]
#[TestDox('prints scalars and strings with number formatting and newline')]
public function scalarsAndStrings(): void
@@ -23,7 +40,7 @@ public function scalarsAndStrings(): void
ob_start();
$pp('Hello', 123, 4.56);
$out = ob_get_clean();
- self::assertSame("Hello 123 4.5600\n", $out);
+ self::assertSame("'Hello' 123 4.5600\n", $out);
}
#[Test]
@@ -133,7 +150,7 @@ public function endOptionTrailingArray(): void
ob_start();
$pp('Line without newline', ['end' => '']);
$out = ob_get_clean();
- self::assertSame('Line without newline', $out);
+ self::assertSame("'Line without newline'", $out);
}
#[Test]
@@ -144,7 +161,7 @@ public function endOptionNamedArgument(): void
ob_start();
$pp('Named', end: '');
$out = ob_get_clean();
- self::assertSame('Named', $out);
+ self::assertSame("'Named'", $out);
}
#[Test]
@@ -155,7 +172,7 @@ public function startOptionTrailingArray(): void
ob_start();
$pp('Hello', ['start' => "\t", 'end' => '']);
$out = ob_get_clean();
- self::assertSame("\tHello", $out);
+ self::assertSame("\t'Hello'", $out);
}
#[Test]
@@ -166,7 +183,7 @@ public function startOptionNamedArgument(): void
ob_start();
$pp('World', start: '>>> ', end: '');
$out = ob_get_clean();
- self::assertSame('>>> World', $out);
+ self::assertSame(">>> 'World'", $out);
}
#[Test]
diff --git a/tests/ValidatorTest.php b/tests/ValidatorTest.php
new file mode 100644
index 0000000..ffb240d
--- /dev/null
+++ b/tests/ValidatorTest.php
@@ -0,0 +1,90 @@
+ [[1, 2, 3], true],
+ 'floats only' => [[1.0, 2.5, -3.14], true],
+ 'ints and floats' => [[1, 2.0, -3.5], true],
+ 'empty array' => [[], true],
+ 'contains string' => [[1, '2', 3], true],
+ 'contains bool' => [[1, true, 3], true],
+ 'contains null' => [[1, null, 3], true],
+ 'nested array' => [[1, [2], 3], false],
+ 'non-array int' => [123, false],
+ 'non-array string' => ['abc', false],
+ ];
+ }
+
+ #[Test]
+ #[TestDox('is1D returns true only for 1D arrays of int|float|string|bool|null')]
+ #[DataProvider('is1DProvider')]
+ public function testIs1D(mixed $value, bool $expected): void
+ {
+ self::assertSame($expected, Validator::is1D($value));
+ }
+
+ public static function is2DProvider(): array
+ {
+ return [
+ 'empty outer array' => [[], true],
+ 'single empty row' => [[[ ]], true],
+ 'multiple empty rows' => [[[ ], [ ]], true],
+ 'numeric matrix' => [[[1,2,3],[4,5,6]], true],
+ 'mixed scalars' => [[[1,'2',true,null],[3.5, 'x', false, 0]], true],
+ 'ragged rows allowed' => [[[1,2],[3,4,5]], true],
+ 'assoc rows allowed' => [[['a' => 1,'b' => 2], ['c' => 3]], true],
+ 'row contains array (nested) invalid' => [[[1,[2],3]], false],
+ 'row is not array invalid' => [[1,2,3], false],
+ 'outer non-array invalid' => [123, false],
+ ];
+ }
+
+ #[Test]
+ #[TestDox('is2D returns true only for arrays-of-arrays with scalar|null cells')]
+ #[DataProvider('is2DProvider')]
+ public function testIs2D(mixed $value, bool $expected): void
+ {
+ self::assertSame($expected, Validator::is2D($value));
+ }
+
+ public static function is3DProvider(): array
+ {
+ return [
+ 'empty outer array' => [[], true],
+ 'single empty 2D matrix' => [[[]], true],
+ 'multiple empty 2D matrices' => [[[], []], true],
+ 'single 2D numeric matrix' => [[[[1,2,3],[4,5,6]]], true],
+ 'two 2D matrices ragged rows' => [[[[1,2],[3]], [[4,5,6],[7]]], true],
+ '2D matrices with mixed scalars' => [[[[1,'2',true,null],[3.5,'x',false,0]]], true],
+ 'outer contains non-array (invalid)' => [[1,2,3], false],
+ 'matrix has row that is not array (invalid)' => [[ [1,2,3] ], false],
+ 'matrix contains nested array cell (invalid)' => [[ [[1,[2],3]] ], false],
+ 'not an array invalid' => ['abc', false],
+ ];
+ }
+
+ #[Test]
+ #[TestDox('is3D returns true only for arrays of 2D arrays with scalar|null cells')]
+ #[DataProvider('is3DProvider')]
+ public function testIs3D(mixed $value, bool $expected): void
+ {
+ self::assertSame($expected, Validator::is3D($value));
+ }
+}