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));
+    }
+}