Última versión: 1.0.0-beta
Biblioteca PHP 5.4+ para representar relaciones de equivalencia y estrategias para hash y clasificación de valores.
Las relaciones de equivalencia son útiles para establecer una forma generalizada de comparar valores con respecto a requisitos específicos del dominio, así como para representar criterios personalizados para comparar valores en contextos acotados, especialmente para su uso en colecciones.
Esta biblioteca también cubre capacidades complementarias, como hash y sorting, lo que la convierte en una valiosa adición a su conjunto de herramientas de desarrollo.
La API está ampliamente documentada en el código fuente. Además, también está disponible una versión HTML para una visualización más cómoda en el navegador.
Utilice Composer para instalar el paquete:
$ composer require phpcommon/comparison
Una relación es una herramienta matemática para describir asociaciones entre elementos de conjuntos. Las relaciones se utilizan ampliamente en informática, especialmente en bases de datos y aplicaciones de programación.
A diferencia de la mayoría de los lenguajes modernos, PHP no admite la sobrecarga de operadores, algo que históricamente se ha evitado como opción de diseño. En otras palabras, no es posible anular el comportamiento predeterminado de los operadores nativos, como igual, idéntico, mayor que, menor que, etc. Por ejemplo, Java proporciona la interfaz Comparable, mientras que Python proporciona algunos métodos mágicos.
La importancia de tal concepto se vuelve más evidente en situaciones donde la noción de equivalencia u ordenamiento varía según el tema de comparación o el contexto, como se analiza en las siguientes secciones.
En matemáticas, una relación de equivalencia es una relación binaria que es reflexiva , simétrica y transitiva . En el ámbito informático, sin embargo, hay otra propiedad que hay que tener en cuenta: la coherencia . Consistencia significa que una relación no debe producir resultados diferentes para la misma entrada.
Una relación de equivalencia ubicua es la relación de igualdad entre elementos de cualquier conjunto. Otros ejemplos incluyen:
A los efectos de esta biblioteca, una relación de equivalencia puede ser genérica o específica de tipo. Las relaciones específicas de tipo se definen implementando interfaces Equatable
o Equivalence
, mientras que las equivalencias genéricas deben implementar la última.
La interfaz Equatable
define un método generalizado que una clase implementa para crear un método específico de tipo para determinar la igualdad de instancias.
Para ilustrar, consideremos una clase Money
, cuyo objetivo es representar valores monetarios. Esta clase es una buena candidata para implementar la interfaz Equatable
, porque Money
es un objeto de valor, es decir, la noción de igualdad de esos objetos no se basa en la identidad. En cambio, dos instancias de Money
son iguales si tienen los mismos valores. Por lo tanto, mientras Money::USD(5) === Money::USD(5)
devuelve false
, Money::USD(5)->equals(Money::USD(5))
devuelve true
.
Aquí está la clase mencionada anteriormente:
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 ;
}
}
Sin embargo, hay muchos casos en los que es necesario contar con una forma no estándar o externa para comparar dos valores. Quizás, el caso de uso más obvio para esas relaciones personalizadas es su uso con colecciones, pero también es útil para proporcionar esas capacidades a valores escalares o a una clase existente que no puede proporcionarlas por sí misma, porque pertenece a un paquete de terceros o está integrado. en PHP.
Supongamos que está desarrollando un software para ayudar a los hospitales a gestionar las donaciones de sangre. Uno de los requisitos dice que una enfermera no puede recolectar sangre de donantes que tengan el mismo tipo de sangre. Una relación para este escenario se vería así:
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 ();
}
}
Esta relación determina si dos personas son del mismo grupo sanguíneo:
$ 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)
Dado que BloodGroupEquivalence
establece una relación de equivalencia entre personas según su grupo sanguíneo, cualquier intento de agregar a John a la colección será ignorado, porque James ya está presente y son del mismo tipo de sangre.
Puede parecer un poco complicado para un requisito simple al principio, pero en casos reales puede usarse para comparar la equivalencia entre tipos de sangre compatibles, con el fin de dividir a los donantes en grupos.
Esta biblioteca proporciona algunas relaciones de equivalencia genéricas como parte de la biblioteca estándar, como se describe a continuación.
Compara dos valores de identidad.
Esta relación se basa en el operador idéntico. En la mayoría de los casos, dos valores se consideran equivalentes si tienen el mismo tipo y valor, pero existen algunas excepciones:
NAN
no es igual a ningún otro valor, incluido él mismo.La siguiente tabla resume cómo se comparan los operandos de los distintos tipos:
$A $B | NULO | booleano | Entero | Flotar | Cadena | Recurso | Formación | Objeto |
---|---|---|---|---|---|---|---|---|
NULO | true | false | false | false | false | false | false | false |
booleano | false | $A === $B | false | false | false | false | false | false |
Entero | false | false | $A === $B | false | false | false | false | false |
Flotar | false | false | false | $A === $B | false | false | false | false |
Cadena | false | false | false | false | $A === $B | false | false | false |
Recurso | false | false | false | false | false | $A === $B | false | false |
Formación | false | false | false | false | false | false | $A === $B | false |
Objeto | false | false | false | false | false | false | false | $A === $B |
Compara dos valores para determinar la igualdad.
La equivalencia de valor se comporta exactamente como la equivalencia de identidad, excepto que delega la comparación entre objetos Equatable
a los objetos que se comparan. Además, se pueden especificar relaciones externas para comparar valores de un tipo particular. Es útil en los casos en los que es deseable anular el comportamiento predeterminado para un tipo específico, pero conservar todos los demás. También es útil para definir una relación para objetos de clases que pertenecen a paquetes de terceros o están integrados en PHP.
Las siguientes reglas se utilizan para determinar si dos valores se consideran equivalentes:
NAN
no es igual a ningún otro valor, incluido él mismo.Equatable
y la expresión $left->equals($right)
se evalúa como true
.$relation->equivalent($left, $right)
se evalúa como true
.La siguiente tabla resume cómo se comparan los operandos de los distintos tipos:
$A $B | NULO | booleano | Entero | Flotar | Cadena | Recurso | Formación | Objeto | equiparable |
---|---|---|---|---|---|---|---|---|---|
NULO | true | false | false | false | false | false | false | false | false |
booleano | false | $A === $B | false | false | false | false | false | false | false |
Entero | false | false | $A === $B | false | false | false | false | false | false |
Flotar | false | false | false | $A === $B | false | false | false | false | false |
Cadena | false | false | false | false | $A === $B | false | false | false | false |
Recurso | false | false | false | false | false | $A === $B | false | false | false |
Formación | false | false | false | false | false | false | eq($A, $B) | false | false |
Objeto | false | false | false | false | false | false | false | $A === $B | false |
equiparable | false | false | false | false | false | false | false | false | $A‑>equals($B) |
Donde eq()
denota una función que compara cada par de entradas correspondientes de forma recursiva, de acuerdo con las reglas descritas anteriormente.
Esta relación también proporciona una manera de anular la lógica de equivalencia para una clase particular sin la necesidad de crear una nueva relación. Por ejemplo, supongamos que desea comparar instancias de DateTime
en función de sus valores, pero mantener el comportamiento predeterminado para los otros tipos. Se puede lograr especificando una relación personalizada que se utilizará cada vez que se compare una instancia de DateTime
con otro valor:
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 ));
Compara dos valores para determinar la igualdad semántica.
Está prevista una equivalencia semántica para futuras versiones. Permitiría la comparación de valores que parecen semánticamente similares, incluso si son de diferentes tipos. Es similar a cómo funciona la comparación flexible en PHP, pero bajo condiciones más restrictivas, de tal manera que se mantienen las propiedades de reflexividad, simetría y transitividad.
Compara dos instancias DateTime
según su fecha, hora y zona horaria.
Esta relación considera que dos instancias de DateTime
son equivalentes si tienen la misma fecha, hora y zona horaria:
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 ));
En PHP, las claves de una matriz solo se pueden representar como números y cadenas. Sin embargo, hay varios casos en los que resulta útil almacenar tipos complejos como claves. Tomemos como ejemplo clases que representan diferentes tipos de números o cadenas, como objetos GMP, cadenas Unicode, etc. También sería conveniente poder utilizar dichos objetos como claves de matriz.
Para llenar este vacío, esta biblioteca presenta las interfaces Hashable
y Hasher
, que especifican un protocolo para proporcionar códigos hash para valores. Estas interfaces no requieren que los implementadores proporcionen funciones hash perfectas. Es decir, dos valores que no son equivalentes pueden tener el mismo código hash. Sin embargo, para determinar si dos valores con el mismo código hash son, en realidad, iguales, se deben combinar los conceptos de hash y equivalencia de forma complementaria. Explica por qué Hasher
y Hashable
extienden Equivalence
y Equatable
respectivamente.
Una palabra de advertencia
Un código hash está diseñado para una inserción y búsqueda eficientes en colecciones que se basan en una tabla hash y para verificaciones rápidas de desigualdad. Un código hash no es un valor permanente. Por esta razón:
- No serialice valores de códigos hash ni los almacene en bases de datos.
- No utilice el código hash como clave para recuperar un objeto de una colección con clave.
- No envíe códigos hash entre dominios o procesos de aplicaciones. En algunos casos, los códigos hash se pueden calcular por proceso o por dominio de aplicación.
- No utilice el código hash en lugar de un valor devuelto por una función hash criptográfica si necesita un hash criptográficamente sólido.
- No pruebe la igualdad de códigos hash para determinar si dos objetos son iguales, ya que los valores desiguales pueden tener códigos hash idénticos.
Hay casos en los que podría ser conveniente definir una lógica hash personalizada para una clase que se ajuste mejor a sus necesidades. Por ejemplo, supongamos que tiene una clase Punto para representar un punto 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 ;
}
}
Un Point
contiene las coordenadas xey de un punto. Según la definición de la clase, dos puntos se consideran iguales si tienen las mismas coordenadas. Sin embargo, si pretende almacenar instancias de Point
en un mapa basado en hash, por ejemplo, porque desea asociar coordenadas a etiquetas, debe asegurarse de que su clase produzca códigos hash que sean coherentes con la lógica utilizada para determinar cuándo dos Los puntos se consideran iguales:
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 ;
}
}
De esa manera, el método getHash()
funciona de acuerdo con el método equals()
, aunque el algoritmo hash puede no ser ideal. La implementación de algoritmos eficientes para la generación de código hash está fuera del alcance de esta guía. Sin embargo, se recomienda utilizar un algoritmo rápido que produzca resultados razonablemente diferentes para valores desiguales y cambiar la lógica de comparación pesada a Equatable::equals()
.
Tenga en cuenta que los objetos hash deben ser inmutables o debe ejercer disciplina para no cambiarlos después de que se hayan utilizado en estructuras basadas en hash.
Un Hasher proporciona funcionalidad hash para tipos primitivos y objetos de clases que no implementan Hashable
.
El método hash()
introducido por esta interfaz tiene como objetivo proporcionar un medio para realizar comprobaciones rápidas de inequivalencia y una inserción y búsqueda eficientes en estructuras de datos basadas en hash. Este método siempre es coherente con equivalent()
, lo que significa que para cualquier referencia $x
y $y
, si equivalent($x, $y)
, entonces hash($x) === hash($y)
. Sin embargo, si equivalence($x, $y)
se evalúa como false
, hash($x) === hash($y)
aún puede ser verdadero. De ahí que el método hash()
sea adecuado para comprobaciones de inequivalencia , pero no para comprobaciones de equivalencia .
Todas las implementaciones de Equivalence
incluidas en esta biblioteca también proporcionan funcionalidad hash. Puede encontrar más información sobre cómo se procesan los valores en la documentación de la implementación respectiva.
Siguiendo la misma lógica de los conceptos discutidos anteriormente, Comparable
y Comparator
son interfaces para proporcionar estrategias de clasificación naturales y personalizadas, respectivamente. Ambas interfaces especifican una relación de orden total, una relación que es reflexiva , antisimétrica y transitiva .
Esta interfaz impone un ordenamiento total a los objetos de cada clase que la implementa. Este orden se conoce como ordenamiento natural de la clase y el método Comparable::compareTo()
se conoce como su método de comparación natural .
El siguiente ejemplo muestra cómo una clase puede definir el orden natural de sus instancias:
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 );
}
}
El propósito de un Comparator
es permitirle definir una o más estrategias de comparación que no son la estrategia de comparación natural para una clase. Idealmente, un Comparator
debe ser implementado por una clase diferente de aquella para la que define la estrategia de comparación. Si desea definir una estrategia de comparación natural para una clase, puede implementar Comparable
en su lugar.
Los comparadores se pueden pasar a un método de clasificación de una colección para permitir un control preciso sobre su orden de clasificación. También se puede utilizar para controlar el orden de determinadas estructuras de datos, como conjuntos ordenados o mapas ordenados. Por ejemplo, considere el siguiente comparador que ordena cadenas según su longitud:
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 ' ));
Esta implementación representa una de las muchas formas posibles de ordenar cadenas. Otras estrategias incluyen ordenar alfabéticamente, lexicográficamente, etc.
Consulte CHANGELOG para obtener más información sobre los cambios recientes.
$ composer test
Consulte la documentación de prueba para obtener más detalles.
¡Las contribuciones al paquete siempre son bienvenidas!
Consulte CONTRIBUCIÓN y CONDUCTA para obtener más detalles.
Si descubre algún problema relacionado con la seguridad, envíe un correo electrónico a [email protected] en lugar de utilizar el rastreador de problemas.
Todo el contenido de este paquete tiene la licencia MIT.