Skip to content

Commit

Permalink
Added Nette\Utils\Floats class for comparing floats (#221)
Browse files Browse the repository at this point in the history
  • Loading branch information
mesour committed Sep 3, 2020
1 parent 35fa415 commit 960bc90
Show file tree
Hide file tree
Showing 9 changed files with 365 additions and 0 deletions.
109 changes: 109 additions & 0 deletions src/Utils/Floats.php
@@ -0,0 +1,109 @@
<?php

/**
* This file is part of the Nette Framework (https://nette.org)
* Copyright (c) 2004 David Grudl (https://davidgrudl.com)
*/

declare(strict_types=1);

namespace Nette\Utils;

use Nette;


/**
* Floats compare tools library.
*/
class Floats
{
use Nette\StaticClass;

/** @var float */
private const EPSILON = 1e-10;


public static function isZero(float $value): bool
{
return abs($value) < self::EPSILON;
}


public static function isInteger(float $value): bool
{
return abs(round($value) - $value) < self::EPSILON;
}


/**
* Compare two floats. If $a < $b it returns -1, if they are equal it returns 0 and if $a > $b it returns 1
* @throws \LogicException if one of parameters is NAN
*/
public static function compare(float $a, float $b): int
{
if (is_nan($a) || is_nan($b)) {
throw new \LogicException('Trying to compare NAN');

} elseif (!is_finite($a) && !is_finite($b) && $a === $b) {
return 0;

} else {
$diff = abs($a - $b);
if (($diff < self::EPSILON || ($diff / max(abs($a), abs($b)) < self::EPSILON))) {
return 0;
}
}

return $a < $b ? -1 : 1;
}


/**
* Returns true if $a = $b
* @throws \LogicException if one or both parameters are NAN
*/
public static function areEqual(float $a, float $b): bool
{
return self::compare($a, $b) === 0;
}


/**
* Returns true if $a < $b
* @throws \LogicException if one or both parameters are NAN
*/
public static function isLessThan(float $a, float $b): bool
{
return self::compare($a, $b) < 0;
}


/**
* Returns true if $a <= $b
* @throws \LogicException if one or both parameters are NAN
*/
public static function isLessThanOrEqualTo(float $a, float $b): bool
{
return self::compare($a, $b) <= 0;
}


/**
* Returns true if $a > $b
* @throws \LogicException if one or both parameters are NAN
*/
public static function isGreaterThan(float $a, float $b): bool
{
return self::compare($a, $b) > 0;
}


/**
* Returns true if $a >= $b
* @throws \LogicException if one or both parameters are NAN
*/
public static function isGreaterThanOrEqualTo(float $a, float $b): bool
{
return self::compare($a, $b) >= 0;
}
}
41 changes: 41 additions & 0 deletions tests/Utils/Floats.areEqual().phpt
@@ -0,0 +1,41 @@
<?php

/**
* Test: Nette\Utils\Floats::areEqual()
*/

declare(strict_types=1);

use Nette\Utils\Floats;
use Tester\Assert;


require __DIR__ . '/../bootstrap.php';


Assert::true(Floats::areEqual(9, 9));
Assert::true(Floats::areEqual(9, 9.0));
Assert::true(Floats::areEqual(3.0, 3));
Assert::true(Floats::areEqual(0.0, 0));
Assert::true(Floats::areEqual(0.0, 0.0));
Assert::true(Floats::areEqual(0.1 + 0.2, 0.3));
Assert::true(Floats::areEqual(0.1 - 0.5, -0.4));
Assert::false(Floats::areEqual(0.0, 5));
Assert::false(Floats::areEqual(-5, 5));
Assert::false(Floats::areEqual(0.001, 0.01));

$float1 = 1 / 3;
$float2 = 1 - 2 / 3;
Assert::true(Floats::areEqual($float1, $float2));
Assert::true(Floats::areEqual($float1 * 1e9, $float2 * 1e9));
Assert::true(Floats::areEqual($float1 - $float2, 0.0));
Assert::true(Floats::areEqual($float1 - $float2 + 123, $float2 - $float1 + 123));
Assert::true(Floats::areEqual($float1 - $float2, $float2 - $float1));

Assert::true(Floats::areEqual(INF, INF));
Assert::false(Floats::areEqual(INF, -INF));
Assert::false(Floats::areEqual(-INF, INF));

Assert::exception(function () {
Floats::areEqual(NAN, NAN);
}, \LogicException::class);
37 changes: 37 additions & 0 deletions tests/Utils/Floats.compare().phpt
@@ -0,0 +1,37 @@
<?php

/**
* Test: Nette\Utils\Floats::compare()
*/

declare(strict_types=1);

use Nette\Utils\Floats;
use Tester\Assert;


require __DIR__ . '/../bootstrap.php';


Assert::same(0, Floats::compare(0, 0));
Assert::same(0, Floats::compare(0.0, 0));
Assert::same(0, Floats::compare(0, 0x0));
Assert::same(1, Floats::compare(0, -25.7));
Assert::same(-1, Floats::compare(-2, 30.7));
Assert::same(1, Floats::compare(0.0, -5));
Assert::same(1, Floats::compare(20, 10));
Assert::same(-1, Floats::compare(20, 30));
Assert::same(1, Floats::compare(-20, -30));
Assert::same(-1, Floats::compare(-50, -30));

Assert::exception(function () {
Floats::compare(NAN, -30);
}, \LogicException::class);

Assert::exception(function () {
Floats::compare(6, NAN);
}, \LogicException::class);

Assert::exception(function () {
Floats::compare(NAN, NAN);
}, \LogicException::class);
32 changes: 32 additions & 0 deletions tests/Utils/Floats.isGreaterThan().phpt
@@ -0,0 +1,32 @@
<?php

/**
* Test: Nette\Utils\Floats::isGreaterThan()
*/

declare(strict_types=1);

use Nette\Utils\Floats;
use Tester\Assert;


require __DIR__ . '/../bootstrap.php';


Assert::false(Floats::isGreaterThan(-9, 9));
Assert::false(Floats::isGreaterThan(-9.7, 0.0));
Assert::false(Floats::isGreaterThan(10, 150));
Assert::false(Floats::isGreaterThan(0, 0.0));
Assert::false(Floats::isGreaterThan(10, 10));
Assert::false(Floats::isGreaterThan(-50, -50));
Assert::true(Floats::isGreaterThan(170, 150));
Assert::true(Floats::isGreaterThan(170.879, -20));
Assert::true(Floats::isGreaterThan(11.879, 0.0));

Assert::true(Floats::isGreaterThan(INF, -INF));
Assert::false(Floats::isGreaterThan(INF, INF));
Assert::false(Floats::isGreaterThan(-INF, INF));

Assert::exception(function () {
Floats::isGreaterThan(NAN, NAN);
}, \LogicException::class);
32 changes: 32 additions & 0 deletions tests/Utils/Floats.isGreaterThanOrEqualTo().phpt
@@ -0,0 +1,32 @@
<?php

/**
* Test: Nette\Utils\Floats::isGreaterOrEqualThan()
*/

declare(strict_types=1);

use Nette\Utils\Floats;
use Tester\Assert;


require __DIR__ . '/../bootstrap.php';


Assert::false(Floats::isGreaterThanOrEqualTo(-9, 9));
Assert::false(Floats::isGreaterThanOrEqualTo(-9.7, 0.0));
Assert::false(Floats::isGreaterThanOrEqualTo(10, 150));
Assert::true(Floats::isGreaterThanOrEqualTo(0, 0.0));
Assert::true(Floats::isGreaterThanOrEqualTo(10, 10));
Assert::true(Floats::isGreaterThanOrEqualTo(-50, -50));
Assert::true(Floats::isGreaterThanOrEqualTo(170, 150));
Assert::true(Floats::isGreaterThanOrEqualTo(170.879, -20));
Assert::true(Floats::isGreaterThanOrEqualTo(11.879, 0.0));

Assert::true(Floats::isGreaterThanOrEqualTo(INF, INF));
Assert::true(Floats::isGreaterThanOrEqualTo(INF, -INF));
Assert::false(Floats::isGreaterThanOrEqualTo(-INF, INF));

Assert::exception(function () {
Floats::isGreaterThanOrEqualTo(NAN, NAN);
}, \LogicException::class);
25 changes: 25 additions & 0 deletions tests/Utils/Floats.isInteger().phpt
@@ -0,0 +1,25 @@
<?php

/**
* Test: Nette\Utils\Floats::isInteger()
*/

declare(strict_types=1);

use Nette\Utils\Floats;
use Tester\Assert;


require __DIR__ . '/../bootstrap.php';


Assert::true(Floats::isInteger(0));
Assert::true(Floats::isInteger(0.0));
Assert::true(Floats::isInteger(5.0));
Assert::true(Floats::isInteger(-5.0));
Assert::true(Floats::isInteger(-5));
Assert::true(Floats::isInteger((1 - (0.1 + 0.2)) * 10));
Assert::false(Floats::isInteger(-5.1));
Assert::false(Floats::isInteger(0.000001));
Assert::false(Floats::isInteger(NAN));
Assert::false(Floats::isInteger(INF));
32 changes: 32 additions & 0 deletions tests/Utils/Floats.isLessThan().phpt
@@ -0,0 +1,32 @@
<?php

/**
* Test: Nette\Utils\Floats::isLowerThan()
*/

declare(strict_types=1);

use Nette\Utils\Floats;
use Tester\Assert;


require __DIR__ . '/../bootstrap.php';


Assert::true(Floats::isLessThan(-9, 9));
Assert::true(Floats::isLessThan(-9.7, 0.0));
Assert::true(Floats::isLessThan(10, 150));
Assert::false(Floats::isLessThan(0, 0.0));
Assert::false(Floats::isLessThan(10, 10));
Assert::false(Floats::isLessThan(-50, -50));
Assert::false(Floats::isLessThan(170, 150));
Assert::false(Floats::isLessThan(170.879, -20));
Assert::false(Floats::isLessThan(11.879, 0.0));

Assert::true(Floats::isLessThan(-INF, INF));
Assert::false(Floats::isLessThan(INF, INF));
Assert::false(Floats::isLessThan(INF, -INF));

Assert::exception(function () {
Floats::isLessThan(NAN, NAN);
}, \LogicException::class);
32 changes: 32 additions & 0 deletions tests/Utils/Floats.isLessThanOrEqualTo().phpt
@@ -0,0 +1,32 @@
<?php

/**
* Test: Nette\Utils\Floats::isLowerOrEqualThan()
*/

declare(strict_types=1);

use Nette\Utils\Floats;
use Tester\Assert;


require __DIR__ . '/../bootstrap.php';


Assert::true(Floats::isLessThanOrEqualTo(-9, 9));
Assert::true(Floats::isLessThanOrEqualTo(-9.7, 0.0));
Assert::true(Floats::isLessThanOrEqualTo(10, 150));
Assert::true(Floats::isLessThanOrEqualTo(0, 0.0));
Assert::true(Floats::isLessThanOrEqualTo(10, 10));
Assert::true(Floats::isLessThanOrEqualTo(-50, -50));
Assert::false(Floats::isLessThanOrEqualTo(170, 150));
Assert::false(Floats::isLessThanOrEqualTo(170.879, -20));
Assert::false(Floats::isLessThanOrEqualTo(11.879, 0.0));

Assert::true(Floats::isLessThanOrEqualTo(-INF, INF));
Assert::true(Floats::isLessThanOrEqualTo(INF, INF));
Assert::false(Floats::isLessThanOrEqualTo(INF, -INF));

Assert::exception(function () {
Floats::isLessThanOrEqualTo(NAN, NAN);
}, \LogicException::class);
25 changes: 25 additions & 0 deletions tests/Utils/Floats.isZero().phpt
@@ -0,0 +1,25 @@
<?php

/**
* Test: Nette\Utils\Floats::isZero()
*/

declare(strict_types=1);

use Nette\Utils\Floats;
use Tester\Assert;


require __DIR__ . '/../bootstrap.php';


Assert::true(Floats::isZero(0));
Assert::true(Floats::isZero(0.0));
Assert::true(Floats::isZero(0x0));
Assert::false(Floats::isZero(-12.5));
Assert::false(Floats::isZero(0.2));
Assert::false(Floats::isZero(20));
Assert::false(Floats::isZero(-2));
Assert::false(Floats::isZero(0x5));
Assert::false(Floats::isZero(INF));
Assert::false(Floats::isZero(NAN));

0 comments on commit 960bc90

Please sign in to comment.