Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
0.5.1 / 2025-12-12
- Improve tensor block formatting in `Formatter` for PyTorch-style visualization
- Refactor `PrettyPrint` for simplified default formatting, remove redundant label-based handling
- Added new function pdiff() for array comparison

0.5.0 / 2025-12-08
- Refactored `ppd` function for improved CLI test handling with conditional exit strategy
- Added support for automatic object-to-array conversion in `PrettyPrint` (`asArray()`/`toArray()`)
Expand Down
46 changes: 32 additions & 14 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ use function Apphp\PrettyPrint\ppd;
```
or simply
```php
use function Apphp\PrettyPrint\{pprint, pp, ppd};
use function Apphp\PrettyPrint\{pprint, pp, ppd, pdiff};
```

### Global helper functions
Expand All @@ -33,19 +33,37 @@ pprint('Hello', 123, 4.56);
// Hello 123 4.5600
```

Print multiple 1D rows aligned as a 2D table
Print 1D arrays
```php
pprint([1, 23, 456], [12, 3, 45]);
// [[ 1, 23, 456],
// [12, 3, 45]]
// [1, 23, 456]
// [12, 3, 45]
```

Compare two arrays with a difference matrix
```php
$a = [
[1, 2, 3],
[4, 5, 6],
];

$b = [
[1, 9, 3],
[0, 5, 7],
];

pdiff($a, $b);
// [[1, -, 3],
// [-, 5, -]]
```

Label + 2D matrix
```php
pprint('Confusion matrix:', [[1, 23], [456, 7]]);
// Confusion matrix:
// [[ 1, 23],
// [456, 7]]
pprint([[1, 23], [456, 7]], label: 'Confusion matrix:');
// Confusion matrix:([
// [ 1, 23],
// [456, 7]
// ])
```

2D tensor-style formatting
Expand Down Expand Up @@ -102,9 +120,7 @@ pprint($tensor3d, headB: 1, tailB: 1, headRows: 1, tailRows: 1, headCols: 1, tai
// tensor([
// [[1, ..., 3],
// [4, ..., 6]],
//
// ...,
//
// [[13, ..., 15],
// [16, ..., 18]]
// ])
Expand Down Expand Up @@ -172,7 +188,7 @@ $pp('Metrics:', [[0.91, 0.02], [0.03, 0.88]]);
If you pass an object that exposes an `asArray()` or `toArray()` method, `pprint` / `PrettyPrint` will automatically convert it to an array before formatting:

```php
class UserCollection
class Users
{
public function asArray(): array
{
Expand All @@ -183,11 +199,13 @@ class UserCollection
}
}

$users = new UserCollection();
$users = new Users();

pprint($users);
// [[1, 'Alice'],
// [2, 'Bob']]
// Users([
// [1, Alice],
// [2, Bob]
// ])
```

If `asArray()` is not present but `toArray()` is, `toArray()` will be used instead.
Expand Down
4 changes: 3 additions & 1 deletion src/Formatter.php
Original file line number Diff line number Diff line change
Expand Up @@ -322,7 +322,9 @@ public static function format3DTorch(array $tensor3d, int $headB = 5, int $tailB
}
}

$joined = implode(",\n\n ", $blocks);
// Use a single newline between blocks for simple tensors without batch
$separator = ",\n ";
$joined = implode($separator, $blocks);
return $label . "([\n " . $joined . "\n])";
}
}
130 changes: 33 additions & 97 deletions src/PrettyPrint.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@

namespace Apphp\PrettyPrint;

use ReflectionException;

