460 lines
9.4 KiB
Go
460 lines
9.4 KiB
Go
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
|
|
}
|