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.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.3.1 / 2025-11-27
- Adjust default formatting dimensions in PrettyPrint tensor methods
- README.md example updates
Expand Down
75 changes: 75 additions & 0 deletions src/Helper.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
<?php

namespace Apphp\PrettyPrint;

class Helper
{
/**
* 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.
*
* @param mixed $value
* @return bool True if $value is an array where every element is int or float.
*/
public static function is1D(mixed $value): bool
{
if (!is_array($value)) {
return false;
}
foreach ($value as $cell) {
if (!is_scalar($cell) && $cell !== null) {
return false;
}
}
return true;
}

/**
* Determine if the given value is a 2D numeric matrix.
*
* Accepts empty arrays as 2D.
*
* @param mixed $value
* @return bool True if $value is an array of arrays of int|float.
*/
public static function is2D(mixed $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_scalar($cell) && $cell !== null) {
return false;
}
}
}
return true;
}
}
89 changes: 12 additions & 77 deletions src/PrettyPrint.php
Original file line number Diff line number Diff line change
Expand Up @@ -176,7 +176,7 @@ public function __invoke(...$args)
}

// Label + 2D matrix (supports numeric and string matrices)
if (count($args) === 2 && !is_array($args[0]) && is_array($args[1]) && $this->is2D($args[1])) {
if (count($args) === 2 && !is_array($args[0]) && is_array($args[1]) && Helper::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]);
echo $start . ($label . "\n" . $out) . $end;
Expand All @@ -195,7 +195,7 @@ public function __invoke(...$args)
}
$allRows = true;
for ($i = $startIndex; $i < count($args); $i++) {
if ($this->is1D($args[$i])) {
if (Helper::is1D($args[$i])) {
$rows[] = $args[$i];
} else {
$allRows = false;
Expand Down Expand Up @@ -233,7 +233,7 @@ public function __invoke(...$args)
(int)($fmt['tailCols'] ?? 5),
(string)($fmt['label'] ?? 'tensor')
);
} elseif ($this->is2D($arg)) {
} elseif (Helper::is2D($arg)) {
$parts[] = $this->format2DTorch(
$arg,
(int)($fmt['headRows'] ?? 5),
Expand All @@ -245,13 +245,13 @@ public function __invoke(...$args)
} else {
$parts[] = $this->formatForArray($arg);
}
} elseif (!$containsArray) {
} else {
if (is_bool($arg)) {
$parts[] = $arg ? 'True' : 'False';
} elseif (is_null($arg)) {
$parts[] = 'None';
} elseif (is_int($arg) || is_float($arg)) {
$parts[] = $this->formatNumber($arg);
$parts[] = Helper::formatNumber($arg, $this->precision);
} else {
$parts[] = (string)$arg;
}
Expand All @@ -264,74 +264,7 @@ public function __invoke(...$args)

// ---- Private helpers ----

/**
* 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
* @return string
*/
private function formatNumber($v): string
{
if (is_int($v)) {
return (string)$v;
}
if (is_float($v)) {
return number_format($v, $this->precision, '.', '');
}
return (string)$v;
}

/**
* Determine if the given value is a 1D array of numeric scalars.
*
* @param mixed $value
* @return bool True if $value is an array where every element is int or float.
*/
private function is1D($value): bool
{
if (!is_array($value)) {
return false;
}
foreach ($value as $cell) {
if (!is_int($cell) && !is_float($cell)) {
return false;
}
}
return true;
}

/**
* Determine if the given value is a 2D numeric matrix.
*
* Accepts empty arrays as 2D.
*
* @param mixed $value
* @return bool True if $value is an array of arrays of int|float.
*/
private function is2D($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) && !is_string($cell)) {
return false;
}
}
}
return true;
}