/**
* PrettyPrint
*
Expand Down Expand Up @@ -39,6 +41,7 @@ class PrettyPrint
private const MAX_LABEL_LEN = 50;
private const MAX_ARGS = 32;
private int $precision = 4;
private array $labels = [];

/**
* Invoke the pretty printer.
Expand Down Expand Up @@ -88,11 +91,7 @@ public function __invoke(...$args): string
$this->precision = max(0, (int) $fmt['precision']);
}

$out =
$this->tryLabel3D($args, $fmt, $start, $end)
?? $this->tryLabel2D($args, $start, $end)
?? $this->tryMultiple1DRows($args, $start, $end)
?? $start . implode($sep, $this->formatDefaultParts($args, $fmt)) . $end;
$out = $start . implode($sep, $this->formatDefaultParts($args, $fmt)) . $end;

// Restore default precision
$this->precision = $prevPrecision;
Expand Down Expand Up @@ -240,6 +239,7 @@ private function applyAutoPreWrapping(bool $isCli, string &$start, string &$end)
*
* @param array $args
* @return array
* @throws ReflectionException
*/
private function normalizeArgs(array $args): array
{
Expand All @@ -252,103 +252,39 @@ private function normalizeArgs(array $args): array

// Convert objects to arrays if possible
foreach ($args as $i => $value) {
if (is_object($value)) {
if (is_callable([$value, 'asArray'])) {
$args[$i] = $value->asArray();
} elseif (is_callable([$value, 'toArray'])) {
$args[$i] = $value->toArray();
}
if (!is_object($value)) {
continue;
}
}

if (count($args) > self::MAX_ARGS) {
$args = array_slice($args, 0, self::MAX_ARGS);
}
return $args;
}
$array = null;

/**
* Handle the special case: label + single 3D tensor.
* Echoes output and restores precision if matched.
*
* @param array $args
* @param array $fmt
* @param string $start
* @param string $end
* @return string|null True if handled, false otherwise
*/
private function tryLabel3D(array $args, array $fmt, string $start, string $end): ?string
{
if (count($args) === 2 && !is_array($args[0]) && is_array($args[1]) && Validator::is3D($args[1])) {
$out = Formatter::format3DTorch(
$args[1],
(int)($fmt['headB'] ?? 5),
(int)($fmt['tailB'] ?? 5),
(int)($fmt['headRows'] ?? 5),
(int)($fmt['tailRows'] ?? 5),
(int)($fmt['headCols'] ?? 5),
(int)($fmt['tailCols'] ?? 5),
(string)($fmt['label'] ?? 'tensor'),
$this->precision
);
return $start . $out . $end;
}
return null;
}

/**
* Handle the special case: label + single 2D matrix.
* Echoes output and restores precision if matched.
*
* @param array $args
* @param string $start
* @param string $end
* @return string|null True if handled, false otherwise
*/
private function tryLabel2D(array $args, string $start, string $end): ?string
{
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 = Formatter::format2DAligned($args[1], $this->precision);
return $start . ($label . "\n" . $out) . $end;
}
return null;
}
if (is_callable([$value, 'asArray'])) {
$array = $value->asArray();
} elseif (is_callable([$value, 'toArray'])) {
$array = $value->toArray();
}

