15 1 0 4000 1 https://codeblock.co.za 300 true 0
theme-sticky-logo-alt
How to Validate a South African ID Number With PHP

How to Validate a South African ID Number With PHP

8 Comments

If you have a website or app that requires users to submit their South African identity number, you will definitely want to make sure it’s valid. The South African ID is validated by matching the following criteria.

  • The total number of digits must be 13.
  • The format is [YYMMDD] [GGGG] [C] [A] [Z]
    • [YYMMDD] for the ID holder’s date of birth
    • [GGGG] for ID holder’s gender. 0000 – 4999 for female and 5000 – 9999 for male.
    • [C] is their citizenship status. 0 is a South African Citizen and 1 is a permanent resident.
    • [A] can be any number but, at the time of writing this post, is usually an 8.
    • [Z] is a check digit which uses the Luhn Algorithm to validate the rest of the ID number.

Try a working version of a plugin I built with this code.

Prerequisites

  • A basic understanding of PHP

Since the check digit is the tricky part, we’ll leave that for last, so let’s begin with our basic function. I’m going to use the example ID number: 8001015009087.

<?php
function verify_id_number($id_number, $gender = '', $foreigner = 0) {

	$validated = false;

        // Validation code will go here

	return $validated;

}

The verify_id_number() function will accept three parameters: a required ($id_number) and another two, $gender and $foreigner, which are optional. If you prefer to make them required, you may remove their values. You may be wondering why I declared the last variable as $foreigner instead of $citizen, but that will make sense when we get to validating citizenship.

Inside the function, I’ve declared a $validated variable and given it a value of false. When the function is called, it will return $validated, which at this time is false.

Validate the Length of the ID Number

The first check is to ensure the ID number is exactly 13 digits long using PHP’s built-in strlen() function. We also know that the ID number has to be numbers so we’ll validate that as well using is_numeric().

<?php
if (is_numeric($id_number) && strlen($id_number) === 13) {
   $errors = false;
   // The rest of the conditional statements will go in here
}

Inside the if statement, I’ve declared the $errors variable which is set to false. When we find an error, we’ll set it to true and by the end of all the checks, if it’s still false and hasn’t been changed by an error, we’ll make sure $validated is set to true and pass validation.

Convert the ID Number to an Array

For the next parts, I’m going to turn the ID number into an array. It will make it easier to select the parts we need. To do this, we can use another built-in PHP function called str_split().

<?php
$num_array = str_split($id_number);

If we print_r(), we’ll get our array with indices numbered 0 through 12. Like this:

Array (
[0] => 8
[1] => 0
[2] => 0
[3] => 1
[4] => 0
[5] => 1
[6] => 5
[7] => 0
[8] => 0
[9] => 9
[10] => 0
[11] => 8
[12] => 7
)

Now we can access the indices much easier.

Validate The ID Holder’s Date of Birth

To ensure the date of birth is valid, we can use some general knowledge.

  • We know the maximum amount of days in a month have to be 31 and none of them can be 0.
  • We know there are only 12 months in a year and none of them can be 0.

I’m not going to validate the year. Because the ID number is only two digits, this could pose a problem for ID holders aged 100 or more. Maybe your site or app only allows users between a specific age range in which case you may want to validate the year.

<?php
// Validate the day and month

$id_month = $num_array[2] . $num_array[3];

$id_day = $num_array[4] . $num_array[5];

if ( $id_month < 1 || $id_month > 12) {
   $errors = true;
}

if ( $id_day < 1 || $id_day > 31) {
   $errors = true;
}

The above code concatenated the array indices 2 and 3 to define the $id_month while the $id_day is a concatenation of indices 4 and 5 ie. 80 [01] [01] 5009087 in our example ID number.

If the month is not a number between 1 and 12, or if the day is not between 1 and 31, validation fails.

Validate Expected Gender vs Submitted Gender

In $num_array, indices 6 to 9 are used to validate gender, but we only need to use index 6. In the following code, I’m using a ternary if statement to assign a ‘male‘ or ‘female‘ value. ‘Male’ if the value is 5 or more, ‘female’ if not.

