I'm working on implementing a Mod-30 check digit algorithm in PHP for validating identifiers, similar to what's used in systems like OpenMRS. However, I'm encountering issues with the algorithm producing incorrect results.
The Mod-30 algorithm is supposed to calculate a check digit based on a given identifier string, using a character set of "0123456789ACDEFGHJKLMNPRTUVWXY" and a set of weights applied to each character. The correct implementation should return the expected check digit for identifiers.
Here's the PHP code I've written:
function generateMod30CheckDigit($identifier) {
// Define the character set for Mod-30 algorithm
$charset = "0123456789ACDEFGHJKLMNPRTUVWXY";
// Initialize checksum
$checksum = 0;
// Reverse the identifier for easier processing
$identifier = strrev($identifier);
// Define weights for Mod-30 algorithm (starting from rightmost digit)
$weights = [2, 3, 4, 5, 6, 7]; // Adjusted weights
// Iterate over the identifier
for ($i = 0; $i < strlen($identifier); $i++) {
$char = $identifier[$i];
// Convert character to its position in the charset
$position = strpos($charset, strtoupper($char));
// Multiply the value by its weight
$position *= $weights[$i % count($weights)];
// Add the weighted value to the checksum
$checksum += $position;
}
// Calculate the check digit position (30 - remainder of checksum / 30)
$checkDigitPosition = (30 - ($checksum % 30)) % 30;
// Determine the check digit from the character set
$checkDigit = $charset[$checkDigitPosition];
return $checkDigit;
}
// Example usage:
$identifier1 = "10005";
$checkDigit1 = generateMod30CheckDigit($identifier1);
echo "Check digit for identifier $identifier1 is: $checkDigit1 <br>";
$identifier2 = "139MT";
$checkDigit2 = generateMod30CheckDigit($identifier2);
echo "Check digit for identifier $identifier2 is: $checkDigit2";
I have also tried to follow what is available here Invalid check digit for identifier : While create a new patient?
For example, for the identifier "10005", the expected check digit is "K", but the algorithm is returning "F".
I've reviewed the algorithm and character set multiple times, but I can't seem to identify where the issue lies. Can someone please help me identify the problem with my implementation and suggest a fix?
Additional Context:
Any insights or assistance would be greatly appreciated. Thank you!
I didn't find where you got the weights from (2, 3, 4, 5, 6, 7). I looked at the code available for openmrs-module-idgen, and there only two weights are used: 2 and 1. This is also in line with what wiki.openmrs.org explains about the weights:
Work right-to-left, [...] and doubling every other digit.
Secondly, instead of just adding $position
to the checksum, it adds the equivalent of intdiv($position, 30) + $position % 30
to it. This is to cover for the case that $position
has two "digits" in 30-base notation, in which case those digits should be added separately.
When I apply those two changes to your script, it produces the desired outputs for the two examples you've given:
function generateMod30CheckDigit($identifier) {
$charset = "0123456789ACDEFGHJKLMNPRTUVWXY";
$checksum = 0;
$identifier = strrev($identifier);
$weights = [2, 1]; // Weights changed
for ($i = 0; $i < strlen($identifier); $i++) {
$char = $identifier[$i];
$position = strpos($charset, strtoupper($char));
$position *= $weights[$i % count($weights)];
$checksum += intdiv($position, 30) + $position % 30; // Modified formula
}
$checkDigitPosition = (30 - ($checksum % 30)) % 30;
$checkDigit = $charset[$checkDigitPosition];
return $checkDigit;
}