Skip to content

Commit

Permalink
Add support for Romanian ID numbers (#1323)
Browse files Browse the repository at this point in the history
  • Loading branch information
asolntsev authored Aug 12, 2024
1 parent 28631b8 commit 081137a
Show file tree
Hide file tree
Showing 11 changed files with 271 additions and 39 deletions.
12 changes: 3 additions & 9 deletions src/main/java/net/datafaker/idnumbers/AlbanianIdNumber.java
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,10 @@

import java.time.LocalDate;

import static net.datafaker.idnumbers.Utils.gender;
import static net.datafaker.idnumbers.Utils.birthday;
import static net.datafaker.idnumbers.Utils.digit;
import static net.datafaker.idnumbers.Utils.digitAt;
import static net.datafaker.idnumbers.Utils.gender;
import static net.datafaker.providers.base.PersonIdNumber.Gender.FEMALE;

/**
Expand Down Expand Up @@ -66,12 +68,4 @@ char checksum(String text) {
int checksumOfFirstChar(char c) {
return Character.isLetter(c) ? CHECKSUM_CHAR.indexOf(c) : digit(c);
}

private int digitAt(String text, int index) {
return digit(text.charAt(index));
}

int digit(char c) {
return c - '0';
}
}
10 changes: 2 additions & 8 deletions src/main/java/net/datafaker/idnumbers/BulgarianIdNumber.java
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

import static net.datafaker.idnumbers.Utils.gender;
import static net.datafaker.idnumbers.Utils.birthday;
import static net.datafaker.idnumbers.Utils.multiply;
import static net.datafaker.idnumbers.Utils.randomGender;

/**
Expand Down Expand Up @@ -67,14 +68,7 @@ private String order(BaseProviders faker, Gender gender) {
}

int checksum(String text) {
int checksum = 0;
for (int i = 0; i < text.length(); i++) {
checksum += digitAt(text, i) * CHECKSUM_WEIGHTS[i];
}
int checksum = multiply(text, CHECKSUM_WEIGHTS);
return (checksum % 11) % 10;
}

private int digitAt(String text, int index) {
return text.charAt(index) - '0';
}
}
7 changes: 2 additions & 5 deletions src/main/java/net/datafaker/idnumbers/MacedonianIdNumber.java
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,9 @@
import java.time.LocalDate;
import java.util.List;

import static net.datafaker.idnumbers.Utils.gender;
import static net.datafaker.idnumbers.Utils.birthday;
import static net.datafaker.idnumbers.Utils.digitAt;
import static net.datafaker.idnumbers.Utils.gender;
import static net.datafaker.idnumbers.Utils.randomGender;

/**
Expand Down Expand Up @@ -104,8 +105,4 @@ int checksum(String text) {
default -> m;
};
}

private int digitAt(String text, int index) {
return text.charAt(index) - '0';
}
}
10 changes: 2 additions & 8 deletions src/main/java/net/datafaker/idnumbers/MoldovanIdNumber.java
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import java.time.LocalDate;

import static net.datafaker.idnumbers.Utils.birthday;
import static net.datafaker.idnumbers.Utils.multiply;
import static net.datafaker.idnumbers.Utils.randomGender;

/**
Expand Down Expand Up @@ -74,14 +75,7 @@ private String YYYYY(BaseProviders faker) {
}

char checksum(String text) {
int checksum = 0;
for (int i = 0; i < text.length(); i++) {
checksum += digitAt(text, i) * CHECKSUM_MASK[i];
}
int checksum = multiply(text, CHECKSUM_MASK);
return (char) ('0' + checksum % 10);
}

private int digitAt(String text, int index) {
return text.charAt(index) - '0';
}
}
108 changes: 108 additions & 0 deletions src/main/java/net/datafaker/idnumbers/RomanianIdNumber.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
package net.datafaker.idnumbers;

import net.datafaker.providers.base.BaseProviders;
import net.datafaker.providers.base.IdNumber.IdNumberRequest;
import net.datafaker.providers.base.PersonIdNumber;
import net.datafaker.providers.base.PersonIdNumber.Gender;

import java.time.LocalDate;
import java.time.format.DateTimeFormatter;

import static net.datafaker.idnumbers.Utils.birthday;
import static net.datafaker.idnumbers.Utils.gender;
import static net.datafaker.idnumbers.Utils.multiply;
import static net.datafaker.idnumbers.Utils.randomGender;

/**
* The Romanian Cod Numeric Personal (CNP), or Personal Numeric Code
* is a unique identifying number consisting of 13 digits.
*
* <a href="https://en.wikipedia.org/wiki/Romanian_identity_card#CNP">Description</a>
*/
public class RomanianIdNumber implements IdNumberGenerator {
private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyMMdd");
private static final int[] CHECKSUM_WEIGHTS = {2, 7, 9, 1, 4, 6, 3, 5, 8, 2, 7, 9};

@Override
public String countryCode() {
return "RO";
}

@Override
public PersonIdNumber generateValid(BaseProviders faker, IdNumberRequest request) {
LocalDate birthday = birthday(faker, request);
Gender gender = gender(faker, request);
String basePart = basePart(faker, birthday, gender);
String idNumber = basePart + checksum(basePart);
return new PersonIdNumber(idNumber, birthday, gender);
}

@Override
public String generateInvalid(BaseProviders faker) {
LocalDate birthday = faker.timeAndDate().birthday();
Gender gender = randomGender(faker);
String basePart = basePart(faker, birthday, gender);
return basePart + (checksum(basePart) + 1) % 10;
}

private String basePart(BaseProviders faker, LocalDate birthday, Gender gender) {
return firstCharacter(birthday, gender) +
dateOfBirth(birthday) + countyCode(faker) + sequenceNumber(faker);
}

/**
* Represents the gender and century in which the person was born and can be:
* – 1 for male persons born between 1900-1999;
* – 2 for female persons born between 1900-1999;
* – 3 for male persons born between 1800-1899;
* – 4 for female persons born between 1800-1899;
* – 5 for male persons born between 2000-2099;
* – 6 for female persons born between the years 2000-2099;
*/
int firstCharacter(LocalDate birthday, Gender gender) {
int digit = switch (birthday.getYear() / 100) {
case 18 -> 3;
case 19 -> 1;
case 20 -> 5;
default -> throw new IllegalArgumentException("Too far in the past or future: " + birthday);
};

return switch (gender) {
case FEMALE -> digit + 1;
case MALE -> digit;
};
}

String dateOfBirth(LocalDate birthday) {
return DATE_TIME_FORMATTER.format(birthday);
}

/**
* Character 8–9: 01–46 or 51 or 52
*/
String countyCode(BaseProviders faker) {
int countyCode = faker.bool().bool() ?
faker.number().numberBetween(1, 47) :
faker.number().numberBetween(51, 53);
return "%02d".formatted(countyCode);
}

/**
* next 3 digits is a number between 001 and 999.
* Each number is allocated only once per person per day.
*/
String sequenceNumber(BaseProviders faker) {
return "%03d".formatted(faker.number().numberBetween(1, 1_000));
}

/**
* last digit is a control digit calculated from all the other 12 digits in the code as follows:
* (n1*2+n2*7+n3*9+n4*1+n5*4+n6*6+n7*3+n8*5+n9*8+n10*2+n11*7+n12*9)%11
*
* if the result is 10 then the digit is 1, otherwise is the result.
*/
int checksum(String basePart) {
int result = multiply(basePart, CHECKSUM_WEIGHTS) % 11;
return result == 10 ? 1 : result;
}
}
15 changes: 15 additions & 0 deletions src/main/java/net/datafaker/idnumbers/Utils.java
Original file line number Diff line number Diff line change
Expand Up @@ -29,4 +29,19 @@ static Gender randomGender(BaseProviders faker) {
return faker.bool().bool() ? FEMALE : MALE;
}

static int digitAt(String text, int index) {
return digit(text.charAt(index));
}

static int digit(char c) {
return c - '0';
}

static int multiply(String text, int[] weights) {
int checksum = 0;
for (int i = 0; i < text.length(); i++) {
checksum += digitAt(text, i) * weights[i];
}
return checksum;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ net.datafaker.idnumbers.MexicanIdNumber
net.datafaker.idnumbers.MoldovanIdNumber
net.datafaker.idnumbers.PolishIdNumber
net.datafaker.idnumbers.PortugueseIdNumber
net.datafaker.idnumbers.RomanianIdNumber
net.datafaker.idnumbers.SingaporeIdNumber
net.datafaker.idnumbers.SouthAfricanIdNumber
net.datafaker.idnumbers.SouthKoreanIdNumber
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -83,13 +83,4 @@ void checksumOfFirstChar() {
assertThat(generator.checksumOfFirstChar('V')).isEqualTo(22);
assertThat(generator.checksumOfFirstChar('W')).isEqualTo(0);
}

@Test
void digit() {
assertThat(generator.digit('0')).isEqualTo(0);
assertThat(generator.digit('1')).isEqualTo(1);
assertThat(generator.digit('2')).isEqualTo(2);
assertThat(generator.digit('8')).isEqualTo(8);
assertThat(generator.digit('9')).isEqualTo(9);
}
}
96 changes: 96 additions & 0 deletions src/test/java/net/datafaker/idnumbers/RomanianIdNumberTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
package net.datafaker.idnumbers;

import net.datafaker.Faker;
import org.junit.jupiter.api.RepeatedTest;
import org.junit.jupiter.api.Test;

import java.time.LocalDate;
import java.util.HashSet;
import java.util.Set;
import java.util.regex.Pattern;

import static net.datafaker.providers.base.PersonIdNumber.Gender.FEMALE;
import static net.datafaker.providers.base.PersonIdNumber.Gender.MALE;
import static org.assertj.core.api.Assertions.assertThat;

class RomanianIdNumberTest {
private static final Pattern RE_TWO_DIGITS = Pattern.compile("\\d{2}");
private static final Pattern RE_THREE_DIGITS = Pattern.compile("\\d{3}");
private static final Pattern RE_THIRTEEN_DIGITS = Pattern.compile("\\d{13}");

private final RomanianIdNumber impl = new RomanianIdNumber();
private final Faker faker = new Faker();

@RepeatedTest(100)
void sample() {
assertThat(impl.generateValid(faker)).matches(RE_THIRTEEN_DIGITS);
}

@Test
void firstDigit_18xx() {
for (int year = 1800; year <= 1899; year++) {
assertThat(impl.firstCharacter(LocalDate.of(year, 1, 1), MALE)).isEqualTo(3);
assertThat(impl.firstCharacter(LocalDate.of(year, 1, 1), FEMALE)).isEqualTo(4);
}
}

@Test
void firstDigit_19xx() {
for (int year = 1900; year <= 1999; year++) {
assertThat(impl.firstCharacter(LocalDate.of(year, 1, 1), MALE)).isEqualTo(1);
assertThat(impl.firstCharacter(LocalDate.of(year, 1, 1), FEMALE)).isEqualTo(2);
}
}

@Test
void firstDigit_20xx() {
for (int year = 2000; year <= 2099; year++) {
assertThat(impl.firstCharacter(LocalDate.of(year, 1, 1), MALE)).isEqualTo(5);
assertThat(impl.firstCharacter(LocalDate.of(year, 1, 1), FEMALE)).isEqualTo(6);
}
}

@Test
void dateOfBirth() {
assertThat(impl.dateOfBirth(LocalDate.of(1990, 1, 1))).isEqualTo("900101");
assertThat(impl.dateOfBirth(LocalDate.of(1234, 12, 31))).isEqualTo("341231");
}

@Test
void countyCode() {
Set<String> allCodes = new HashSet<>(48);
for (int i = 0; i < 10_000; i++) {
String countyCode = impl.countyCode(faker);
assertThat(countyCode).matches(RE_TWO_DIGITS);
allCodes.add(countyCode);
}

assertThat(allCodes).hasSize(48);
assertThat(allCodes).contains("01");
assertThat(allCodes).contains("09");
assertThat(allCodes).contains("10");
assertThat(allCodes).contains("11");
assertThat(allCodes).contains("19");
assertThat(allCodes).contains("20");
assertThat(allCodes).contains("21");
assertThat(allCodes).contains("45");
assertThat(allCodes).contains("46");
assertThat(allCodes).contains("51");
assertThat(allCodes).contains("52");
assertThat(allCodes).doesNotContain("53");
assertThat(allCodes).doesNotContain("47");
assertThat(allCodes).doesNotContain("49");
assertThat(allCodes).doesNotContain("50");
}

@RepeatedTest(10)
void sequenceNumber() {
assertThat(impl.sequenceNumber(faker)).matches(RE_THREE_DIGITS);
}

@Test
void checksum() {
assertThat(impl.checksum("198081945678")).isEqualTo(1);
assertThat(impl.checksum("293052637289")).isEqualTo(4);
}
}
35 changes: 35 additions & 0 deletions src/test/java/net/datafaker/idnumbers/UtilsTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package net.datafaker.idnumbers;

import org.junit.jupiter.api.Test;

import static net.datafaker.idnumbers.Utils.digit;
import static net.datafaker.idnumbers.Utils.digitAt;
import static net.datafaker.idnumbers.Utils.multiply;
import static org.assertj.core.api.Assertions.assertThat;

class UtilsTest {
@Test
void digit_parsesGivenCharToNumber() {
assertThat(digit('0')).isEqualTo(0);
assertThat(digit('1')).isEqualTo(1);
assertThat(digit('2')).isEqualTo(2);
assertThat(digit('8')).isEqualTo(8);
assertThat(digit('9')).isEqualTo(9);
}

@Test
void digitAt_parsesGivenCharToNumber() {
assertThat(digitAt("12345", 0)).isEqualTo(1);
assertThat(digitAt("12345", 1)).isEqualTo(2);
assertThat(digitAt("12345", 2)).isEqualTo(3);
assertThat(digitAt("12345", 3)).isEqualTo(4);
assertThat(digitAt("12345", 4)).isEqualTo(5);
}

@Test
void multiply_digits() {
assertThat(multiply("1", new int[]{1})).isEqualTo(1);
assertThat(multiply("1", new int[]{2})).isEqualTo(2);
assertThat(multiply("23", new int[]{4, 5})).isEqualTo(2 * 4 + 3 * 5);
}
}
7 changes: 7 additions & 0 deletions src/test/java/net/datafaker/providers/base/IdNumberTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ class IdNumberTest extends BaseFakerTest<BaseFaker> {
private static final Faker ALBANIAN = new Faker(new Locale("sq", "AL"));
private static final Faker BULGARIAN = new Faker(new Locale("bg", "BG"));
private static final Faker MACEDONIAN = new Faker(new Locale("mk", "MK"));
private static final Faker ROMANIAN = new Faker(new Locale("ro", "RO"));

private static final Pattern SWEDISH_ID_NUMBER_PATTERN = Pattern.compile("\\d{6}[-+]\\d{4}");
private static final Pattern SOUTH_AFRICA_ID_NUMBER_PATTERN = Pattern.compile("[0-9]{10}([01])8[0-9]");
Expand Down Expand Up @@ -169,6 +170,12 @@ void macedonianPersonalCode_valid() {
assertThatPin(pin).matches("\\d{13}");
}

@RepeatedTest(100)
void romanianPersonalCode_valid() {
String pin = ROMANIAN.idNumber().valid();
assertThatPin(pin).matches("\\d{13}");
}

@RepeatedTest(100)
void macedonianPersonalCode_invalid() {
String pin = MACEDONIAN.idNumber().invalid();
Expand Down

0 comments on commit 081137a

Please sign in to comment.