Skip to content

Instantly share code, notes, and snippets.

@heetbeet
Created March 17, 2023 07:24
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save heetbeet/e319701ee9c27888606bea270a0a35bc to your computer and use it in GitHub Desktop.
Save heetbeet/e319701ee9c27888606bea270a0a35bc to your computer and use it in GitHub Desktop.
VBA test valid South African ID

USER

Tell me how to check for valid South african ID numbers. Are there checksums? It starts with yymmdd so at least you know that 009999 is an invalid start. Write a list of all things that can be considered

ASSISTANT

Valid South African ID numbers follow a specific format and contain a few checks that can be used to determine their validity. Here is a list of things you can consider when checking the validity of a South African ID number:

  1. Length: A valid South African ID number should be 13 digits long.

  2. Birthdate: The first six digits should represent the birthdate of the person in the format YYMMDD. This means that an ID number starting with "009999" would be invalid as you mentioned.

  3. Gender: The 7th to 9th digits indicate the gender of the person. A value of 0000-4999 represents a female, while 5000-9999 represents a male.

  4. Citizenship: The 10th digit represents citizenship. A value of 0 indicates a South African citizen, while 1 indicates a permanent resident.

  5. 11th and 12th digits: These are sequential numbers for applicants with the same birthdate and gender.

  6. Checksum: The 13th digit is a checksum calculated using the Luhn algorithm. To verify the checksum:

    a. Double the value of every second digit from the right (i.e., the 2nd, 4th, 6th, 8th, 10th, and 12th digits). b. If doubling any digit results in a two-digit number, add the two digits together to get a single-digit result. c. Add the results from step (b) together with the remaining (non-doubled) digits. d. Calculate the remainder when the sum from step (c) is divided by 10. e. If the remainder is 0, the checksum digit should be 0. Otherwise, the checksum digit should be 10 minus the remainder.

For example, consider the ID number 8001015009087:

  1. Length: 13 digits
  2. Birthdate: 1980-01-01
  3. Gender: Male (5009)
  4. Citizenship: South African citizen (0)
  5. 11th and 12th digits: 09
  6. Checksum: (8+0+0+2+1+2+5+1+0+18+0+16) % 10 = 53 % 10 = 3; Checksum = 10 - 3 = 7; Matches the last digit, so it's valid.

By checking each of these components, you can determine the validity of a South African ID number.

USER

Here is a Python implementation. Write this into a single VBA function with lots of comments:

from enum import Enum, auto

RSA_ID_LENGTH = 13
DATE_OF_BIRTH_FORMAT = "%y%m%d"
GENDER_FEMALE_MIN = 0
GENDER_FEMALE_MAX = 4999
GENDER_MALE_MIN = 5000
GENDER_MALE_MAX = 9999
SA_CITIZEN_DIGIT = "0"
PERMANENT_RESIDENT_DIGIT = "1"
RACE_DIGIT = "8"  # used until 1980s


class Gender(Enum):
    FEMALE = auto()
    MALE = auto()


class Citizenship(Enum):
    SA_CITIZEN = auto()
    PERMANENT_RESIDENT = auto()

===============================================

import logging
import re
from datetime import datetime

from .constants import (
    DATE_OF_BIRTH_FORMAT,
    GENDER_FEMALE_MAX,
    GENDER_FEMALE_MIN,
    PERMANENT_RESIDENT_DIGIT,
    RSA_ID_LENGTH,
    SA_CITIZEN_DIGIT,
    Citizenship,
    Gender,
)
from .random import generate

__version__ = "0.0.3"

__all__ = ["Gender", "Citizenship", "IdNumber", "parse", "generate"]

logger = logging.getLogger(__name__)


