83 lines
3.0 KiB
PHP
83 lines
3.0 KiB
PHP
<?php
|
|
|
|
namespace App\Domains\Purchase\Services;
|
|
|
|
/**
|
|
* Measures likely DNI typing errors using the optimal-string-alignment
|
|
* variant of the Damerau-Levenshtein distance.
|
|
*
|
|
* The returned value is the minimum number of single-character edits needed
|
|
* to transform one DNI into the other. Supported edits are insertion,
|
|
* deletion, substitution and transposition of two adjacent digits.
|
|
*/
|
|
class DniDistanceService
|
|
{
|
|
/**
|
|
* Calculate the edit distance between two normalized DNI strings.
|
|
*
|
|
* Each matrix cell [row][column] stores the minimum edits required to
|
|
* transform the first $row digits of $left into the first $column digits
|
|
* of $right. The bottom-right cell therefore contains the final distance.
|
|
*/
|
|
public function distance(string $left, string $right): int
|
|
{
|
|
$left = $this->normalize($left);
|
|
$right = $this->normalize($right);
|
|
$leftLength = strlen($left);
|
|
$rightLength = strlen($right);
|
|
$matrix = [];
|
|
|
|
// Transforming a prefix into an empty string requires deleting every digit.
|
|
for ($row = 0; $row <= $leftLength; $row++) {
|
|
$matrix[$row] = [$row];
|
|
}
|
|
|
|
// Transforming an empty string into a prefix requires inserting every digit.
|
|
for ($column = 0; $column <= $rightLength; $column++) {
|
|
$matrix[0][$column] = $column;
|
|
}
|
|
|
|
for ($row = 1; $row <= $leftLength; $row++) {
|
|
for ($column = 1; $column <= $rightLength; $column++) {
|
|
$substitutionCost = $left[$row - 1] === $right[$column - 1] ? 0 : 1;
|
|
$deletionDistance = $matrix[$row - 1][$column] + 1;
|
|
$insertionDistance = $matrix[$row][$column - 1] + 1;
|
|
$substitutionDistance = $matrix[$row - 1][$column - 1] + $substitutionCost;
|
|
|
|
// Keep the cheapest way to align the two prefixes at this position.
|
|
$matrix[$row][$column] = min(
|
|
$deletionDistance,
|
|
$insertionDistance,
|
|
$substitutionDistance,
|
|
);
|
|
|
|
// Count two adjacent inverted digits as one edit instead of two substitutions.
|
|
if (
|
|
$row > 1
|
|
&& $column > 1
|
|
&& $left[$row - 1] === $right[$column - 2]
|
|
&& $left[$row - 2] === $right[$column - 1]
|
|
) {
|
|
$matrix[$row][$column] = min(
|
|
$matrix[$row][$column],
|
|
$matrix[$row - 2][$column - 2] + 1,
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
return $matrix[$leftLength][$rightLength];
|
|
}
|
|
|
|
/**
|
|
* Keep only digits and left-pad seven-digit DNIs so comparisons preserve
|
|
* the leading zero that is present when the DNI is extracted from a CUIT.
|
|
*/
|
|
public function normalize(string $dni): string
|
|
{
|
|
$digits = preg_replace('/\D+/', '', $dni) ?? '';
|
|
|
|
return str_pad($digits, 8, '0', STR_PAD_LEFT);
|
|
}
|
|
}
|