phpcommon/comparison

用于表示等价关系以及值哈希和排序策略的库。

1.0.0-beta2 2016-07-15 12:48 UTC

This package is auto-updated.

Last update: 2024-09-19 00:27:59 UTC


README

Build Status Code Coverage Scrutinizer Code Quality StyleCI Latest Stable Version Dependency Status

最新版本: 1.0.0-beta

PHP 5.4+ 库,用于表示等价关系以及值哈希和排序策略。

等价关系对于根据特定领域需求比较值以及将自定义比较标准表示为有限上下文中的值非常有用,特别是在集合中使用时。

此库还涵盖了如哈希和排序等补充功能,使它成为您开发工具箱中的宝贵补充。

API 在源代码中有详细的文档。此外,还提供了 HTML 版本,便于在浏览器中查看。

安装

使用 Composer 安装此包

$ composer require phpcommon/comparison

关系

关系是描述集合中元素之间关联的数学工具。关系在计算机科学中广泛使用,尤其是在数据库和调度应用中。

与大多数现代语言不同,PHP 不支持 操作符重载,这是作为设计选择而避免的。换句话说,无法覆盖原生操作符(如等于、相同、大于、小于等)的默认行为。例如,Java 提供了 Comparable 接口,而 Python 提供了一些 魔术方法

在比较主题或上下文根据比较主题或上下文而变化的情况下,这种概念的重要性变得更加明显,以下章节将讨论这一点。

等价关系

在数学中,一个 等价关系 是一个 自反的对称的传递的 二元关系。然而,在计算领域,还必须考虑另一个属性:一致性。一致性意味着关系应该对相同的输入不产生不同的结果。

一个普遍存在的等价关系是任何集合中元素的等价关系。其他例子包括

  • 在所有人的集合中 "有相同的生日"
  • 在所有三角形的集合中 "相似" 或 "全等"
  • 在实数的集合中 "有相同的绝对值"

对于此库的目的,等价关系可以是通用的或类型特定的。类型特定的关系通过实现 EquatableEquivalence 接口来定义,而通用等价关系必须实现最后一个接口。

等价对象

Equatable 接口定义了一个类实现的一般化方法,用于创建确定实例相等性的特定类型的方法。

为了说明,考虑一个名为Money的类,该类旨在表示货币值。这个类是实现Equatable接口的好候选人,因为Money是一个值对象,即这些对象的等价概念不是基于身份。相反,如果两个Money实例具有相同的值,则它们是相等的。因此,虽然Money::USD(5) === Money::USD(5)返回false,但Money::USD(5)->equals(Money::USD(5))返回true

下面是之前提到的类

final class Money implements Equatable
{
    private $amount;
    private $currency;
    
    public function __construct($amount, $currency)
    {
        $this->amount = (int) $amount;
        $this->currency = (string) $currency;
    }
    
    public function equals(Equatable $other)
    {
        if (!$other instanceof self) {
            return false;
        }
        
        return $this->amount === $other->amount && $this->currency === $other->currency; 
    }
}

等价关系

然而,在很多情况下,需要一种非标准或外部的方式来比较两个值。也许,这些自定义关系最明显的用例是与集合一起使用,但它也适用于为标量值或无法自行提供该功能(因为它属于第三方包或内置在PHP中)的现有类提供这些功能。

假设你正在开发一款帮助医院管理血液捐赠的软件。其中一项要求是,护士不能收集具有相同血型的捐赠者的血液。这种情况的关系看起来如下

use PhpCommon\Comparison\Equivalence;

class BloodGroupEquivalence implements Equivalence
{
    public function equals(Equatable $other)
    {
        return get_class($other) === static::class;
    }

    public function equivalent($left, $right)
    {
        if (!$left instanceof Person) {
            UnexpectedTypeException::forType(Person::class, $left);
        }

        if (!$right instanceof Person) {
            return false;
        }

        return $left->getBloodType() === $right->getBloodType();
    }
}