class IdNumber:
    def __init__(self, value: str):
        self.value = value
        self.error = None
        self.date_of_birth = None
        self.gender = None
        self.citizenship = None
        self.parse()

    def clean(self):
        """Return the value without any whitespace."""
        return re.sub(r"\s", "", self.value or "")

    def parse(self):
        """Parse the value and validate against the RSA ID number format."""
        self.error = None
        self.date_of_birth = None
        self.gender = None
        self.citizenship = None

        value = self.clean()
        if not value:
            self.error = f"'{value}' is not a valid RSA ID number!"
            return

        if not value.isdigit():
            self.error = f"'{value}' contains non-digit characters!"
            return

        if len(value) != RSA_ID_LENGTH:
            self.error = f"'{value}' is not {RSA_ID_LENGTH} digits!"
            return

        year = value[0:2]
        month = value[2:4]
        day = value[4:6]
        try:
            self.date_of_birth = datetime.strptime(
                f"{year}{month}{day}", DATE_OF_BIRTH_FORMAT
            )

            if self.date_of_birth > datetime.now():
                correct_year = self.date_of_birth.year - 100

                self.date_of_birth = self.date_of_birth.replace(
                    year=correct_year
                )
        except ValueError:
            self.error = f"'{value}' contains an invalid date of birth!"
            logger.debug(self.error, exc_info=True)
            return

        gender = int(value[6:10])
        if gender >= GENDER_FEMALE_MIN and gender <= GENDER_FEMALE_MAX:
            self.gender = Gender.FEMALE
        else:
            self.gender = Gender.MALE

        citizenship = value[10]
        if citizenship == SA_CITIZEN_DIGIT:
            self.citizenship = Citizenship.SA_CITIZEN
        elif citizenship == PERMANENT_RESIDENT_DIGIT:
            self.citizenship = Citizenship.PERMANENT_RESIDENT
        else:
            self.error = f"Invalid citizenship indicator: '{citizenship}'!"
            return

        digits = [int(d) for d in value]
        digits.reverse()
        sum = 0
        for index, digit in enumerate(digits):
            if (index + 1) % 2 == 0:
                digit = digit * 2
                if digit > 9:
                    digit = digit - 9
            sum = sum + digit

        if not sum % 10 == 0:
            self.error = f"'{value}' contains an invalid checksum digit!"
            return

    @property
    def valid(self) -> bool:
        """Return True if there is not error, False otherwise."""
        return not self.error

    def __repr__(self):
        return self.clean()


def parse(value: str, raise_exc: bool = True) -> IdNumber:
    """Parse `value` and validate against the RSA ID number format.
    Args:
        value: ID number string to parse and validate.
    Returns:
        A new `IdNumber` instance.
    Raises:
        ValueError: If the ID number is invalid and `raise_exc` is True.
    Examples:
        >>> import rsaidnumber
        >>> value = '1234567890123'
        >>> id_number = rsaidnumber.parse(value)
    """
    id_number = IdNumber(value)
    id_number.parse()
    if not id_number.valid and raise_exc:
        raise ValueError(id_number.error)
    return id_number

ASSISTANT

Here's a single VBA function implementing the provided Python code. Please note that VBA does not support Enums, so the Gender and Citizenship Enums are replaced with their respective string values:

Function IsValidSouthAfricanID(IDNumber As String) As Boolean
    Const ID_LENGTH As Integer = 13
    Const GENDER_FEMALE_MIN As Integer = 0
    Const GENDER_FEMALE_MAX As Integer = 4999
    Const GENDER_MALE_MIN As Integer = 5000
    Const GENDER_MALE_MAX As Integer = 9999
    Const SA_CITIZEN_DIGIT As String = "0"
    Const PERMANENT_RESIDENT_DIGIT As String = "1"
    
    Dim IDValue As String
    Dim IDLength As Integer
    Dim dob As String
    Dim genderValue As Integer
    Dim citizenship As String
    
    ' Clean and check input
    IDValue = Replace(IDNumber, " ", "")
    IDLength = Len(IDValue)
    
    If IDLength <> ID_LENGTH Then
        IsValidSouthAfricanID = False
        Exit Function
    End If
    
    ' Check date of birth
    dob = Left(IDValue, 6)
    If Not IsDate(Format(dob, "00\/00\/00")) Then
        IsValidSouthAfricanID = False
        Exit Function
    End If
    
    ' Check gender
    genderValue = Mid(IDValue, 7, 4)
    If genderValue < GENDER_FEMALE_MIN Or genderValue > GENDER_MALE_MAX Then
        IsValidSouthAfricanID = False
        Exit Function
    End If
    
    ' Check citizenship
    citizenship = Mid(IDValue, 11, 1)
    If citizenship <> SA_CITIZEN_DIGIT And citizenship <> PERMANENT_RESIDENT_DIGIT Then
        IsValidSouthAfricanID = False
        Exit Function
    End If
    
    ' Check Luhn checksum
    Dim i As Integer
    Dim currentDigit As Integer
    Dim doubledDigit As Integer
    Dim total As Integer
    total = 0
    For i = ID_LENGTH To 1 Step -1
        currentDigit = Mid(IDValue, i, 1)
        If (ID_LENGTH - i) Mod 2 = 1 Then
            doubledDigit = currentDigit * 2
            If doubledDigit > 9 Then
                doubledDigit = doubledDigit - 9
            End If
            total = total + doubledDigit
        Else
            total = total + currentDigit
        End If
    Next i
    
    IsValidSouthAfricanID = (total Mod 10 = 0)
