Page 1 of 1

Validate VIN

Posted: Wed Nov 26, 2014 7:18 am
by jassing
I am working on an inventory system for a car lot. One of the data elements is the car's VIN... Something that could be semi-validated (make,year, as well as overall 'correctness' via a checksum).

however, as the project grew, I was informed that they only wanted to use the last 9 digits (instead of the full 17); so validation became impossible, and pointless.

In case anyone else needs it; here is my "wwip" (was work in progress) of vin validation. Hope someone else can use it.

Code: Select all

EnableExplicit

;- Constants & Enumerations
Enumeration vinResults
	#vin_Valid
	#vin_RegExErr
	#vin_FailChkDigit
	#vin_Invalid
	#vin_InvalidLength
	#vin_InvalidCharacter
EndEnumeration

;{ Notes on VIN
; VIN contains,encoded, these parts: WMI, Maker, Model, Axles, Type, GVWR
; 	1-3 WMI
; 	4-8 VDS Vehicle Attributes (body/etc)
; 	9 Check Digit
; 	10 Year
; 	11 Plant Code
; 	12-17 sequential number 

Enumeration vinWMIParts
	#vinpart_WMI
	#vinpart_Country
	#vinpart_Maker
	#vinpart_Model
	#vinpart_Axles
	#vinpart_Type
	#vinpart_GVWR
EndEnumeration
;}

; RegEx for vin match/extract
#vinRegExMatch$ 	= "^(([A-H,J-N,P-Z,0-9]{9})([A-H,J-N,P,R-T,V-Z,0-9])([A-H,J-N,P-Z,0-9]{7}))$"
#vinRegExExtract$	= "(([A-H,J-N,P-Z,0-9]{9})([A-H,J-N,P,R-T,V-Z,0-9])([A-H,J-N,P-Z,0-9]{7}))"
									; Could use a more presice w/ lookahead, but it's not needed (?=.{17,})(([A-H,J-N,P-Z,0-9]{9})([A-H,J-N,P,R-T,V-Z,0-9])([A-H,J-N,P-Z,0-9]{7}))
									; If you want to exract more "invalid" to look for invalid entries (characters/lengths/etc) you'd need to craft your own extract routine

Macro isdigit(c)
	(FindString("0123456789",c) <> -1)
EndMacro

Procedure VINValidation(cVehicleIdentNumber.s) 
	Protected.i nResult, nCheckDigit, i, nSum
	Protected.s cChar
	Static Dim Weights(16) ; 17 elements
	Static NewMap Transliterations() ; Use of static vars to help speed up subsequent calls. Doesn't help too much; but it does help some.
	Static hRegEx										 ; help a little you are validating a million,etc (Static vs creating/releasing it each call)
	
	If Len(cVehicleIdentNumber) = 17
		If IsRegularExpression(hRegEx)=#False 
			hRegEx = CreateRegularExpression(#PB_Any, #vinRegExMatch$, #PB_RegularExpression_NoCase)
		EndIf 
		If IsRegularExpression(hRegEx)
			If MatchRegularExpression(hRegEx,cVehicleIdentNumber)
				If MapSize( Transliterations() ) = 0 ;{ Load Weights & Transliteration tables
					Restore vinWeights
					For i = 0 To 16
						Read.i weights(i)
					Next
					Restore vinTransliterations
					For i = 1 To 23
						Read.s cChar
						Transliterations(cChar)=0
					Next
					ForEach Transliterations()
						Read.i Transliterations()
					Next
				EndIf ;} End loading weights & transliterations tables
				
				; Start processing... 
				cVehicleIdentNumber = LCase(cVehicleIdentNumber);
				For i = 0 To 16
					cChar = Mid(cVehicleIdentNumber, i+1, 1)
					; add transliterations * weight of their positions To get the sum
					If isdigit( cChar) 
						nSum + ( Val(cChar) * weights(i) );
					Else	
						nSum + ( transliterations(cChar) * weights(i) )
					EndIf
				Next 
				
				nCheckDigit = nSum % 11 ; find checkdigit by taking the mod of the sum
				If(nCheckDigit = 10)  
					nCheckDigit = 0	
				EndIf 
				
				If nCheckDigit = Val(Mid(cVehicleIdentNumber, 9, 1))
					nResult = #vin_Valid
				Else
					nResult = #vin_FailChkDigit
				EndIf
				
			Else
				nResult = #vin_Invalid
			EndIf ; match regex
			;FreeRegularExpression(hRegEx)
		Else
			nResult = #vin_RegExErr
		EndIf
	Else
		nResult = #vin_InvalidLength
	EndIf
	
	ProcedureReturn nResult
EndProcedure

; Named 'potential' because it still needs to be validated ...
Procedure.s ExtractPotentialVIN( cTextSample.s, nIndex.i=0)
	Protected.s cPotentialVIN
	Protected Dim aResults.s(1)
	Static hRegEx
	
	If IsRegularExpression(hRegEx)=#False 
		hRegEx = CreateRegularExpression(#PB_Any,#vinRegExExtract$,#PB_RegularExpression_NoCase)
	EndIf 
	
	If IsRegularExpression(hRegEx)
		If ExtractRegularExpression(hRegEx, cTextSample, aResults()) > nIndex
			cPotentialVIN = aResults(nIndex)
		EndIf
		;FreeArray( aResults() )
	Else
		Debug "Failed to create regex for extract"
	EndIf
	
	;FreeRegularExpression(hRegEx)
	
	ProcedureReturn cPotentialVIN
EndProcedure

DataSection ;{ Note: using data&read vs assigning each array/map individually was marginally slower, so I left it as data as in reality it should only be loaded once per program run.
	vinWeights:
		Data.i 8,7,6,5,4,3,2,10,0,9,8,7,6,5,4,3,2
	vinTransliterations:
		Data.s "a","b","c","d","e","f","g","h","j","k","l","m","n","p","r","s","t","u","v","w","x","y","z"
		Data.i  1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 , 1 , 2 , 3 , 4 , 5 , 7 , 9 , 2 , 3 , 4 , 5 , 6 , 7 , 8 , 9			
EndDataSection ;}

CompilerIf #False ; Data not used, but left here in case it helps someone else
; Personally.... I would load this into an sql database if you plan on using this on a regular basis
DataSection ; Read group at at time (2 or 3 items) and blank = end of data
	vinWMI:;{  code,country,company
		Data.s "1C", "United States", "Chrysler", "1F", "United States", "Ford", "1G", "United States", "General Motors", "1G3", "United States", "Oldsmobile", "1GC", "United States", "Chevrolet", "1GM", "United States", "Pontiac", "1H", "United States", "Honda", "1J", "United States", "Jeep", "1L", "United States", "Lincoln", "1M", "United States", "Mercury"
		Data.s "1N", "United States", "Nissan", "1VW", "United States", "Volkswagen", "1YV", "United States", "Mazda", "2F", "Canada", "Ford", "2G", "Canada", "General Motors", "2G1", "Canada", "Chevrolet", "2G2", "Canada", "Pontiac", "2H", "Canada", "Honda", "2HM", "Canada", "Hyundai", "2M", "Canada", "Mercury"
		Data.s "2T", "Canada", "Toyota", "3F", "Mexico", "Ford", "3G", "Mexico", "General Motors", "3H", "Mexico", "Honda", "3N", "Mexico", "Nissan", "3VW", "Mexico", "Volkswagen", "4F", "United States", "Mazda", "4J", "United States", "Mercedes-Benz", "4M", "United States", "Mercury", "4S", "United States", "Subaru"
		Data.s "4T", "United States", "Toyota", "4US", "United States", "BMW", "5F", "United States", "Honda", "5L", "United States", "Lincoln", "5T", "United States", "Toyota", "5YJ", "United States", "Tesla", "6F", "Australia", "Ford", "6G", "Australia", "General Motors", "6G1", "Australia", "Chevrolet", "6G2", "Australia", "Pontiac"
		Data.s "6H", "Australia", "Holden", "6MM", "Australia", "Mitsubishi", "6T1", "Australia", "Toyota", "8AP", "Argentina", "Fiat", "8AT", "Argentina", "Iveco", "93H", "Brazil", "Honda", "93W", "Brazil", "Fiat Professional", "93Z", "Brazil", "Iveco", "9BD", "Brazil", "Fiat Autom�veis", "9BH", "Brazil", "Hyundai"
		Data.s "9BW", "Brazil", "Volkswagen", "AAV", "South Africa", "Volkswagen", "AFA", "South Africa", "Ford", "CL9", "Tunisia", "Wallyscar", "JA", "Japan", "Isuzu", "JF", "Japan", "Fuji Heavy Industries", "JH", "Japan", "Honda", "JMB", "Japan", "Mitsubishi", "JMZ", "Japan", "Mazda", "JN", "Japan", "Nissan"
		Data.s "JS", "Japan", "Suzuki", "JT", "Japan", "Toyota", "KL", "South Korea", "Daewoo", "KMH", "South Korea", "Hyundai", "KN", "South Korea", "Kia", "KPT", "South Korea", "SsangYong", "SAJ", "United Kingdom", "Jaguar", "SAL", "United Kingdom", "Land Rover", "SAR", "United Kingdom", "Rover", "SCC", "United Kingdom", "Lotus Cars"
		Data.s "SCE", "Ireland", "DeLorean", "TCC", "Switzerland", "Micro Compact Car AG", "TMA", "Czech Republic", "Hyundai", "TMB", "Czech Republic", "�koda", "TRU", "Hungary", "Audi", "UU", "Romania", "Dacia", "VA0", "Austria", "�AF", "VF1", "France", "Renault", "VF3", "France", "Peugeot", "VF6", "France", "Renault Trucks/Volvo"
		Data.s "VF7", "France", "Citro�n", "VFE", "France", "IvecoBus", "VSS", "Spain", "SEAT", "VV9", "Spain", "Tauro Sport Auto", "W0L", "Germany", "Opel/Vauxhall", "W0SV", "Germany", "Opel Special Vehicles", "WAP", "Germany", "Alpina", "WAU", "Germany", "Audi", "WBA", "Germany", "BMW", "WBS", "Germany", "BMW M"
		Data.s "WDB", "Germany", "Mercedes-Benz", "WDC", "Germany", "DaimlerChrysler AG/Daimler AG", "WDD", "Germany", "DaimlerChrysler AG/Daimler AG", "WMX", "Germany", "DaimlerChrysler AG/Daimler AG", "WEB", "Germany", "EvoBus", "WF0", "Germany", "Ford Germany", "WJM", "Germany", "Iveco", "WJR", "Germany", "Irmscher", "WKK", "Germany", "K�ssbohrer", "WMA", "Germany", "MAN"
		Data.s "WME", "Germany", "Smart", "WMW", "Germany", "Mini", "WP0", "Germany", "Porsche car", "WP1", "Germany", "Porsche SUV", "WUA", "Germany", "Quattro", "WV1", "Germany", "Volkswagen Commercial Vehicles", "WV2", "Germany", "Volkswagen Commercial Vehicles", "WVG", "Germany", "Volkswagen", "WVW", "Germany", "Volkswagen", "YK1", "Finland", "Saab"
		Data.s "YS3", "Sweden", "Saab", "YTN", "Sweden", "Saab NEVS", "YV1", "Sweden", "Volvo Cars", "ZAM", "Italy", "Maserati", "ZAR", "Italy", "Alfa Romeo", "ZCF", "Italy", "Iveco", "ZFA", "Italy", "Fiat Automobiles", "ZFF", "Italy", "Ferrari", "ZGA", "Italy", "IvecoBus", "ZHW", "Italy", "Lamborghini"
		Data.s "ZLA", "Italy", "Lancia"
		Data.s "","","" ;}
	
	vinCountryData: ;{ start-code, end-code, country
		; A–H = Africa	J–R = Asia	S–Z = Europe	1–5 = North America	6–7 = Oceania	8–9 = South America
		Data.s "AA", "AH", "South Africa", "AJ", "AN", "Ivory Coast", "AP", "A0", "not assigned", "BA", "BE", "Angola", "BF", "BK", "Kenya", "BL", "BR", "Tanzania", "BS", "B0", "not assigned", "CA", "CE", "Benin", "CF", "CK", "Madagascar", "CL", "CR", "Tunisia", "CS", "C0", "not assigned", "DA", "DE", "Egypt", "DF", "DK", "Morocco", "DL", "DR", "Zambia", "DS", "D0", "not assigned", "EA", "EE", "Ethiopia", "EF", "EK", "Mozambique", "EL", "E0", "not assigned", "FA", "FE", "Ghana", "FF", "FK", "Nigeria", "FL", "F0", "not assigned", "GA", "G0", "not assigned", "HA", "H0", "not assigned"
		Data.s "JA", "J0", "Japan", "KA", "KE", "Sri Lanka", "KF", "KK", "Israel", "KL", "KR", "Korea (South)", "KS", "K0", "Kazakhstan", "LA", "L0", "China", "MA", "ME", "India", "MF", "MK", "Indonesia", "ML", "MR", "Thailand", "MS", "M0", "not assigned", "NA", "NE", "Iran", "NF", "NK", "Pakistan", "NL", "NR", "Turkey", "NS", "N0", "not assigned", "PA", "PE", "Philippines", "PF", "PK", "Singapore", "PL", "PR", "Malaysia", "PS", "P0", "not assigned", "RA", "RE", "United Arab Emirates", "RF", "RK", "Taiwan", "RL", "RR", "Vietnam", "RS", "R0", "Saudi Arabia"
		Data.s "SA", "SM", "United Kingdom", "SN", "ST", "East Germany", "SU", "SZ", "Poland", "S1", "S4", "Latvia", "S5", "S0", "not assigned", "TA", "TH", "Switzerland", "TJ", "TP", "Czech Republic", "TR", "TV", "Hungary", "TW", "T1", "Portugal", "T2", "T0", "not assigned", "UA", "UG", "not assigned", "UH", "UM", "Denmark", "UN", "UT", "Ireland", "UU", "UZ", "Romania", "U1", "U4", "not assigned", "U5", "U7", "Slovakia", "U8", "U0", "not assigned", "VA", "VE", "Austria", "VF", "VR", "France", "VS", "VW", "Spain", "VX", "V2", "Serbia", "V3", "V5", "Croatia", "V6", "V0", "Estonia", "WA", "W0", "West Germany", "XA", "XE", "Bulgaria", "XF", "XK", "Greece", "XL", "XR", "Netherlands", "XS", "XW", "USSR", "XX", "X2", "Luxembourg", "X3", "X0", "Russia", "YA", "YE", "Belgium", "YF", "YK", "Finland", "YL", "YR", "Malta", "YS", "YW", "Sweden", "YX", "Y2", "Norway", "Y3", "Y5", "Belarus", "Y6", "Y0", "Ukraine", "ZA", "ZR", "Italy", "ZS", "ZW", "not assigned", "ZX", "Z2", "Slovenia", "Z3", "Z5", "Lithuania", "Z6", "Z0", "not assigned"
		Data.s "1A", "10", "United States", "2A", "20", "Canada", "3A", "37", "Mexico", "38", "30", "Cayman Islands", "4A", "40", "United States", "5A", "50", "United States"
		Data.s "6A", "6W", "Australia", "6X", "60", "not assigned", "7A", "7E", "New Zealand", "7F", "70", "not assigned"
		Data.s "8A", "8E", "Argentina", "8F", "8K", "Chile", "8L", "8R", "Ecuador", "8S", "8W", "Peru", "8X", "82", "Venezuela", "83", "80", "not assigned", "9A", "9E", "Brazil", "9F", "9K", "Colombia", "9L", "9R", "Paraguay", "9S", "9W", "Uruguay", "9X", "92", "Trinidad & Tobago", "93�99", "", "Brazil", "90 no", "", " assigned"
		Data.s "","","" ;}
	
	vinYearData: ;{ code, year
		Data.s "A","1980", "L","1990", "Y","2000", "A","2010", "L","2020", "Y","2030"
		Data.s "B","1981", "M","1991", "1","2001", "B","2011", "M","2021", "1","2031"
		Data.s "C","1982", "N","1992", "2","2002", "C","2012", "N","2022", "2","2032"
		Data.s "D","1983", "P","1993", "3","2003", "D","2013", "P","2023", "3","2033"
		Data.s "E","1984", "R","1994", "4","2004", "E","2014", "R","2024", "4","2034"
		Data.s "F","1985", "S","1995", "5","2005", "F","2015", "S","2025", "5","2035"
		Data.s "G","1986", "T","1996", "6","2006", "G","2016", "T","2026", "6","2036"
		Data.s "H","1987", "V","1997", "7","2007", "H","2017", "V","2027", "7","2037"
		Data.s "J","1988", "W","1998", "8","2008", "J","2018", "W","2028", "8","2038"
		Data.s "K","1989", "X","1999", "9","2009", "K","2019", "X","2029", "9","2039"
		Data.s "","" ;}
	
	vinVechicleConfigData: ;{ code, description
		Data.s "1", "Passenger Car (only If vehicle has HM Placard)"
		Data.s "2", "Light Truck (only If vehicle has HM Placard)"
		Data.s "3", "Bus (seats For 9-15 people, including driver)"
		Data.s "4", "Bus (seats For 16 people Or more, including driver)"
		Data.s "5", "Single-Unit Truck (2 axles, 6 tires)"
		Data.s "6", "Single-Unit Truck (3 Or more axles)"
		Data.s "7", "Truck/Trailer(s) (Single-Unit Truck With Trailer(s))"
		Data.s "8", "Truck/Tractor (without trailer, Bobtail Or saddlemount)"
		Data.s "9", "Tractor/Semi-Trailer (one trailer)"
		Data.s "10", "Tractor/Doubles (two trailers)"
		Data.s "11", "Tractor/Triples (three trailers)"
		Data.s "99", "Other Heavy Truck >10,000 lbs. Unclassified (Not listed above)"
		Data.s "","" ;}
	
	vinVechicleGVWR: ;{ code, weight
		Data.s, "1", "10,000 lbs. Or less"
		Data.s, "2", " 10,001 lbs. To 26,000 lbs."
		Data.s, "3", "Greater than 26,000 lbs."
		Data.s "","" ;}
  
	vinVehicleCargoBodyType: ;{ code, type
		Data.s "1", "Bus (seats For 9-15 people, including driver)"
		Data.s "2", "Bus (seats For 16 people Or more, including driver)"
		Data.s "3", "Van/Enclosed Box"
		Data.s "4", "Cargo Tank"
		Data.s "5", "Flatbed"
		Data.s "6", "Dump"
		Data.s "7", "Concrete Mixer"
		Data.s "8", "Auto Transporter"
		Data.s "9", "Garbage/Refuse"
		Data.s "10", "Grain, Chips, Gravel"
		Data.s "11", "Pole"
		Data.s "12", "Not Applicable/No Cargo Body Type"
		Data.s "13", "Intermodal Chassis"
		Data.s "14", "Logging"
		Data.s "15", "Vehicle Towing Another Motor Vehicle"
		Data.s "98", "Other"
		Data.s "","" ;}
EndDataSection
CompilerEndIf

CompilerIf #PB_Compiler_IsMainFile
	Debug Str(#vin_Valid)+" = success"
	Debug Str(#vin_FailChkDigit)+" = fail chkdigit"
	Debug Str(#vin_InvalidCharacter)+" = invalid char"
	Debug "Test: "+VINValidation("1M8GDM9AXKP042788") ; Valid
	Debug "Test: "+VINValidation("1FTYR10D49PA42063") ; Valid
	Debug "Test: "+VINValidation("1MZGDM9AXKP042788") ; Invalid
	Debug "Test: "+VINValidation("1MZG*M9AXKP042788")	; invalid character
	
	Define.s cText = "1M8GDM9AXKP042788"+#CRLF$+"1FTYR10D49PA42063"+"1MZGDM9AXKP042788"+#TAB$+"1ZZGDM9AXKP042788"+"1MZG*M9AXKP042788", cSample
	; note, the last will not be picked up since it fails the regex for extraction.
	
	Debug "Showing potential vins extracted"
	Define.i i, nResultCode
	While ExtractPotentialVIN(cText, i)
		cSample = ExtractPotentialVIN(cText,i) 
		nResultCode = VINValidation( cSample )
		Select nResultCode
			Case #vin_Valid				: Debug Str(i)+" VALID "+ cSample
			Case #vin_RegExErr					: Debug Str(i)+" RegEx error "+cSample
			Case #vin_FailChkDigit 		: Debug Str(i)+" Failed check digit "+cSample
			Case #vin_Invalid					: Debug Str(i)+" Invalid "+cSample
			Case #vin_InvalidLength		: Debug Str(i)+" Too Short/long "+cSample
			Case #vin_InvalidCharacter	: Debug Str(i)+"  InvalidChar found "+cSample
			Default 							: Debug Str(i)+" Unknown Result code: "+nResultCode
		EndSelect
	
		i+1
	Wend
	
CompilerEndIf