Since 1981 Vehicle Identification Numbers in North America have followed a standard which provides a check digit to aid in the identification of transcription errors. Reproduced below is my PHP implementation that calculates a checksum and compares it to the check digit.
class VehicleUtilities{ public static function validateVin($vin){ $vin=strtoupper($vin); //17 Chars <> I,O,Q - Last 4 Numeric if(preg_match('/[A-HJ-NPR-Z0-9]{13}[0-9]{4}/', $vin) === 1){ $sum = self::getNumVal($vin[0]) * 8; $sum += self::getNumVal($vin[1]) * 7; $sum += self::getNumVal($vin[2]) * 6; $sum += self::getNumVal($vin[3]) * 5; $sum += self::getNumVal($vin[4]) * 4; $sum += self::getNumVal($vin[5]) * 3; $sum += self::getNumVal($vin[6]) * 2; $sum += self::getNumVal($vin[7]) * 10; $sum += self::getNumVal($vin[9]) * 9; $sum += self::getNumVal($vin[10]) * 8; $sum += self::getNumVal($vin[11]) * 7; $sum += self::getNumVal($vin[12]) * 6; $sum += self::getNumVal($vin[13]) * 5; $sum += self::getNumVal($vin[14]) * 4; $sum += self::getNumVal($vin[15]) * 3; $sum += self::getNumVal($vin[16]) * 2; $checksum = $sum % 11; //Modulus return $checksum == $vin[8] || ($checksum == 10 && $vin[8] == 'X'); } return false; } private static function getNumVal($char){ if(in_array($char, ['A', 'J', '1'])) {return 1;} elseif(in_array($char, ['B', 'K', 'S', '2'])) {return 2;} elseif(in_array($char, ['C', 'L', 'T', '3'])) {return 3;} elseif(in_array($char, ['D', 'M', 'U', '4'])) {return 4;} elseif(in_array($char, ['E', 'N', 'V', '5'])) {return 5;} elseif(in_array($char, ['F', 'W', '6'])) {return 6;} elseif(in_array($char, ['G', 'P', 'X', '7'])) {return 7;} elseif(in_array($char, ['H', 'Y', '8'])) {return 8;} elseif(in_array($char, ['R', 'Z', '9'])) {return 9;} elseif($char == 0) {return 0;} } }
Author’s Note: The code above has been revised to replace the deprecated eregi function.
The code begins by converting the string to uppercase and checking that the passed value matches a regex looking for exactly 13 uppercase alphanumeric characters, excluding the letters I, O and Q, followed by exactly 4 digits. On success the code proceeds to implement the weighted checksum algorithm using the private helper function getNumVal($char)
as a lookup table. Note that the 9th character is the checksum, and thus excluded from the calculation.
Once the total is calculated it is divided by 11 and its remainder becomes the checksum, with ‘X’ representing 10. It should be obvious that the mod 11 operation provides limited protection against errors since there is a 1 in 11 chance an invalid VIN code will pass the test. Nevertheless, the algorithm has saved countless invalid VIN codes from making into my databases.
As shown below, it is important to do some cursory error checking in the web form before passing the input to this method. This greatly assists the user in determining the most likely causes of error.
//Validate VIN if(empty($VIN)) $errors['VIN']="VIN cannot be blank"; else{ $VIN=strtoupper($VIN); if(strlen($VIN) == 17){ if(preg_match('/[A-HJ-NPR-Z0-9]{13}[0-9]{4}/',$VIN)){ if(!VehicleUtilities::validateVin($VIN)) $errors['VIN']='Invalid VIN Code.'; } else $errors['VIN']='Letters I, O, Q invalid / Last 4 must be numeric.'; } else $errors['VIN']='Requires exactly 17 characters.'; }
This is fantastic. Thanks for the great tutorial and working code. I was able to add it to my small project seamlessly.