Initial cloud-services repo - gateway service + pkg modules

This commit is contained in:
Chris Rai
2026-01-30 23:14:52 -05:00
commit fbb820d7b3
1037 changed files with 171318 additions and 0 deletions

View File

@@ -0,0 +1,210 @@
package vindecoder_test
import (
"strings"
"testing"
"fiskerinc.com/modules/testhelper"
"fiskerinc.com/modules/vindecoder"
)
var vwGer2013Valid = "WVWBN7AN6DE546002"
var vwGer2013Invalid1 = "WVWBN7AN1DE546002"
var vwGer2013Invalid2 = "WVWBN7AN6DE54600"
var miniGer2009Valid = "WMWMS335X9TY38985"
var bugatFra1998Valid = "VF9SP3V31JM795073"
var dodgeUsa1998Valid = "1B3ES42C4WD736523"
var fiskerVinValid = "VCF1ZBU23PG001209"
// TEMP
var fskUsa2021Valid = "1F1BN7AN7MA000001"
// TEMP
//var unkJap2013Valid = "JS1GR7MA7D2101136"
//var audiGer2012Valid = "WAUFFAFM3CA000000"
func TestVinDecoderCountry(t *testing.T) {
// there should be more tests for cases where second character is 1-9, 0, but it's hard to find valid vins
// USA
vi1, _ := vindecoder.DecodeVIN(dodgeUsa1998Valid)
if !strings.Contains(vi1.Country, "United States") {
t.Errorf(testhelper.TestErrorTemplate, dodgeUsa1998Valid, "United States", vi1.Country)
}
// Germany
vi2, _ := vindecoder.DecodeVIN(vwGer2013Valid)
if !strings.Contains(vi2.Country, "Germany") {
t.Errorf(testhelper.TestErrorTemplate, vwGer2013Valid, "Germany", vi2.Country)
}
valid := vindecoder.ValidateVINSimple(fiskerVinValid)
if !valid {
t.Errorf(testhelper.TestErrorTemplate, fiskerVinValid, "Invalid", vi2.Country)
}
}
func TestVinDecoderWmi(t *testing.T) {
// normal
vi1, _ := vindecoder.DecodeVIN(vwGer2013Valid)
if !strings.Contains(vi1.Manufacturer, "Volkswagen") {
t.Errorf(testhelper.TestErrorTemplate, vwGer2013Valid, "Volkswagen", vi1.Manufacturer)
}
// small production
vi2, _ := vindecoder.DecodeVIN(bugatFra1998Valid)
if !strings.Contains(vi2.Manufacturer, "Bugatti") {
t.Errorf(testhelper.TestErrorTemplate, bugatFra1998Valid, "Bugatti", vi2.Manufacturer)
}
// wild card
mfg1 := vindecoder.LookupManufacturerByWmiCode("4F3")
if !strings.Contains(mfg1, "Mazda") {
t.Errorf(testhelper.TestErrorTemplate, "4F3", "Mazda", mfg1)
}
// normal unknown wmi
mfg2 := vindecoder.LookupManufacturerByWmiCode("JM2")
if mfg2 != "JM2" {
t.Errorf(testhelper.TestErrorTemplate, "JM2", "JM2", mfg2)
}
// TEMP
// normal
viFsk, _ := vindecoder.DecodeVIN(fskUsa2021Valid)
if !strings.Contains(viFsk.Manufacturer, "Fisker") {
t.Errorf(testhelper.TestErrorTemplate, fskUsa2021Valid, "Fisker", viFsk.Manufacturer)
}
// TEMP
}
func TestVinDecoderSerialNumber(t *testing.T) {
// normal
vi1, _ := vindecoder.DecodeVIN(vwGer2013Valid)
if vi1.SerialNumber != "546002" {
t.Errorf(testhelper.TestErrorTemplate, vwGer2013Valid, "546002", vi1.SerialNumber)
}
// small production
vi2, _ := vindecoder.DecodeVIN(bugatFra1998Valid)
if vi2.SerialNumber != "073" {
t.Errorf(testhelper.TestErrorTemplate, bugatFra1998Valid, "073", vi2.SerialNumber)
}
}
func TestVinDecoderCheckDigit(t *testing.T) {
// valid
vi1, ok := vindecoder.DecodeVIN(vwGer2013Valid)
if ok == false {
t.Errorf(testhelper.TestErrorTemplate, vwGer2013Valid, true, ok)
}
if !vi1.IsValid {
t.Errorf(testhelper.TestErrorTemplate, vwGer2013Valid, true, vi1.IsValid)
}
// valid - lowercase
var vwGer2013Valid_lower = strings.ToLower(vwGer2013Valid)
vi1a, ok := vindecoder.DecodeVIN(vwGer2013Valid_lower)
if ok == false {
t.Errorf(testhelper.TestErrorTemplate, vwGer2013Valid_lower, true, ok)
}
if !vi1a.IsValid {
t.Errorf(testhelper.TestErrorTemplate, vwGer2013Valid_lower, true, vi1a.IsValid)
}
// invalid
vi2, ok := vindecoder.DecodeVIN(vwGer2013Invalid1)
if ok == true {
t.Errorf(testhelper.TestErrorTemplate, vwGer2013Invalid1, false, ok)
}
if vi2.IsValid {
t.Errorf(testhelper.TestErrorTemplate, vwGer2013Invalid1, false, vi2.IsValid)
}
vi3, ok := vindecoder.DecodeVIN(vwGer2013Invalid2)
if ok == true {
t.Errorf(testhelper.TestErrorTemplate, vwGer2013Invalid2, false, ok)
}
if vi3.IsValid {
t.Errorf(testhelper.TestErrorTemplate, vwGer2013Invalid2, false, vi3.IsValid)
}
}
func TestVinDecoderYear(t *testing.T) {
// before 2010
vi1, _ := vindecoder.DecodeVIN(miniGer2009Valid)
if vi1.Year != 2009 {
t.Errorf(testhelper.TestErrorTemplate, miniGer2009Valid, 2013, vi1.Year)
}
// after 2010
vi2, _ := vindecoder.DecodeVIN(vwGer2013Valid)
if vi2.Year != 2013 {
t.Errorf(testhelper.TestErrorTemplate, vwGer2013Valid, 2013, vi2.Year)
}
}
func TestVinDecoderModelDetails(t *testing.T) {
vi, _ := vindecoder.DecodeVIN(vwGer2013Valid)
if vi.ModelDetails != "BN7AN" {
t.Errorf(testhelper.TestErrorTemplate, vwGer2013Valid, "BN7AN", vi.ModelDetails)
}
}
func TestNextVin(t *testing.T) {
// test vin generation
startVin := "WVWBN7AN6DE546002"
nextVin, err := vindecoder.NextVIN(startVin)
if err != nil {
t.Errorf(testhelper.TestErrorTemplate, startVin, "nil", err)
}
if strings.LastIndex(nextVin, "546003") < 0 {
t.Errorf(testhelper.TestErrorTemplate, startVin, "WVWBN7AN*DE546003", nextVin)
}
startVin = "WVWBN7AN6DE546XX9"
nextVin, err = vindecoder.NextVIN(startVin)
if err != nil {
t.Errorf(testhelper.TestErrorTemplate, startVin, "nil", err)
}
if strings.LastIndex(nextVin, "546X00") < 0 {
t.Errorf(testhelper.TestErrorTemplate, startVin, "WVWBN7AN6DE546X00", nextVin)
}
startVin = "WVWBN7AN6DE546999"
nextVin, err = vindecoder.NextVIN(startVin)
if err != nil {
t.Errorf(testhelper.TestErrorTemplate, startVin, "nil", err)
}
if strings.LastIndex(nextVin, "547000") < 0 {
t.Errorf(testhelper.TestErrorTemplate, startVin, "WVWBN7AN6DE547000", nextVin)
}
startVin = "WVWBN7AN6DE999999"
nextVin, err = vindecoder.NextVIN(startVin)
if err == nil {
t.Errorf(testhelper.TestErrorTemplate, startVin, "overflow", "nil")
}
if strings.LastIndex(nextVin, "000000") < 0 {
t.Errorf(testhelper.TestErrorTemplate, startVin, "WVWBN7AN6DE000000", nextVin)
}
startVin = "WVWBN7AN6DE999995"
nextVins, err := vindecoder.NextNVINs(startVin, 10)
if err == nil {
t.Errorf(testhelper.TestErrorTemplate, startVin, "rollover", "nil")
}
if len(nextVins) != 10 {
t.Errorf(testhelper.TestErrorTemplate, startVin, 10, len(nextVins))
}
}
func TestEmptyVIN(t *testing.T){
ok := vindecoder.ValidateVINSimple("")
if ok {
t.Error("Expected Invalid VIN")
}
}

