diff --git a/composer.json b/composer.json index a8c9579..ecc5fc3 100644 --- a/composer.json +++ b/composer.json @@ -17,7 +17,8 @@ "require": { "php": "^8.1|^8.2|^8.3", "designbycode/luhn-algorithm": "^1.0", - "nesbot/carbon": "^3.5" + "nesbot/carbon": "^3.6", + "symfony/translation": "^7.1" }, "require-dev": { "pestphp/pest": "^2.20", diff --git a/src/SouthAfricanIdValidator.php b/src/SouthAfricanIdValidator.php index 7a27d61..72a1688 100644 --- a/src/SouthAfricanIdValidator.php +++ b/src/SouthAfricanIdValidator.php @@ -2,62 +2,68 @@ namespace Designbycode\SouthAfricanIdValidator; +use Carbon\Carbon; use Designbycode\LuhnAlgorithm\LuhnAlgorithm; class SouthAfricanIdValidator { + + private const GENDER_MALE_MIN = 5000; + private const GENDER_MALE_MAX = 9999; + private const GENDER_FEMALE_MIN = 0; + private const GENDER_FEMALE_MAX = 4999; + /** - * @param string $idNumber + * @param mixed $idNumber * @return bool * Check if id is valid */ - public function isValid(string $idNumber): bool + public function isValid(mixed $idNumber): bool { - return ! (! $this->isLength13($idNumber) || ! $this->isNumber($idNumber) || ! $this->isValidLuhn($idNumber)); + $idNumber = $this->trimWhiteSpaces($idNumber); + return ! (! $this->isLength13($idNumber) || ! $this->isNumber($idNumber) || ! $this->passesLuhnCheck($idNumber)); } // Method to validate if the ID number has a length of 13 digits public function isLength13($idNumber): bool { - return strlen((string) $idNumber) === 13; + return strlen((string) $this->trimWhiteSpaces($idNumber)) === 13; } public function isNumber($idNumber): bool { - return is_numeric($idNumber); + return is_numeric($this->trimWhiteSpaces($idNumber)); } // Method to validate the ID number using the Luhn Algorithm - public function isValidLuhn(string $idNumber): bool + public function passesLuhnCheck(string $idNumber): bool { - return (new LuhnAlgorithm)->isValid($idNumber); + return (new LuhnAlgorithm)->isValid($this->trimWhiteSpaces($idNumber)); } - // Method to determine if the ID number is for a male - public function isMale($idNumber): bool + private function extractGenderDigits(string $idNumber): int { - // Extract the gender digits (4 digits after the first 6 digits) - $genderDigits = substr($idNumber, 6, 4); - $genderNumber = (int) $genderDigits; + return (int) substr($idNumber, 6, 4); + } - // Determine if the number falls in the male range - return ($genderNumber >= 5000) && ($genderNumber <= 9999); + // Method to determine if the ID number is for a male + public function isMale(string $idNumber): bool + { + $genderDigits = $this->extractGenderDigits($idNumber); + return $genderDigits >= self::GENDER_MALE_MIN && $genderDigits <= self::GENDER_MALE_MAX; } // Method to determine if the ID number is for a female - public function isFemale($idNumber): bool + public function isFemale(string $idNumber): bool { - // Extract the gender digits (4 digits after the first 6 digits) - $genderDigits = substr($idNumber, 6, 4); - $genderNumber = (int) $genderDigits; - - // Determine if the number falls in the female range - return $genderNumber >= 0 && $genderNumber <= 4999; + $genderDigits = $this->extractGenderDigits($idNumber); + return $genderDigits >= self::GENDER_FEMALE_MIN && $genderDigits <= self::GENDER_FEMALE_MAX; } // Method to determine if the person is a South African citizen public function isSACitizen($idNumber): bool { + $idNumber = $this->trimWhiteSpaces($idNumber); // The 11th digit (index 10) indicates citizenship status return $idNumber[10] == '0'; } @@ -65,6 +71,7 @@ public function isSACitizen($idNumber): bool // Method to determine if the person is a permanent resident public function isPermanentResident($idNumber): bool { + $idNumber = $this->trimWhiteSpaces($idNumber); // The 11th digit (index 10) indicates citizenship status return $idNumber[10] == '1'; } @@ -72,22 +79,15 @@ public function isPermanentResident($idNumber): bool // Method to parse the ID number public function parse($idNumber): array { - // Check if the ID number is valid - if (! $this->isValid($idNumber)) { - return ['error' => 'Invalid ID number']; + $idNumber = $this->trimWhiteSpaces($idNumber); + if (!$this->isValid($idNumber)) { + throw new \InvalidArgumentException('Invalid ID number'); } - // Extract the birthdate - $year = substr($idNumber, 0, 2); - $month = substr($idNumber, 2, 2); - $day = substr($idNumber, 4, 2); - - // Determine century - $currentYear = (int) date('Y'); - $year = (int) $year + ($year > substr($currentYear, 2, 2) ? 1900 : 2000); + $birthDate = $this->extractBirthDate($idNumber); - $birthDate = sprintf('%04d/%02d/%02d', $year, $month, $day); - $age = $currentYear - $year; + // Determine age + $age = $birthDate->diffInYears(Carbon::now()); // Determine gender $gender = $this->isMale($idNumber) ? 'Male' : 'Female'; @@ -97,10 +97,34 @@ public function parse($idNumber): array return [ 'valid' => $this->isValid($idNumber), - 'birthday' => $birthDate, + 'birthday' => [ + 'default' => $birthDate, + 'iso' => $birthDate->format('Y-m-d'), + 'american' => $birthDate->format('m/d/Y'), + 'european' => $birthDate->format('d/m/Y'), + 'long' => $birthDate->format('F j, Y'), + ], 'age' => $age, 'gender' => $gender, 'citizenship' => $citizenship, ]; } + + private function trimWhiteSpaces(mixed $idNumber): string + { + $idNumber = trim((string) $idNumber); // Remove spaces around the string + return str_replace(' ', '', $idNumber); // Remove spaces within the string + } + + private function extractBirthDate(string $idNumber): Carbon + { + $year = (int) substr($idNumber, 0, 2); + $month = (int) substr($idNumber, 2, 2); + $day = (int) substr($idNumber, 4, 2); + + $currentYear = (int) date('Y'); + $year = $year + ($year > substr($currentYear, 2, 2) ? 1900 : 2000); + + return Carbon::createFromDate($year, $month, $day); + } } diff --git a/tests/ValidatorTest.php b/tests/ValidatorTest.php index 282d90d..77560a8 100644 --- a/tests/ValidatorTest.php +++ b/tests/ValidatorTest.php @@ -4,48 +4,82 @@ beforeEach(function () { $this->validator = new SouthAfricanIdValidator; - $this->id = '7804295117087'; - // $this->id = "9202204720082"; -}); + $this->id = '921223 0051 08 0'; -it('can equal id number', function () { - expect($this->id)->toEqual('7804295117087'); }); -it('can validate that number has a length of 13', function () { - expect($this->validator->isLength13($this->id)) +it('can validate that number has a length of 13', function ($id) { + expect($this->validator->isLength13($id)) ->toEqual(13); -}); +})->with(['6901240689086', '8907290565082', '7809090453082', 9108200519082, '900601 0051 08 2']); it('can test that ID is a number', function () { expect($this->validator->isNumber($this->id))->toBeTrue(); }); it('check if id pass luhna validation', function () { - expect($this->validator->isValidLuhn($this->id)) + expect($this->validator->passesLuhnCheck($this->id)) ->toBeTrue(); }); it('can validate if is male', function () { expect($this->validator->isMale($this->id)) - ->toBeTrue(); + ->toBeFalse(); }); it('can validate if is female', function () { expect($this->validator->isFemale($this->id)) - ->toBeFalse(); + ->toBeTrue(); }); -it('can determine if the person is a South African citizen', function () { - expect($this->validator->isSACitizen($this->id)) +it('can determine if the person is a South African citizen', function ($id) { + expect($this->validator->isSACitizen($id)) ->toBeTrue(); -}); +})->with(['690124 0689 08 6', 8907290565082, '7809090453082', '9108200519082', '9006010051082']); -it('can determine if the person is a permanent resident', function () { - expect($this->validator->isPermanentResident($this->id)) +it('can determine if the person is a permanent resident', function ($id) { + expect($this->validator->isPermanentResident($id)) ->toBeFalse(); -}); +})->with(['6901240689086', '8907290565082', '7809090453082', 9108200519082, '9006010051082']); it('can validate ID', function () { expect($this->validator->isValid($this->id))->toBeTrue(); }); + + +it('can validate list of ID', function ($value) { + expect($this->validator->isValid($this->id))->toBeTrue(); +})->with(['690124 0689 08 6', '8907290565082', '7809090453082', 9108200519082, '9006010051082']); + + + +it('parses a valid ID number', function () { + $result = $this->validator->parse($this->id); + expect($result['valid'])->toBeTrue() + ->and($result['birthday']['default'])->toBeInstanceOf(Carbon\Carbon::class) + ->and($result['age'])->toBeGreaterThan(0) + ->and($result['gender'])->toBeString() + ->and($result['citizenship'])->toBeString(); +}); + + +it('should equal date of ISO from', function() { + $result = $this->validator->parse($this->id); + expect($result['birthday']['iso'])->toEqual('1992-12-23'); +}); + +it('should equal date of american from', function() { + $result = $this->validator->parse($this->id); + expect($result['birthday']['american'])->toEqual('12/23/1992'); +}); + +it('should equal date of european from', function() { + $result = $this->validator->parse($this->id); + expect($result['birthday']['european'])->toEqual('23/12/1992'); +}); + +it('should equal date of long format from', function() { + $result = $this->validator->parse($this->id); + expect($result['birthday']['long'])->toEqual('December 23, 1992'); +}); +