-
Notifications
You must be signed in to change notification settings - Fork 179
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add support for Romanian ID numbers (#1323)
- Loading branch information
Showing
11 changed files
with
271 additions
and
39 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
108 changes: 108 additions & 0 deletions
108
src/main/java/net/datafaker/idnumbers/RomanianIdNumber.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
96 changes: 96 additions & 0 deletions
96
src/test/java/net/datafaker/idnumbers/RomanianIdNumberTest.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters