package vindecoder import ( "regexp" "strings" "unicode" "github.com/pkg/errors" ) const vin_size = 17 const made_in_start = 0 const made_in_size = 2 const manufacturer_start = 0 const manufacturer_size = 3 const manufacturer_small_start = 11 const manufacturer_small_size = 3 const small_manufacturer_indicator_index = 2 const details_start = 3 const details_size = 5 const check_digit_index = 8 const year_index = 9 const assembly_plant_index = 10 const serial_number_start = 11 const serial_number_size = 6 const serial_number_small_start = 14 const serial_number_small_size = 3 type vinRawInfo struct { country string manufacturer string details string checkDigit string year string assemblyPlant string serialNumber string smallManufacturer bool } // source: https://en.wikipedia.org/wiki/Vehicle_identification_number var vinPositionWeights = []int{8, 7, 6, 5, 4, 3, 2, 10, 0, 9, 8, 7, 6, 5, 4, 3, 2} // source: https://en.wikipedia.org/wiki/Vehicle_identification_number var transliterationArray = []int{ 1 /* A */, 2 /* B */, 3, /* C */ 4 /* D */, 5 /* E */, 6, /* F */ 7 /* G */, 8 /* H */, 0, /* I */ 1 /* J */, 2 /* K */, 3, /* L */ 4 /* M */, 5 /* N */, 0, /* O */ 7 /* P */, 0 /* Q */, 9, /* R */ /* */ 2 /* S */, 3, /* T */ 4 /* U */, 5 /* V */, 6, /* W */ 7 /* X */, 8 /* Y */, 9, /* Z */ } // source: https://en.wikipedia.org/wiki/Vehicle_identification_number var yearOffsetTable = map[byte]int{ 'A': 0, 'B': 1, 'C': 2, 'D': 3, 'E': 4, 'F': 5, 'G': 6, 'H': 7, 'J': 8, 'K': 9, 'L': 10, 'M': 11, 'N': 12, 'P': 13, 'R': 14, 'S': 15, 'T': 16, 'V': 17, 'W': 18, 'X': 19, 'Y': 20, '1': 21, '2': 22, '3': 23, '4': 24, '5': 25, '6': 26, '7': 27, '8': 28, '9': 29, } func getData(vin string) vinRawInfo { // split VIN into fields var rawInfo vinRawInfo rawInfo.smallManufacturer = (vin[small_manufacturer_indicator_index] == '9') rawInfo.country = vin[made_in_start : made_in_start+made_in_size] rawInfo.details = vin[details_start : details_start+details_size] rawInfo.checkDigit = vin[check_digit_index : check_digit_index+1] rawInfo.year = vin[year_index : year_index+1] rawInfo.assemblyPlant = vin[assembly_plant_index : assembly_plant_index+1] if rawInfo.smallManufacturer { rawInfo.manufacturer = vin[manufacturer_start:manufacturer_start+manufacturer_size] + "-" + vin[manufacturer_small_start:manufacturer_small_start+manufacturer_small_size] rawInfo.serialNumber = vin[serial_number_small_start : serial_number_small_start+serial_number_small_size] } else { rawInfo.manufacturer = vin[manufacturer_start : manufacturer_start+manufacturer_size] rawInfo.serialNumber = vin[serial_number_start : serial_number_start+serial_number_size] } return rawInfo } func getYear(vin string) int { // decode year var year = 0 var yearChar = vin[9] var pos7Char = vin[6] offset, found := yearOffsetTable[yearChar] if found { if unicode.IsDigit(rune(pos7Char)) { year = 1980 + offset } else { year = 2010 + offset } } return year } func getModel(vin string) string { switch vin[3] { case '1': return "Ocean" default: return "" } } const ( MODEL_OCEAN = "Ocean" ) func getTrim(vin string) string { switch vin[4] { case 'S': return TRIM_SPORT case 'U': return TRIM_ULTRA case 'E': return TRIM_EXTREME case 'Z': return TRIM_OCEAN_ONE default: return "" } } // Equivalent to trim level const ( TRIM_SPORT = "Sport" TRIM_ULTRA = "Ultra" TRIM_EXTREME = "Extreme" TRIM_OCEAN_ONE = "Ocean One" ) func getPowertrainType(vin string) string { switch vin[5] { case 'A': return POWERTRAIN_TYPE_SBP_SM_FWD case 'B': return POWERTRAIN_TYPE_LBP_DM_AWD default: return "" } } const ( POWERTRAIN_TYPE_SBP_SM_FWD = "SBP/SM/FWD" POWERTRAIN_TYPE_LBP_DM_AWD = "LBP/DM/AWD" ) func getRestraint(vin string) string { switch vin[6] { case 'U': return RESTRAIN_US_SPECS case 'E': return RESTRAIN_EU_SPECS case 'C': return RESTRAIN_CN_SPECS default: return "" } } const ( RESTRAIN_US_SPECS = "US Specs" RESTRAIN_EU_SPECS = "EU Specs" RESTRAIN_CN_SPECS = "CN Specs" ) func getBodyTypeAndGVWR(vin string) string { switch vin[7] { case '1': return "5-Door MPV, 5-Seater, Class D" case '2': return "5-Door MPV, 5-Seater, Class E" case '3': return "5-Door MPV, 7-Seater, Class D" case '4': return "5-Door MPV, 7-Seater, Class E" default: return "" } } func translitChar(character byte) int { // numeric digits as their value if character >= '0' && character <= '9' { return int(character - '0') } if character >= 'A' && character <= 'Z' { var translitAlpha = transliterationArray[character-'A'] if translitAlpha > 0 { return translitAlpha } } return -1 } func validate(vin string) bool { // compare calculated check digit with value found calculatedCheckDigit, err := calculateCheckDigit(vin) if err != nil { return false } checkDigit := vin[8] return (calculatedCheckDigit == checkDigit) } func calculateCheckDigit(vin string) (byte, error) { // calculate check digit if len(vin) != vin_size { return '0', errors.New("invalid vin size") } sum := 0 for i := range vin { value := translitChar(vin[i]) if value < 0 { return '0', errors.New("invalid vin character " + string(vin[i])) } product := vinPositionWeights[i] * value sum += product } // find the divisor calculatedCheckDigit := byte(sum % 11) if calculatedCheckDigit == 10 { calculatedCheckDigit = 'X' } else { calculatedCheckDigit = '0' + calculatedCheckDigit } return calculatedCheckDigit, nil } // in north america, last 5 digits must be numeric, here we'll create all numeric digits // also we'll use natural numeric sort order where 0 is the lowest digit value func getNextNumber(vinNumber string) (string, error) { // cannot process empty string if len(vinNumber) == 0 { return vinNumber, errors.New("empty vin number") } nextChars := []byte(vinNumber) var i = 0 for i = len(vinNumber) - 1; i >= 0; i-- { num := vinNumber[i] // if we encounter a non-numeric SN digit, convert to numeric if 'A' <= num && num < 'Z' { nextChars[i] = '0' break } if '0' <= num && num < '9' { nextChars[i] = num + 1 break } nextChars[i] = '0' } // overflow err := (error)(nil) if i < 0 { err = errors.New("overflow") } next := string(nextChars) return next, err } func getNextVin(vin string) (string, error) { // get next SN number and mae part of base vin data := getData(vin) sn := data.serialNumber overflow := (error)(nil) nextSn, err := getNextNumber(sn) if err != nil { if err.Error() == "overflow" { overflow = err } else { return "", err } } nextVin := vin[:strings.LastIndex(vin, sn)] + nextSn checkDigit, err := calculateCheckDigit(nextVin) if err != nil { return vin, err } nextVin = vin[:check_digit_index] + string(checkDigit) + nextVin[check_digit_index+1:] return nextVin, overflow } func getNextNVins(startVin string, count int) ([]string, error) { // check that base vin is valid _, err := calculateCheckDigit(startVin) if err != nil { return nil, err } if count < 1 { return nil, errors.New("count is negative") } vins := make([]string, count) rollover := (error)(nil) nextVin, err := getNextVin(startVin) for i := 0; i < count; i++ { if err != nil { if err.Error() == "overflow" { rollover = errors.New("rollover") } else { return nil, err } } vins[i] = nextVin nextVin, err = getNextVin(nextVin) } return vins, rollover } type VinInfo struct { Manufacturer string Country string SerialNumber string ModelDetails string Model string Trim string Year int Powertrain string Restraint string BodyType string IsValid bool } func decode(vin string) VinInfo { // ensure valid vin var result VinInfo result.IsValid = true // simple regex validation vs := ValidateVINSimple(vin) if !vs { result.IsValid = false return result } var data vinRawInfo = getData(vin) result.SerialNumber = data.serialNumber result.Manufacturer = LookupManufacturerByWmiCode(data.manufacturer) result.Country = LookupCountry(data.country) result.ModelDetails = data.details result.Model = getModel(vin) result.Trim = getTrim(vin) result.Powertrain = getPowertrainType(vin) result.Restraint = getRestraint(vin) result.BodyType = getBodyTypeAndGVWR(vin) var year = getYear(vin) if year >= 1980 { result.Year = year } else { result.IsValid = false } // checksum digit validation if !validate(vin) { result.IsValid = false return result } return result } // entry point // VIN should be uppercase func DecodeVIN(vin string) (info VinInfo, ok bool) { vin = strings.ToUpper(vin) info = decode(vin) return info, info.IsValid } func CalculateCheckDigit(vin string) (byte, error) { return calculateCheckDigit(vin) } func VerifyVinCheckDigit(vin string) bool { vin = strings.ToUpper(vin) return validate(vin) } func NextVIN(startVin string) (string, error) { return getNextVin(startVin) } func NextNVINs(startVin string, count int) ([]string, error) { return getNextNVins(startVin, count) } func IsEU(vin string) bool { return getRestraint(vin) == "EU Specs" } // Pre instantiate vin match regex var VINSimpleRegexMatch *regexp.Regexp func init() { VINSimpleRegexMatch = regexp.MustCompile(`^[a-hj-npr-zA-HJ-NPR-Z0-9]{17}$`) } func ValidateVINSimple(vin string) (matched bool) { matched = VINSimpleRegexMatch.Match([]byte(vin)) return matched }