diff --git a/readme.md b/readme.md index 72f2eab5ed..8a76e36b52 100644 --- a/readme.md +++ b/readme.md @@ -1203,6 +1203,18 @@ echo $faker->prefixFemale; // "d-na." echo $faker->firstNameMale; // "Adrian" // Generates a random female fist name echo $faker->firstNameFemale; // "Miruna" + + +// Generates a random Personal Numerical Code (CNP) +echo $faker->cnp; // "2800523081231" +// Valid option values: +// $gender: null (random), male, female +// $dateOfBirth (1800+): null (random), Y-m-d, Y-m (random day), Y (random month and day) +// i.e. '1981-06-16', '2015-03', '1900' +// $county: 2 letter ISO 3166-2:RO county codes and B1, B2, B3, B4, B5, B6 for Bucharest's 6 sectors +// $isResident true/false flag if the person resides in Romania +echo $faker->cnp($gender = null, $dateOfBirth = null, $county = null, $isResident = true); + ``` ### `Faker\Provider\ro_RO\PhoneNumber` diff --git a/src/Faker/Provider/ro_RO/Person.php b/src/Faker/Provider/ro_RO/Person.php index f4e275c40f..e93f3eaf01 100644 --- a/src/Faker/Provider/ro_RO/Person.php +++ b/src/Faker/Provider/ro_RO/Person.php @@ -87,4 +87,152 @@ class Person extends \Faker\Provider\Person protected static $titleMale = array('dl.', 'ing.', 'dr.'); protected static $titleFemale = array('d-na.', 'd-șoara', 'ing.', 'dr.'); + + protected static $cnpCountyCodes = array( + 'AB' => '01', 'AR' => '02', 'AG' => '03', 'B' => '40', 'BC' => '04', 'BH' => '05', + 'BN' => '06', 'BT' => '07', 'BV' => '08', 'BR' => '09', 'BZ' => '10', 'CS' => '11', + 'CL' => '51', 'CJ' => '12', 'CT' => '13', 'CV' => '14', 'DB' => '15', 'DJ' => '16', + 'GL' => '17', 'GR' => '52', 'GJ' => '18', 'HR' => '19', 'HD' => '20', 'IL' => '21', + 'IS' => '22', 'IF' => '23', 'MM' => '24', 'MH' => '25', 'MS' => '26', 'NT' => '27', + 'OT' => '28', 'PH' => '29', 'SM' => '30', 'SJ' => '31', 'SB' => '32', 'SV' => '33', + 'TR' => '34', 'TM' => '35', 'TL' => '36', 'VS' => '37', 'VL' => '38', 'VN' => '39', + + 'B1' => '41', 'B2' => '42', 'B3' => '43', 'B4' => '44', 'B5' => '45', 'B6' => '46' + ); + + /** + * Personal Numerical Code (CNP) + * + * @link http://ro.wikipedia.org/wiki/Cod_numeric_personal + * @example 1111111111118 + * + * @param null|string $gender Person::GENDER_MALE or Person::GENDER_FEMALE + * @param null|string $dateOfBirth (1800-2099) 'Y-m-d', 'Y-m', 'Y' I.E. '1981-06-16', '2085-03', '1900' + * @param null|string $county county code where the CNP was issued + * @param null|bool $isResident flag if the person resides in Romania + * @return string 13 digits CNP code + */ + public function cnp($gender = null, $dateOfBirth = null, $county = null, $isResident = true) + { + $genders = array(Person::GENDER_MALE, Person::GENDER_FEMALE); + if (empty($gender)) { + $gender = static::randomElement($genders); + } elseif (!in_array($gender, $genders)) { + throw new \InvalidArgumentException("Gender must be '{Person::GENDER_MALE}' or '{Person::GENDER_FEMALE}'"); + } + + $date = $this->getDateOfBirth($dateOfBirth); + + if (is_null($county)) { + $countyCode = static::randomElement(array_values(static::$cnpCountyCodes)); + } elseif (!array_key_exists($county, static::$cnpCountyCodes)) { + throw new \InvalidArgumentException("Invalid county code '{$county}' received"); + } else { + $countyCode = static::$cnpCountyCodes[$county]; + } + + $cnp = (string)$this->getGenderDigit($date, $gender, $isResident) + . $date->format('ymd') + . $countyCode + . static::numerify('##%') + ; + + $checksum = $this->getChecksumDigit($cnp); + + return $cnp.$checksum; + } + + /** + * @param $dateOfBirth + * @return \DateTime + */ + protected function getDateOfBirth($dateOfBirth) + { + if (empty($dateOfBirth)) { + $dateOfBirthParts = array(static::numberBetween(1800, 2099)); + } else { + $dateOfBirthParts = explode('-', $dateOfBirth); + } + $baseDate = \Faker\Provider\DateTime::dateTimeBetween("first day of {$dateOfBirthParts[0]}", "last day of {$dateOfBirthParts[0]}"); + + switch (count($dateOfBirthParts)) { + case 1: + $dateOfBirthParts[] = $baseDate->format('m'); + //don't break, we need the day also + case 2: + $dateOfBirthParts[] = $baseDate->format('d'); + //don't break, next line will + case 3: + break; + default: + throw new \InvalidArgumentException("Invalid date of birth - must be null or in the 'Y-m-d', 'Y-m', 'Y' format"); + } + + if ($dateOfBirthParts[0] < 1800 || $dateOfBirthParts[0] > 2099) { + throw new \InvalidArgumentException("Invalid date of birth - year must be between 1900 and 2099, '{$dateOfBirthParts[0]}' received"); + } + + $dateOfBirthFinal = implode('-', $dateOfBirthParts); + $date = \DateTime::createFromFormat('Y-m-d', $dateOfBirthFinal); + //a full (invalid) date might have been supplied, check if it converts + if ($date->format('Y-m-d') !== $dateOfBirthFinal) { + throw new \InvalidArgumentException("Invalid date of birth - '{$date->format('Y-m-d')}' generated based on '{$dateOfBirth}' received"); + } + + return $date; + } + + /** + * + * https://ro.wikipedia.org/wiki/Cod_numeric_personal#S + * + * @param \DateTime $dateOfBirth + * @param bool $isResident + * @param string $gender + * @return int + */ + protected static function getGenderDigit(\DateTime $dateOfBirth, $gender, $isResident) + { + if (!$isResident) { + return 9; + } + + if ($dateOfBirth->format('Y') < 1900) { + if ($gender == Person::GENDER_MALE) { + return 3; + } + return 4; + } + + if ($dateOfBirth->format('Y') < 2000) { + if ($gender == Person::GENDER_MALE) { + return 1; + } + return 2; + } + + if ($gender == Person::GENDER_MALE) { + return 5; + } + return 6; + } + + /** + * Calculates a checksum for the Personal Numerical Code (CNP). + * + * @param string $value 12 digit CNP + * @return int checksum digit + */ + protected function getChecksumDigit($value) + { + $checkNumber = 279146358279; + + $checksum = 0; + foreach (range(0, 11) as $digit) { + $checksum += (int)substr($value, $digit, 1) * (int)substr($checkNumber, $digit, 1); + } + $checksum = $checksum % 11; + + return $checksum == 10 ? 1 : $checksum; + } } diff --git a/test/Faker/Provider/ro_RO/PersonTest.php b/test/Faker/Provider/ro_RO/PersonTest.php new file mode 100644 index 0000000000..fbd3949f65 --- /dev/null +++ b/test/Faker/Provider/ro_RO/PersonTest.php @@ -0,0 +1,254 @@ +originalTz = @date_default_timezone_get(); + date_default_timezone_set('Europe/Bucharest'); + + $faker = new Generator(); + $faker->addProvider(new DateTime($faker)); + $faker->addProvider(new Person($faker)); + $this->faker = $faker; + } + + public function tearDown() + { + date_default_timezone_set($this->originalTz); + } + + public function invalidGenderProvider() + { + return array( + array('elf'), + array('ent'), + array('fmle'), + array('mal'), + ); + } + + public function invalidYearProvider() + { + return array( + array(1652), + array(1799), + array(2100), + array(2252), + ); + } + + public function validYearProvider() + { + return array( + array(null), + array(''), + array(1800), + array(1850), + array(1900), + array(1990), + array(2000), + array(2099), + ); + } + + public function validCountyCodeProvider() + { + return array( + array('AB'), array('AR'), array('AG'), array('B'), array('BC'), array('BH'), array('BN'), array('BT'), + array('BV'), array('BR'), array('BZ'), array('CS'), array('CL'), array('CJ'), array('CT'), array('CV'), + array('DB'), array('DJ'), array('GL'), array('GR'), array('GJ'), array('HR'), array('HD'), array('IL'), + array('IS'), array('IF'), array('MM'), array('MH'), array('MS'), array('NT'), array('OT'), array('PH'), + array('SM'), array('SJ'), array('SB'), array('SV'), array('TR'), array('TM'), array('TL'), array('VS'), + array('VL'), array('VN'), array('B1'), array('B2'), array('B3'), array('B4'), array('B5'), array('B6') + ); + } + + public function invalidCountyCodeProvider() + { + return array( + array('JK'), array('REW'), array('x'), array('FF'), array('aaaddadaada') + ); + } + + public function validInputDataProvider() + { + return array( + array(Person::GENDER_MALE, '1981-06-16','B2', true, '181061642'), + array(Person::GENDER_FEMALE, '1981-06-16','B2', true, '281061642'), + array(Person::GENDER_MALE, '1981-06-16','B2', false, '981061642'), + array(Person::GENDER_FEMALE, '1981-06-16','B2', false, '981061642'), + ); + } + /** + * + */ + public function test_allRandom_returnsValidCnp() + { + $cnp = $this->faker->cnp; + $this->assertTrue( + $this->isValidCnp($cnp), + sprintf("Invalid CNP '%' generated", $cnp) + ); + } + + /** + * + */ + public function test_validGender_returnsValidCnp() + { + $cnp = $this->faker->cnp(Person::GENDER_MALE); + $this->assertTrue( + $this->isValidMaleCnp($cnp), + sprintf("Invalid CNP '%' generated for '%s' gender", $cnp, Person::GENDER_MALE) + ); + + $cnp = $this->faker->cnp(Person::GENDER_FEMALE); + $this->assertTrue( + $this->isValidFemaleCnp($cnp), + sprintf("Invalid CNP '%' generated for '%s' gender", $cnp, Person::GENDER_FEMALE) + ); + } + + /** + * @param string $value + * + * @dataProvider invalidGenderProvider + */ + public function test_invalidGender_throwsException($value) + { + $this->setExpectedException('InvalidArgumentException'); + $this->faker->cnp($value); + } + + /** + * @param string $value year of birth + * + * @dataProvider validYearProvider + */ + public function test_validYear_returnsValidCnp($value) + { + $cnp = $this->faker->cnp(null, $value); + $this->assertTrue( + $this->isValidCnp($cnp), + sprintf("Invalid CNP '%' generated for valid year '%s'", $cnp, $value) + ); + } + + /** + * @param string $value year of birth + * + * @dataProvider invalidYearProvider + */ + public function test_invalidYear_throwsException($value) + { + $this->setExpectedException('InvalidArgumentException'); + $this->faker->cnp(null, $value); + } + + /** + * @param $value + * @dataProvider validCountyCodeProvider + */ + public function test_validCountyCode_returnsValidCnp($value) + { + $cnp = $this->faker->cnp(null, null, $value); + $this->assertTrue( + $this->isValidCnp($cnp), + sprintf("Invalid CNP '%' generated for valid year '%s'", $cnp, $value) + ); + } + + /** + * @param $value + * @dataProvider invalidCountyCodeProvider + */ + public function test_invalidCountyCode_throwsException($value) + { + $this->setExpectedException('InvalidArgumentException'); + $this->faker->cnp(null, null, $value); + } + + /** + * + */ + public function test_nonResident_returnsValidCnp() + { + $cnp = $this->faker->cnp(null, null, null, false); + $this->assertTrue( + $this->isValidCnp($cnp), + sprintf("Invalid CNP '%' generated for non resident", $cnp) + ); + $this->assertStringStartsWith( + '9', + $cnp, + sprintf("Invalid CNP '%' generated for non resident (should start with 9)", $cnp) + ); + } + + /** + * + * @param $gender + * @param $dateOfBirth + * @param $county + * @param $isResident + * @param $expectedCnpStart + * + * @dataProvider validInputDataProvider + */ + public function test_validInputData_returnsValidCnp($gender, $dateOfBirth, $county, $isResident, $expectedCnpStart) + { + $cnp = $this->faker->cnp($gender, $dateOfBirth, $county, $isResident); + $this->assertStringStartsWith( + $expectedCnpStart, + $cnp, + sprintf("Invalid CNP '%' generated for non valid data", $cnp) + ); + } + + + protected function isValidFemaleCnp($value) + { + return $this->isValidCnp($value) && in_array($value[0], array(2, 4, 6, 8, 9)); + } + + protected function isValidMaleCnp($value) + { + return $this->isValidCnp($value) && in_array($value[0], array(1, 3, 5, 7, 9)); + } + + protected function isValidCnp($cnp) + { + if (preg_match(static::TEST_CNP_REGEX, $cnp) !== false) { + $checkNumber = 279146358279; + + $checksum = 0; + foreach (range(0, 11) as $digit) { + $checksum += (int)substr($cnp, $digit, 1) * (int)substr($checkNumber, $digit, 1); + } + $checksum = $checksum % 11; + $checksum = $checksum == 10 ? 1 : $checksum; + + if ($checksum == substr($cnp, -1)) { + return true; + } + } + + return false; + } +} diff --git a/test/Faker/Provider/ro_RO/PhoneNumberTest.php b/test/Faker/Provider/ro_RO/PhoneNumberTest.php index 97314d5fef..8a406da999 100644 --- a/test/Faker/Provider/ro_RO/PhoneNumberTest.php +++ b/test/Faker/Provider/ro_RO/PhoneNumberTest.php @@ -21,7 +21,7 @@ public function testPhoneNumberReturnsNormalPhoneNumber() public function testTollFreePhoneNumberReturnsTollFreePhoneNumber() { - $this->assertRegExp('/^08(?:0[1267]|70)\d{6}$/', $this->faker->tollFreePhoneNumber()); + $this->assertRegExp('/^08(?:0[01267]|70)\d{6}$/', $this->faker->tollFreePhoneNumber()); } public function testPremiumRatePhoneNumberReturnsPremiumRatePhoneNumber()