此关系确定两个人是否具有相同的血型

$equivalence = new BloodGroupEquivalence();
$donors = new BloodDonors($equivalence);
$james = new Person('James', 'A');
$john = new Person('John', 'A');

// James and John are considered equivalent once they are of the same blood group
var_dump($equivalence->equivalent($james, $john)); // Outputs bool(true)

// Initially, none of them are present in the collection
var_dump($volunteers->contains($james)); // Outputs bool(false)
var_dump($volunteers->contains($john)); // Outputs bool(false) 

// Add James to the set of volunteers
$donors->add($james);

// Now, considering only the blood group of each donor for equality, both of
// them are considered present in the collection
$donors->contains($james); // Outputs bool(true)
$donors->contains($john); // Outputs bool(true)

由于BloodGroupEquivalence基于人们的血型在人们之间建立等价关系,因此尝试将John添加到集合中将被忽略,因为James已经存在,并且他们具有相同的血型。

一开始这可能看起来有点复杂,但在实际情况下,它可以用来比较兼容血型的等价性,以便将捐赠者分组。

内置等价关系

此库提供了一些通用等价关系,作为标准库的一部分,如下所述。

身份等价

比较两个值的身份。

此关系基于相同的操作符。大多数情况下,如果两个值具有相同的类型和值,则认为它们是等价的,但有少数例外

  • 如果两个字符串具有相同的字符序列、相同的长度和对应位置上的相同字符,则它们是等价的。
  • 如果两个数字数值相等(具有相同的数值),则它们是等价的。
    • 正零和负零是彼此等价的。
    • NAN与任何其他值都不相等,包括它自己。
    • 正无穷和负无穷仅等于它们自己。
  • 如果两个布尔值都为真或都为假,则它们是等价的。
  • 两个不同的对象永远不相等。比较对象的运算符仅在操作数引用相同的实例时才为真。
  • 如果两个数组根据此关系包含等价的条目,并且顺序相同,则它们是等价的。空数组彼此等价。
  • 如果两个资源具有相同的唯一资源编号,则它们是等价的。
  • 空值只等于它自己。

以下表格总结了各种类型操作数的比较方式

值等价

比较两个值的相等性。

值等价的行为与身份等价完全相同,但它将Equatable对象之间的比较委托给被比较的对象。此外,可以指定外部关系来比较特定类型的值。在希望覆盖特定类型的默认行为但保留其他所有行为的情况下,它很有用。它还适用于为属于第三方包或内置在PHP中的类的对象定义关系。

以下规则用于确定两个值是否被认为是等价的

  • 两个字符串如果具有相同的字符序列、相同的长度以及对应位置的相同字符,则它们是等价的。
  • 如果两个数字数值相等(具有相同的数值),则它们是等价的。
    • 正零和负零是彼此等价的。
    • NAN与任何其他值都不相等,包括它自己。
    • 正无穷和负无穷仅等于它们自己。
  • 如果两个布尔值都为真或都为假,则它们是等价的。
  • 两个对象等价的条件如下
    • 两个对象都是 Equatable 的实例,且表达式 $left->equals($right) 评估为 true
    • 一个特定的等价关系映射到左值类型,且表达式 $relation->equivalent($left, $right) 评估为 true
    • 两个值引用了特定命名空间中同一类的同一实例。
  • 如果两个数组根据此关系包含等价的条目,并且顺序相同,则它们是等价的。空数组彼此等价。
  • 如果两个资源具有相同的唯一资源编号,则它们是等价的。
  • 空值只等于它自己。

以下表格总结了各种类型操作数的比较方式

其中,eq() 表示一个递归比较对应条目的函数,根据上述规则。

此关系还提供了一种覆盖特定类等价逻辑的方法,而无需创建新的关系。例如,假设您想根据它们的值比较 \DateTime 实例,但保留其他类型的默认行为。可以通过指定一个自定义关系来完成,该关系将在比较 \DateTime 实例与其他值时使用。

