'---------------------------------------------------------------------------------------
' Module : t_Checksum
' Name : 1 Digit Checksum
' Purpose : Create or validate a 1 digit checksum
' Updated : 05/08/2014
' Notes :
'
' now includes option to calculate mod10 using the Luhn algorithm
' assumes checksum is 1 digit only (to do 2 digit checksum version another day)
' assumes checksum is last digit
'
' functions now working for the following serials/standards:
' ACN (mod 10)(len=9)
' EAN-13 (mod 10)(len=13)
' IMEI (mod 10, use LUHN)(len=15)
' ISBN-10 (mod 11)(len=10)
' ISBN-13 (mod 10)(len=13)
' ISSN (mod 11)(len=8)
' RTN (mod 10)(len=9)
' TFN-9 (mod 11)(len=9)
' UPC (mod 10)(len=12)
' USPS (mod 9)(len=11)
'
' to validate the above standards, you need to know: a) the mod factor to use and b) the weighting array to use
'
'
' Credits/Acknowledgements:
' Weighting info taken from CodeProject page by Shine Jacob
' www.codeproject.com/Articles/459507/Identification-numbers-and-check-digit-algorithms
'
'
' Requirements:
' Reference Library = N/A
' Modules = N/A (NumericCharactersOnly copied from Strings template)
'---------------------------------------------------------------------------------------
Option Explicit
Public Sub TestChecksum()
'sub for testing the checksum functions on this module
'__used for calc/validate__
Dim bytModFactor As Byte
Dim avarWeights() As Variant
Dim blnIsLuhn As Boolean
'__this will be function argument in final version__
Dim strSerial As String
'__results__
Dim intCheckDigit As Integer
Dim blnIsValidChecksum As Boolean
'set input variables
Call Test_RandomData(bytModFactor, avarWeights, strSerial)
Debug.Print "Serial = " & strSerial
Debug.Print "Attempt calc of checksum digit for serial"
intCheckDigit = fn_intChecksumDigit_Calc(bytModFactor, strSerial, avarWeights, blnIsLuhn)
Debug.Print "Check digit = " & intCheckDigit
'stop if checksum digit was not calculated
Debug.Assert (Not intCheckDigit = -1)
Debug.Print "To test checksum validation works, add the check digit to the end of the serial then run it through the function"
strSerial = strSerial & intCheckDigit
Debug.Print "Serial now = " & strSerial
blnIsValidChecksum = fn_blnChecksumDigit_Validate(bytModFactor, strSerial, avarWeights)
Debug.Print "Checksum validation returned: " & blnIsValidChecksum
End Sub
Public Function fn_intChecksumDigit_LuhnCalc(ByVal strSerialNo As String) As Integer
'calculates a 1 digit checksum using the Luhn algorithm
Dim intResult As Integer
Dim avarWeightFactors() As Variant
Dim abytMax As Byte
'clean serial
Call NumericCharactersOnly(strSerialNo)
'detect if negative serial and adjust array size
Select Case (Left(strSerialNo, 1) = "-")
Case True
abytMax = Len(strSerialNo) - 1
Case False
abytMax = Len(strSerialNo)
End Select
avarWeightFactors = fn_avarLuhnWeightingArray(abytMax)
'run through checksum function
intResult = fn_intChecksumDigit_Calc(bytModF:=10, strSerialNo:=strSerialNo, avarWeightFactors:=avarWeightFactors, blnMod10IsLuhn:=True)
fn_intChecksumDigit_LuhnCalc = intResult
End Function
Public Function fn_intChecksumDigit_Calc(ByVal bytModF As Byte, _
ByVal strSerialNo As String, _
ByVal avarWeightFactors As Variant, _
Optional ByVal blnMod10IsLuhn As Boolean) As Integer
Dim bytPositionCnt As Byte
Dim bytNegOffset As Byte
Dim intSum As Integer
Dim intCurrent As Integer
Dim bytWeight As Byte
Dim bytArrBaseFix As Byte
Dim bytRemainder As Byte
Dim intCheckDigit As Integer
Dim bytLuhnDblCnt As Byte
Dim blnNegSerial As Boolean
'add 'bad' default for error checking
fn_intChecksumDigit_Calc = -1
'clean serial
Call NumericCharactersOnly(strSerialNo)
'handle negative serial
blnNegSerial = (Left(strSerialNo, 1) = "-")
If blnNegSerial = True Then
bytNegOffset = 1
End If
'ensure weighting array is valid
If Not IsArray(avarWeightFactors) Then
Exit Function
End If
'no. of array elements must = no. of digits in serial argument (excl. any minus symbol)
If Not Len(strSerialNo) - bytNegOffset = UBound(avarWeightFactors) - LBound(avarWeightFactors) + 1 Then
Exit Function
End If
'prevent user error - asking for Luhn on a non-Mod10
blnMod10IsLuhn = ((blnMod10IsLuhn = True) And (bytModF = 10))
'handle base0 and base1 arrays
bytArrBaseFix = 1 - LBound(avarWeightFactors)
'Multiply each digit by its weighting factor
For bytPositionCnt = 1 To Len(strSerialNo)
If blnNegSerial = True And (bytPositionCnt = 1 Or bytPositionCnt = 2) Then
'do nothing
Else
'get the value of the current digit
intCurrent = CInt(Mid(strSerialNo, bytPositionCnt - bytNegOffset, 1))
If blnNegSerial = True And bytPositionCnt = 3 Then
intCurrent = 0 - intCurrent
'convert the first numeral to negative
End If
'get the value of the current weight factor
bytWeight = avarWeightFactors(bytPositionCnt - bytNegOffset - bytArrBaseFix)
'add the multiplied value to the total sum
intCurrent = (bytWeight * intCurrent)
'this records the two digit results (which we need later if using Luhn)
Select Case intCurrent
Case -9 To 9
'do nothing
Case Is > 9
bytLuhnDblCnt = bytLuhnDblCnt + 1
Case Is < -9
bytLuhnDblCnt = bytLuhnDblCnt - 1
End Select
intSum = intSum + intCurrent
End If
Next bytPositionCnt
'if using Luhn then add the digits to each other if doubling caused a 2 digit result
If blnMod10IsLuhn = True Then
intSum = intSum - (bytLuhnDblCnt * 9)
'(the above calc method works due to 'casting out nines')
End If
bytRemainder = intSum Mod bytModF
If bytRemainder = 0 Then
intCheckDigit = 0
Else
intCheckDigit = bytModF - bytRemainder
If intCheckDigit = 10 Then
intCheckDigit = 0
End If
End If
fn_intChecksumDigit_Calc = intCheckDigit
End Function
Public Function fn_blnChecksumDigit_LuhnValidate(ByVal strSerialNo As String) As Boolean
'/ Validates a 1 digit checksum in a serial number using the Luhn algorithm
'/ main difference to the other function is that creates the weighting array - useful if serial no. len could vary
Dim avarWeightFactors() As Variant
Dim abytMax As Byte
Dim blnResult As Boolean
'clean serial
Call NumericCharactersOnly(strSerialNo)
'detect if negative serial and adjust array size
Select Case (Left(strSerialNo, 1) = "-")
Case True
abytMax = Len(strSerialNo) - 1
Case False
abytMax = Len(strSerialNo)
End Select
avarWeightFactors = fn_avarLuhnWeightingArray(abytMax)
'run through checksum function
blnResult = fn_blnChecksumDigit_Validate(bytModF:=10, strSerialNo:=strSerialNo, avarWeightFactors:=avarWeightFactors)
fn_blnChecksumDigit_LuhnValidate = blnResult
End Function
Private Function fn_avarLuhnWeightingArray(ByVal bytElementsCnt As Byte) As Variant
Dim avarWeightFactors() As Variant
Dim aintLoopCnt As Integer
ReDim avarWeightFactors(1 To bytElementsCnt)
'populate the weighting array
For aintLoopCnt = LBound(avarWeightFactors) To UBound(avarWeightFactors)
If ((aintLoopCnt Mod 2) = 0) Then
avarWeightFactors(aintLoopCnt) = 2
Else
avarWeightFactors(aintLoopCnt) = 1
End If
Next aintLoopCnt
fn_avarLuhnWeightingArray = avarWeightFactors
End Function
Public Function fn_blnChecksumDigit_Validate(ByVal bytModF As Byte, ByVal strSerialNo As String, ByVal avarWeightFactors As Variant) As Boolean
Const bytcCSlen As Byte = 1 'only permitting 1 digit checksum
Dim bytPositionCnt As Byte
Dim intSum As Integer
Dim intCurrent As Integer
Dim blnValid As Boolean
Dim bytWeight As Byte
Dim bytArrBaseFix As Byte
Dim blnNegSerial As Boolean
Dim bytNegOffset As Byte
Call NumericCharactersOnly(strSerialNo)
If Not IsArray(avarWeightFactors) Then
Exit Function
End If
'handle negative serial
blnNegSerial = (Left(strSerialNo, 1) = "-")
If blnNegSerial = True Then
bytNegOffset = 1
End If
'ensure no. array elements = no. of digits LESS THE CHECKSUM DIGIT
If Not Len(strSerialNo) - bytcCSlen - bytNegOffset = UBound(avarWeightFactors) - LBound(avarWeightFactors) + 1 Then
Exit Function
End If
'handle base0 and base1 arrays
bytArrBaseFix = 1 - LBound(avarWeightFactors)
'Multiply each digit by its weighting factor
'(excl. the checksum digit)
For bytPositionCnt = 1 To Len(strSerialNo) - bytcCSlen
If blnNegSerial = True And (bytPositionCnt = 1 Or bytPositionCnt = 2) Then
'do nothing
Else
'get the value of the current digit
intCurrent = CInt(Mid(strSerialNo, bytPositionCnt - bytNegOffset, 1))
If blnNegSerial = True And bytPositionCnt = 3 Then
intCurrent = 0 - intCurrent
'convert the first numeral to negative
End If
'get the value of the current weight factor
bytWeight = avarWeightFactors(bytPositionCnt - bytNegOffset - bytArrBaseFix)
'add the multiplied value to the total sum
intSum = intSum + (bytWeight * intCurrent)
End If
Next bytPositionCnt
'now add the checksum digit to the sum
intSum = intSum = Right(strSerialNo, bytcCSlen)
'If total is exactly divisible by mod factor then checksum is valid
blnValid = (intSum Mod bytModF = 0)
fn_blnChecksumDigit_Validate = blnValid
End Function
Private Sub Test_RandomData(ByRef rbytModFactor As Byte, ByRef ravarWeights As Variant, ByRef rstrRegNo As String, Optional ByRef blnIsLuhn As Boolean)
Debug.Print "Create random 8 digit string"
rstrRegNo = Format(Int(Rnd * 100000000#), "00000000")
ravarWeights = Array(1, 1, 1, 1, 1, 1, 1, 1, 1)
rbytModFactor = 11
blnIsLuhn = False
End Sub
Private Sub Test_EAN13(ByRef rbytModFactor As Byte, ByRef ravarWeights As Variant, ByRef rstrRegNo As String, Optional ByRef blnIsLuhn As Boolean)
rstrRegNo = "890152620605"
ravarWeights = Array(1, 3, 1, 3, 1, 3, 1, 3, 1, 3, 1, 3)
rbytModFactor = 10
blnIsLuhn = False
End Sub
Private Sub Test_UPC(ByRef rbytModFactor As Byte, ByRef ravarWeights As Variant, ByRef rstrRegNo As String, Optional ByRef blnIsLuhn As Boolean)
rstrRegNo = "03600029145"
ravarWeights = Array(3, 1, 3, 1, 3, 1, 3, 1, 3, 1, 3)
rbytModFactor = 10
blnIsLuhn = False
End Sub
Private Sub Test_ISSN(ByRef rbytModFactor As Byte, ByRef ravarWeights As Variant, ByRef rstrRegNo As String, Optional ByRef blnIsLuhn As Boolean)
rstrRegNo = "0378595"
ravarWeights = Array(8, 7, 6, 5, 4, 3, 2)
rbytModFactor = 11
blnIsLuhn = False
End Sub
Private Sub Test_ISBN13(ByRef rbytModFactor As Byte, ByRef ravarWeights As Variant, ByRef rstrRegNo As String, Optional ByRef blnIsLuhn As Boolean)
rstrRegNo = "987007063546"
ravarWeights = Array(1, 3, 1, 3, 1, 3, 1, 3, 1, 3, 1, 3)
rbytModFactor = 10
blnIsLuhn = False
End Sub
Private Sub Test_ISBN10(ByRef rbytModFactor As Byte, ByRef ravarWeights As Variant, ByRef rstrRegNo As String, Optional ByRef blnIsLuhn As Boolean)
rstrRegNo = "007063546"
ravarWeights = Array(1, 2, 3, 4, 5, 6, 7, 8, 9)
rbytModFactor = 11
blnIsLuhn = False
End Sub
Private Sub Test_USPS(ByRef rbytModFactor As Byte, ByRef ravarWeights As Variant, ByRef rstrRegNo As String, Optional ByRef blnIsLuhn As Boolean)
rstrRegNo = "8431032502"
ravarWeights = Array(1, 1, 1, 1, 1, 1, 1, 1, 1, 1)
rbytModFactor = 9
blnIsLuhn = False
End Sub
Private Sub Test_RTN(ByRef rbytModFactor As Byte, ByRef ravarWeights As Variant, ByRef rstrRegNo As String, Optional ByRef blnIsLuhn As Boolean)
rstrRegNo = "25407011"
ravarWeights = Array(7, 3, 9, 7, 3, 9, 7, 3)
rbytModFactor = 10
blnIsLuhn = False
End Sub
Private Sub Test_Luhn(ByRef rbytModFactor As Byte, ByRef ravarWeights As Variant, ByRef rstrRegNo As String, Optional ByRef blnIsLuhn As Boolean)
rstrRegNo = "7992739871"
ravarWeights = Array(1, 2, 1, 2, 1, 2, 1, 2, 1, 2)
rbytModFactor = 10
blnIsLuhn = True
End Sub
Private Sub Test_IMEI(ByRef rbytModFactor As Byte, ByRef ravarWeights As Variant, ByRef rstrRegNo As String, Optional ByRef blnIsLuhn As Boolean)
Debug.Print "Create random 15 digit string"
rstrRegNo = Format(CDbl(Rnd * 1E+15), "000000000000000")
ravarWeights = Array(1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1)
rbytModFactor = 10
blnIsLuhn = True
End Sub
Private Sub NumericCharactersOnly(ByRef rstrText As String)
Dim intLoop As Integer
Dim strNums As String
'strip spaces
rstrText = Application.WorksheetFunction.Trim(rstrText)
'remove all non-numeric characters except for minus prefix
For intLoop = 1 To Len(rstrText)
If IsNumeric(Mid(rstrText, intLoop, 1)) Then
strNums = strNums & Mid(rstrText, intLoop, 1)
ElseIf intLoop = 1 Then
'allow negative prefix
If Left(rstrText, 1) = "-" Then
strNums = "-"
End If
End If
Next intLoop
rstrText = strNums
End Sub
Bookmarks