With $id_gender defined, I check that there is a value set in the function’s $gender parameter, and if there is, I check that it matches $id_gender.

<?php
// Validate gender

$id_gender = $num_array[6] >= 5 ? 'male' : 'female';

if ($gender && strtolower($gender) !== $id_gender) {

	$errors = true;

}

If it’s not a match, $errors is set to true and validation fails.

Validate South African Citizenship

The South African ID number uses the number 0 to classify a South African citizen and the number 1 to classify the ID holder as a permanent resident while in programming, 0 is false and 1 is true. For that reason, I’ve chosen to use the $foreigner variable. ie. $foreigner = 0 (false) means that this person is a citizen.

$id_foreigner inherits the value set in the 11th position (index 10) of our $num_array.

Next, we check whether $foreigner or $id_foreigner are true. (Note: I’m assuming that the function’s $foreigner parameter will get data submitted via a checkbox. If the checkbox is checked, the submitted value will be 1. In PHP, any value other than 0 is true). If either $foreigner or $id_foreigner are true, we also check that they match. If they do, it will validate. Notice, I’ve explicitly converted the strings to integers using (int) to ensure they are numbers. We don’t need an else statement. If both values are 0 (empty / false) they match by default.

<?php
// Validate citizenship

// citizenship as per id number
$id_foreigner = $num_array[10];

// citizenship as per submission
if ( ( $foreigner || $id_foreigner ) && (int)$foreigner !== (int)$id_foreigner ) {

   $errors = true;

}

Now for the more tricky part…

Validating the ID Number Check Digit With PHP

The validation process for the South African ID check digit contains quite a few rules. It uses the Luhn Algotrithm and goes something like this:

  1. All digits in odd positions (excluding the check digit) must be added together.
    • From our example ID number: [8] 0 [0] 1 [0] 1 [5] 0 [0] 9 [0] 8 7
    • 8 + 0 + 0 + 5 + 0 + 0 which equals 13.
  2. All digits in even positions must be concatenated to form a 6 digit number. This 6 digit number must then be multiplied by 2.
    • From our example ID number: 8 [0] 0 [1] 0 [1] 5 [0] 0 [9] 0 [8] 7
    • 011098 x 2 equals 22196.
  3. Add all the digits from the result of 2. ie (22196).
    • 2 + 2 + 1 + 9 + 6 = 20.
  4. Add the answer from 1 to the answer in 3. (13 and 20).
    • 13 + 20 = 33.
  5. Grab the answer from 4 and subtract the last digit from 10. 3[3].
    • 10 – 3 = 7.
  6. If the answer from 5 is one digit, that should be the same as the check digit. If it’s more than one digit, the last digit of the answer from 5 should the same as the check digit.

So let’s code this mofo!

Move All Digits in Even and Odd Positions Into Separate Arrays

<?php
// Declare the arrays
$even_digits = array();
$odd_digits = array();

// Loop through modified $num_array, storing the keys and their values in the above arrays
foreach ( $num_array as $index => $digit) {

      if ($index === 0 || $index % 2 === 0) {

	  $odd_digits[] = $digit;
		       
      }
      else {

	  $even_digits[] = $digit;

      }

}

Here, I’ve declared two arrays to store the even and odd numbers, ($even_digits and $odd_digits). Then we loop through the digits in the ID number ($num_array) and if the digit’s key is 0 or divisible by 2 with no remainder (%), it’s in an odd position so we push it to the $odd_digits array. (Because arrays start at position 0, we have to include that position specifically, and all subsequent keys with an odd number are actually in even positions. For example, array[1] is in position 2 and array[2] is in position 3 and so on).

Pop the Check Digit and Add the Odds

Next up, I’m going to use a built-in PHP function to take care of two things. Remember, the check digit must be excluded when we add up the odd digits, so we first we need to remove it from the odds array. While we’re at it, we can store that check digit we’ve popped in a variable. PHP’s array_pop() can do both of these at once.

