Skip to content

Commit

Permalink
Support for BCMath\Number objects
Browse files Browse the repository at this point in the history
  • Loading branch information
derrabus committed Jan 6, 2025
1 parent b864c8f commit 73d4b3e
Show file tree
Hide file tree
Showing 4 changed files with 212 additions and 1 deletion.
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ jobs:
- name: Install PHP
uses: shivammathur/setup-php@v2
with:
php-version: 8.3
php-version: 8.4
coverage: none

- name: Install dependencies with Composer
Expand Down
5 changes: 5 additions & 0 deletions phpstan.neon
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
parameters:
level: 10
phpVersion: 80402
paths:
- src
- tests/unit

ignoreErrors:
# see https://github.com/phpstan/phpstan/issues/12099
- '~^Binary operation "[\+\-]" between BcMath\\Number and BcMath\\Number results in an error\.$~'
57 changes: 57 additions & 0 deletions src/NumberComparator.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
<?php declare(strict_types=1);
/*
* This file is part of sebastian/comparator.
*
* (c) Sebastian Bergmann <[email protected]>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace SebastianBergmann\Comparator;

use function is_int;
use function is_numeric;
use function is_string;
use function max;
use function number_format;
use BcMath\Number;

final class NumberComparator extends ObjectComparator
{
public function accepts(mixed $expected, mixed $actual): bool
{
return ($expected instanceof Number || $actual instanceof Number) &&
($expected instanceof Number || is_int($expected) || is_string($expected) && is_numeric($expected)) &&
($actual instanceof Number || is_int($actual) || is_string($actual) && is_numeric($actual));
}

/**
* @param array<mixed> $processed
*
* @throws ComparisonFailure
*/
public function assertEquals(mixed $expected, mixed $actual, float $delta = 0.0, bool $canonicalize = false, bool $ignoreCase = false, array &$processed = []): void
{
if (!$expected instanceof Number) {
assert(is_string($expected) || is_int($expected));

Check warning on line 36 in src/NumberComparator.php

View check run for this annotation

Codecov / codecov/patch

src/NumberComparator.php#L36

Added line #L36 was not covered by tests
$expected = new Number($expected);
}

if (!$actual instanceof Number) {
assert(is_string($actual) || is_int($actual));

Check warning on line 41 in src/NumberComparator.php

View check run for this annotation

Codecov / codecov/patch

src/NumberComparator.php#L41

Added line #L41 was not covered by tests
$actual = new Number($actual);
}

$deltaNumber = new Number(number_format($delta, max($expected->scale, $actual->scale)));

if ($actual < $expected - $deltaNumber || $actual > $expected + $deltaNumber) {
throw new ComparisonFailure(
$expected,
$actual,
(string) $expected,
(string) $actual,
'Failed asserting that two Number objects are equal.',
);
}
}
}
149 changes: 149 additions & 0 deletions tests/unit/NumberComparatorTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
<?php declare(strict_types=1);
/*
* This file is part of sebastian/comparator.
*
* (c) Sebastian Bergmann <[email protected]>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace SebastianBergmann\Comparator;

use BcMath\Number;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\Attributes\RequiresPhp;
use PHPUnit\Framework\Attributes\RequiresPhpExtension;
use PHPUnit\Framework\Attributes\UsesClass;
use PHPUnit\Framework\TestCase;

#[RequiresPhp('8.4')]
#[RequiresPhpExtension('bcmath')]
#[CoversClass(NumberComparator::class)]
#[UsesClass(Comparator::class)]
#[UsesClass(ComparisonFailure::class)]
#[UsesClass(Factory::class)]
final class NumberComparatorTest extends TestCase
{
/**
* @return non-empty-list<array{0: mixed, 1: mixed}>
*/
public static function acceptsSucceedsProvider(): array
{
$number = new Number('13.37');
$anotherNumber = new Number('13');

return [
[$number, $anotherNumber],
[$number, '13'],
[$number, 13],
[$anotherNumber, $number],
['13', $number],
[13, $number],
];
}

/**
* @return non-empty-list<array{0: mixed, 1: mixed}>
*/
public static function acceptsFailsProvider(): array
{
$number = new Number('13.37');

return [
[$number, null],
[$number, 'foo'],
[null, $number],
['foo', $number],
[null, null],
];
}

/**
* @return non-empty-list<array{0: int|Number|numeric-string, 1: int|Number|numeric-string, 2?: float}>
*/
public static function assertEqualsSucceedsProvider(): array
{
return [
[new Number('13.37'), new Number('13.37')],
[new Number('13.37'), new Number('13.370000')],
['13.37', new Number('13.370000')],
['13.37', new Number('13.37')],
['13.370000', new Number('13.37')],
[13, new Number('13')],
[new Number('13.37'), new Number('13.38'), .1],
['13.37', new Number('13.38'), .1],
[13, new Number('13.38'), 1],
[new Number('47.11'), new Number('47.11'), .00001],
];
}

/**
* @return non-empty-list<array{0: int|Number|numeric-string, 1: int|Number|numeric-string, 2?: float}>
*/
public static function assertEqualsFailsProvider(): array
{
return [
[new Number('13'), new Number('13.37')],
['13', new Number('13.37')],
[13, new Number('13.37')],
[new Number('13'), new Number('13.37'), .1],
['13', new Number('13.37'), .1],
[13, new Number('13.37'), .1],
[new Number('47.11'), new Number('47.12'), .00001],
];
}

public function testAcceptsSucceeds(): void
{
$this->assertTrue(
(new NumberComparator)->accepts(
new Number('1'),
new Number('2'),
),
);
}

#[DataProvider('acceptsFailsProvider')]
public function testAcceptsFails(mixed $expected, mixed $actual): void
{
$this->assertFalse(
(new NumberComparator)->accepts($expected, $actual),
);
}

#[DataProvider('assertEqualsSucceedsProvider')]
public function testAssertEqualsSucceeds(int|Number|string $expected, int|Number|string $actual, float $delta = 0.0): void
{
$exception = null;

try {
(new NumberComparator)->assertEquals($expected, $actual, $delta);
} catch (ComparisonFailure $exception) {
}

$this->assertNull($exception, 'Unexpected ComparisonFailure');
}

#[DataProvider('assertEqualsSucceedsProvider')]
public function testAssertEqualsCanBeInverted(int|Number|string $actual, int|Number|string $expected, float $delta = 0.0): void
{
$exception = null;

try {
(new NumberComparator)->assertEquals($expected, $actual, $delta);
} catch (ComparisonFailure $exception) {
}

$this->assertNull($exception, 'Unexpected ComparisonFailure');
}

#[DataProvider('assertEqualsFailsProvider')]
public function testAssertEqualsFails(int|Number|string $expected, int|Number|string $actual, float $delta = 0.0): void
{
$this->expectException(ComparisonFailure::class);
$this->expectExceptionMessage('Failed asserting that two Number objects are equal.');

(new NumberComparator)->assertEquals($expected, $actual, $delta);
}
}

0 comments on commit 73d4b3e

Please sign in to comment.