/**
* Handle the case of multiple 1D rows (optionally preceded by a label) and align them.
* Echoes output and restores precision if matched.
*
* @param array $args
* @param string $start
* @param string $end
* @return string|null True if handled, false otherwise
*/
private function tryMultiple1DRows(array $args, string $start, string $end): ?string
{
$label = null;
$rows = [];
if (count($args) > 1) {
$startIndex = 0;
if (!is_array($args[0])) {
$label = is_bool($args[0]) ? ($args[0] ? 'True' : 'False') : (is_null($args[0]) ? 'None' : (string)$args[0]);
$startIndex = 1;
if (!is_array($array)) {
continue;
}
$allRows = true;
for ($i = $startIndex; $i < count($args); $i++) {
if (Validator::is1D($args[$i])) {
$rows[] = $args[$i];
} else {
$allRows = false;
break;

// If this is a 2D structure with associative rows, normalize rows to indexed arrays
if (Validator::is2D($array)) {
foreach ($array as $rowIndex => $row) {
if (is_array($row) && !array_is_list($row)) {
$array[$rowIndex] = array_values($row);
}
}
}
if ($allRows && count($rows) > 1) {
$out = Formatter::format2DAligned($rows, $this->precision);
return $start . ((($label !== null) ? ($label . "\n" . $out) : $out)) . $end;
}

$args[$i] = $array;
$this->labels[$i] = (new \ReflectionClass($value))->getShortName();
}
return null;

if (count($args) > self::MAX_ARGS) {
$args = array_slice($args, 0, self::MAX_ARGS);
}
return $args;
}

/**
Expand All @@ -362,7 +298,7 @@ private function tryMultiple1DRows(array $args, string $start, string $end): ?st
private function formatDefaultParts(array $args, array $fmt): array
{
$parts = [];
foreach ($args as $arg) {
foreach ($args as $i => $arg) {
if (is_array($arg)) {
if (Validator::is3D($arg)) {
$parts[] = Formatter::format3DTorch(
Expand All @@ -373,7 +309,7 @@ private function formatDefaultParts(array $args, array $fmt): array
(int)($fmt['tailRows'] ?? 5),
(int)($fmt['headCols'] ?? 5),
(int)($fmt['tailCols'] ?? 5),
(string)($fmt['label'] ?? 'tensor'),
(string)($fmt['label'] ?? ($this->labels[$i] ?? 'tensor')),
$this->precision
);
} elseif (Validator::is2D($arg)) {
Expand All @@ -383,7 +319,7 @@ private function formatDefaultParts(array $args, array $fmt): array
(int)($fmt['tailRows'] ?? 5),
(int)($fmt['headCols'] ?? 5),
(int)($fmt['tailCols'] ?? 5),
(string)($fmt['label'] ?? 'tensor'),
(string)($fmt['label'] ?? ($this->labels[$i] ?? 'tensor')),
$this->precision
);
} else {
Expand Down
42 changes: 42 additions & 0 deletions src/functions.php
Original file line number Diff line number Diff line change
Expand Up @@ -46,3 +46,45 @@ function ppd(...$args)
$exiter();
}

/**
* Print a difference matrix between two arrays.
*
* For each corresponding element, prints the same value when equal,
* or '-' when different or missing.
*
* @param array $a
* @param array $b
* @return string
*/
function pdiff(array $a, array $b): string
{
$rows = max(count($a), count($b));
$diff = [];

for ($i = 0; $i < $rows; $i++) {
$rowA = $a[$i] ?? [];
$rowB = $b[$i] ?? [];

// Handle scalar-vs-scalar rows directly
if (!is_array($rowA) && !is_array($rowB)) {
$diff[] = [$rowA === $rowB ? $rowA : '-'];
continue;
}

// Normalize to row arrays
$rowA = is_array($rowA) ? $rowA : [$rowA];
$rowB = is_array($rowB) ? $rowB : [$rowB];

$cols = max(count($rowA), count($rowB));
$rowDiff = [];
for ($j = 0; $j < $cols; $j++) {
$vA = $rowA[$j] ?? null;
$vB = $rowB[$j] ?? null;
$rowDiff[] = ($vA === $vB) ? $vA : '-';
}
$diff[] = $rowDiff;
}

return pprint($diff);
}

4 changes: 2 additions & 2 deletions tests/FormatterTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -204,7 +204,7 @@ public static function format3DTorchProvider(): array
5, 5, 5, 5, 5, 5,
'tensor',
2,
"tensor([\n [[1, 2],\n [3, 4]],\n\n [[5, 6],\n [7, 8]]\n])",
"tensor([\n [[1, 2],\n [3, 4]],\n [[5, 6],\n [7, 8]]\n])",
],
'block ellipsis with inner 2D ellipses' => [
[
Expand All @@ -215,7 +215,7 @@ public static function format3DTorchProvider(): array
1, 1, 1, 1, 1, 1,
'tensor',
2,
"tensor([\n [[1, ..., 3],\n ...,\n [7, ..., 9]],\n\n ...,\n\n [[1, ..., 3],\n ...,\n [7, ..., 9]]\n])",
"tensor([\n [[1, ..., 3],\n ...,\n [7, ..., 9]],\n ...,\n [[1, ..., 3],\n ...,\n [7, ..., 9]]\n])",
],
];
}
Expand Down
Loading