<?php
// use array pop to remove the last digit from $odd_digits and store it in $check_digit
$check_digit = array_pop($odd_digits);

Now we can add the remaining digits in the modified $odd_digits array using the built-in PHP function, array_sum().

<?php
//All digits in odd positions (excluding the check digit) must be added together.
$added_odds = array_sum($odd_digits);

Concatenate the Evens to Create a 6 Digit Number and Multiply it by 2

To concatenate the numbers into a string, we can use the PHP implode() function. implode() accepts two parameters which include a string that separates each value and the array to search. I don’t want the numbers separated, so the first parameter is empty. After that I multiply the $concatenated_evens by 2.

<?php
//All digits in even positions must be concatenated to form a 6 digit number.
$concatenated_evens = implode('', $even_digits);

//This 6 digit number must then be multiplied by 2.
$evensx2 = $concatenated_evens * 2;

Add The Digits In The Concatenated Evens

Similarly to what we did with the odd numbers, I’m going to convert this new number ($evensx2) into an array using str_split(), then use array_sum() so as to calculate the total of the all the numbers. This time I’ll do it in one line, like so:

<?php
// Add all the numbers produced from the even numbers x 2
$added_evens = array_sum( str_split($evensx2) );

Add the Sum of Odds to the Sum of the Evens

$added_evens isn’t really the sum of evens, but for simplicity, we’ll call it that. So let’s tally up the numbers and finalise everything.

<?php
// get the last digit of the $sum
$last_digit = substr($sum, -1);

/* 10 - $last_digit
* $verify_check_digit = 10 - (int)$last_digit; (Will break if $last_digit = 0)
* Or as suggested by Ruan Luies
* verify check digit is the resulting remainder of 10 minus the last digit divided by 10
*/
$verify_check_digit = (10 – (int)$last_digit) % 10;

// test expected last digit against the last digit in $id_number submitted
if ((int)$verify_check_digit !== (int)$check_digit) {
  $errors = true;
}

// if errors haven't been set to true by any one of the checks, we can change verified to true;
if (!$errors) {
  $validated = true;
}

As you can see from the above code, I’ve used the PHP substr() function which returns a part of a string. Using -1, we go back one space to get the last digit from our $sum and store that in $last_digit.

After that, we subtract that last digit from 10 or as suggested by Ruan Luies, instead of simply subtracting the last digit, we should check the remainder of 10 minus the last digit divided by 10. This ensures that if the last digit is 0, we don’t end up with 10 again, in which case validation will fail. Thanks Ruan.

Next we check that the expected last digit calculated from the other numbers in $id_number match the actual last digit in $id_number. If they don’t match, $errors will once again be set to true. Notice that I’ve explicitly converted these numbers to integers using (int) to prevent any possible errors.

Finally, if there aren’t any errors, the $errors variable will remain unchanged and will still be false so we can change $validated to true.

Full PHP Function to Validate the South African ID Number

<?php
function verify_id_number($id_number, $gender = '', $foreigner = 0) {

	$validated = false;


	if (is_numeric($id_number) && strlen($id_number) === 13) {

		$errors = false;

		$num_array = str_split($id_number);


		// Validate the day and month

		$id_month = $num_array[2] . $num_array[3];

		$id_day = $num_array[4] . $num_array[5];


		if ( $id_month < 1 || $id_month > 12) {
			$errors = true;
		}

		if ( $id_day < 1 || $id_day > 31) {
			$errors = true;
		}
		

		// Validate gender

		$id_gender = $num_array[6] >= 5 ? 'male' : 'female';

		if ($gender && strtolower($gender) !== $id_gender) {

			$errors = true;

		}

		// Validate citizenship

		// citizenship as per id number
		$id_foreigner = $num_array[10];

		// citizenship as per submission
		if ( ( $foreigner || $id_foreigner ) && (int)$foreigner !== (int)$id_foreigner ) {

		 	$errors = true;
		}

		/**********************************
			Check Digit Verification
		**********************************/

		// Declare the arrays
		$even_digits = array();
		$odd_digits = array();

		// Loop through modified $num_array, storing the keys and their values in the above arrays
		foreach ( $num_array as $index => $digit) {

		    if ($index === 0 || $index % 2 === 0) {

		        $odd_digits[] = $digit;
		       
		    }

		    else {

		        $even_digits[] = $digit;

		    }

		}

		// use array pop to remove the last digit from $odd_digits and store it in $check_digit
		$check_digit = array_pop($odd_digits);

		//All digits in odd positions (excluding the check digit) must be added together.
		$added_odds = array_sum($odd_digits);

		//All digits in even positions must be concatenated to form a 6 digit number.
		$concatenated_evens = implode('', $even_digits);

		//This 6 digit number must then be multiplied by 2.
		$evensx2 = $concatenated_evens * 2;

		// Add all the numbers produced from the even numbers x 2
		$added_evens = array_sum( str_split($evensx2) );

		$sum = $added_odds + $added_evens;

		// get the last digit of the $sum
		$last_digit = substr($sum, -1);

		/* 10 - $last_digit
         * $verify_check_digit = 10 - (int)$last_digit; (Will break if $last_digit = 0)
         * Edit suggested by Ruan Luies
         * verify check digit is the resulting remainder of
         *  10 minus the last digit divided by 10
         */
         $verify_check_digit = (10 – (int)$last_digit) % 10;

		// test expected last digit against the last digit in $id_number submitted
		if ((int)$verify_check_digit !== (int)$check_digit) {
			$errors = true;
		}

		// if errors haven't been set to true by any one of the checks, we can change verified to true;
		if (!$errors) {
			$validated = true;
		}

	}


	return $validated;


}

I built a WordPress plugin using this code which available to download on the WordPress plug repository. You can also view this plugin on GitHub.

Where To Use This Code

Copy and paste the function into the script that handles your form submission and insert the necessary POST/GET variables as parameters. As an example: if  (verify_id_number($_POST[‘id-number’], $_POST[‘gender’], $_POST[‘gender’) { // Do something if ID number is validated } else { // Do something if ID number not validated }

Is this still valid in 2024? Please let me know in the comments below.

Was This Helpful?

How to Create an Order Programmatically in WooCommerce
Previous Post
How to Create an Order Programmatically in WooCommerce
Prevent Multiple Form Submissions With JQuery or Vanilla JavaScript
Next Post
Prevent Multiple Form Submissions With JQuery or Vanilla JavaScript

8 Comments

  • […] How to Validate a South African ID Number With PHP 12th Nov 2019 […]

    Reply
  • 10th May 2020 at 9:52 pm
    Hi nice website

    Hi nice website.

    Reply
  • 19th June 2020 at 9:16 am
    Ruan Luies

    Line 102 should be, $verify_check_digit = (10 – (int)$last_digit) % 10;

    Reply
    • 19th June 2020 at 2:55 pm

      Hey Ruan. Thanks for the suggested correction. I've updated the code in the post.

      Reply
  • 16th November 2020 at 6:18 am
    Dru Kakai

    Any way I can use this on an existing form and take the age and gender values to populate existing fields?

    Reply
    • 17th November 2020 at 12:00 pm

      Yes of course. Check the function I used in the WordPress plugin, here. You could do something like that but instead of what I did, you could output the HTML as HTML inputs. Alternatively, you could call this function via an Ajax call if you need to do this without reload. Let me know if this helps. I'd be happy to write a post covering this.

      Reply
  • 16th August 2021 at 5:28 pm
    Walkman

    Hey thanks, I used this to write a C# validator if anyone is interested: https://gist.github.com/Walkman100/efc7293cb782fa72849a79617c7e01ff

    (You probably want to simplify the checksum failed text if the message is shown to users)

    Reply
  • 14th September 2021 at 8:26 am
    Johann

    Hi Brinley, I like the function and would love to test it, I am using PHPrunner, Would you perhaps know where I can put a custom function in PHPrunner and use it in my project

    Reply

Leave a Reply