use PhpCommon\Comparison\Hasher\ValueHasher as ValueEquivalence;
use PhpCommon\Comparison\Hasher\DateTimeHasher as DateTimeEquivalence;
use DateTime;

$relation = new ValueEquivalence([
    DateTime::class => new DateTimeEquivalence()
]);

$date = '2017-01-01';
$timezone = new DateTimeZone('Pacific/Nauru');

$left = new DateTime($date, $timezone);
$right = new DateTime($date, $timezone);

// Outputs bool(true)
var_dump($relation->equivalent($left, $right));

语义等价

比较两个值的语义等价。

语义等价计划在未来版本中实现。它将允许比较在语义上相似但类型不同的值。这与 PHP 中松散比较的工作方式相似,但在更严格的条件下,使自反性、对称性和传递性属性成立。

DateTime 值等价

根据日期、时间和时区比较两个 \DateTime 实例。

此关系认为两个 \DateTime 实例如果具有相同的日期、时间和时区,则它们是等价的。

use PhpCommon\Comparison\Hasher\IdentityHasher as IdentityEquivalence;
use PhpCommon\Comparison\Hasher\DateTimeHasher as DateTimeEquivalence;
use DateTime;

$identity = new IdentityEquivalence();
$value = new DateTimeEquivalence();

$date = '2017-01-01';
$timezone = new DateTimeZone('Pacific/Nauru');

$left = new DateTime($date, $timezone);
$right = new DateTime($date, $timezone);

// Outputs bool(false)
var_dump($identity->equivalent($left, $right));

// Outputs bool(true)
var_dump($value->equivalent($left, $right));

哈希

在 PHP 中,数组键只能表示为数字和字符串。然而,在许多情况下,将复杂类型作为键存储是有帮助的。例如,代表不同类型的数字或字符串的类,如 GMP 对象、Unicode 字符串等。能够将这些对象用作数组键将非常方便。

为了填补这一空白,此库引入了 HashableHasher 接口,这些接口指定了为值提供哈希码的协议。这些接口不要求实现者提供完美的哈希函数。也就是说,两个不等价的价值可能具有相同的哈希码。然而,为了确定具有相同哈希码的两个值实际上是否相等,应将哈希和等价的概念以互补的方式结合起来。这解释了为什么 HasherHashable 分别扩展了 EquivalenceEquatable

注意事项

哈希码旨在在基于哈希表的集合中实现高效的插入和查找,以及快速的不等性检查。哈希码不是永久值。因此

  • 不要序列化哈希码值或将它们存储在数据库中。
  • 不要将哈希码用作从键控集合中检索对象的键。
  • 不要在应用域或进程中发送哈希码。在某些情况下,哈希码可能基于每个进程或应用域计算。
  • 如果您需要加密强哈希,不要使用哈希码代替加密哈希函数返回的值。
  • 不要测试哈希码的等价性以确定两个对象是否相等,因为一旦不等值的哈希码可能相同。

Hashable

在可能的情况下,定义适合您需求的自定义哈希逻辑可能是有益的。例如,假设您有一个用于表示 2D 点的点类。

namespace PhpCommon\Comparison\Equatable;

final class Point implements Equatable
{
    private $x;
    private $y;
    
    public function __construct($x, $y)
    {
        $this->x = (int) $x;
        $this->y = (int) $y;
    }
    
    public function equals(Equatable $point)
    {
        if (!$point instanceof Point) {
            return false;
        }
        
        return $this->x === $point->x && $this->y === $point->y;
    }
}

一个 Point 对象包含一个点的 x 和 y 坐标。根据类的定义,如果两个点的坐标相同,则认为它们相等。然而,如果您打算将 Point 实例存储在基于哈希的映射中,例如,因为您想将坐标与标签关联起来,那么您必须确保您的类生成的哈希码与确定两个点是否相等的逻辑保持 一致性

namespace PhpCommon\Comparison\Equatable;

final class Point implements Hashable
{
    private $x;
    private $y;
    
    public function __construct($x, $y)
    {
        $this->x = (int) $x;
        $this->y = (int) $y;
    }
    
    public function equals(Equatable $point)
    {
        if (!$point instanceof Point) {
            return false;
        }
        
        return $this->x === $point->x && $this->y === $point->y;
    }

    public function getHash()
    {
        return 37 * (31 + $this->$x) + $this->$x;
    }
}

因此,getHash() 方法与 equals() 方法保持一致,尽管哈希算法可能不是理想的。生成哈希码的高效算法超出了本指南的范围。然而,建议使用一种快速算法,该算法对于不等值产生合理不同的结果,并将复杂的比较逻辑转移到 Equatable::equals()

请注意,可哈希对象应该是不可变的,或者您需要在使用它们作为基于哈希的结构之后保持谨慎,不要更改它们。

哈希器

哈希器为原始类型以及未实现 Hashable 的类的对象提供哈希功能。

本接口引入的 hash() 方法旨在提供一种进行快速 不等价 检查以及在基于哈希的数据结构中进行高效插入和查找的手段。此方法始终与 equivalent() 保持 一致性,这意味着对于任何引用 $x$y,如果 equivalent($x, $y),则 hash($x) === hash($y)。然而,如果 equivalence($x, $y) 评估为 false,则 hash($x) === hash($y) 可能仍然为真。这就是为什么 hash() 方法适用于 不等价 检查,但不适用于 等价 检查的原因。

本库包含的所有 Equivalence 实现也提供了哈希功能。有关如何对值进行哈希的更多信息,请参阅相应实现的文档。

排序

遵循之前讨论的概念的逻辑,ComparableComparator 分别是提供 自然 和自定义排序策略的接口。这两个接口都指定了一个 全序关系,这是一个 自反的反对称的传递的 关系。

Comparable

此接口强加了一个全序于实现它的每个类的对象。这种排序被称为该类的 自然排序,而 Comparable::compareTo() 方法被称为其 自然比较方法

以下示例展示了如何定义类的实例的自然顺序

use PhpCommon\Comparison\UnexpectedTypeException;

final class BigInteger implements Comparable
{
    private $value;

    public function __construct($value)
    {
        $this->value = (string) $value;
    }

    public function compareTo(Comparable $other)
    {
        if (!$other instanceof self) {
            throw UnexpectedTypeException::forType(BigInteger::class, $other);
        }

        return bccomp($this->value, $other->value);
    }
}

Comparator

Comparator 的目的是允许您定义一个或多个不是类自然比较策略的比较策略。理想情况下,Comparator 必须由一个与定义比较策略的类不同的类实现。如果您想为类定义一个自然比较策略,则可以实施 Comparable

Comparators 可以传递给集合的排序方法,以允许精确控制其排序顺序。它也可以用于控制某些数据结构的顺序,例如排序集合或排序映射。例如,考虑以下根据字符串长度排序的 comparator

use PhpCommon\Comparison\Comparator;

class StringLengthComparator implements Comparator
{ 
    public function compare($left, $right)
    {
        return strlen($left) <=> strlen($right);
    }
}

$comparator = new StringLengthComparator();

// Outputs int(-1)
var_dump($comparator->compare('ab', 'a'));

此实现代表了多种可能的字符串排序方法之一。其他策略包括按字母顺序、字典顺序等排序。

变更日志

有关最近更改的更多信息,请参阅 CHANGELOG

测试

$ composer test

有关更多详细信息,请参阅 测试文档

贡献

欢迎对包的贡献!

  • 请在本 问题跟踪器 上报告您发现的任何错误或问题。
  • 您可以在该软件包的Git仓库中获取源代码。

请参阅CONTRIBUTINGCONDUCT以获取详细信息。

安全

如果您发现任何安全相关的问题,请通过电子邮件marcos@marcospassos.com报告,而不是使用问题跟踪器。

致谢

许可协议

本软件包所有内容均采用MIT许可协议授权。