End Function

This VBA function takes an ID number as input and returns True if the ID number is valid, otherwise it returns False. It checks for length, date of birth, gender, citizenship, and Luhn checksum.

USER

is this a valid ID? 8905141372087

ASSISTANT

To check if the given South African ID number (8905141372087) is valid, let's go through the checks:

  1. Length: 13 digits
  2. Birthdate: 1989-05-14
  3. Gender: Female (1372 falls between 0000 and 4999)
  4. Citizenship: South African citizen (0)
  5. 11th and 12th digits: 20
  6. Checksum: a. Doubled digits: 12=2, 02=0, 72=14 (1+4=5), 12=2, 92=18 (1+8=9), 82=16 (1+6=7) b. Sum of the results with the non-doubled digits: 2+8+0+5+1+4+2+1+9+3+7 = 42 c. Calculate the remainder when the sum is divided by 10: 42 % 10 = 2 d. Checksum digit: 10 - 2 = 8

The checksum digit (8) matches the last digit of the given ID number (8905141372087). Therefore, this ID number is valid.

@heetbeet
Copy link
Author

I basically copied the Python content from
https://github.com/teamgeek-io/rsa-id-number/blob/develop/src/rsaidnumber/constants.py
and
https://github.com/teamgeek-io/rsa-id-number/blob/develop/src/rsaidnumber/constants.py

Interesting enough, ChatGPT provided correct code for validating the invalid ID "8905141372087" as invalid, but when trying to validate it itself, it botched the checksum check and suggested the ID is valid.

@heetbeet
Copy link
Author

heetbeet commented Mar 17, 2023

For my purposes, I changed the function a bit also to handle Excel's greedy type conversion for strings that look like numbers (as a result, the first "0" digit entries are removed). I suggest using ChatGPT's version since this is just a purpose-specific band-aid.

Function IsValidSouthAfricanID(IDNumber As Variant) As Boolean
    ' Adapted from https://github.com/teamgeek-io/rsa-id-number
    
    Const ID_LENGTH As Integer = 13
    Const GENDER_FEMALE_MIN As Integer = 0
    Const GENDER_FEMALE_MAX As Integer = 4999
    Const GENDER_MALE_MIN As Integer = 5000
    Const GENDER_MALE_MAX As Integer = 9999
    Const SA_CITIZEN_DIGIT As String = "0"
    Const PERMANENT_RESIDENT_DIGIT As String = "1"
    
    Dim IDValue As String
    Dim IDLength As Integer
    Dim dob As String
    Dim genderValue As Integer
    Dim citizenship As String
    
    ' Convert the input to a string
    IDValue = CStr(IDNumber)
    
    ' Pad the input with zeros if it's less than the required length
    If VarType(IDNumber) <> vbString Then
        While Len(IDValue) < ID_LENGTH
            IDValue = "0" & IDValue
        Wend
    End If
    
    IDLength = Len(IDValue)
    
    If IDLength <> ID_LENGTH Then
        IsValidSouthAfricanID = False
        Exit Function
    End If
    
    ' Check date of birth
    dob = Left(IDValue, 6)
    If Not IsDate(Format(dob, "00\/00\/00")) Then
        IsValidSouthAfricanID = False
        Exit Function
    End If
    
    ' Check gender
    genderValue = Mid(IDValue, 7, 4)
    If genderValue < GENDER_FEMALE_MIN Or genderValue > GENDER_MALE_MAX Then
        IsValidSouthAfricanID = False
        Exit Function
    End If
    
    ' Check citizenship
    citizenship = Mid(IDValue, 11, 1)
    If citizenship <> SA_CITIZEN_DIGIT And citizenship <> PERMANENT_RESIDENT_DIGIT Then
        IsValidSouthAfricanID = False
        Exit Function
    End If
    
    ' Check Luhn checksum
    Dim i As Integer
    Dim currentDigit As Integer
    Dim doubledDigit As Integer
    Dim total As Integer
    total = 0
    For i = ID_LENGTH To 1 Step -1
        currentDigit = Mid(IDValue, i, 1)
        If (ID_LENGTH - i) Mod 2 = 1 Then
            doubledDigit = currentDigit * 2
            If doubledDigit > 9 Then
                doubledDigit = doubledDigit - 9
            End If
            total = total + doubledDigit
        Else
            total = total + currentDigit
        End If
    Next i
    
    IsValidSouthAfricanID = (total Mod 10 = 0)
End Function

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment