Initial cloud-services repo - gateway service + pkg modules
This commit is contained in:
459
pkg/vindecoder/vin_decoder.go
Normal file
459
pkg/vindecoder/vin_decoder.go
Normal file
@@ -0,0 +1,459 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user