View File

@@ -0,0 +1,249 @@
package vindecoder
type rangeSegment struct {
low byte
high byte
country string
}
// source: https://en.wikipedia.org/wiki/Vehicle_identification_number
//
// note ascending sort order: A-Z. then 1-9, then 0
var vinCountryCodeRangeMap = map[byte][]rangeSegment{
'A': {
rangeSegment{low: 'A', high: 'H', country: "South Africa"},
rangeSegment{low: 'J', high: 'K', country: "Cote d'Ivoire"},
rangeSegment{low: 'L', high: 'L', country: "algeria"},
rangeSegment{low: 'M', high: 'N', country: "Cote d'Ivoire"},
// rangeSegment{low: 'P', high: 'P', country: ""},
rangeSegment{low: 'R', high: 'R', country: "algeria"},
// rangeSegment{low: 'S', high: 'Z', country: ""},
// rangeSegment{low: '1', high: '9', country: ""},
// rangeSegment{low: '0', high: '0', country: ""}
},
'B': {
rangeSegment{low: 'A', high: 'E', country: "SAngola"},
rangeSegment{low: 'F', high: 'K', country: "Kenya"},
rangeSegment{low: 'L', high: 'R', country: "Tanzania"},
// rangeSegment{low: 'S', high: '0', country: ""},
},
'C': {
rangeSegment{low: 'A', high: 'E', country: "Benin"},
rangeSegment{low: 'F', high: 'K', country: "Madagascar"},
rangeSegment{low: 'L', high: 'R', country: "Tunisia"},
// rangeSegment{low: 'S', high: '0", "" },
},
'D': {
rangeSegment{low: 'A', high: 'E', country: "Egypt"},
rangeSegment{low: 'F', high: 'K', country: "Morocco"},
rangeSegment{low: 'L', high: 'R', country: "Zambia"},
// rangeSegment{low: 'S', high: '0', country: ""},
},
'E': {
rangeSegment{low: 'A', high: 'E', country: "Ethiopia"},
rangeSegment{low: 'F', high: 'K', country: "Mozambique"},
// rangeSegment{low: 'L', high: '0', country: ""},
},
'F': {
rangeSegment{low: 'A', high: 'E', country: "Ghana"},
rangeSegment{low: 'F', high: 'K', country: "Nigeria"},
// rangeSegment{low: 'L', high: '0', country: ""},
},
// 'G': {
// rangeSegment{low: 'A', high: '0', country: ""},
// },
// 'H': {
// rangeSegment{low: 'A', high: '0', country: ""},
// },
'J': {
rangeSegment{low: 'A', high: 'Z', country: "Japan"},
rangeSegment{low: '1', high: '9', country: "Japan"},
rangeSegment{low: '0', high: '0', country: "Japan"},
},
'K': {
rangeSegment{low: 'A', high: 'E', country: "Sri Lanka"},
rangeSegment{low: 'F', high: 'K', country: "Israel"},
rangeSegment{low: 'L', high: 'R', country: "Korea (South)"},
rangeSegment{low: 'S', high: 'S', country: "Jordan"},
rangeSegment{low: 'T', high: 'Z', country: "Kazakhstan"},
rangeSegment{low: '1', high: '9', country: "Kazakhstan"},
rangeSegment{low: '0', high: '0', country: "Kazakhstan"},
},
'L': {
rangeSegment{low: 'A', high: 'Z', country: "China"},
rangeSegment{low: '1', high: '9', country: "China"},
rangeSegment{low: '0', high: '0', country: "China"},
},
'M': {
rangeSegment{low: 'A', high: 'E', country: "India"},
rangeSegment{low: 'F', high: 'K', country: "Indonesia"},
rangeSegment{low: 'L', high: 'R', country: "Thailand"},
rangeSegment{low: 'S', high: 'Y', country: "Myanmar"},
rangeSegment{low: 'Z', high: 'Z', country: "India"},
rangeSegment{low: '1', high: '9', country: "Myanmar"},
rangeSegment{low: '0', high: '0', country: "Myanmar"},
},
'N': {
rangeSegment{low: 'A', high: 'E', country: "Iran"},
rangeSegment{low: 'F', high: 'K', country: "Pakistan"},
rangeSegment{low: 'L', high: 'R', country: "Turkey"},
// rangeSegment{low: 'S', high: '0', country: "" },
},
'P': {
rangeSegment{low: 'A', high: 'E', country: "Philippines"},
rangeSegment{low: 'F', high: 'K', country: "Singapore"},
rangeSegment{low: 'L', high: 'R', country: "Malaysia"},
// rangeSegment{low: 'S', high: '0', country: "" },
rangeSegment{low: 'A', high: 'E', country: "United Arab Emirates"},
rangeSegment{low: 'F', high: 'K', country: "Taiwan"},
},
'R': {
rangeSegment{low: 'L', high: 'R', country: "Vietnam"},
rangeSegment{low: 'S', high: 'Z', country: "Saudi Arabia"},
rangeSegment{low: '1', high: '9', country: "Saudi Arabia"},
rangeSegment{low: '0', high: '0', country: "Saudi Arabia"},
},
'S': {
rangeSegment{low: 'A', high: 'M', country: "United Kingdom"},
rangeSegment{low: 'N', high: 'T', country: "Germany"},
rangeSegment{low: 'U', high: 'Z', country: "Poland"},
rangeSegment{low: '1', high: '4', country: "Latvia"},
// rangeSegment{low: '5', high: '0', country: "" },
},
'T': {
rangeSegment{low: 'A', high: 'H', country: "Switzerland"},
rangeSegment{low: 'J', high: 'P', country: "Czech Republic"},
rangeSegment{low: 'R', high: 'V', country: "Hungary"},
rangeSegment{low: 'W', high: 'Z', country: "Portugal"},
rangeSegment{low: '1', high: '1', country: "Portugal"},
// rangeSegment{low: '2', high: '0', country: "" },
},
'U': {
// rangeSegment{low: 'A', high: 'G', country: "" },
rangeSegment{low: 'H', high: 'M', country: "Denmark"},
rangeSegment{low: 'N', high: 'T', country: "Ireland"},
rangeSegment{low: 'U', high: 'Z', country: "Romania"},
// rangeSegment{low: '1', high: '4', country: "" },
rangeSegment{low: '5', high: '7', country: "Slovakia"},
// rangeSegment{low: '8', high: '0', country: "" },
},
'V': {
rangeSegment{low: 'A', high: 'E', country: "Austria"},
rangeSegment{low: 'F', high: 'R', country: "France"},
rangeSegment{low: 'S', high: 'W', country: "Spain"},
rangeSegment{low: 'X', high: 'Z', country: "Serbia"},
rangeSegment{low: '1', high: '2', country: "Serbia"},
rangeSegment{low: '3', high: '5', country: "Croatia"},
rangeSegment{low: '6', high: '9', country: "Estonia"},
rangeSegment{low: '0', high: '0', country: "Estonia"},
},
'W': {
rangeSegment{low: 'A', high: 'Z', country: "Germany"},
rangeSegment{low: '1', high: '9', country: "Germany"},
rangeSegment{low: '0', high: '0', country: "Germany"},
},
'X': {
rangeSegment{low: 'A', high: 'E', country: "Bulgaria"},
rangeSegment{low: 'F', high: 'K', country: "Greece"},
rangeSegment{low: 'L', high: 'R', country: "Netherlands"},
rangeSegment{low: 'S', high: 'W', country: "Russia"},
rangeSegment{low: 'X', high: 'Z', country: "Luxembourg"},
rangeSegment{low: '1', high: '2', country: "Luxembourg"},
rangeSegment{low: '3', high: '9', country: "Russia"},
rangeSegment{low: '0', high: '0', country: "Russia"},
},
'Y': {
rangeSegment{low: 'A', high: 'E', country: "Belgium"},
rangeSegment{low: 'F', high: 'K', country: "Finland"},
rangeSegment{low: 'L', high: 'R', country: "Malta"},
rangeSegment{low: 'S', high: 'W', country: "Sweden"},
rangeSegment{low: 'X', high: 'Z', country: "Norway"},
rangeSegment{low: '1', high: '2', country: "Norway"},
rangeSegment{low: '3', high: '5', country: "Belarus"},
rangeSegment{low: '6', high: '9', country: "Ukraine"},
rangeSegment{low: '0', high: '0', country: "Ukraine"},
},
'Z': {
rangeSegment{low: 'A', high: 'R', country: "Italy"},
// rangeSegment{low: 'S', high: 'W', country: "" },
rangeSegment{low: 'X', high: 'Z', country: "Slovenia"},
rangeSegment{low: '1', high: '2', country: "Slovenia"},
rangeSegment{low: '3', high: '5', country: "Lithuania"},
// rangeSegment{low: '6', high: '0', country: "" },
},
'1': {
rangeSegment{low: 'A', high: 'Z', country: "United States"},
rangeSegment{low: '1', high: '9', country: "United States"},
rangeSegment{low: '0', high: '0', country: "United States"},
},
'2': {
rangeSegment{low: 'A', high: 'Z', country: "Canada"},
rangeSegment{low: '1', high: '9', country: "Canada"},
rangeSegment{low: '0', high: '0', country: "Canada"},
},
'3': {
rangeSegment{low: 'A', high: 'W', country: "Mexico"},
rangeSegment{low: 'X', high: 'Z', country: "Costa Rica"},
rangeSegment{low: '1', high: '7', country: "Costa Rica"},
rangeSegment{low: '8', high: '9', country: "Cayman Islands"},
// rangeSegment{low: '0', high: '0', country: "" },
},
'4': {
rangeSegment{low: 'A', high: 'Z', country: "United States"},
rangeSegment{low: '1', high: '9', country: "United States"},
rangeSegment{low: '0', high: '0', country: "United States"},
},
'5': {
rangeSegment{low: 'A', high: 'Z', country: "United States"},
rangeSegment{low: '1', high: '9', country: "United States"},
rangeSegment{low: '0', high: '0', country: "United States"},
},
'6': {
rangeSegment{low: 'A', high: 'Z', country: "Australia"},
rangeSegment{low: '1', high: '9', country: "Australia"},
rangeSegment{low: '0', high: '0', country: "Australia"},
},
'7': {
rangeSegment{low: 'A', high: 'Z', country: "New Zealand"},
rangeSegment{low: '1', high: '9', country: "New Zealand"},
rangeSegment{low: '0', high: '0', country: "New Zealand"},
},
'8': {
rangeSegment{low: 'A', high: 'E', country: "Argentina"},
rangeSegment{low: 'F', high: 'K', country: "Chile"},
rangeSegment{low: 'L', high: 'R', country: "Ecuador"},
rangeSegment{low: 'S', high: 'W', country: "Peru"},
rangeSegment{low: 'X', high: 'Z', country: "Venezuela"},
rangeSegment{low: '1', high: '2', country: "Venezuela"},
rangeSegment{low: '2', high: '2', country: "Bolivia"},
// rangeSegment{low: '3', high: '0', country: "" },
},
'9': {
rangeSegment{low: 'A', high: 'E', country: "Brazil"},
rangeSegment{low: 'F', high: 'K', country: "Colombia"},
rangeSegment{low: 'L', high: 'R', country: "Paraguay"},
rangeSegment{low: 'S', high: 'W', country: "Uruguay"},
rangeSegment{low: 'X', high: 'Z', country: "Trinidad & Tobago"},
rangeSegment{low: '1', high: '2', country: "Trinidad & Tobago"},
rangeSegment{low: '3', high: '9', country: "Brazil"},
// rangeSegment{low: '0', high: '0', country: "" }
},
}
func LookupCountry(countryCode string) string {
var key = countryCode[0]
var elem = countryCode[1]
segments, found := vinCountryCodeRangeMap[key]
if !found {
return countryCode
}
for _, segment := range segments {
if segment.low <= elem && elem <= segment.high {
return segment.country
}
}
return countryCode
}

