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