// TODO: >>>>>>>>
/**
* Determine if the given value is a 3D tensor of numeric matrices.
*
Expand Down Expand Up @@ -406,7 +339,7 @@ private function format2DAligned(array $matrix): string
if (array_key_exists($c, $row)) {
$cell = $row[$c];
if (is_int($cell) || is_float($cell)) {
$s = $this->formatNumber($cell);
$s = Helper::formatNumber($cell, $this->precision);
} elseif (is_string($cell)) {
$s = "'" . addslashes($cell) . "'";
} elseif (is_bool($cell)) {
Expand Down Expand Up @@ -501,7 +434,7 @@ private function format2DSummarized(array $matrix, int $headRows = 5, int $tailR
} elseif (isset($matrix[$rIndex][$pos])) {
$cell = $matrix[$rIndex][$pos];
if (is_int($cell) || is_float($cell)) {
$s = $this->formatNumber($cell);
$s = Helper::formatNumber($cell, $this->precision);
} elseif (is_string($cell)) {
$s = "'" . addslashes($cell) . "'";
} elseif (is_bool($cell)) {
Expand Down Expand Up @@ -563,14 +496,14 @@ private function format2DSummarized(array $matrix, int $headRows = 5, int $tailR
private function formatForArray($value): string
{
if (is_array($value)) {
if ($this->is2D($value)) {
if (Helper::is2D($value)) {
return $this->format2DAligned($value);
}
$formattedItems = array_map(fn ($v) => $this->formatForArray($v), $value);
return '[' . implode(', ', $formattedItems) . ']';
}
if (is_int($value) || is_float($value)) {
return $this->formatNumber($value);
return Helper::formatNumber($value, $this->precision);
}
if (is_bool($value)) {
return $value ? 'True' : 'False';
Expand Down Expand Up @@ -669,3 +602,5 @@ private function format2DTorch(array $matrix, int $headRows = 5, int $tailRows =
return $s;
}
}

// 672/605==
118 changes: 118 additions & 0 deletions tests/HelperTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
<?php

declare(strict_types=1);

namespace Apphp\PrettyPrint\Tests;

use Apphp\PrettyPrint\Helper;
use Apphp\PrettyPrint\PrettyPrint;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\Group;
use PHPUnit\Framework\TestCase;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\Attributes\TestDox;
use PHPUnit\Framework\Attributes\DataProvider;

#[CoversClass(PrettyPrint::class)]
#[Group('Helper')]
final class HelperTest extends TestCase
{
public static function formatNumberProvider(): array
{
return [
'int value ignored precision' => [5, 2, '5'],
'zero int' => [0, 4, '0'],
'positive float with rounding' => [1.23456, 2, '1.23'],
'positive float with padding' => [1.2, 4, '1.2000'],
'negative float rounding' => [-3.14159, 3, '-3.142'],
'string passthrough' => ['abc', 2, 'abc'],
'null' => [null, 2, ''],
'false casts to empty string' => [false, 2, ''],
'true casts to 1' => [true, 2, '1'],
'zero precision rounds halves up' => [2.5, 0, '3'],
'high precision pads zeros' => [1.23, 6, '1.230000'],
'small scientific notation' => [1e-6, 8, '0.00000100'],
'large integer unchanged' => [123456789, 5, '123456789'],
];
}

public static function defaultFormatNumberProvider(): array
{
return [
'int default precision ignored' => [5, '5'],
'zero int default' => [0, '0'],
'float rounds to 2 by default' => [1.23456, '1.23'],
'float pads to 2 by default' => [1.2, '1.20'],
'negative float default rounding' => [-3.14159, '-3.14'],
'string passthrough default' => ['abc', 'abc'],
'null to empty string default' => [null, ''],
'true to 1 default' => [true, '1'],
'false to empty string default' => [false, ''],
'zero float default' => [0.0, '0.00'],
];
}

#[Test]
#[TestDox('formatNumber formats ints, floats (with precision), and falls back to string casting')]
#[DataProvider('formatNumberProvider')]
public function testFormatNumber(mixed $value, int $precision, string $expected): void
{
self::assertSame($expected, Helper::formatNumber($value, $precision));
}

#[Test]
#[TestDox('formatNumber uses default precision when none is provided')]
#[DataProvider('defaultFormatNumberProvider')]
public function testFormatNumberDefaultPrecision(mixed $value, string $expected): void
{
self::assertSame($expected, Helper::formatNumber($value));
}

public static function is1DProvider(): 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],
];
}

#[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, 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));
}
}
14 changes: 1 addition & 13 deletions tests/PrettyPrintTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
use PHPUnit\Framework\Attributes\CoversClass;

#[CoversClass(PrettyPrint::class)]
#[Group('prettyprint')]
#[Group('PrettyPrint')]
final class PrettyPrintTest extends TestCase
{
#[Test]
Expand Down Expand Up @@ -336,16 +336,4 @@ public function multipleRowsWithLabel(): void
$out = ob_get_clean();
self::assertSame("Label\n[[1, 2],\n [3, 4]]\n", $out);
}

#[Test]
#[TestDox('covers fallback branch in formatNumber for non-numeric values')]
public function formatNumberFallbackCoversString(): void
{
$pp = new PrettyPrint();
$caller = \Closure::bind(function ($v) {
return $this->formatNumber($v);
}, $pp, PrettyPrint::class);
$result = $caller('abc');
self::assertSame('abc', $result);
}
}