View 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
}

View File

@@ -0,0 +1,591 @@
package vindecoder
// source: https://en.wikibooks.org/wiki/Vehicle_Identification_Numbers_(VIN_codes)/World_Manufacturer_Identifier_(WMI)
var vinWmiCodeMap = map[string]string{
// Africa
"AAV": "Volkswagen South Africa",
"AC5": "Hyundai South Africa",
"ADD": "Hyundai South Africa",
"AFA": "Ford South Africa",
"AHT": "Toyota South Africa",
// Japan
"JA3": "Mitsubishi",
"JA4": "Mitsubishi",
"JA*": "Isuzu",
"JD*": "Daihatsu",
"JF*": "Fuji Heavy Industries (Subaru)",
"JHA": "Hino",
"JHB": "Hino",
"JHC": "Hino",
"JHD": "Hino",
"JHE": "Hino",
"JHF": "Honda",
"JHG": "Honda",
"JHL": "Honda",
"JHM": "Honda",
"JHN": "Honda",
"JHZ": "Honda",
"JH1": "Honda",
"JH2": "Honda",
"JH3": "Honda",
"JH4": "Honda",
"JH5": "Honda",
"JK*": "Kawasaki (motorcycles)",
"JL5": "Mitsubishi Fuso",
"JM1": "Mazda",
"JMB": "Mitsubishi Motors",
"JMY": "Mitsubishi Motors",
"JMZ": "Mazda",
"JN*": "Nissan",
"JS*": "Suzuki",
"JT*": "Toyota",
"JY*": "Yamaha (motorcycles)",
// Korea
"KL*": "Daewoo General Motors South Korea",
"KM*": "Hyundai",
"KMY": "Daelim (motorcycles)",
"KM1": "Hyosung (motorcycles)",
"KN*": "Kia",
"KNM": "Renault Samsung",
"KPA": "SsangYong",
"KPT": "SsangYong",
// China
"LAE": "Jinan Qingqi Motorcycle",
"LAL": "Sundiro Honda Motorcycle",
"LAN": "Changzhou Yamasaki Motorcycle",
"LBB": "Zhejiang Qianjiang Motorcycle (Keeway/Generic)",
"LBE": "Beijing Hyundai",
"LBM": "Zongshen Piaggio",
"LBP": "Chongqing Jainshe Yamaha (motorcycles)",
"LB2": "Geely Motorcycles",
"LCE": "Hangzhou Chunfeng Motorcycles (CFMOTO)",
"LDC": "Dong Feng Peugeot Citroen (DPCA), China",
"LDD": "Dandong Huanghai Automobile",
"LDF": "Dezhou Fulu Vehicle (motorcycles)",
"LDN": "SouEast Motor",
"LDY": "Zhongtong Coach, China",
"LET": "Jiangling-Isuzu Motors, China",
"LE4": "Beijing Benz, China",
"LFB": "FAW, China (busses)",
"LFG": "Taizhou Chuanl Motorcycle Manufacturing",
"LFP": "FAW, China (passenger vehicles)",
"LFT": "FAW, China (trailers)",
"LFV": "FAW-Volkswagen, China",
"LFW": "FAW JieFang, China",
"LFY": "Changshu Light Motorcycle Factory",
"LGB": "Dong Feng (DFM), China",
"LGH": "Qoros (formerly Dong Feng (DFM)), China",
"LGX": "BYD Auto, China",
"LHB": "Beijing Automotive Industry Holding",
"LH1": "FAW-Haima, China",
"LJC": "JAC, China",
"LJ1": "JAC, China",
"LKL": "Suzhou King Long, China",
"LL6": "Hunan Changfeng Manufacture Joint-Stock",
"LL8": "Linhai (ATV)",
"LMC": "Suzuki Hong Kong (motorcycles)",
"LPR": "Yamaha Hong Kong (motorcycles)",
"LPS": "Polestar (Volvo) (Sweden)",
"LSG": "Shanghai General Motors, China",
"LSJ": "MG Motor UK Limited - SAIC Motor, Shanghai, China",
"LSV": "Shanghai Volkswagen, China",
"LSY": "Brilliance Zhonghua",
"LTP": "National Electric Vehicle Sweden AB (NEVS)",
"LTV": "Toyota Tian Jin",
"LUC": "Guangqi Honda, China",
"LVS": "Ford Chang An",
"LVV": "Chery, China",
"LVZ": "Dong Feng Sokon Motor Company (DFSK)",
"LV3": "National Electric Vehicle Sweden AB (NEVS)",
"LZM": "MAN China",
"LZE": "Isuzu Guangzhou, China",
"LZG": "Shaanxi Automobile Group, China",
"LZP": "Zhongshan Guochi Motorcycle (Baotian)",
"LZY": "Yutong Zhengzhou, China",
"LZZ": "Chongqing Shuangzing Mech & Elec (Howo)",
"L4B": "Xingyue Group (motorcycles)",
"L5C": "KangDi (ATV)",
"L5K": "Zhejiang Yongkang Easy Vehicle",
"L5N": "Zhejiang Taotao, China (ATV & motorcycles)",
"L5Y": "Merato Motorcycle Taizhou Zhongneng",
"L85": "Zhejiang Yongkang Huabao Electric Appliance",
"L8X": "Zhejiang Summit Huawin Motorcycle",
// India
"MAB": "Mahindra & Mahindra",
"MAC": "Mahindra & Mahindra",
"MAJ": "Ford India",
"MAK": "Honda Siel Cars India",
"MAL": "Hyundai India",
"MAT": "Tata Motors",
"MA1": "Mahindra & Mahindra",
"MA3": "Suzuki India (Maruti)",
"MA6": "GM India",
"MA7": "Mitsubishi India (formerly Honda)",
"MB8": "Suzuki India Motorcycles",
"MBH": "Suzuki India (Maruti)",
"MBJ": "Toyota India",
"MBR": "Mercedes-Benz Indiav",
"MB1": "Ashok Leyland",
"MCA": "Fiat India",
"MCB": "GM India",
"MC2": "Volvo Eicher commercial vehicles limited.",
"MDH": "Nissan India",
"MD2": "Bajaj Auto",
"MD9": "Shuttle Cars India",
"MEC": "Daimler India Commercial Vehicles",
"MEE": "Renault India",
"MEX": "Volkswagen India",
// Asia
"MHF": "Toyota Indonesia",
"MHR": "Honda Indonesia",
"MLC": "Suzuki Thailand",
"NAA": "Iran Khodro (Peugeot Iran)",
"NAP": "Pars Khodro",
"MLH": "Honda Thailand",
"MMA": "Mitsubishi Thailand",
"MMB": "Mitsubishi Thailand",
"MMC": "Mitsubishi Thailand",
"MMM": "Chevrolet Thailand",
"MMS": "Suzuki Thailand",
"MMT": "Mitsubishi Thailand",
"MMU": "Holden Thailand",
"MM8": "Mazda Thailand",
"MNB": "Ford Thailand",
"MNT": "Nissan Thailand",
"MPA": "Isuzu Thailand",
"MP1": "Isuzu Thailand",
"MRH": "Honda Thailand",
"MR0": "Toyota Thailand",
"MS0": "SSS MOTORS Myanmar",
"MS3": "Suzuki Myanmar Motor Co.,Ltd.",
"NLA": "Honda Türkiye",
"NLE": "Mercedes-Benz Türk Truck",
"NLH": "Hyundai Assan",
"NLN": "Karsan",
"NLR": "OTOKAR",
"NLT": "TEMSA",
"NMB": "Mercedes-Benz Türk Buses",
"NMC": "BMC",
"NM0": "Ford Turkey",
"NM4": "Tofaş Türk",
"NMT": "Toyota Türkiye",
"NNA": "Isuzu Turkey",
"PE1": "Ford Philippines",
"PE3": "Mazda Philippines",
"PL1": "Proton, Malaysia",
"PNA": "NAZA, Malaysia (Peugeot)",
"R2P": "Evoke Electric Motorcycles HK",
"RA1": "Steyr Trucks International FZE, UAE",
"RFB": "Kymco, Taiwan",
"RFG": "Sanyang SYM, Taiwan",
"RFL": "Adly, Taiwan",
"RFT": "CPI, Taiwan",
"RF3": "Aeon Motor, Taiwan",
// UK
"SAB": "Optare",
"SAD": "Jaguar (F-Pace, I-Pace)",
"SAL": "Land Rover",
"SAJ": "Jaguar",
"SAR": "Rover",
"SAX": "Austin-Rover",
"SB1": "Toyota UK",
"SBM": "McLaren",
"SCA": "Rolls Royce",
"SCB": "Bentley",
"SCC": "Lotus Cars",
"SCE": "DeLorean Motor Cars N. Ireland (UK)",
"SCF": "Aston",
"SCK": "iFor Williams",
"SDB": "Peugeot UK (formerly Talbot)",
"SED": "General Motors Luton Plant",
"SEY": "LDV",
"SFA": "Ford UK",
"SFD": "Alexander Dennis UK",
"SHH": "Honda UK",
"SHS": "Honda UK",
"SJN": "Nissan UK",
"SKF": "Vauxhall",
"SLP": "JCB Research UK",
"SMT": "Triumph Motorcycles",
// Europe
"SUF": "Fiat Auto Poland",
"SUL": "FSC (Poland)",
"SUP": "FSO-Daewoo (Poland)",
"SU9": "Solaris Bus & Coach (Poland)",
"SUU": "Solaris Bus & Coach (Poland)",
"SWV": "TA-NO (Poland)",
"TCC": "Micro Compact Car AG (smart 1998-1999)",
"TDM": "QUANTYA Swiss Electric Movement (Switzerland)",
"TK9": "SOR buses (Czech Republic)",
"TMA": "Hyundai Motor Manufacturing Czech",
"TMB": "Škoda (Czech Republic)",
"TMK": "Karosa (Czech Republic)",
"TMP": "Škoda trolleybuses (Czech Republic)",
"TMT": "Tatra (Czech Republic)",
"TM9": "Škoda trolleybuses (Czech Republic)",
"TNE": "TAZ",
"TN9": "Karosa (Czech Republic)",
"TRA": "Ikarus Bus",
"TRU": "Audi Hungary",
"TSB": "Ikarus Bus",
"TSE": "Ikarus Egyedi Autobuszgyar, (Hungary)",
"TSM": "Suzuki Hungary",
"TW1": "Toyota Caetano Portugal",
"TYA": "Mitsubishi Trucks Portugal",
"TYB": "Mitsubishi Trucks Portugal",
"UU1": "Renault Dacia, (Romania)",
"UU2": "Oltcit",
"UU3": "ARO",
"UU4": "Roman SA",
"UU5": "Rocar",
"UU6": "Daewoo Romania",
"UU7": "Euro Bus Diamond",
"UU9": "Astra Bus",
"UZT": "UTB (Uzina de Tractoare Brașov)",
"U5Y": "Kia Motors Slovakia",
"U6Y": "Kia Motors Slovakia",
"VAG": "Magna Steyr Puch",
"VAN": "MAN Austria",
"VBK": "KTM (Motorcycles)",
"VCF": "Fisker GmbH",
"VF1": "Renault",
"VF2": "Renault",
"VF3": "Peugeot",
"VF4": "Talbot",
"VF6": "Renault (Trucks & Buses)",
"VF7": "Citroën",
"VF8": "Matra",
"VG5": "MBK (motorcycles)",
"VLU": "Scania France",
"VN1": "SOVAB (France)",
"VNE": "Irisbus (France)",
"VNK": "Toyota France",
"VNV": "Renault-Nissan",
"VSA": "Mercedes-Benz Spain",
"VSE": "Suzuki Spain (Santana Motors)",
"VSK": "Nissan Spain",
"VSS": "SEAT",
"VSX": "Opel Spain",
"VS6": "Ford Spain",
"VS7": "Citroën Spain",
"VS9": "Carrocerias Ayats (Spain)",
"VTH": "Derbi (motorcycles)",
"VTL": "Yamaha Spain (motorcycles)",
"VTT": "Suzuki Spain (motorcycles)",
"VV9": "TAURO Spain",
"VWA": "Nissan Spain",
"VWV": "Volkswagen Spain",
"VX1": "Zastava / Yugo Serbia",
// Germany - formerly West, former East has other codes
"WAG": "Neoplan",
"WAU": "Audi",
"WA1": "Audi SUV",
"WBA": "BMW",
"WBS": "BMW M",
"WBW": "BMW",
"WBY": "BMW",
"WDA": "Daimler",
"WDB": "Mercedes-Benz",
"WDC": "DaimlerChrysler",
"WDD": "Mercedes-Benz",
"WDF": "Mercedes-Benz (commercial vehicles)",
"WEB": "Evobus GmbH (Mercedes-Bus)",
"WJM": "Iveco Magirus",
"WF0": "Ford Germany",
"WKE": "Fahrzeugwerk Bernard Krone (truck trailers)",
"WKK": "Kässbohrer/Setra",
"WMA": "MAN Germany",
"WME": "smart",
"WMW": "MINI",
"WMX": "Mercedes-AMG",
"WP0": "Porsche",
"WP1": "Porsche SUV",
"WSM": "Schmitz-Cargobull (truck trailers)",
"W09": "RUF",
"W0L": "Opel",
"W0V": "Opel (since 2017)",
"WUA": "Audi Sport GmbH (formerly quattro GmbH)",
"WVG": "Volkswagen MPV/SUV",
"WVW": "Volkswagen",
"WV1": "Volkswagen Commercial Vehicles",
"WV2": "Volkswagen Bus/Van",
"WV3": "Volkswagen Trucks",
// Europe
"XLB": "Volvo (NedCar)",
"XLE": "Scania Netherlands",
"XLR": "DAF (trucks)",
"XMC": "Mitsubishi (NedCar)",
"XMG": "VDL Bus & Coach",
"XTA": "Lada/AvtoVAZ (Russia)",
"XTC": "KAMAZ (Russia)",
"XTH": "GAZ (Russia)",
"XTT": "UAZ/Sollers (Russia)",
"XTU": "Trolza (Russia)",
"XTY": "LiAZ (Russia)",
"XUF": "General Motors Russia",
"XUU": "AvtoTor (Russia, General Motors SKD)",
"XW8": "Volkswagen Group Russia",
"XWB": "UZ-Daewoo (Uzbekistan)",
"XWE": "AvtoTor (Russia, Hyundai-Kia SKD)",
"X1M": "PAZ (Russia)",
"X4X": "AvtoTor (Russia, BMW SKD)",
"X7L": "Renault AvtoFramos (Russia)",
"X7M": "Hyundai TagAZ (Russia)",
"YBW": "Volkswagen Belgium",
"YB1": "Volvo Trucks Belgium",
"YCM": "Mazda Belgium",
"YE2": "Van Hool (buses)",
"YH2": "BRP Finland (Lynx snowmobiles)",
"YK1": "Saab-Valmet Finland",
"YSC": "Cadillac (Saab)",
"YS2": "Scania AB",
"YS3": "Saab",
"YS4": "Scania Bus",
"YTN": "Saab NEVS",
"YU7": "Husaberg (motorcycles)",
"YVV": "Polestar (Volvo) (Sweden)",
"YV1": "Volvo Cars",
"YV4": "Volvo Cars",
"YV2": "Volvo Trucks",
"YV3": "Volvo Buses",
"Y3M": "MAZ (Belarus)",
"Y6D": "Zaporozhets/AvtoZAZ (Ukraine)",
"ZAA": "Autobianchi",
"ZAM": "Maserati",
"ZAP": "Piaggio/Vespa/Gilera",
"ZAR": "Alfa Romeo",
"ZBN": "Benelli",
"ZCG": "Cagiva SpA / MV Agusta",
"ZCF": "Iveco",
"ZDC": "Honda Italia Industriale SpA",
"ZDM": "Ducati Motor Holdings SpA",
"ZDF": "Ferrari Dino",
"ZD0": "Yamaha Italy",
"ZD3": "Beta Motor",
"ZD4": "Aprilia",
"ZFA": "Fiat",
"ZFC": "Fiat V.I.",
"ZFF": "Ferrari",
"ZGU": "Moto Guzzi",
"ZHW": "Lamborghini",
"ZJM": "Malaguti",
"ZJN": "Innocenti",
"ZKH": "Husqvarna Motorcycles Italy",
"ZLA": "Lancia",
"Z8M": "Marussia (Russia)",
// European small production
"VF9-795": "Bugatti",
"XL9-363": "Spyker",
"YT9-007": "Koenigsegg",
"YT9-034": "Carvia",
// USA
"1B3": "Dodge",
"1C3": "Chrysler",
"1C4": "Chrysler",
"1C6": "Chrysler",
"1D3": "Dodge",
"1FA": "Ford Motor Company",
"1FB": "Ford Motor Company",
"1FC": "Ford Motor Company",
"1FD": "Ford Motor Company",
"1FM": "Ford Motor Company",
"1FT": "Ford Motor Company",
"1FU": "Freightliner",
"1FV": "Freightliner",
"1F9": "FWD Corp.",
"1G*": "General Motors USA",
"1GC": "Chevrolet Truck USA",
"1GT": "GMC Truck USA",
"1G1": "Chevrolet USA",
"1G2": "Pontiac USA",
"1G3": "Oldsmobile USA",
"1G4": "Buick USA",
"1G6": "Cadillac USA",
"1G8": "Saturn USA",
"1GM": "Pontiac USA",
"1GY": "Cadillac USA",
"1H*": "Honda USA",
"1HD": "Harley-Davidson",
"1HT": "International Truck and Engine Corp. USA",
"1J4": "Jeep",
"1J8": "Jeep",
"1L*": "Lincoln USA",
"1ME": "Mercury USA",
"1M1": "Mack Truck USA",
"1M2": "Mack Truck USA",
"1M3": "Mack Truck USA",
"1M4": "Mack Truck USA",
"1M9": "Mynatt Truck & Equipment",
"1N*": "Nissan USA",
"1NX": "NUMMI USA",
"1P3": "Plymouth USA",
"1PY": "John Deere USA",
"1R9": "Roadrunner Hay Squeeze USA",
"1VW": "Volkswagen USA",
"1XK": "Kenworth USA",
"1XP": "Peterbilt USA",
"1YV": "Mazda USA (AutoAlliance International)",
"1ZV": "Ford (AutoAlliance International)",
// Canada
"2A4": "Chrysler Canada",
"2BP": "Bombardier Recreational Products",
"2B3": "Dodge Canada",
"2B7": "Dodge Canada",
"2C3": "Chrysler Canada",
"2CN": "CAMI",
"2D3": "Dodge Canada",
"2FA": "Ford Motor Company Canada",
"2FB": "Ford Motor Company Canada",
"2FC": "Ford Motor Company Canada",
"2FM": "Ford Motor Company Canada",
"2FT": "Ford Motor Company Canada",
"2FU": "Freightliner",
"2FV": "Freightliner",
"2FZ": "Sterling",
"2Gx": "General Motors Canada",
"2G1": "Chevrolet Canada",
"2G2": "Pontiac Canada",
"2G3": "Oldsmobile Canada",
"2G4": "Buick Canada",
"2G9": "mfr. of less than 1000/ yr. Canada",
"2HG": "Honda Canada",
"2HK": "Honda Canada",
"2HJ": "Honda Canada",
"2HM": "Hyundai Canada",
"2M*": "Mercury",
"2NV": "Nova Bus Canada",
"2P3": "Plymouth Canada",
"2T*": "Toyota Canada",
"2TP": "Triple E Canada LTD",
"2V4": "Volkswagen Canada",
"2V8": "Volkswagen Canada",
"2WK": "Western Star",
"2WL": "Western Star",
"2WM": "Western Star",
// Mexico and other North America
"3C4": "Chrysler Mexico",
"3C6": "RAM Mexico",
"3D3": "Dodge Mexico",
"3D4": "Dodge Mexico",
"3FA": "Ford Motor Company Mexico",
"3FE": "Ford Motor Company Mexico",
"3G*": "General Motors Mexico",
"3H*": "Honda Mexico",
"3JB": "BRP Mexico (all-terrain vehicles)",
"3MD": "Mazda Mexico",
"3MZ": "Mazda Mexico",
"3N*": "Nissan Mexico",
"3NS": "Polaris Industries USA",
"3NE": "Polaris Industries USA",
"3P3": "Plymouth Mexico",
"3VW": "Volkswagen Mexico",
// USA
"46J": "Federal Motors Inc. USA",
"4EN": "Emergency One USA",
"4F*": "Mazda USA",
"4JG": "Mercedes-Benz USA",
"4M*": "Mercury",
"4P1": "Pierce Manufacturing Inc. USA",
"4RK": "Nova Bus USA",
"4S*": "Subaru-Isuzu Automotive",
"4T*": "Toyota",
"4T9": "Lumen Motors",
"4UF": "Arctic Cat Inc.",
"4US": "BMW USA",
"4UZ": "Frt-Thomas Bus",
"4V1": "Volvo",
"4V2": "Volvo",
"4V3": "Volvo",
"4V4": "Volvo",
"4V5": "Volvo",
"4V6": "Volvo",
"4VL": "Volvo",
"4VM": "Volvo",
"4VZ": "Volvo",
"538": "Zero Motorcycles (USA)",
"5F(": "Honda USA-Alabama",
"5J*": "Honda USA-Ohio",
"5L*": "Lincoln",
"5N1": "Nissan USA",
"5NP": "Hyundai USA",
"5T*": "Toyota USA - trucks",
"5YJ": "Tesla, Inc.",
"56K": "Indian Motorcycle USA",
// Australia and Oceanea
"6AB": "MAN Australia",
"6F4": "Nissan Motor Company Australia",
"6F5": "Kenworth Australia",
"6FP": "Ford Motor Company Australia",
"6G1": "General Motors-Holden (post Nov 2002)",
"6G2": "Pontiac Australia (GTO & G8)",
"6H8": "General Motors-Holden (pre Nov 2002)",
"6MM": "Mitsubishi Motors Australia",
"6T1": "Toyota Motor Corporation Australia",
"6U9": "Privately Imported car in Australia",
// South America
"8AD": "Peugeot Argentina",
"8AF": "Ford Motor Company Argentina",
"8AG": "Chevrolet Argentina",
"8AJ": "Toyota Argentina",
"8AK": "Suzuki Argentina",
"8AP": "Fiat Argentina",
"8AW": "Volkswagen Argentina",
"8A1": "Renault Argentina",
"8GD": "Peugeot Chile",
"8GG": "Chevrolet Chile",
"8LD": "Chevrolet Ecuador",
"935": "Citroën Brazil",
"936": "Peugeot Brazil",
"93H": "Honda Brazil",
"93R": "Toyota Brazil",
"93U": "Audi Brazil",
"93V": "Audi Brazil",
"93X": "Mitsubishi Motors Brazil",
"93Y": "Renault Brazil",
"94D": "Nissan Brazil",
"9BF": "Ford Motor Company Brazil",
"9BG": "Chevrolet Brazil",
"9BM": "Mercedes-Benz Brazil",
"9BR": "Toyota Brazil",
"9BS": "Scania Brazil",
"9BW": "Volkswagen Brazil",
"9FB": "Renault Colombia",
"WB1": "BMW Motorrad of North America",
// TEMP
"1F1": "Fisker Inc.",
// TEMP
}
func LookupManufacturerByWmiCode(wmiCode string) string {
mfg, found := vinWmiCodeMap[wmiCode]
if !found {
var wmiWildcard = wmiCode[0:len(wmiCode)-1] + "*"
mfg, found = vinWmiCodeMap[wmiWildcard]
if !found {
mfg = wmiCode
}
}
return mfg
}